[
  {
    "path": ".gitmodules",
    "content": "[submodule \"t/toolkit\"]\n\tpath = t/toolkit\n\turl = https://github.com/api7/test-toolkit.git\n[submodule \".github/actions/action-semantic-pull-request\"]\n\tpath = .github/actions/action-semantic-pull-request\n\turl = https://github.com/amannn/action-semantic-pull-request.git\n[submodule \".github/actions/autocorrect\"]\n\tpath = .github/actions/autocorrect\n\turl = https://github.com/huacnlee/autocorrect.git\n"
  },
  {
    "path": ".ignore_words",
    "content": "iam\nte\nba\nue\nshttp\nnd\nhel\nnulll\nsmove\naks\nnin\n"
  },
  {
    "path": ".licenserc.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nheader:\n  license:\n    spdx-id: Apache-2.0\n    copyright-owner: Apache Software Foundation\n\n  license-location-threshold: 360\n\n  paths-ignore:\n    - '.gitignore'\n    - '.gitattributes'\n    - '.gitmodules'\n    - 'LICENSE'\n    - 'NOTICE'\n    - '**/*.json'\n    - '**/*.key'\n    - '**/*.crt'\n    - '**/*.pem'\n    - '**/*.pb.go'\n    - '**/pnpm-lock.yaml'\n    - '.github/'\n    - 'conf/mime.types'\n    - '**/*.svg'\n    # Exclude CI env_file\n    - 'ci/pod/**/*.env'\n    # eyes has some limitation to handle git pattern\n    - '**/*.log'\n    # Exclude test toolkit files\n    - 't/toolkit'\n    - 'go.mod'\n    - 'go.sum'\n    # Exclude non-Apache licensed files\n    - 'apisix/balancer/ewma.lua'\n    # Exclude plugin-specific configuration files\n    - 't/plugin/authz-casbin'\n    - 't/coredns'\n    - 't/fuzzing/requirements.txt'\n    - 'autodocs/'\n    - 'docs/**/*.md'\n    - '.ignore_words'\n    - '.luacheckrc'\n    # Exclude file contains certificate revocation information\n    - 't/certs/ocsp/index.txt'\n\n  comment: on-failure\n"
  },
  {
    "path": ".markdownlint.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nMD001: false\nMD004: false\nMD005: false\nMD006: false\nMD007: false\nMD010: false\nMD013: false\nMD014: false\nMD024: false\nMD026: false\nMD029: false\nMD033: false\nMD034: false\nMD036: false\nMD040: false\nMD041: false\nMD046: false\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "---\ntitle: Changelog\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Table of Contents\n\n- [3.15.0](#3150)\n- [3.14.1](#3141)\n- [3.14.0](#3140)\n- [3.13.0](#3130)\n- [3.12.0](#3120)\n- [3.11.0](#3110)\n- [3.10.0](#3100)\n- [3.9.0](#390)\n- [3.8.0](#380)\n- [3.7.0](#370)\n- [3.6.0](#360)\n- [3.5.0](#350)\n- [3.4.0](#340)\n- [3.3.0](#330)\n- [3.2.1](#321)\n- [3.2.0](#320)\n- [3.1.0](#310)\n- [3.0.0](#300)\n- [3.0.0-beta](#300-beta)\n- [2.15.3](#2153)\n- [2.15.2](#2152)\n- [2.15.1](#2151)\n- [2.15.0](#2150)\n- [2.14.1](#2141)\n- [2.14.0](#2140)\n- [2.13.3](#2133)\n- [2.13.2](#2132)\n- [2.13.1](#2131)\n- [2.13.0](#2130)\n- [2.12.1](#2121)\n- [2.12.0](#2120)\n- [2.11.0](#2110)\n- [2.10.5](#2105)\n- [2.10.4](#2104)\n- [2.10.3](#2103)\n- [2.10.2](#2102)\n- [2.10.1](#2101)\n- [2.10.0](#2100)\n- [2.9.0](#290)\n- [2.8.0](#280)\n- [2.7.0](#270)\n- [2.6.0](#260)\n- [2.5.0](#250)\n- [2.4.0](#240)\n- [2.3.0](#230)\n- [2.2.0](#220)\n- [2.1.0](#210)\n- [2.0.0](#200)\n- [1.5.0](#150)\n- [1.4.1](#141)\n- [1.4.0](#140)\n- [1.3.0](#130)\n- [1.2.0](#120)\n- [1.1.0](#110)\n- [1.0.0](#100)\n- [0.9.0](#090)\n- [0.8.0](#080)\n- [0.7.0](#070)\n- [0.6.0](#060)\n\n## 3.15.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: fix: disallow creating duplicate plugins in global rules [#12800](https://github.com/apache/apisix/pull/12800)\n\n### Core\n\n- feat: kubernetes discovery readiness check [#12852](https://github.com/apache/apisix/pull/12852)\n- feat: standalone mode status api [#12810](https://github.com/apache/apisix/pull/12810)\n- feat: add validate API to standalone mode [#12718](https://github.com/apache/apisix/pull/12718)\n- feat: add dependency protocol checking and deletion checking for stream routing [#12794](https://github.com/apache/apisix/pull/12794)\n- feat: relax resource name length restriction to 256 [#11822](https://github.com/apache/apisix/pull/11822)\n- feat: add support for wildcard on SNIs for SSL [#12668](https://github.com/apache/apisix/pull/12668)\n- refactor: use secret URI as key for cache and refactor lrucache [#12682](https://github.com/apache/apisix/pull/12682)\n- fix: maintain node_version for independent upstream [#12856](https://github.com/apache/apisix/pull/12856)\n- fix: request failure during reload after any Eureka node fails [#12906](https://github.com/apache/apisix/pull/12906)\n- fix: nacos service discovery request lacks retries after failure [#12734](https://github.com/apache/apisix/pull/12734)\n- fix: load full data during init_worker phase require a new apisix-runtime [#12678](https://github.com/apache/apisix/pull/12678)\n- chore: upgrade lua-resty-logger-socket [#12898](https://github.com/apache/apisix/pull/12898)\n- chore: upgrade lua-resty-dns-client to 7.1.0 [#12851](https://github.com/apache/apisix/pull/12851)\n- change: remove lua-resty-worker-events from the core dependencies [#12930](https://github.com/apache/apisix/pull/12930)\n\n### Plugins\n\n- feat: rate limiting plugins support setting keepalive for redis policy [#12861](https://github.com/apache/apisix/pull/12861)\n- feat: support `apisix_request_id` variable with request-id plugin [#12931](https://github.com/apache/apisix/pull/12931)\n- feat: support vertex-ai [#12933](https://github.com/apache/apisix/pull/12933)\n- feat: support gemini openai api [#12883](https://github.com/apache/apisix/pull/12883)\n- feat: support anthropic openai api [#12881](https://github.com/apache/apisix/pull/12881)\n- feat: add support for openrouter [#12878](https://github.com/apache/apisix/pull/12878)\n- feat: auth plugins respond with `www-authenticate` header with realm [#12864](https://github.com/apache/apisix/pull/12864)\n- feat: allow grpc web in non prefix based routes [#12830](https://github.com/apache/apisix/pull/12830)\n- feat(file-logger): add path properties to file-logger plugin metadata [#12825](https://github.com/apache/apisix/pull/12825)\n- feat(log): add nested log format support for logger plugins [#12697](https://github.com/apache/apisix/pull/12697)\n- feat: add max pending entries to all logger plugins [#12709](https://github.com/apache/apisix/pull/12709)\n- feat(kafka-logger): add support for scram for authentication [#12693](https://github.com/apache/apisix/pull/12693)\n- fix(limit-conn): implement configurable redis key expiry [#12872](https://github.com/apache/apisix/pull/12872)\n- fix(skywalking): start timer when route is hit [#12855](https://github.com/apache/apisix/pull/12855)\n- fix: eliminate deepcopy when destroying prometheus [#12905](https://github.com/apache/apisix/pull/12905)\n- fix(limit-req): ensure safe eviction of keys in redis [#12911](https://github.com/apache/apisix/pull/12911)\n- fix(limit-count): use meta parent to identify plugin source [#12900](https://github.com/apache/apisix/pull/12900)\n- fix: Make protocol_name optional and default to 'MQTT' for mqtt plugin [#12831](https://github.com/apache/apisix/pull/12831)\n- fix(batch-requests): the number of sub-responses does not match that of sub-requests [#12779](https://github.com/apache/apisix/pull/12779)\n- fix(ai-proxy): correct logging schema key in ai-proxy-multi [#12795](https://github.com/apache/apisix/pull/12795)\n- fix(plugin_metadata): ensure enable_data_encryption initialization & querying issue [#12624](https://github.com/apache/apisix/pull/12624)\n\n### Bugfixes\n\n- fix: correct handling of endpointSlices in Kubernetes service discovery [#12634](https://github.com/apache/apisix/pull/12634)\n- fix: Adding request-id header in case of empty header value in request [#12837](https://github.com/apache/apisix/pull/12837)\n- fix(docker): adjust permissions for apisix directory to run in openshift without anyuid command [#12824](https://github.com/apache/apisix/pull/12824)\n- fix: correct pre/post hook typos in Kubernetes discovery and improve cleanup safety [#12288](https://github.com/apache/apisix/pull/12288)\n- fix(performance): move the ipv6 check to schema validation [#12714](https://github.com/apache/apisix/pull/12714)\n- fix(authz-keycloak): strip query string when resolving resources with lazy_load_paths [#12914](https://github.com/apache/apisix/pull/12914)\n\n## 3.14.1\n\n### Bugfixes\n\n- fix: port conflict in worker process for prometheus port [#12667](https://github.com/apache/apisix/pull/12667)\n\n### Core\n\n- fix: add warning log when skipping check for disabled plugin [#12655](https://github.com/apache/apisix/pull/12655)\n- chore: add test for verifying lua-resty-openssl bug fix [#12656](https://github.com/apache/apisix/pull/12656)\n\n## Doc improvements\n\n- docs: remove unnecessary sentence in opentelemetry plugin doc [#12660](https://github.com/apache/apisix/pull/12660)\n\n## 3.14.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: feat: admin api no longer populates default values when writing [#12603](https://github.com/apache/apisix/pull/12603)\n- :warning: change(jwt-auth): when algorithm is not RS256 or ES256, require the user to fill in secret [#12611](https://github.com/apache/apisix/pull/12611)\n- :warning: change(openid-connect): when bearer_only is false, require the user to fill in session.secret [#12609](https://github.com/apache/apisix/pull/12609)\n\n### Bugfixes\n\n- fix: redact encrypted fields from error log [#12629](https://github.com/apache/apisix/pull/12629)\n- fix: run init_worker of apisix.admin module in stream subsystem [#12632](https://github.com/apache/apisix/pull/12632)\n- fix(ai-proxy-multi): inconsistent resolved nodes for healthcheck [#12594](https://github.com/apache/apisix/pull/12594)\n- fix: only trust X-Forwarded-* headers from trusted_addresses [#12551](https://github.com/apache/apisix/pull/12551)\n- fix(plugin/redirect): ensure redirect when scheme is not https [#12561](https://github.com/apache/apisix/pull/12561)\n- fix: fix ui redirect error when behind proxy [#12566](https://github.com/apache/apisix/pull/12566)\n- fix(secret): refresh stale lru cache item in background [#12614](https://github.com/apache/apisix/pull/12614)\n- fix: healthcheck manager missing runtime information [#12607](https://github.com/apache/apisix/pull/12607)\n- fix(standalone): support stream route in admin api mode [#12604](https://github.com/apache/apisix/pull/12604)\n- fix: only log response body when include_resp_body is enabled [#12599](https://github.com/apache/apisix/pull/12599)\n- fix: correct spelling error in get_healthcheck_events_module function name [#12587](https://github.com/apache/apisix/pull/12587)\n- fix: typo in ai-proxy-multi [#12601](https://github.com/apache/apisix/pull/12601)\n- fix(ai-proxy-multi): panic when instance dont have custom endpoint [#12584](https://github.com/apache/apisix/pull/12584)\n- fix(ai-prompt-decorator): prevent message accumulation across requests [#12582](https://github.com/apache/apisix/pull/12582)\n- fix: docker entrypoint remove stream_worker_events.sock if exists [#12546](https://github.com/apache/apisix/pull/12546)\n- fix: add exptime to ewma shared dict items [#12557](https://github.com/apache/apisix/pull/12557)\n- fix(ai-proxy): catch malformed override endpoint in schema validation [#12563](https://github.com/apache/apisix/pull/12563)\n- fix: missing ctx.llm_raw_usage in non-stream mode [#12564](https://github.com/apache/apisix/pull/12564)\n- fix(ai-proxy): set llm variables default value to 0 [#12549](https://github.com/apache/apisix/pull/12549)\n- fix(ai-proxy): check type of choices/usage/content fields before use it [#12548](https://github.com/apache/apisix/pull/12548)\n- fix(discovery/kubernetes): adjust id length [#12536](https://github.com/apache/apisix/pull/12536)\n- fix: basic auth scheme supports case insensitivity [#12539](https://github.com/apache/apisix/pull/12539)\n- fix: when only tls.verify, skip the logic of judging client cert [#12527](https://github.com/apache/apisix/pull/12527)\n- fix(etcd): load full data from etcd while worker restart [#12523](https://github.com/apache/apisix/pull/12523)\n- fix(etcd): upgrade revision when watch request timeout [#12514](https://github.com/apache/apisix/pull/12514)\n- fix: enable issue of endpointslices for k8s discovery [#11654](https://github.com/apache/apisix/pull/11654)\n- fix(grpc-web): missing trailers when empty resp body [#12490](https://github.com/apache/apisix/pull/12490)\n- fix: can not get hostname in redhat [#12267](https://github.com/apache/apisix/pull/12267)\n- fix: batch processor cache not working when configure plugin in service [#12474](https://github.com/apache/apisix/pull/12474)\n- fix(forward-auth): extra_headers not resolving variable on $post_arg. [#12435](https://github.com/apache/apisix/pull/12435)\n- fix: skipped failing bailedout tests in CI [#12462](https://github.com/apache/apisix/pull/12462)\n- fix(api-breaker): inconsistent circuit breaking due to premature breaker_time increment [#12451](https://github.com/apache/apisix/pull/12451)\n- fix(standalone): lack of configuration validation in api [#12424](https://github.com/apache/apisix/pull/12424)\n- fix(log-rotate): skip access log when enable_access_log is set to false [#11310](https://github.com/apache/apisix/pull/11310)\n- fix(opentelemetry): remove plugin attr set_ngx_var [#12411](https://github.com/apache/apisix/pull/12411)\n- fix: broken mcp-bridge test cases [#12425](https://github.com/apache/apisix/pull/12425)\n- fix(request-validation): support Content-Type header with charset for urlencoded data [#12406](https://github.com/apache/apisix/pull/12406)\n- fix: zipkin trace_id and span_id format in ngx_var [#12403](https://github.com/apache/apisix/pull/12403)\n- fix(consumer): missed consumer update due to wrong version in cache [#12413](https://github.com/apache/apisix/pull/12413)\n- revert: fix: forward-auth request body too large [#12404](https://github.com/apache/apisix/pull/12404)\n- fix: get_keys only return first 1024 items in shared dict by default [#12380](https://github.com/apache/apisix/pull/12380)\n\n### Core\n\n- ci: migrate docker image for testing to bitnamilegacy repo [#12562](https://github.com/apache/apisix/pull/12562)\n- chore: remove redundant profile.apisix_home assignment in start [#12529](https://github.com/apache/apisix/pull/12529)\n- chore: upgrade deps to solve vulnerability alerts [#12473](https://github.com/apache/apisix/pull/12473)\n- refactor: add healthcheck manager to decouple upstream [#12426](https://github.com/apache/apisix/pull/12426)\n- feat: add last modified and digest metadata to standalone API [#12526](https://github.com/apache/apisix/pull/12526)\n- feat: support ctx.var.post_arg for vars based route matching on request body [#12388](https://github.com/apache/apisix/pull/12388)\n- feat: add a global switch to disable upstream health check [#12407](https://github.com/apache/apisix/pull/12407)\n- feat: support multiple json.delay_encode objects in single log [#12395](https://github.com/apache/apisix/pull/12395)\n\n### Plugins\n\n- feat: support OIDC claim validator [#11824](https://github.com/apache/apisix/pull/11824)\n- feat: support traffic split plugin for stream routes [#12630](https://github.com/apache/apisix/pull/12630)\n- feat: add ksuid algorithm on request-id plugin [#12573](https://github.com/apache/apisix/pull/12573)\n- feat: add fallback mechanism for specific error codes in ai-proxy-multi [#12571](https://github.com/apache/apisix/pull/12571)\n- feat(ai-proxy): add upstream_response_time in access log [#12555](https://github.com/apache/apisix/pull/12555)\n- feat(ai-proxy): add new ctx variable for request llm model [#12554](https://github.com/apache/apisix/pull/12554)\n- feat: add support for azure-ai driver [#12565](https://github.com/apache/apisix/pull/12565)\n- feat(ai-proxy): add support for pushing logs in ai-proxy plugins [#12515](https://github.com/apache/apisix/pull/12515)\n- feat: add ai-aliyun-content-moderation plugin [#12530](https://github.com/apache/apisix/pull/12530)\n- feat: allow to use environment variables for openid-connect plugin [#11451](https://github.com/apache/apisix/pull/11451)\n- feat(ai-proxy-multi): add support for healthcheck [#12509](https://github.com/apache/apisix/pull/12509)\n- feat(ai-proxy): add latency and usage in access log and prometheus metrics [#12518](https://github.com/apache/apisix/pull/12518)\n- feat: support limit-conn in workflow plugin [#12465](https://github.com/apache/apisix/pull/12465)\n- feat(datadog): Improve Datadog plugin tag support [#11943](https://github.com/apache/apisix/pull/11943)\n- feat: decoupled prometheus exporter's calculation and output [#12383](https://github.com/apache/apisix/pull/12383)\n- feat: add support for extra_headers in forward-auth plugin [#12405](https://github.com/apache/apisix/pull/12405)\n- feat: Add AIMLAPI provider support to AI plugins [#12379](https://github.com/apache/apisix/pull/12379)\n\n## Doc improvements\n\n- docs: update admin api documentation for plugin metadata list endpoint [#12621](https://github.com/apache/apisix/pull/12621)\n- docs: add new dashboard documentation [#12616](https://github.com/apache/apisix/pull/12616)\n- docs: update note for API-drive standalone mode [#12612](https://github.com/apache/apisix/pull/12612)\n- docs: Improve chaitin-waf plugin docs and remove unintended highlights [#12608](https://github.com/apache/apisix/pull/12608)\n- docs: remove outdate dashboard doc [#12596](https://github.com/apache/apisix/pull/12596)\n- docs: update apisix_upstream_response_time and request_llm_model in access log info [#12583](https://github.com/apache/apisix/pull/12583)\n- docs: remove LLM variable in access log examples [#12503](https://github.com/apache/apisix/pull/12503)\n- docs: update jwt-auth docs [#12450](https://github.com/apache/apisix/pull/12450)\n- docs: update rpm installation guide [#12460](https://github.com/apache/apisix/pull/12460)\n- docs: fix typo in credentials doc [#12434](https://github.com/apache/apisix/pull/12434)\n- docs: add dashboard ui tips [#12420](https://github.com/apache/apisix/pull/12420)\n- docs: correct minor typo for openwhisk [#12401](https://github.com/apache/apisix/pull/12401)\n- docs: update changelog with breakchange notices [#12396](https://github.com/apache/apisix/pull/12396)\n- docs: improve openid-connect plugin doc and add keycloak OIDC tutorial [#11889](https://github.com/apache/apisix/pull/11889)\n\n## 3.13.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: mark server-info plugin as deprecated [#12244](https://github.com/apache/apisix/pull/12244)\n- :warning: fill in the metadata of resource schema [#12224](https://github.com/apache/apisix/pull/12224).\nThis PR sets additionalProperties to false for consumer credentials.\n\n### Bugfixes\n\n- fix: running stale healthchecker when new node count <= 1 [#12118](https://github.com/apache/apisix/pull/12118)\n- fix: release healthchecker on 0 nodes [#12126](https://github.com/apache/apisix/pull/12126)\n- fix: only parse and validate apisix.yaml in cli when startup [#12216](https://github.com/apache/apisix/pull/12216)\n- fix(standalone): API-driven mode does not properly handle consumer schema [#12256](https://github.com/apache/apisix/pull/12256)\n- fix: added restriction for TLSv1.3 cross-SNI session resumption [#12366](https://github.com/apache/apisix/pull/12366)\n- fix: flaky t/admin/filter.t due to url encoding for query params [#12370](https://github.com/apache/apisix/pull/12370)\n- fix(workflow/push-dev-image-on-commit): remove already defined uses [#12365](https://github.com/apache/apisix/pull/12365)\n- fix(workflow): use runners with different architectures instead of QEMU [#12322](https://github.com/apache/apisix/pull/12322)\n- fix: kubernetes service discovery single mode data dump [#12284](https://github.com/apache/apisix/pull/12284)\n- fix: handle consul nil port cases by defaulting to port 80 [#12304](https://github.com/apache/apisix/pull/12304)\n- fix: check if config contains duplicate resources in API-driven standalone mode [#12317](https://github.com/apache/apisix/pull/12317)\n- fix: original key being modified causing cache inconsistency [#12299](https://github.com/apache/apisix/pull/12299)\n- fix: access to the apisix dashboard in dev returns 404 [#12376](https://github.com/apache/apisix/pull/12376)\n\n### Core\n\n- feat(consumer): consumer username allows - in it [#12296](https://github.com/apache/apisix/pull/12296)\n- chore: change log level to debug to avoid unnecessary logs [#12361](https://github.com/apache/apisix/pull/12361)\n- chore: change log level from warn to info for stale batch processor removal [#12297](https://github.com/apache/apisix/pull/12297)\n- feat(standalone): allow more characters in credential_id for API-driven mode [#12295](https://github.com/apache/apisix/pull/12295)\n- feat: add standalone admin api [#12179](https://github.com/apache/apisix/pull/12179)\n- feat: support health checker for stream subsystem [#12180](https://github.com/apache/apisix/pull/12180)\n- feat(standalone): support revision in API-driven standalone mode like etcd [#12214](https://github.com/apache/apisix/pull/12214)\n- feat: add healthcheck for sync configuration [#12200](https://github.com/apache/apisix/pull/12200)\n- perf: compare service discovery nodes by address [#12258](https://github.com/apache/apisix/pull/12258)\n- feat: fill in the metadata of resource schema [#12224](https://github.com/apache/apisix/pull/12224)\n- feat: add embedded apisix dashboard ui [#12276](https://github.com/apache/apisix/pull/12276)\n- feat: add apisix dashboard to dev image [#12369](https://github.com/apache/apisix/pull/12369)\n- feat: add max pending entries option to batch-processor [#12338](https://github.com/apache/apisix/pull/12338)\n- feat(standalone): support JSON format [#12333](https://github.com/apache/apisix/pull/12333)\n- feat: enhance admin api filter [#12291](https://github.com/apache/apisix/pull/12291)\n- feat: add warning for data plane writing to etcd [#12241](https://github.com/apache/apisix/pull/12241)\n- chore: upgrade openresty version to v1.27.1.2 [#12307](https://github.com/apache/apisix/pull/12307)\n- chore: upgrade luarocks version to 3.12.0 [#12305](https://github.com/apache/apisix/pull/12305)\n\n### Plugins\n\n- refactor(ai-proxy): move read_response into ai_driver.request function [#12101](https://github.com/apache/apisix/pull/12101)\n- refactor: mcp server framework implementation [#12168](https://github.com/apache/apisix/pull/12168)\n- feat: add mcp-bridge plugin [#12151](https://github.com/apache/apisix/pull/12151)\n- feat: add lago plugin [#12196](https://github.com/apache/apisix/pull/12196)\n- feat: add headers attribute for loki-logger [#12243](https://github.com/apache/apisix/pull/12243)\n- feat: expose apisix version in prometheus node info metric [#12367](https://github.com/apache/apisix/pull/12367)\n\n## Doc improvements\n\n- docs: update stream proxy doc for proxy_mode and some formatting [#12108](https://github.com/apache/apisix/pull/12108)\n- docs: improve loki-logger plugin docs [#11921](https://github.com/apache/apisix/pull/11921)\n- docs: improve ua-restriction plugin docs [#11956](https://github.com/apache/apisix/pull/11956)\n- docs: improve elasticsearch-logger plugin docs [#11922](https://github.com/apache/apisix/pull/11922)\n- fix file logger example wrong data structure [#12125](https://github.com/apache/apisix/pull/12125)\n- docs: improve limit-req plugin docs [#11873](https://github.com/apache/apisix/pull/11873)\n- docs: improve body-transformer plugin docs [#11856](https://github.com/apache/apisix/pull/11856)\n- docs: update ai-rate-limiting and ai-rag docs [#12107](https://github.com/apache/apisix/pull/12107)\n- docs: improve basic-auth docs and update docs for anonymous consumer [#11859](https://github.com/apache/apisix/pull/11859)\n- docs: improve key-auth docs and update docs for anonymous consumer [#11860](https://github.com/apache/apisix/pull/11860)\n- docs: improve hmac-auth plugin docs and update docs for anonymous consumer [#11867](https://github.com/apache/apisix/pull/11867)\n- docs: improve jwt-auth plugin docs and update docs for anonymous consumer [#11865](https://github.com/apache/apisix/pull/11865)\n- docs: improve request-validation plugin docs [#11853](https://github.com/apache/apisix/pull/11853)\n- docs: update variable in building apisix from source [#11640](https://github.com/apache/apisix/pull/11640)\n- docs: update readme with APISIX AI Gateway product link and MCP feature [#12166](https://github.com/apache/apisix/pull/12166)\n- docs: improve plugin-develop docs [#12242](https://github.com/apache/apisix/pull/12242)\n- docs: fix typo in real-ip.md [#12236](https://github.com/apache/apisix/pull/12236)\n- docs: the configuration type of the WASM plugin can be an object. [#12251](https://github.com/apache/apisix/pull/12251)\n\n## Developer productivity\n\n- feat: support devcontainer for containerized development of APISIX [#11765](https://github.com/apache/apisix/pull/11765)\n\n## 3.12.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: replace plugin attribute with plugin metadata in `opentelemetry` plugin [#11940](https://github.com/apache/apisix/pull/11940)\n- :warning: refactor: ai-content-moderation to ai-aws-content-moderation [#12010](https://github.com/apache/apisix/pull/12010)\n- add expiration time for all Prometheus metrics [#11838](https://github.com/apache/apisix/pull/11838)\n- allow workflow config without case [#11787](https://github.com/apache/apisix/pull/11787)\n- unify google-cloud-oauth.lua file [#11596](https://github.com/apache/apisix/pull/11596)\n- :warning: ai-proxy remove passthrough [#12014](https://github.com/apache/apisix/pull/12014)\n- :warning: remove model options' `stream` default value [#12013](https://github.com/apache/apisix/pull/12013)\n- :warning: grpc-web response contains two trailer chunks [#11988](https://github.com/apache/apisix/pull/11988).\nThis PR returns `405 Method not allowed` instead of `400 Bad Request` when request HTTP method errors.\n- :warning: disallow empty key configuration attributes [#11852](https://github.com/apache/apisix/pull/11852)\n- :warning: set default value of ssl_trusted_certificate to system [#11993](https://github.com/apache/apisix/pull/11993)\n\n### Bugfixes\n\n- Fix: timeout risk in usages of lua-resty-aws [#12070](https://github.com/apache/apisix/pull/12070)\n- Fix: ai-rate-limiting not allowed to limit to a single instance [#12061](https://github.com/apache/apisix/pull/12061)\n- Fix: update watch_ctx.revision to avoid multiple resyncs [#12021](https://github.com/apache/apisix/pull/12021)\n- Fix: ai-proxy remove passthrough [#12014](https://github.com/apache/apisix/pull/12014)\n- Fix: ai-proxy dead loop when retrying [#12012](https://github.com/apache/apisix/pull/12012)\n- Fix: error while trying to log table in ai-content-moderation plugin [#11994](https://github.com/apache/apisix/pull/11994)\n- Fix: resync etcd when a lower revision is found [#12015](https://github.com/apache/apisix/pull/12015)\n- Fix: remove model options' `stream` default value [#12013](https://github.com/apache/apisix/pull/12013)\n- Fix: grpc-web response contains two trailer chunks [#11988](https://github.com/apache/apisix/pull/11988)\n- Fix: event_id is nil in chaitin-waf [#11651](https://github.com/apache/apisix/pull/11651)\n- Fix: race condition problem while update upstream.nodes [#11916](https://github.com/apache/apisix/pull/11916)\n- Fix: `upstream_obj.upstream` should not be a string [#11932](https://github.com/apache/apisix/pull/11932)\n- Fix: query params in override.endpoint not being sent to LLMs [#11863](https://github.com/apache/apisix/pull/11863)\n- Fix: add support for ignoring \"load\" global variable [#11862](https://github.com/apache/apisix/pull/11862)\n- Fix: corrupt data in routes() response due to healthchecker data [#11844](https://github.com/apache/apisix/pull/11844)\n- Fix: deepcopy should copy same table exactly only once [#11861](https://github.com/apache/apisix/pull/11861)\n- Fix: disallow empty key configuration attributes [#11852](https://github.com/apache/apisix/pull/11852)\n- Fix: etcd watch restart when receive invalid revision [#11833](https://github.com/apache/apisix/pull/11833)\n- Fix: missing parsed_url nil check [#11637](https://github.com/apache/apisix/pull/11637)\n- Fix: use `plugin.get` to fetch plugin configured in multi-auth plugin [#11794](https://github.com/apache/apisix/pull/11794)\n- Fix: allow special characters in uri params [#11788](https://github.com/apache/apisix/pull/11788)\n- Fix: add nil check to conf in body-transformer [#11768](https://github.com/apache/apisix/pull/11768)\n- Fix: use max_req_body_bytes field in custom_format [#11771](https://github.com/apache/apisix/pull/11771)\n- Fix: health checker can't be released due to health parent being released early [#11760](https://github.com/apache/apisix/pull/11760)\n- Fix: use right modifiedIndex for consumer when use credential [#11649](https://github.com/apache/apisix/pull/11649)\n\n### Core\n\n- set default value of ssl_trusted_certificate to system [#11993](https://github.com/apache/apisix/pull/11993)\n- upgrade openresty version to v1.27.11 [#11936](https://github.com/apache/apisix/pull/11936)\n- Support the use of system-provided CA certs in `ssl_trusted_certificate` [#11809](https://github.com/apache/apisix/pull/11809)\n- support _meta.pre_function to execute custom logic before execution of each phase [#11793](https://github.com/apache/apisix/pull/11793)\n- support anonymous consumer [#11917](https://github.com/apache/apisix/pull/11917)\n- accelerate the creation of the consumer cache [#11840](https://github.com/apache/apisix/pull/11840)\n- replace 'string.find' with 'core.string.find' [#11886](https://github.com/apache/apisix/pull/11886)\n- workflow plugin registration [#11832](https://github.com/apache/apisix/pull/11832)\n\n### Plugins\n\n- refactor ai-proxy and ai-proxy-multi [#12030](https://github.com/apache/apisix/pull/12030)\n- support embeddings API [#12062](https://github.com/apache/apisix/pull/12062)\n- implement rate limiting based fallback strategy [#12047](https://github.com/apache/apisix/pull/12047)\n- ai-rate-limiting plugin [#12037](https://github.com/apache/apisix/pull/12037)\n- add `valid_issuers` field in `openid-connect` plugin [#12002](https://github.com/apache/apisix/pull/12002)\n- add ai-prompt-guard plugin [#12008](https://github.com/apache/apisix/pull/12008)\n- add jwt audience validator [#11987](https://github.com/apache/apisix/pull/11987)\n- store JWT in the request context [#11675](https://github.com/apache/apisix/pull/11675)\n- support proxying openai compatible LLMs [#12004](https://github.com/apache/apisix/pull/12004)\n- add `ai-proxy-multi` plugin [#11986](https://github.com/apache/apisix/pull/11986) [#12030](https://github.com/apache/apisix/pull/12030)\n- make rate limiting response header names configurable [#11831](https://github.com/apache/apisix/pull/11831)\n- support mulipart content-type in `body-transformer` [#11767](https://github.com/apache/apisix/pull/11767)\n- plugins in multi-auth returns error instead of logging it [#11775](https://github.com/apache/apisix/pull/11775)\n- support configuring `key_claim_name` [#11772](https://github.com/apache/apisix/pull/11772)\n- add Total request per second panel in grafana dashboard [#11692](https://github.com/apache/apisix/pull/11692)\n- add ai-rag plugin [#11568](https://github.com/apache/apisix/pull/11568)\n- add ai-content-moderation plugin [#11541](https://github.com/apache/apisix/pull/11541)\n- use setmetatable to set hidden variables without effecting serialisation [#11770](https://github.com/apache/apisix/pull/11770)\n\n## 3.11.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: remove JWT signing endpoint and no longer require a private key to be uploaded in the jwt-auth plugin. [#11597](https://github.com/apache/apisix/pull/11597)\n- :warning: rewrite hmac-auth plugin for usability [#11581](https://github.com/apache/apisix/pull/11581)\n\n### Plugins\n\n- allow configuring keepalive_timeout in splunk-logger [#11611](https://github.com/apache/apisix/pull/11611)\n- add plugin attach-consmer-label [#11604](https://github.com/apache/apisix/pull/11604)\n- ai-proxy plugin [#11499](https://github.com/apache/apisix/pull/11499)\n- ai-prompt-decorator plugin [#11515](https://github.com/apache/apisix/pull/11515)\n- ai-prompt-template plugin [#11517](https://github.com/apache/apisix/pull/11517)\n\n### Bugfixes\n\n- Fix: adjust the position of enums in pb_option_def [#11448](https://github.com/apache/apisix/pull/11448)\n- Fix: encryption/decryption for non-auth plugins in consumer [#11600](https://github.com/apache/apisix/pull/11600)\n- Fix: confusion when substituting ENV in config file [#11545](https://github.com/apache/apisix/pull/11545)\n\n### Core\n\n- support gcp secret manager [#11436](https://github.com/apache/apisix/pull/11436)\n- support aws secret manager [#11417](https://github.com/apache/apisix/pull/11417)\n- add credential resource and include `X-Consumer-Username`, `X-Credential-Identifier`, and `X-Consumer-Custom-ID` headers in requests to upstream services [#11601](https://github.com/apache/apisix/pull/11601)\n\n## 3.10.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: remove `core.grpc` module [#11427](https://github.com/apache/apisix/pull/11427)\n- add max req/resp body size attributes [#11133](https://github.com/apache/apisix/pull/11133)\n- :warning: autogenerate admin api key if not passed [#11080](https://github.com/apache/apisix/pull/11080)\n- :warning: enable sensitive fields encryption by default [#11076](https://github.com/apache/apisix/pull/11076)\n- support more sensitive fields for encryption [#11095](https://github.com/apache/apisix/pull/11095)\n- :warning: avoid overwriting `Access-Control-Expose-Headers` response header [#11136](https://github.com/apache/apisix/pull/11136)\nThis change removes the default `*` value for `expose_headers` and only sets the header when explicitly configured.\n- :warning: add a default limit of 100 for `get_headers()` [#11140](https://github.com/apache/apisix/pull/11140)\n- :warning: core.request.header return strings instead of table [#11127](https://github.com/apache/apisix/pull/11127)\nThis function now always returns strings, previously it returned tables when duplicate headers existed.\n\n### Plugins\n\n- allow set headers in introspection request [#11090](https://github.com/apache/apisix/pull/11090)\n\n### Bugfixes\n\n- Fix: add libyaml-dev dependency for apt [#11291](https://github.com/apache/apisix/pull/11291)\n- Fix: etcd sync data checker should work [#11457](https://github.com/apache/apisix/pull/11457)\n- Fix: plugin metadata add id value for etcd checker [#11452](https://github.com/apache/apisix/pull/11452)\n- Fix: allow trailing period in SNI and CN for SSL [#11414](https://github.com/apache/apisix/pull/11414)\n- Fix: filter out illegal INT(string) formats [#11367](https://github.com/apache/apisix/pull/11367)\n- Fix: make the message clearer when API key is missing [#11370](https://github.com/apache/apisix/pull/11370)\n- Fix: report consumer username tag in datadog [#11354](https://github.com/apache/apisix/pull/11354)\n- Fix: after updating the header, get the old value from the ctx.var [#11329](https://github.com/apache/apisix/pull/11329)\n- Fix: ssl key rotation caused request failure [#11305](https://github.com/apache/apisix/pull/11305)\n- Fix: validation fails causing etcd events not to be handled correctly [#11268](https://github.com/apache/apisix/pull/11268)\n- Fix: stream route matcher is nil after first match [#11269](https://github.com/apache/apisix/pull/11269)\n- Fix: rectify the way to fetch secret resource by id [#11164](https://github.com/apache/apisix/pull/11164)\n- Fix: multi-auth raise 500 error when use default conf [#11145](https://github.com/apache/apisix/pull/11145)\n- Fix: avoid overwriting `Access-Control-Expose-Headers` response header [#11136](https://github.com/apache/apisix/pull/11136)\n- Fix: close session in case of error to avoid blocked session [#11089](https://github.com/apache/apisix/pull/11089)\n- Fix: restore `pb.state` appropriately [#11135](https://github.com/apache/apisix/pull/11135)\n- Fix: add a default limit of 100 for `get_headers()` [#11140](https://github.com/apache/apisix/pull/11140)\n- Fix: disable features when prometheus plugin is turned off [#11117](https://github.com/apache/apisix/pull/11117)\n- Fix: add post request headers only if auth request method is POST [#11021](https://github.com/apache/apisix/pull/11021)\n- Fix: core.request.header return strings instead of table [#11127](https://github.com/apache/apisix/pull/11127)\n- Fix: brotli partial response [#11087](https://github.com/apache/apisix/pull/11087)\n- Fix: the port value greater than 65535 should not be allowed [#11043](https://github.com/apache/apisix/pull/11043)\n\n### Core\n\n- upgrade openresty version to 1.25.3.2 [#11419](https://github.com/apache/apisix/pull/11419)\n- move config-default.yaml to hardcoded lua file [#11343](https://github.com/apache/apisix/pull/11343)\n- warn log when sending requests to external services insecurely [#11403](https://github.com/apache/apisix/pull/11403)\n- update casbin to 1.41.9 [#11400](https://github.com/apache/apisix/pull/11400)\n- update lua-resty-t1k to 1.1.5 [#11391](https://github.com/apache/apisix/pull/11391)\n- support store ssl.keys ssl.certs in secrets mamager [#11339](https://github.com/apache/apisix/pull/11339)\n- move tinyyaml to lyaml [#11312](https://github.com/apache/apisix/pull/11312)\n- support hcv namespace [#11277](https://github.com/apache/apisix/pull/11277)\n- add discovery k8s dump data interface [#11111](https://github.com/apache/apisix/pull/11111)\n- make fetch_secrets use cache for performance [#11201](https://github.com/apache/apisix/pull/11201)\n- replace 'string.len' with '#' [#11078](https://github.com/apache/apisix/pull/11078)\n\n## 3.9.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: use apisix.enable_http2 to enable HTTP/2 in APISIX [#11032](https://github.com/apache/apisix/pull/11032)\n- :warning: unify the keyring and key_encrypt_salt fields [#10771](https://github.com/apache/apisix/pull/10771)\n\n### Core\n\n- :sunrise: Support HTTP3/QUIC\n  - [#10989](https://github.com/apache/apisix/pull/10989)\n  - [#11010](https://github.com/apache/apisix/pull/11010)\n  - [#11027](https://github.com/apache/apisix/pull/11027)\n- :sunrise: add plugins/reload to control api [#10905](https://github.com/apache/apisix/pull/10905)\n- :sunrise: consul deduplicate and sort [#10941](https://github.com/apache/apisix/pull/10941)\n- :sunrise: support uri_arg_ when use radixtree_uri_with_parameter [#10645](https://github.com/apache/apisix/pull/10645)\n\n### Plugins\n\n- :sunrise: add session.cookie configuration [#10919](https://github.com/apache/apisix/pull/10919)\n- :sunrise: support endpointslices in kubernetes discovery [#10916](https://github.com/apache/apisix/pull/10916)\n- :sunrise: add redis and redis-cluster in limit-req [#10874](https://github.com/apache/apisix/pull/10874)\n- :sunrise: support expire prometheus metrics [#10869](https://github.com/apache/apisix/pull/10869)\n- :sunrise: add redis and redis-cluster in limit-conn [#10866](https://github.com/apache/apisix/pull/10866)\n- :sunrise: allow configuring allow-headers in grpc-web plugin [#10904](https://github.com/apache/apisix/pull/10904)\n- :sunrise: Add forward-auth plugin exception configuration status_on_error [#10898](https://github.com/apache/apisix/pull/10898)\n- :sunrise: add option to include request body and response body in log util [#10888](https://github.com/apache/apisix/pull/10888)\n- :sunrise: support compressed responses in loggers [#10884](https://github.com/apache/apisix/pull/10884)\n- :sunrise: add http-dubbo plugin [#10703](https://github.com/apache/apisix/pull/10703)\n- :sunrise: support built-in variables in response_headers in mocking plugin [#10872](https://github.com/apache/apisix/pull/10872)\n- :sunrise: support other data formats without warnings [#10862](https://github.com/apache/apisix/pull/10862)\n- :sunrise: add ocsp-stapling plugin [#10817](https://github.com/apache/apisix/pull/10817)\n\n### Bug Fixes\n\n- Fix: keep different strategy response header consistency [#11048](https://github.com/apache/apisix/pull/11048)\n- Fix: add apisix/plugin/limit-req to makefile [#10955](https://github.com/apache/apisix/pull/10959)\n- Fix: wrong namespace related endpoint in k8s [#10917](https://github.com/apache/apisix/pull/10917)\n- Fix: when delete the secret cause 500 error [#10902](https://github.com/apache/apisix/pull/10902)\n- Fix: jwe-decrypt secret length restriction [#10928](https://github.com/apache/apisix/pull/10928)\n- Fix: unnecessary YAML Config reloads [#9065](https://github.com/apache/apisix/pull/9065)\n- Fix: real_payload was overridden by malicious payload [#10982](https://github.com/apache/apisix/pull/10982)\n- Fix: all origins could pass when allow_origins_by_metadata is set [#10948](https://github.com/apache/apisix/pull/10948)\n- Fix: add compatibility headers [#10828](https://github.com/apache/apisix/pull/10828)\n- Fix: missing trailers issue [#10851](https://github.com/apache/apisix/pull/10851)\n- Fix: decryption failure [#10843](https://github.com/apache/apisix/pull/10843)\n- Fix: linux-install-luarocks are not compatible with the openresty environment [#10813](https://github.com/apache/apisix/pull/10813)\n- Fix: server-side sessions locked by not calling explicit session:close() [#10788](https://github.com/apache/apisix/pull/10788)\n- Fix: skip brotli compression for upstream compressed response [#10740](https://github.com/apache/apisix/pull/10740)\n- Fix: use_jwks breaking authentication header [#10670](https://github.com/apache/apisix/pull/10670)\n- Fix: authz_keycloak plugin giving 500 error [#10763](https://github.com/apache/apisix/pull/10763)\n\n## 3.8.0\n\n### Core\n\n- :sunrise: Support the use of lua-resty-events module for better performance:\n  - [#10550](https://github.com/apache/apisix/pull/10550)\n  - [#10558](https://github.com/apache/apisix/pull/10558)\n- :sunrise: Upgrade OpenSSL 1.1.1 to OpenSSL 3: [#10724](https://github.com/apache/apisix/pull/10724)\n\n### Plugins\n\n- :sunrise: Add jwe-decrypt plugin: [#10252](https://github.com/apache/apisix/pull/10252)\n- :sunrise: Support brotli when use filters.regex option (response-rewrite): [#10733](https://github.com/apache/apisix/pull/10733)\n- :sunrise: Add multi-auth plugin: [#10482](https://github.com/apache/apisix/pull/10482)\n- :sunrise: Add `required scopes` configuration property to `openid-connect` plugin: [#10493](https://github.com/apache/apisix/pull/10493)\n- :sunrise: Support for the Timing-Allow-Origin header (cors): [#9365](https://github.com/apache/apisix/pull/9365)\n- :sunrise: Add brotli plugin: [#10515](https://github.com/apache/apisix/pull/10515)\n- :sunrise: Body-transformer plugin enhancement(#10472): [#10496](https://github.com/apache/apisix/pull/10496)\n- :sunrise: Set minLength of redis_cluster_nodes to 1 for limit-count plugin: [#10612](https://github.com/apache/apisix/pull/10612)\n- :sunrise: Allow to use environment variables for limit-count plugin settings: [#10607](https://github.com/apache/apisix/pull/10607)\n\n### Bugfixes\n\n- Fix: When the upstream nodes are of array type, the port should be an optional field: [#10477](https://github.com/apache/apisix/pull/10477)\n- Fix: Incorrect variable extraction in fault-injection plugin: [#10485](https://github.com/apache/apisix/pull/10485)\n- Fix: All consumers should share the same counter (limit-count): [#10541](https://github.com/apache/apisix/pull/10541)\n- Fix: Safely remove upstream when sending route to opa plugin: [#10552](https://github.com/apache/apisix/pull/10552)\n- Fix: Missing etcd init_dir and unable to list resource: [#10569](https://github.com/apache/apisix/pull/10569)\n- Fix: Forward-auth request body is too large: [#10589](https://github.com/apache/apisix/pull/10589)\n- Fix: Memory leak caused by timer that never quit: [#10614](https://github.com/apache/apisix/pull/10614)\n- Fix: Do not invoke add_header if value resolved as nil in proxy-rewrite plugin: [#10619](https://github.com/apache/apisix/pull/10619)\n- Fix: Frequent traversal of all keys in etcd leads to high CPU usage: [#10671](https://github.com/apache/apisix/pull/10671)\n- Fix: For prometheus upstream_status metrics, mostly_healthy is healthy: [#10639](https://github.com/apache/apisix/pull/10639)\n- Fix: Avoid getting a nil value in log phase in zipkin: [#10666](https://github.com/apache/apisix/pull/10666)\n- Fix: Enable openid-connect plugin without redirect_uri got 500 error: [#7690](https://github.com/apache/apisix/pull/7690)\n- Fix: Add redirect_after_logout_uri for ODIC that do not have an end_session_endpoint: [#10653](https://github.com/apache/apisix/pull/10653)\n- Fix: Response-rewrite filters.regex does not apply when content-encoding is gzip: [#10637](https://github.com/apache/apisix/pull/10637)\n- Fix: The leak of prometheus metrics: [#10655](https://github.com/apache/apisix/pull/10655)\n- Fix: Authz-keycloak add return detail err: [#10691](https://github.com/apache/apisix/pull/10691)\n- Fix: upstream nodes was not updated correctly by service discover: [#10722](https://github.com/apache/apisix/pull/10722)\n- Fix: apisix restart failed: [#10696](https://github.com/apache/apisix/pull/10696)\n\n## 3.7.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: Creating core resources does not allow passing in `create_time` and `update_time`: [#10232](https://github.com/apache/apisix/pull/10232)\n- :warning: Remove self-contained info fields `exptime` and `validity_start` and `validity_end` from ssl schema: [10323](https://github.com/apache/apisix/pull/10323)\n- :warning: Replace `route` with `apisix.route_name`, `service` with `apisix.service_name` in the attributes of opentelemetry plugin to follow the standards for span name and attributes: [#10393](https://github.com/apache/apisix/pull/10393)\n\n### Core\n\n- :sunrise: Added token to support access control for consul discovery: [#10278](https://github.com/apache/apisix/pull/10278)\n- :sunrise: Support configuring `service_id` in stream_route to reference service resources: [#10298](https://github.com/apache/apisix/pull/10298)\n- :sunrise: Using `apisix-runtime` as the apisix runtime:\n  - [#10415](https://github.com/apache/apisix/pull/10415)\n  - [#10427](https://github.com/apache/apisix/pull/10427)\n\n### Plugins\n\n- :sunrise: Add tests for authz-keycloak with apisix secrets: [#10353](https://github.com/apache/apisix/pull/10353)\n- :sunrise: Add authorization params to openid-connect plugin: [#10058](https://github.com/apache/apisix/pull/10058)\n- :sunrise: Support set variable in zipkin plugin: [#10361](https://github.com/apache/apisix/pull/10361)\n- :sunrise: Support Nacos ak/sk authentication: [#10445](https://github.com/apache/apisix/pull/10445)\n\n### Bugfixes\n\n- Fix: Use warn log for get healthcheck target status failure:\n  - [#10156](https://github.com/apache/apisix/pull/10156)\n- Fix: Keep healthcheck target state when upstream changes:\n  - [#10312](https://github.com/apache/apisix/pull/10312)\n  - [#10307](https://github.com/apache/apisix/pull/10307)\n- Fix: Add name field in plugin_config schema for consistency: [#10315](https://github.com/apache/apisix/pull/10315)\n- Fix: Optimize tls in upstream_schema and wrong variable: [#10269](https://github.com/apache/apisix/pull/10269)\n- Fix(consul): Failed to exit normally: [#10342](https://github.com/apache/apisix/pull/10342)\n- Fix: The request header with `Content-Type: application/x-www-form-urlencoded;charset=utf-8` will cause vars condition `post_arg_xxx` matching to failed: [#10372](https://github.com/apache/apisix/pull/10372)\n- Fix: Make install failed on mac: [#10403](https://github.com/apache/apisix/pull/10403)\n- Fix(log-rotate): Log compression timeout caused data loss: [#8620](https://github.com/apache/apisix/pull/8620)\n- Fix(kafka-logger): Remove 0 from enum of required_acks: [#10469](https://github.com/apache/apisix/pull/10469)\n\n## 3.6.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: Remove gRPC support between APISIX and etcd and remove `etcd.use_grpc` configuration option: [#10015](https://github.com/apache/apisix/pull/10015)\n- :warning: Remove conf server. The data plane no longer supports direct communication with the control plane, and the configuration should be adjusted from `config_provider: control_plane` to `config_provider: etcd`: [#10012](https://github.com/apache/apisix/pull/10012)\n- :warning: Enforce strict schema validation on the properties of the core APISIX resources: [#10233](https://github.com/apache/apisix/pull/10233)\n\n### Core\n\n- :sunrise: Support configuring the buffer size of the access log: [#10225](https://github.com/apache/apisix/pull/10225)\n- :sunrise: Support the use of local DNS resolvers in service discovery by configuring `resolv_conf`: [#9770](https://github.com/apache/apisix/pull/9770)\n- :sunrise: Remove Rust dependency for installation: [#10121](https://github.com/apache/apisix/pull/10121)\n- :sunrise: Support Dubbo protocol in xRPC [#9660](https://github.com/apache/apisix/pull/9660)\n\n### Plugins\n\n- :sunrise: Support https in traffic-split plugin: [#9115](https://github.com/apache/apisix/pull/9115)\n- :sunrise: Support rewrite request body in external plugin:[#9990](https://github.com/apache/apisix/pull/9990)\n- :sunrise: Support set nginx variables in opentelemetry plugin: [#8871](https://github.com/apache/apisix/pull/8871)\n- :sunrise: Support unix sock host pattern in the chaitin-waf plugin: [#10161](https://github.com/apache/apisix/pull/10161)\n\n### Bugfixes\n\n- Fix GraphQL POST request route matching exception: [#10198](https://github.com/apache/apisix/pull/10198)\n- Fix error on array of multiline string in `apisix.yaml`: [#10193](https://github.com/apache/apisix/pull/10193)\n- Add error handlers for invalid `cache_zone` configuration in the `proxy-cache` plugin: [#10138](https://github.com/apache/apisix/pull/10138)\n\n## 3.5.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: remove snowflake algorithm in the request-id plugin: [#9715](https://github.com/apache/apisix/pull/9715)\n- :warning: No longer compatible with OpenResty 1.19, it needs to be upgraded to 1.21+: [#9913](https://github.com/apache/apisix/pull/9913)\n- :warning: Remove the configuration item `apisix.stream_proxy.only`, the L4/L7 proxy needs to be enabled through the configuration item `apisix.proxy_mode`: [#9607](https://github.com/apache/apisix/pull/9607)\n- :warning: The admin-api `/apisix/admin/plugins?all=true` marked as deprecated: [#9580](https://github.com/apache/apisix/pull/9580)\n- :warning: allowlist and denylist can't be enabled at the same time in ua-restriction plugin: [#9841](https://github.com/apache/apisix/pull/9841)\n\n### Core\n\n- :sunrise: Support host level dynamic setting of tls protocol version: [#9903](https://github.com/apache/apisix/pull/9903)\n- :sunrise: Support force delete resource: [#9810](https://github.com/apache/apisix/pull/9810)\n- :sunrise: Support pulling env vars from yaml keys: [#9855](https://github.com/apache/apisix/pull/9855)\n- :sunrise: Add schema validate API in admin-api: [#10065](https://github.com/apache/apisix/pull/10065)\n\n### Plugins\n\n- :sunrise: Add chaitin-waf plugin: [#9838](https://github.com/apache/apisix/pull/9838)\n- :sunrise: Support vars for file-logger plugin: [#9712](https://github.com/apache/apisix/pull/9712)\n- :sunrise: Support adding response headers for mock plugin: [#9720](https://github.com/apache/apisix/pull/9720)\n- :sunrise: Support regex_uri with unsafe_uri for proxy-rewrite plugin: [#9813](https://github.com/apache/apisix/pull/9813)\n- :sunrise: Support set client_email field for google-cloud-logging plugin: [#9813](https://github.com/apache/apisix/pull/9813)\n- :sunrise: Support sending headers upstream returned by OPA server for opa plugin: [#9710](https://github.com/apache/apisix/pull/9710)\n- :sunrise: Support configuring proxy server for openid-connect plugin: [#9948](https://github.com/apache/apisix/pull/9948)\n\n### Bugfixes\n\n- Fix(log-rotate): the max_kept configuration doesn't work when using custom name: [#9749](https://github.com/apache/apisix/pull/9749)\n- Fix(limit_conn): do not use the http variable in stream mode: [#9816](https://github.com/apache/apisix/pull/9816)\n- Fix(loki-logger): getting an error with log_labels: [#9850](https://github.com/apache/apisix/pull/9850)\n- Fix(limit-count): X-RateLimit-Reset shouldn't be set to 0 after request be rejected: [#9978](https://github.com/apache/apisix/pull/9978)\n- Fix(nacos): attempt to index upvalue 'applications' (a nil value): [#9960](https://github.com/apache/apisix/pull/9960)\n- Fix(etcd): can't sync etcd data if key has special character: [#9967](https://github.com/apache/apisix/pull/9967)\n- Fix(tencent-cloud-cls): dns parsing failure: [#9843](https://github.com/apache/apisix/pull/9843)\n- Fix(reload): worker not exited when executing quit or reload command [#9909](https://github.com/apache/apisix/pull/9909)\n- Fix(traffic-split): upstream_id validity verification [#10008](https://github.com/apache/apisix/pull/10008)\n\n## 3.4.0\n\n### Core\n\n- :sunrise: Support route-level MTLS [#9322](https://github.com/apache/apisix/pull/9322)\n- :sunrise: Support id schema for global_rules [#9517](https://github.com/apache/apisix/pull/9517)\n- :sunrise: Support use a single long http connection to watch all resources for etcd [#9456](https://github.com/apache/apisix/pull/9456)\n- :sunrise: Support max len 256 for ssl label [#9301](https://github.com/apache/apisix/pull/9301)\n\n### Plugins\n\n- :sunrise: Support multiple regex pattern matching for proxy_rewrite plugin [#9194](https://github.com/apache/apisix/pull/9194)\n- :sunrise: Add loki-logger plugin [#9399](https://github.com/apache/apisix/pull/9399)\n- :sunrise: Allow user configure DEFAULT_BUCKETS for prometheus plugin [#9673](https://github.com/apache/apisix/pull/9673)\n\n### Bugfixes\n\n- Fix(body-transformer): xml2lua: replace empty table with empty string [#9669](https://github.com/apache/apisix/pull/9669)\n- Fix: opentelemetry and grpc-transcode plugins cannot work together [#9606](https://github.com/apache/apisix/pull/9606)\n- Fix(skywalking-logger, error-log-logger): support $hostname in skywalking service_instance_name [#9401](https://github.com/apache/apisix/pull/9401)\n- Fix(admin): fix secrets do not support to update attributes by PATCH [#9510](https://github.com/apache/apisix/pull/9510)\n- Fix(http-logger): default request path should be '/' [#9472](https://github.com/apache/apisix/pull/9472)\n- Fix: syslog plugin doesn't work [#9425](https://github.com/apache/apisix/pull/9425)\n- Fix: wrong log format for splunk-hec-logging [#9478](https://github.com/apache/apisix/pull/9478)\n- Fix(etcd): reuse cli and enable keepalive [#9420](https://github.com/apache/apisix/pull/9420)\n- Fix: upstream key config add mqtt_client_id support [#9450](https://github.com/apache/apisix/pull/9450)\n- Fix: body-transformer plugin return raw body anytime [#9446](https://github.com/apache/apisix/pull/9446)\n- Fix(wolf-rbac): other plugin in consumer not effective when consumer used wolf-rbac plugin [#9298](https://github.com/apache/apisix/pull/9298)\n- Fix: always parse domain when host is domain name [#9332](https://github.com/apache/apisix/pull/9332)\n- Fix: response-rewrite plugin can't add only one character [#9372](https://github.com/apache/apisix/pull/9372)\n- Fix(consul): support to fetch only health endpoint [#9204](https://github.com/apache/apisix/pull/9204)\n\n## 3.3.0\n\n**The changes marked with :warning: are not backward compatible.**\n\n### Change\n\n- :warning: Change the default router from `radixtree_uri` to `radixtree_host_uri`: [#9047](https://github.com/apache/apisix/pull/9047)\n- :warning: CORS plugin will add `Vary: Origin` header when `allow_origin` is not `*`: [#9010](https://github.com/apache/apisix/pull/9010)\n\n### Core\n\n- :sunrise: Support store route's cert in secrets manager: [#9247](https://github.com/apache/apisix/pull/9247)\n- :sunrise: Support bypassing Admin API Auth by configuration: [#9147](https://github.com/apache/apisix/pull/9147)\n\n### Plugins\n\n- :sunrise: Support header injection for `fault-injection` plugin: [#9039](https://github.com/apache/apisix/pull/9039)\n- :sunrise: Support variable when rewrite header in `proxy-rewrite` plugin: [#9112](https://github.com/apache/apisix/pull/9112)\n- :sunrise: `limit-count` plugin supports `username` and `ssl` for redis policy: [#9185](https://github.com/apache/apisix/pull/9185)\n\n### Bugfixes\n\n- Fix etcd data sync exception: [#8493](https://github.com/apache/apisix/pull/8493)\n- Fix invalidate cache in `core.request.add_header` and fix some calls: [#8824](https://github.com/apache/apisix/pull/8824)\n- Fix the high CPU and memory usage cause by healthcheck impl: [#9015](https://github.com/apache/apisix/pull/9015)\n- Consider using `allow_origins_by_regex` only when it is not `nil`: [#9028](https://github.com/apache/apisix/pull/9028)\n- Check upstream reference in `traffic-split` plugin when delete upstream: [#9044](https://github.com/apache/apisix/pull/9044)\n- Fix failing to connect to etcd at startup: [#9077](https://github.com/apache/apisix/pull/9077)\n- Fix health checker leak for domain nodes: [#9090](https://github.com/apache/apisix/pull/9090)\n- Prevent non `127.0.0.0/24` to access admin api with empty admin_key: [#9146](https://github.com/apache/apisix/pull/9146)\n- Ensure `hold_body_chunk` should use separate buffer for each plugin in case of pollution: [#9266](https://github.com/apache/apisix/pull/9266)\n- Ensure `batch-requests` plugin read trailer headers if existed: [#9289](https://github.com/apache/apisix/pull/9289)\n- Ensure `proxy-rewrite` should set `ngx.var.uri`: [#9309](https://github.com/apache/apisix/pull/9309)\n\n## 3.2.1\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/3.2` branch.**\n\n[https://github.com/apache/apisix/blob/release/3.2/CHANGELOG.md#321](https://github.com/apache/apisix/blob/release/3.2/CHANGELOG.md#321)\n\n## 3.2.0\n\n### Change\n\n- Deprecated separate Vault configuration in jwt-auth. Users can use secret to achieve the same function: [#8660](https://github.com/apache/apisix/pull/8660)\n\n### Core\n\n- :sunrise: Support Vault token to configure secret through environment variables: [#8866](https://github.com/apache/apisix/pull/8866)\n- :sunrise: Supports service discovery on stream subsystem:\n     - [#8583](https://github.com/apache/apisix/pull/8583)\n     - [#8593](https://github.com/apache/apisix/pull/8593)\n     - [#8584](https://github.com/apache/apisix/pull/8584)\n     - [#8640](https://github.com/apache/apisix/pull/8640)\n     - [#8633](https://github.com/apache/apisix/pull/8633)\n     - [#8696](https://github.com/apache/apisix/pull/8696)\n     - [#8826](https://github.com/apache/apisix/pull/8826)\n\n### Plugins\n\n- :sunrise: Add RESTful to graphQL conversion plugin: [#8959](https://github.com/apache/apisix/pull/8959)\n- :sunrise: Supports setting the log format on each log plugin:\n     - [#8806](https://github.com/apache/apisix/pull/8806)\n     - [#8643](https://github.com/apache/apisix/pull/8643)\n- :sunrise: Add request body/response body conversion plugin: [#8766](https://github.com/apache/apisix/pull/8766)\n- :sunrise: Support sending error logs to Kafka: [#8693](https://github.com/apache/apisix/pull/8693)\n- :sunrise: limit-count plugin supports X-RateLimit-Reset: [#8578](https://github.com/apache/apisix/pull/8578)\n- :sunrise: limit-count plugin supports setting TLS to access Redis cluster: [#8558](https://github.com/apache/apisix/pull/8558)\n- :sunrise: consumer-restriction plugin supports permission control via consumer_group_id: [#8567](https://github.com/apache/apisix/pull/8567)\n\n### Bugfixes\n\n- Fix mTLS protection when the host and SNI mismatch: [#8967](https://github.com/apache/apisix/pull/8967)\n- The proxy-rewrite plugin should escape URI parameter parts if they do not come from user config: [#8888](https://github.com/apache/apisix/pull/8888)\n- Admin API PATCH operation should return 200 status code after success: [#8855](https://github.com/apache/apisix/pull/8855)\n- Under certain conditions, the reload after etcd synchronization failure does not take effect: [#8736](https://github.com/apache/apisix/pull/8736)\n- Fix the problem that the nodes found by the Consul service discovery are incomplete: [#8651](https://github.com/apache/apisix/pull/8651)\n- Fix grpc-transcode plugin's conversion of Map data: [#8731](https://github.com/apache/apisix/pull/8731)\n- External plugins should be able to set the content-type response header: [#8588](https://github.com/apache/apisix/pull/8588)\n- When hotloading plugins, redundant timers may be left behind if the request-id plugin initializes the snowflake generator incorrectly: [#8556](https://github.com/apache/apisix/pull/8556)\n- Close previous proto synchronizer for grpc-transcode when hotloading plugins: [#8557](https://github.com/apache/apisix/pull/8557)\n\n## 3.1.0\n\n### Core\n\n- :sunrise: Support for etcd configuration synchronization via gRPC:\n    - [#8485](https://github.com/apache/apisix/pull/8485)\n    - [#8450](https://github.com/apache/apisix/pull/8450)\n    - [#8411](https://github.com/apache/apisix/pull/8411)\n- :sunrise: Support for configuring encrypted fields in plugins:\n    - [#8487](https://github.com/apache/apisix/pull/8487)\n    - [#8403](https://github.com/apache/apisix/pull/8403)\n- :sunrise: Support for placing partial fields in Vault or environment variable using secret resources:\n    - [#8448](https://github.com/apache/apisix/pull/8448)\n    - [#8421](https://github.com/apache/apisix/pull/8421)\n    - [#8412](https://github.com/apache/apisix/pull/8412)\n    - [#8394](https://github.com/apache/apisix/pull/8394)\n    - [#8390](https://github.com/apache/apisix/pull/8390)\n- :sunrise: Allows upstream configuration in the stream subsystem as a domain name: [#8500](https://github.com/apache/apisix/pull/8500)\n- :sunrise: Support Consul service discovery: [#8380](https://github.com/apache/apisix/pull/8380)\n\n### Plugin\n\n- :sunrise: Optimize resource usage for prometheus collection: [#8434](https://github.com/apache/apisix/pull/8434)\n- :sunrise: Add inspect plugin for easy debugging: [#8400](https://github.com/apache/apisix/pull/8400)\n- :sunrise: jwt-auth plugin supports parameters to hide authentication token from upstream : [#8206](https://github.com/apache/apisix/pull/8206)\n- :sunrise: proxy-rewrite plugin supports adding new request headers without overwriting existing request headers with the same name: [#8336](https://github.com/apache/apisix/pull/8336)\n- :sunrise: grpc-transcode plugin supports setting the grpc-status-details-bin response header into the response body: [#7639](https://github.com/apache/apisix/pull/7639)\n- :sunrise: proxy-mirror plugin supports setting the prefix: [#8261](https://github.com/apache/apisix/pull/8261)\n\n### Bugfix\n\n- Fix the problem that the plug-in configured under service object cannot take effect in time under some circumstances: [#8482](https://github.com/apache/apisix/pull/8482)\n- Fix an occasional 502 problem when http and grpc share the same upstream connection due to connection pool reuse: [#8364](https://github.com/apache/apisix/pull/8364)\n- file-logger should avoid buffer-induced log truncation when writing logs: [#7884](https://github.com/apache/apisix/pull/7884)\n- max_kept parameter of log-rotate plugin should take effect on compressed files: [#8366](https://github.com/apache/apisix/pull/8366)\n- Fix userinfo not being set when use_jwks is true in the openid-connect plugin: [#8347](https://github.com/apache/apisix/pull/8347)\n- Fix an issue where x-forwarded-host cannot be changed in the proxy-rewrite plugin: [#8200](https://github.com/apache/apisix/pull/8200)\n- Fix a bug where disabling the v3 admin API resulted in missing response bodies under certain circumstances: [#8349](https://github.com/apache/apisix/pull/8349)\n- In zipkin plugin, pass trace ID even if there is a rejected sampling decision: [#8099](https://github.com/apache/apisix/pull/8099)\n- Fix `_meta.filter` in plugin configuration not working with variables assigned after upstream response and custom variables in APISIX.\n    - [#8162](https://github.com/apache/apisix/pull/8162)\n    - [#8256](https://github.com/apache/apisix/pull/8256)\n\n## 3.0.0\n\n### Change\n\n- `enable_cpu_affinity` is disabled by default to avoid this configuration affecting the behavior of APSISIX deployed in the container: [#8074](https://github.com/apache/apisix/pull/8074)\n\n### Core\n\n- :sunrise: Added Consumer Group entity to manage multiple consumers: [#7980](https://github.com/apache/apisix/pull/7980)\n- :sunrise: Supports configuring the order in which DNS resolves domain name types: [#7935](https://github.com/apache/apisix/pull/7935)\n- :sunrise: Support configuring multiple `key_encrypt_salt` for rotation: [#7925](https://github.com/apache/apisix/pull/7925)\n\n### Plugin\n\n- :sunrise: Added ai plugin to dynamically optimize the execution path of APISIX according to the scene:\n    - [#8102](https://github.com/apache/apisix/pull/8102)\n    - [#8113](https://github.com/apache/apisix/pull/8113)\n    - [#8120](https://github.com/apache/apisix/pull/8120)\n    - [#8128](https://github.com/apache/apisix/pull/8128)\n    - [#8130](https://github.com/apache/apisix/pull/8130)\n    - [#8149](https://github.com/apache/apisix/pull/8149)\n    - [#8157](https://github.com/apache/apisix/pull/8157)\n- :sunrise: Support `session_secret` in openid-connect plugin to resolve the inconsistency of `session_secret` among multiple workers: [#8068](https://github.com/apache/apisix/pull/8068)\n- :sunrise: Support sasl config in kafka-logger plugin: [#8050](https://github.com/apache/apisix/pull/8050)\n- :sunrise: Support set resolve domain in proxy-mirror plugin: [#7861](https://github.com/apache/apisix/pull/7861)\n- :sunrise: Support `brokers` property in kafka-logger plugin, which supports different broker to set the same host: [#7999](https://github.com/apache/apisix/pull/7999)\n- :sunrise: Support get response body in ext-plugin-post-resp: [#7947](https://github.com/apache/apisix/pull/7947)\n- :sunrise: Added cas-auth plugin to support CAS authentication: [#7932](https://github.com/apache/apisix/pull/7932)\n\n### Bugfix\n\n- Conditional expressions of workflow plugin should support operators: [#8121](https://github.com/apache/apisix/pull/8121)\n- Fix loading problem of batch processor plugin when prometheus plugin is disabled: [#8079](https://github.com/apache/apisix/pull/8079)\n- When APISIX starts, delete the old conf server sock file if it exists: [#8022](https://github.com/apache/apisix/pull/8022)\n- Disable core.grpc when gRPC-client-nginx-module module is not compiled: [#8007](https://github.com/apache/apisix/pull/8007)\n\n## 3.0.0-beta\n\nHere we use 2.99.0 as the version number in the source code instead of the code name\n`3.0.0-beta` for two reasons:\n\n1. avoid unexpected errors when some programs try to compare the\nversion, as `3.0.0-beta` contains `3.0.0` and is longer than it.\n2. some package system might not allow package which has a suffix\nafter the version number.\n\n### Change\n\n#### Moves the config_center, etcd and Admin API configuration to the deployment\n\nWe've adjusted the configuration in the static configuration file, so you need to update the configuration in `config.yaml` as well:\n\n- The `config_center` function is now implemented by `config_provider` under `deployment`: [#7901](https://github.com/apache/apisix/pull/7901)\n- The `etcd` field is moved to `deployment`: [#7860](https://github.com/apache/apisix/pull/7860)\n- The following Admin API configuration is moved to the `admin` field under `deployment`: [#7823](https://github.com/apache/apisix/pull/7823)\n    - admin_key\n    - enable_admin_cors\n    - allow_admin\n    - admin_listen\n    - https_admin\n    - admin_api_mtls\n    - admin_api_version\n\nYou can refer to the latest `config-default.yaml` for details.\n\n#### Removing multiple deprecated configurations\n\nWith the new 3.0 release, we took the opportunity to clean out many configurations that were previously marked as deprecated.\n\nIn the static configuration, we removed several fields as follows:\n\n- Removed `enable_http2` and `listen_port` from `apisix.ssl`: [#7717](https://github.com/apache/apisix/pull/7717)\n- Removed `apisix.port_admin`: [#7716](https://github.com/apache/apisix/pull/7716)\n- Removed `etcd.health_check_retry`: [#7676](https://github.com/apache/apisix/pull/7676)\n- Removed `nginx_config.http.lua_shared_dicts`: [#7677](https://github.com/apache/apisix/pull/7677)\n- Removed `apisix.real_ip_header`: [#7696](https://github.com/apache/apisix/pull/7696)\n\nIn the dynamic configuration, we made the following adjustments:\n\n- Moved `disable` of the plugin configuration under `_meta`: [#7707](https://github.com/apache/apisix/pull/7707)\n- Removed `service_protocol` from the Route: [#7701](https://github.com/apache/apisix/pull/7701)\n\nThere are also specific plugin level changes:\n\n- Removed `audience` field from authz-keycloak: [#7683](https://github.com/apache/apisix/pull/7683)\n- Removed `upstream` field from mqtt-proxy: [#7694](https://github.com/apache/apisix/pull/7694)\n- tcp-related configuration placed under the `tcp` field in error-log-logger: [#7700](https://github.com/apache/apisix/pull/7700)\n- Removed `max_retry_times` and `retry_interval` fields from syslog: [#7699](https://github.com/apache/apisix/pull/7699)\n- The `scheme` field has been removed from proxy-rewrite: [#7695](https://github.com/apache/apisix/pull/7695)\n\n#### New Admin API response format\n\nWe have adjusted the response format of the Admin API in several PRs as follows:\n\n- [#7630](https://github.com/apache/apisix/pull/7630)\n- [#7622](https://github.com/apache/apisix/pull/7622)\n\nThe new response format is shown below:\n\nReturns a single configuration:\n\n```json\n{\n  \"modifiedIndex\": 2685183,\n  \"value\": {\n    \"id\": \"1\",\n    ...\n  },\n  \"key\": \"/apisix/routes/1\",\n  \"createdIndex\": 2684956\n}\n```\n\nReturns multiple configurations:\n\n```json\n{\n  \"list\": [\n    {\n      \"modifiedIndex\": 2685183,\n      \"value\": {\n        \"id\": \"1\",\n        ...\n      },\n      \"key\": \"/apisix/routes/1\",\n      \"createdIndex\": 2684956\n    },\n    {\n      \"modifiedIndex\": 2685163,\n      \"value\": {\n        \"id\": \"2\",\n        ...\n      },\n      \"key\": \"/apisix/routes/2\",\n      \"createdIndex\": 2685163\n    }\n  ],\n  \"total\": 2\n}\n```\n\n#### Other\n\n- Port of Admin API changed to 9180: [#7806](https://github.com/apache/apisix/pull/7806)\n- We only support OpenResty 1.19.3.2 and above: [#7625](https://github.com/apache/apisix/pull/7625)\n- Adjusted the priority of the Plugin Config object so that the priority of a plugin configuration with the same name changes from Consumer > Plugin Config > Route > Service to Consumer > Route > Plugin Config > Service: [#7614](https://github.com/apache/apisix/pull/7614)\n\n### Core\n\n- Integrating grpc-client-nginx-module to APISIX: [#7917](https://github.com/apache/apisix/pull/7917)\n- k8s service discovery support for configuring multiple clusters: [#7895](https://github.com/apache/apisix/pull/7895)\n\n### Plugin\n\n- Support for injecting header with specified prefix in opentelemetry plugin: [#7822](https://github.com/apache/apisix/pull/7822)\n- Added openfunction plugin: [#7634](https://github.com/apache/apisix/pull/7634)\n- Added elasticsearch-logger plugin: [#7643](https://github.com/apache/apisix/pull/7643)\n- response-rewrite plugin supports adding response bodies: [#7794](https://github.com/apache/apisix/pull/7794)\n- log-rorate supports specifying the maximum size to cut logs: [#7749](https://github.com/apache/apisix/pull/7749)\n- Added workflow plug-in.\n    - [#7760](https://github.com/apache/apisix/pull/7760)\n    - [#7771](https://github.com/apache/apisix/pull/7771)\n- Added Tencent Cloud Log Service plugin: [#7593](https://github.com/apache/apisix/pull/7593)\n- jwt-auth supports ES256 algorithm: [#7627](https://github.com/apache/apisix/pull/7627)\n- ldap-auth internal implementation, switching from lualdap to lua-resty-ldap: [#7590](https://github.com/apache/apisix/pull/7590)\n- http request metrics within the prometheus plugin supports setting additional labels via variables: [#7549](https://github.com/apache/apisix/pull/7549)\n- The clickhouse-logger plugin supports specifying multiple clickhouse endpoints: [#7517](https://github.com/apache/apisix/pull/7517)\n\n### Bugfix\n\n- gRPC proxy sets :authority request header to configured upstream Host: [#7939](https://github.com/apache/apisix/pull/7939)\n- response-rewrite writing to an empty body may cause AIPSIX to fail to respond to the request: [#7836](https://github.com/apache/apisix/pull/7836)\n- Fix the problem that when using Plugin Config and Consumer at the same time, there is a certain probability that the plugin configuration is not updated: [#7965](https://github.com/apache/apisix/pull/7965)\n- Only reopen log files once when log cutting: [#7869](https://github.com/apache/apisix/pull/7869)\n- Passive health checks should not be enabled by default: [#7850](https://github.com/apache/apisix/pull/7850)\n- The zipkin plugin should pass trace IDs upstream even if it does not sample: [#7833](https://github.com/apache/apisix/pull/7833)\n- Correction of opentelemetry span kind to server: [#7830](https://github.com/apache/apisix/pull/7830)\n- in limit-count plugin, different routes with the same configuration should not share the same counter: [#7750](https://github.com/apache/apisix/pull/7750)\n- Fix occasional exceptions thrown when removing clean_handler: [#7648](https://github.com/apache/apisix/pull/7648)\n- Allow direct use of IPv6 literals when configuring upstream nodes: [#7594](https://github.com/apache/apisix/pull/7594)\n- The wolf-rbac plugin adjusts the way it responds to errors:\n    - [#7561](https://github.com/apache/apisix/pull/7561)\n    - [#7497](https://github.com/apache/apisix/pull/7497)\n- the phases after proxy didn't run when 500 error happens before proxy: [#7703](https://github.com/apache/apisix/pull/7703)\n- avoid error when multiple plugins associated with consumer and have rewrite phase: [#7531](https://github.com/apache/apisix/pull/7531)\n- upgrade lua-resty-etcd to 1.8.3 which fixes various issues: [#7565](https://github.com/apache/apisix/pull/7565)\n\n## 2.15.3\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.15` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.15/CHANGELOG.md#2153](https://github.com/apache/apisix/blob/release/2.15/CHANGELOG.md#2153)\n\n## 2.15.2\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.15` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.15/CHANGELOG.md#2152](https://github.com/apache/apisix/blob/release/2.15/CHANGELOG.md#2152)\n\n## 2.15.1\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.15` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.15/CHANGELOG.md#2151](https://github.com/apache/apisix/blob/release/2.15/CHANGELOG.md#2151)\n\n## 2.15.0\n\n### Change\n\n- We now map the grpc error code OUT_OF_RANGE to http code 400 in grpc-transcode plugin: [#7419](https://github.com/apache/apisix/pull/7419)\n- Rename health_check_retry configuration in etcd section of `config-default.yaml` to startup_retry: [#7304](https://github.com/apache/apisix/pull/7304)\n- Remove `upstream.enable_websocket` which is deprecated since 2020: [#7222](https://github.com/apache/apisix/pull/7222)\n\n### Core\n\n- Support running plugins conditionally: [#7453](https://github.com/apache/apisix/pull/7453)\n- Allow users to specify plugin execution priority: [#7273](https://github.com/apache/apisix/pull/7273)\n- Support getting upstream certificate from ssl object: [#7221](https://github.com/apache/apisix/pull/7221)\n- Allow customizing error response in the plugin: [#7128](https://github.com/apache/apisix/pull/7128)\n- Add metrics to xRPC Redis proxy: [#7183](https://github.com/apache/apisix/pull/7183)\n- Introduce deployment role to simplify the deployment of APISIX:\n    - [#7405](https://github.com/apache/apisix/pull/7405)\n    - [#7417](https://github.com/apache/apisix/pull/7417)\n    - [#7392](https://github.com/apache/apisix/pull/7392)\n    - [#7365](https://github.com/apache/apisix/pull/7365)\n    - [#7249](https://github.com/apache/apisix/pull/7249)\n\n### Plugin\n\n- Add ngx.shared.dict statistic in promethues plugin: [#7412](https://github.com/apache/apisix/pull/7412)\n- Allow using unescaped raw URL in proxy-rewrite plugin: [#7401](https://github.com/apache/apisix/pull/7401)\n- Add PKCE support to the openid-connect plugin: [#7370](https://github.com/apache/apisix/pull/7370)\n- Support custom log format in sls-logger plugin: [#7328](https://github.com/apache/apisix/pull/7328)\n- Export some params for kafka-client in kafka-logger plugin: [#7266](https://github.com/apache/apisix/pull/7266)\n- Add support for capturing OIDC refresh tokens in openid-connect plugin: [#7220](https://github.com/apache/apisix/pull/7220)\n- Add prometheus plugin in stream subsystem: [#7174](https://github.com/apache/apisix/pull/7174)\n\n### Bugfix\n\n- clear remain state from the latest try before retrying in Kubernetes discovery: [#7506](https://github.com/apache/apisix/pull/7506)\n- the query string was repeated twice when enabling both http_to_https and append_query_string in the redirect plugin: [#7433](https://github.com/apache/apisix/pull/7433)\n- don't send empty Authorization header by default in http-logger: [#7444](https://github.com/apache/apisix/pull/7444)\n- ensure both `group` and `disable` configurations can be used in limit-count: [#7384](https://github.com/apache/apisix/pull/7384)\n- adjust the execution priority of request-id so the tracing plugins can use the request id: [#7281](https://github.com/apache/apisix/pull/7281)\n- correct the transcode of repeated Message in grpc-transcode: [#7231](https://github.com/apache/apisix/pull/7231)\n- var missing in proxy-cache cache key should be ignored: [#7168](https://github.com/apache/apisix/pull/7168)\n- reduce memory usage when abnormal weights are given in chash: [#7103](https://github.com/apache/apisix/pull/7103)\n- cache should be bypassed when the method mismatch in proxy-cache: [#7111](https://github.com/apache/apisix/pull/7111)\n- Upstream keepalive should consider TLS param:\n    - [#7054](https://github.com/apache/apisix/pull/7054)\n    - [#7466](https://github.com/apache/apisix/pull/7466)\n- The redirect plugin sets a correct port during redirecting HTTP to HTTPS:\n    - [#7065](https://github.com/apache/apisix/pull/7065)\n\n## 2.14.1\n\n### Bugfix\n\n- The \"unix:\" in the `real_ip_from` configuration should not break the batch-requests plugin: [#7106](https://github.com/apache/apisix/pull/7106)\n\n## 2.14.0\n\n### Change\n\n- To adapt the change of OpenTelemetry spec, the default port of OTLP/HTTP is changed to 4318: [#7007](https://github.com/apache/apisix/pull/7007)\n\n### Core\n\n- Introduce an experimental feature to allow subscribing Kafka message via APISIX. This feature is based on the pubsub framework running above websocket:\n    - [#7028](https://github.com/apache/apisix/pull/7028)\n    - [#7032](https://github.com/apache/apisix/pull/7032)\n- Introduce an experimental framework called xRPC to manage non-HTTP L7 traffic:\n    - [#6885](https://github.com/apache/apisix/pull/6885)\n    - [#6901](https://github.com/apache/apisix/pull/6901)\n    - [#6919](https://github.com/apache/apisix/pull/6919)\n    - [#6960](https://github.com/apache/apisix/pull/6960)\n    - [#6965](https://github.com/apache/apisix/pull/6965)\n    - [#7040](https://github.com/apache/apisix/pull/7040)\n- Now we support adding delay according to the command & key during proxying Redis traffic, which is built above xRPC:\n    - [#6999](https://github.com/apache/apisix/pull/6999)\n- Introduce an experimental support to configure APISIX via xDS:\n    - [#6614](https://github.com/apache/apisix/pull/6614)\n    - [#6759](https://github.com/apache/apisix/pull/6759)\n- Add `normalize_uri_like_servlet` option to normalize uri like servlet: [#6984](https://github.com/apache/apisix/pull/6984)\n- Zookeeper service discovery via apisix-seed: [#6751](https://github.com/apache/apisix/pull/6751)\n\n### Plugin\n\n- The real-ip plugin supports recursive IP search like `real_ip_recursive`: [#6988](https://github.com/apache/apisix/pull/6988)\n- The api-breaker plugin allows configuring response: [#6949](https://github.com/apache/apisix/pull/6949)\n- The response-rewrite plugin supports body filters: [#6750](https://github.com/apache/apisix/pull/6750)\n- The request-id plugin adds nanoid algorithm to generate ID: [#6779](https://github.com/apache/apisix/pull/6779)\n- The file-logger plugin can cache & reopen file handler: [#6721](https://github.com/apache/apisix/pull/6721)\n- Add casdoor plugin: [#6382](https://github.com/apache/apisix/pull/6382)\n- The authz-keycloak plugin supports password grant: [#6586](https://github.com/apache/apisix/pull/6586)\n\n### Bugfix\n\n- Upstream keepalive should consider TLS param: [#7054](https://github.com/apache/apisix/pull/7054)\n- Do not expose internal error message to the client:\n    - [#6982](https://github.com/apache/apisix/pull/6982)\n    - [#6859](https://github.com/apache/apisix/pull/6859)\n    - [#6854](https://github.com/apache/apisix/pull/6854)\n    - [#6853](https://github.com/apache/apisix/pull/6853)\n    - [#6846](https://github.com/apache/apisix/pull/6846)\n- DNS supports SRV record with port 0: [#6739](https://github.com/apache/apisix/pull/6739)\n- client mTLS was ignored sometimes in TLS session reuse: [#6906](https://github.com/apache/apisix/pull/6906)\n- The grpc-web plugin doesn't override Access-Control-Allow-Origin header in response: [#6842](https://github.com/apache/apisix/pull/6842)\n- The syslog plugin's default timeout is corrected: [#6807](https://github.com/apache/apisix/pull/6807)\n- The authz-keycloak plugin's `access_denied_redirect_uri` was bypassed sometimes: [#6794](https://github.com/apache/apisix/pull/6794)\n- Handle `USR2` signal properly: [#6758](https://github.com/apache/apisix/pull/6758)\n- The redirect plugin set a correct port during redirecting HTTP to HTTPS:\n    - [#7065](https://github.com/apache/apisix/pull/7065)\n    - [#6686](https://github.com/apache/apisix/pull/6686)\n- Admin API rejects unknown stream plugin: [#6813](https://github.com/apache/apisix/pull/6813)\n\n## 2.13.3\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.13` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2133](https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2133)\n\n## 2.13.2\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.13` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2132](https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2132)\n\n## 2.13.1\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.13` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2131](https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2131)\n\n## 2.13.0\n\n### Change\n\n- change(syslog): correct the configuration [#6551](https://github.com/apache/apisix/pull/6551)\n- change(server-info): use a new approach(keepalive) to report DP info [#6202](https://github.com/apache/apisix/pull/6202)\n- change(admin): empty nodes should be encoded as array [#6384](https://github.com/apache/apisix/pull/6384)\n- change(prometheus): replace wrong apisix_nginx_http_current_connections{state=\"total\"} label [#6327](https://github.com/apache/apisix/pull/6327)\n- change: don't expose public API by default & remove plugin interceptor [#6196](https://github.com/apache/apisix/pull/6196)\n\n### Core\n\n- :sunrise: feat: add delayed_body_filter phase [#6605](https://github.com/apache/apisix/pull/6605)\n- :sunrise: feat: support for reading environment variables from yaml configuration files [#6505](https://github.com/apache/apisix/pull/6505)\n- :sunrise: feat: rerun rewrite phase for newly added plugins in consumer [#6502](https://github.com/apache/apisix/pull/6502)\n- :sunrise: feat: add config to control write all status to x-upsream-apisix-status [#6392](https://github.com/apache/apisix/pull/6392)\n- :sunrise: feat: add kubernetes discovery module [#4880](https://github.com/apache/apisix/pull/4880)\n- :sunrise: feat(graphql): support http get and post json request [#6343](https://github.com/apache/apisix/pull/6343)\n\n### Plugin\n\n- :sunrise: feat: jwt-auth support custom parameters [#6561](https://github.com/apache/apisix/pull/6561)\n- :sunrise: feat: set cors allow origins by plugin metadata [#6546](https://github.com/apache/apisix/pull/6546)\n- :sunrise: feat: support post_logout_redirect_uri config in openid-connect plugin [#6455](https://github.com/apache/apisix/pull/6455)\n- :sunrise: feat: mocking plugin [#5940](https://github.com/apache/apisix/pull/5940)\n- :sunrise: feat(error-log-logger): add clickhouse for error-log-logger [#6256](https://github.com/apache/apisix/pull/6256)\n- :sunrise: feat: clickhouse logger [#6215](https://github.com/apache/apisix/pull/6215)\n- :sunrise: feat(grpc-transcode): support .pb file [#6264](https://github.com/apache/apisix/pull/6264)\n- :sunrise: feat: development of Loggly logging plugin [#6113](https://github.com/apache/apisix/pull/6113)\n- :sunrise: feat: add opentelemetry plugin [#6119](https://github.com/apache/apisix/pull/6119)\n- :sunrise: feat: add public api plugin [#6145](https://github.com/apache/apisix/pull/6145)\n- :sunrise: feat: add CSRF plugin [#5727](https://github.com/apache/apisix/pull/5727)\n\n### Bugfix\n\n- fix(skywalking,opentelemetry): trace request rejected by auth [#6617](https://github.com/apache/apisix/pull/6617)\n- fix(log-rotate): should rotate logs strictly hourly(or minutely) [#6521](https://github.com/apache/apisix/pull/6521)\n- fix: deepcopy doesn't copy the metatable [#6623](https://github.com/apache/apisix/pull/6623)\n- fix(request-validate): handle duplicate key in JSON [#6625](https://github.com/apache/apisix/pull/6625)\n- fix(prometheus): conflict between global rule and route configure [#6579](https://github.com/apache/apisix/pull/6579)\n- fix(proxy-rewrite): when conf.headers are missing,conf.method can make effect [#6300](https://github.com/apache/apisix/pull/6300)\n- fix(traffic-split): failed to match rule when the first rule failed [#6292](https://github.com/apache/apisix/pull/6292)\n- fix(config_etcd): skip resync_delay while etcd watch timeout [#6259](https://github.com/apache/apisix/pull/6259)\n- fix(proto): avoid sharing state [#6199](https://github.com/apache/apisix/pull/6199)\n- fix(limit-count): keep the counter if the plugin conf is the same [#6151](https://github.com/apache/apisix/pull/6151)\n- fix(admin): correct the count field of plugin-metadata/global-rule [#6155](https://github.com/apache/apisix/pull/6155)\n- fix: add missing labels after merging route and service [#6177](https://github.com/apache/apisix/pull/6177)\n\n## 2.12.1\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.12` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.12/CHANGELOG.md#2121](https://github.com/apache/apisix/blob/release/2.12/CHANGELOG.md#2121)\n\n## 2.12.0\n\n### Change\n\n- change(serverless): rename \"balancer\" phase to \"before_proxy\" [#5992](https://github.com/apache/apisix/pull/5992)\n- change: don't promise to support Tengine [#5961](https://github.com/apache/apisix/pull/5961)\n- change: enable HTTP when stream proxy is set and enable_admin is true [#5867](https://github.com/apache/apisix/pull/5867)\n\n### Core\n\n- :sunrise: feat(L4): support TLS over TCP upstream [#6030](https://github.com/apache/apisix/pull/6030)\n- :sunrise: feat: support registering custom variable [#5941](https://github.com/apache/apisix/pull/5941)\n- :sunrise: feat(vault): vault lua module, integration with jwt-auth authentication plugin [#5745](https://github.com/apache/apisix/pull/5745)\n- :sunrise: feat: enable L4 stream logging [#5768](https://github.com/apache/apisix/pull/5768)\n- :sunrise: feat: add http_server_location_configuration_snippet configuration [#5740](https://github.com/apache/apisix/pull/5740)\n- :sunrise: feat: support resolve default value when environment not set [#5675](https://github.com/apache/apisix/pull/5675)\n- :sunrise: feat(wasm): run in http header_filter [#5544](https://github.com/apache/apisix/pull/5544)\n\n### Plugin\n\n- :sunrise: feat: support hide the authentication header in basic-auth with  a config [#6039](https://github.com/apache/apisix/pull/6039)\n- :sunrise: feat: set proxy_request_buffering dynamically [#6075](https://github.com/apache/apisix/pull/6075)\n- :sunrise: feat(mqtt): balance by client id [#6079](https://github.com/apache/apisix/pull/6079)\n- :sunrise: feat: add forward-auth plugin [#6037](https://github.com/apache/apisix/pull/6037)\n- :sunrise: feat(grpc-web): support gRPC-Web Proxy [#5964](https://github.com/apache/apisix/pull/5964)\n- :sunrise: feat(limit-count): add constant key type [#5984](https://github.com/apache/apisix/pull/5984)\n- :sunrise: feat(limit-count): allow sharing counter [#5881](https://github.com/apache/apisix/pull/5881)\n- :sunrise: feat(splunk): support splunk hec logging plugin [#5819](https://github.com/apache/apisix/pull/5819)\n- :sunrise: feat: basic support OPA plugin [#5734](https://github.com/apache/apisix/pull/5734)\n- :sunrise: feat: rocketmq logger [#5653](https://github.com/apache/apisix/pull/5653)\n- :sunrise: feat(mqtt-proxy): support using route's upstream [#5666](https://github.com/apache/apisix/pull/5666)\n- :sunrise: feat(ext-plugin): support to get request body [#5600](https://github.com/apache/apisix/pull/5600)\n- :sunrise: feat(plugins): aws lambda serverless [#5594](https://github.com/apache/apisix/pull/5594)\n- :sunrise: feat(http/kafka-logger): support to log response body [#5550](https://github.com/apache/apisix/pull/5550)\n- :sunrise: feat: Apache OpenWhisk plugin [#5518](https://github.com/apache/apisix/pull/5518)\n- :sunrise: feat(plugin): support google cloud logging service [#5538](https://github.com/apache/apisix/pull/5538)\n\n### Bugfix\n\n- fix: the prometheus labels are inconsistent when error-log-logger is enabled [#6055](https://github.com/apache/apisix/pull/6055)\n- fix(ipv6): allow disabling IPv6 resolve [#6023](https://github.com/apache/apisix/pull/6023)\n- fix(mqtt): handle properties for MQTT 5 [#5916](https://github.com/apache/apisix/pull/5916)\n- fix(sls-logger): unable to get millisecond part of the timestamp [#5820](https://github.com/apache/apisix/pull/5820)\n- fix(mqtt-proxy): client id can be empty [#5816](https://github.com/apache/apisix/pull/5816)\n- fix(ext-plugin): don't use stale key [#5782](https://github.com/apache/apisix/pull/5782)\n- fix(log-rotate): race between reopen log & compression [#5715](https://github.com/apache/apisix/pull/5715)\n- fix(batch-processor): we didn't free stale object actually [#5700](https://github.com/apache/apisix/pull/5700)\n- fix: data pollution after passive health check is changed [#5589](https://github.com/apache/apisix/pull/5589)\n\n## 2.11.0\n\n### Change\n\n- change(wolf-rbac): change default port number and add `authType` parameter to documentation [#5477](https://github.com/apache/apisix/pull/5477)\n\n### Core\n\n- :sunrise: feat: support advanced matching based on post form [#5409](https://github.com/apache/apisix/pull/5409)\n- :sunrise: feat: initial wasm support [#5288](https://github.com/apache/apisix/pull/5288)\n- :sunrise: feat(control): expose services[#5271](https://github.com/apache/apisix/pull/5271)\n- :sunrise: feat(control): add dump upstream api [#5259](https://github.com/apache/apisix/pull/5259)\n- :sunrise: feat: etcd cluster single node failure APISIX startup failure [#5158](https://github.com/apache/apisix/pull/5158)\n- :sunrise: feat: support specify custom sni in etcd conf [#5206](https://github.com/apache/apisix/pull/5206)\n\n### Plugin\n\n- :sunrise: feat(plugin): azure serverless functions [#5479](https://github.com/apache/apisix/pull/5479)\n- :sunrise: feat(kafka-logger): supports logging request body [#5501](https://github.com/apache/apisix/pull/5501)\n- :sunrise: feat: provide skywalking logger plugin [#5478](https://github.com/apache/apisix/pull/5478)\n- :sunrise: feat(plugins): Datadog for metrics collection [#5372](https://github.com/apache/apisix/pull/5372)\n- :sunrise: feat(limit-* plugin):  fallback to remote_addr when key is missing [#5422](https://github.com/apache/apisix/pull/5422)\n- :sunrise: feat(limit-count): support multiple variables as key [#5378](https://github.com/apache/apisix/pull/5378)\n- :sunrise: feat(limit-conn): support multiple variables as key [#5354](https://github.com/apache/apisix/pull/5354)\n- :sunrise: feat(proxy-rewrite): rewrite method [#5292](https://github.com/apache/apisix/pull/5292)\n- :sunrise: feat(limit-req): support multiple variables as key [#5302](https://github.com/apache/apisix/pull/5302)\n- :sunrise: feat(proxy-cache): support memory-based strategy [#5028](https://github.com/apache/apisix/pull/5028)\n- :sunrise: feat(ext-plugin): avoid sending conf request more times [#5183](https://github.com/apache/apisix/pull/5183)\n- :sunrise: feat: Add ldap-auth plugin [#3894](https://github.com/apache/apisix/pull/3894)\n\n## 2.10.5\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2105](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2105)\n\n## 2.10.4\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2104](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2104)\n\n## 2.10.3\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103)\n\n## 2.10.2\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102)\n\n## 2.10.1\n\n**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101)\n\n## 2.10.0\n\n### Change\n\n- change(debug): move 'enable_debug' form config.yaml to debug.yaml [#5046](https://github.com/apache/apisix/pull/5046)\n- change: use a new name to customize lua_shared_dict in nginx.conf [#5030](https://github.com/apache/apisix/pull/5030)\n- change: drop the support of shell script installation [#4985](https://github.com/apache/apisix/pull/4985)\n\n### Core\n\n- :sunrise: feat(debug-mode): add dynamic debug mode [#5012](https://github.com/apache/apisix/pull/5012)\n- :sunrise: feat: allow injecting logic to APISIX's method [#5068](https://github.com/apache/apisix/pull/5068)\n- :sunrise: feat: allow configuring fallback SNI [#5000](https://github.com/apache/apisix/pull/5000)\n- :sunrise: feat(stream_route): support CIDR in ip match [#4980](https://github.com/apache/apisix/pull/4980)\n- :sunrise: feat: allow route to inherit hosts from service [#4977](https://github.com/apache/apisix/pull/4977)\n- :sunrise: feat: support configurating the node listening address[#4856](https://github.com/apache/apisix/pull/4856)\n\n### Plugin\n\n- :sunrise: feat(hmac-auth): Add validate request body for hmac auth plugin [#5038](https://github.com/apache/apisix/pull/5038)\n- :sunrise: feat(proxy-mirror): support mirror requests sample_ratio [#4965](https://github.com/apache/apisix/pull/4965)\n- :sunrise: feat(referer-restriction): add blacklist and message [#4916](https://github.com/apache/apisix/pull/4916)\n- :sunrise: feat(kafka-logger): add cluster name support [#4876](https://github.com/apache/apisix/pull/4876)\n- :sunrise: feat(kafka-logger): add required_acks option [#4878](https://github.com/apache/apisix/pull/4878)\n- :sunrise: feat(uri-blocker): add case insensitive switch [#4868](https://github.com/apache/apisix/pull/4868)\n\n### Bugfix\n\n- fix(radixtree_host_uri): correct matched host [#5124](https://github.com/apache/apisix/pull/5124)\n- fix(radixtree_host_uri): correct matched path [#5104](https://github.com/apache/apisix/pull/5104)\n- fix(nacos): distinguish services that has same name but in different groups or namespaces [#5083](https://github.com/apache/apisix/pull/5083)\n- fix(nacos): continue to process other services when request failed [#5112](https://github.com/apache/apisix/pull/5112)\n- fix(ssl): match sni in case-insensitive way [#5074](https://github.com/apache/apisix/pull/5074)\n- fix(upstream): should not override default keepalive value [#5054](https://github.com/apache/apisix/pull/5054)\n- fix(DNS): prefer SRV in service discovery [#4992](https://github.com/apache/apisix/pull/4992)\n- fix(consul): retry connecting after a delay [#4979](https://github.com/apache/apisix/pull/4979)\n- fix: avoid copying unwanted data when the domain's IP changed [#4952](https://github.com/apache/apisix/pull/4952)\n- fix(plugin_config): recover plugin when plugin_config changed [#4888](https://github.com/apache/apisix/pull/4888)\n\n## 2.9.0\n\n### Change\n\n- change: rename plugin's balancer method to before_proxy [#4697](https://github.com/apache/apisix/pull/4697)\n\n### Core\n\n- :sunrise: feat: increase timers limitation [#4843](https://github.com/apache/apisix/pull/4843)\n- :sunrise: feat: make A/B test APISIX easier by removing \"additionalProperties = false\" [#4797](https://github.com/apache/apisix/pull/4797)\n- :sunrise: feat: support dash in args (#4519) [#4676](https://github.com/apache/apisix/pull/4676)\n- :sunrise: feat(admin): reject invalid proto [#4750](https://github.com/apache/apisix/pull/4750)\n\n### Plugin\n\n- :sunrise: feat(ext-plugin): support ExtraInfo [#4835](https://github.com/apache/apisix/pull/4835)\n- :sunrise: feat(gzip): support special * to match any type [#4817](https://github.com/apache/apisix/pull/4817)\n- :sunrise: feat(real-ip): implement the first version [#4813](https://github.com/apache/apisix/pull/4813)\n- :sunrise: feat(limit-*): add custom reject-message for traffic control [#4808](https://github.com/apache/apisix/pull/4808)\n- :sunrise: feat: Request-ID plugin add snowflake algorithm [#4559](https://github.com/apache/apisix/pull/4559)\n- :sunrise: feat: Added authz-casbin plugin and doc and tests for it [#4710](https://github.com/apache/apisix/pull/4710)\n- :sunrise: feat: add error log skywalking reporter [#4633](https://github.com/apache/apisix/pull/4633)\n- :sunrise: feat(ext-plugin): send the idempotent key when preparing conf [#4736](https://github.com/apache/apisix/pull/4736)\n\n### Bugfix\n\n- fix: the issue that plugins in global rule may be cached to route [#4867](https://github.com/apache/apisix/pull/4867)\n- fix(grpc-transcode): support converting nested message [#4859](https://github.com/apache/apisix/pull/4859)\n- fix(authz-keycloak): set permissions as empty table when lazy_load_path is false [#4845](https://github.com/apache/apisix/pull/4845)\n- fix(proxy-cache): keep cache_method same with nginx's proxy_cache_methods [#4814](https://github.com/apache/apisix/pull/4814)\n- fix(admin): inject updatetime when the request is PATCH with sub path [#4765](https://github.com/apache/apisix/pull/4765)\n- fix(admin): check username for updating consumer [#4756](https://github.com/apache/apisix/pull/4756)\n- fix(error-log-logger): avoid sending stale error log [#4690](https://github.com/apache/apisix/pull/4690)\n- fix(grpc-transcode): handle enum type [#4706](https://github.com/apache/apisix/pull/4706)\n- fix: when a request caused a 500 error, the status was converted to 405 [#4696](https://github.com/apache/apisix/pull/4696)\n\n## 2.8.0\n\n### Change\n\n- change: enable stream proxy only by default [#4580](https://github.com/apache/apisix/pull/4580)\n\n### Core\n\n- :sunrise: feat: allow user-defined balancer with metadata in node [#4605](https://github.com/apache/apisix/pull/4605)\n- :sunrise: feat: Add option retry_timeout that like nginx's proxy_next_upstream_timeout [#4574](https://github.com/apache/apisix/pull/4574)\n- :sunrise: feat: enable balancer phase for plugins [#4549](https://github.com/apache/apisix/pull/4549)\n- :sunrise: feat: allow setting separate keepalive pool [#4506](https://github.com/apache/apisix/pull/4506)\n- :sunrise: feat: enable etcd health-check [#4191](https://github.com/apache/apisix/pull/4191)\n\n### Plugin\n\n- :sunrise: feat: add gzip plugin [#4640](https://github.com/apache/apisix/pull/4640)\n- :sunrise: feat(plugin): Add new plugin ua-restriction for bot spider restriction [#4587](https://github.com/apache/apisix/pull/4587)\n- :sunrise: feat(stream): add ip-restriction [#4602](https://github.com/apache/apisix/pull/4602)\n- :sunrise: feat(stream): add limit-conn [#4515](https://github.com/apache/apisix/pull/4515)\n- :sunrise: feat: increase ext-plugin timeout to 60s [#4557](https://github.com/apache/apisix/pull/4557)\n- :sunrise: feat(key-auth): supporting key-auth plugin to get key from query string [#4490](https://github.com/apache/apisix/pull/4490)\n- :sunrise: feat(kafka-logger): support for specified the log formats via admin API. [#4483](https://github.com/apache/apisix/pull/4483)\n\n### Bugfix\n\n- fix(stream): sni router is broken when session reuses [#4607](https://github.com/apache/apisix/pull/4607)\n- fix: the limit-conn plugin cannot effectively intercept requests in special scenarios [#4585](https://github.com/apache/apisix/pull/4585)\n- fix: ref check while deleting proto via Admin API [#4575](https://github.com/apache/apisix/pull/4575)\n- fix(skywalking): handle conflict between global rule and route [#4589](https://github.com/apache/apisix/pull/4589)\n- fix: `ctx.var.cookie_*` cookie not found log [#4564](https://github.com/apache/apisix/pull/4564)\n- fix(request-id): we can use different ids with the same request [#4479](https://github.com/apache/apisix/pull/4479)\n\n## 2.7.0\n\n### Change\n\n- change: check metadata_schema with check_schema like the other schema [#4381](https://github.com/apache/apisix/pull/4381)\n- change(echo): remove odd auth_value [#4055](https://github.com/apache/apisix/pull/4055)\n- fix(admin): correct the resources' count field and change its type to integer [#4385](https://github.com/apache/apisix/pull/4385)\n\n### Core\n\n- :sunrise: feat(stream): support client certificate verification [#4445](https://github.com/apache/apisix/pull/4445)\n- :sunrise: feat(stream): accept tls over tcp [#4409](https://github.com/apache/apisix/pull/4409)\n- :sunrise: feat(stream): support domain in the upstream [#4386](https://github.com/apache/apisix/pull/4386)\n- :sunrise: feat(cli): wrap nginx quit cmd [#4360](https://github.com/apache/apisix/pull/4360)\n- :sunrise: feat: allow to set custom timeout for route [#4340](https://github.com/apache/apisix/pull/4340)\n- :sunrise: feat: nacos discovery support group [#4325](https://github.com/apache/apisix/pull/4325)\n- :sunrise: feat: nacos discovery support namespace [#4313](https://github.com/apache/apisix/pull/4313)\n\n### Plugin\n\n- :sunrise: feat(client-control): set client_max_body_size dynamically [#4423](https://github.com/apache/apisix/pull/4423)\n- :sunrise: feat(ext-plugin): stop the runner with SIGTERM [#4367](https://github.com/apache/apisix/pull/4367)\n- :sunrise: feat(limit-req) support nodelay [#4395](https://github.com/apache/apisix/pull/4395)\n- :sunrise: feat(mqtt-proxy): support domain [#4391](https://github.com/apache/apisix/pull/4391)\n- :sunrise: feat(redirect): support appending query string [#4298](https://github.com/apache/apisix/pull/4298)\n\n### Bugfix\n\n- fix: solve memory leak when the client aborts [#4405](https://github.com/apache/apisix/pull/4405)\n- fix(etcd): check res.body.error before accessing the data [#4371](https://github.com/apache/apisix/pull/4371)\n- fix(ext-plugin): when token is stale, refresh token and try again [#4345](https://github.com/apache/apisix/pull/4345)\n- fix(ext-plugin): pass environment variables [#4349](https://github.com/apache/apisix/pull/4349)\n- fix: ensure the plugin is always reloaded [#4319](https://github.com/apache/apisix/pull/4319)\n\n## 2.6.0\n\n### Change\n\n- change(prometheus): redesign the latency metrics & update grafana [#3993](https://github.com/apache/apisix/pull/3993)\n- change(prometheus): don't expose metrics to internet [#3994](https://github.com/apache/apisix/pull/3994)\n- change(limit-count): ensure redis cluster name is set correctly [#3910](https://github.com/apache/apisix/pull/3910)\n- change: drop support of OpenResty 1.15 [#3960](https://github.com/apache/apisix/pull/3960)\n\n### Core\n\n- :sunrise: feat: support passing different host headers in multiple nodes [#4208](https://github.com/apache/apisix/pull/4208)\n- :sunrise: feat: add 50x html for error page [#4164](https://github.com/apache/apisix/pull/4164)\n- :sunrise: feat: support to use upstream_id in stream_route [#4121](https://github.com/apache/apisix/pull/4121)\n- :sunrise: feat: support client certificate verification [#4034](https://github.com/apache/apisix/pull/4034)\n- :sunrise: feat: add nacos support [#3820](https://github.com/apache/apisix/pull/3820)\n- :sunrise: feat: patch tcp.sock.connect to use our DNS resolver [#4114](https://github.com/apache/apisix/pull/4114)\n\n### Plugin\n\n- :sunrise: feat(redirect): support uri encoding [#4244](https://github.com/apache/apisix/pull/4244)\n- :sunrise: feat(key-auth): allow customizing header [#4013](https://github.com/apache/apisix/pull/4013)\n- :sunrise: feat(response-rewrite): allow using variable in the header [#4194](https://github.com/apache/apisix/pull/4194)\n- :sunrise: feat(ext-plugin): APISIX can support Java, Go and other languages to implement custom plugin [#4183](https://github.com/apache/apisix/pull/4183)\n\n### Bugfix\n\n- fix(DNS): support IPv6 resolver [#4242](https://github.com/apache/apisix/pull/4242)\n- fix(healthcheck): only one_loop is needed in the passive health check report [#4116](https://github.com/apache/apisix/pull/4116)\n- fix(traffic-split): configure multiple \"rules\", the request will be confused between upstream [#4092](https://github.com/apache/apisix/pull/4092)\n- fix: ensure upstream with domain is cached [#4061](https://github.com/apache/apisix/pull/4061)\n- fix: be compatible with the router created before 2.5 [#4056](https://github.com/apache/apisix/pull/4056)\n- fix(standalone): the conf should be available during start [#4027](https://github.com/apache/apisix/pull/4027)\n- fix: ensure atomic operation in limit-count plugin [#3991](https://github.com/apache/apisix/pull/3991)\n\n## 2.5.0\n\n**The changes marked with :warning: are not backward compatible.**\n**Please upgrade your data accordingly before upgrading to this version.**\n**[#3809](https://github.com/apache/apisix/pull/3809) Means that empty vars will make the route fail to match any requests.**\n\n### Change\n\n- :warning: change: remove unused consumer.id  [#3868](https://github.com/apache/apisix/pull/3868)\n- :warning: change: remove deprecated upstream.enable_websocket [#3854](https://github.com/apache/apisix/pull/3854)\n- change(zipkin): rearrange the child span [#3877](https://github.com/apache/apisix/pull/3877)\n\n### Core\n\n- :sunrise: feat: support mTLS with etcd [#3905](https://github.com/apache/apisix/pull/3905)\n- :warning: feat: upgrade lua-resty-expr/radixtree to support logical expression [#3809](https://github.com/apache/apisix/pull/3809)\n- :sunrise: feat: load etcd configuration when apisix starts [#3799](https://github.com/apache/apisix/pull/3799)\n- :sunrise: feat: let balancer support priority [#3755](https://github.com/apache/apisix/pull/3755)\n- :sunrise: feat: add control api for discovery module [#3742](https://github.com/apache/apisix/pull/3742)\n\n### Plugin\n\n- :sunrise: feat(skywalking):  allow destroy and configure report interval for reporter [#3925](https://github.com/apache/apisix/pull/3925)\n- :sunrise: feat(traffic-split): the upstream pass_host needs to support IP mode [#3870](https://github.com/apache/apisix/pull/3870)\n- :sunrise: feat: Add filter on HTTP methods for consumer-restriction plugin [#3691](https://github.com/apache/apisix/pull/3691)\n- :sunrise: feat: add allow_origins_by_regex to cors plugin [#3839](https://github.com/apache/apisix/pull/3839)\n- :sunrise: feat: support conditional response rewrite [#3577](https://github.com/apache/apisix/pull/3577)\n\n### Bugfix\n\n- fix(error-log-logger): the logger should be run in each process [#3912](https://github.com/apache/apisix/pull/3912)\n- fix: use the builtin server by default [#3907](https://github.com/apache/apisix/pull/3907)\n- fix(traffic-split): binding upstream via upstream_id is invalid [#3842](https://github.com/apache/apisix/pull/3842)\n- fix: correct the validation for ssl_trusted_certificate [#3832](https://github.com/apache/apisix/pull/3832)\n- fix: don't override cache relative headers [#3789](https://github.com/apache/apisix/pull/3789)\n- fix: fail to run `make deps` on macOS [#3718](https://github.com/apache/apisix/pull/3718)\n\n## 2.4.0\n\n### Change\n\n- change: global rules should not be executed on the internal api by default [#3396](https://github.com/apache/apisix/pull/3396)\n- change: default to cache DNS record according to the TTL [#3530](https://github.com/apache/apisix/pull/3530)\n\n### Core\n\n- :sunrise: feat: support SRV record [#3686](https://github.com/apache/apisix/pull/3686)\n- :sunrise: feat: add dns discovery [#3629](https://github.com/apache/apisix/pull/3629)\n- :sunrise: feat: add consul kv discovery module [#3615](https://github.com/apache/apisix/pull/3615)\n- :sunrise: feat: support to bind plugin config by `plugin_config_id` [#3567](https://github.com/apache/apisix/pull/3567)\n- :sunrise: feat: support listen http2 with plaintext [#3547](https://github.com/apache/apisix/pull/3547)\n- :sunrise: feat: support DNS AAAA record [#3484](https://github.com/apache/apisix/pull/3484)\n\n### Plugin\n\n- :sunrise: feat: the traffic-split plugin supports upstream_id [#3512](https://github.com/apache/apisix/pull/3512)\n- :sunrise: feat(zipkin): support b3 req header [#3551](https://github.com/apache/apisix/pull/3551)\n\n### Bugfix\n\n- fix(chash): ensure retry can try every node [#3651](https://github.com/apache/apisix/pull/3651)\n- fix: script does not work when the route is bound to a service [#3678](https://github.com/apache/apisix/pull/3678)\n- fix: use openssl111 in openresty dir in precedence [#3603](https://github.com/apache/apisix/pull/3603)\n- fix(zipkin): don't cache the per-req sample ratio [#3522](https://github.com/apache/apisix/pull/3522)\n\nFor more changes, please refer to [Milestone](https://github.com/apache/apisix/milestone/13)\n\n## 2.3.0\n\n### Change\n\n- fix: use luajit by default when run apisix [#3335](https://github.com/apache/apisix/pull/3335)\n- feat: use luasocket instead of curl in etcd.lua [#2965](https://github.com/apache/apisix/pull/2965)\n\n### Core\n\n- :sunrise: feat: support to communicate with etcd by TLS without verification in command line [#3415](https://github.com/apache/apisix/pull/3415)\n- :sunrise: feat: chaos test on route could still works when etcd is down [#3404](https://github.com/apache/apisix/pull/3404)\n- :sunrise: feat: ewma use p2c to improve performance [#3300](https://github.com/apache/apisix/pull/3300)\n- :sunrise: feat: support specifying https in upstream to talk with https backend [#3430](https://github.com/apache/apisix/pull/3430)\n- :sunrise: feat: allow customizing lua_package_path & lua_package_cpath [#3417](https://github.com/apache/apisix/pull/3417)\n- :sunrise: feat: allow to pass SNI in HTTPS proxy [#3420](https://github.com/apache/apisix/pull/3420)\n- :sunrise: feat: support gRPCS [#3411](https://github.com/apache/apisix/pull/3411)\n- :sunrise: feat: allow getting upstream health check status via control API [#3345](https://github.com/apache/apisix/pull/3345)\n- :sunrise: feat: support dubbo [#3224](https://github.com/apache/apisix/pull/3224)\n- :sunrise: feat: load balance by least connections [#3304](https://github.com/apache/apisix/pull/3304)\n\n### Plugin\n\n- :sunrise: feat: kafka-logger implemented reuse kafka producer [#3429](https://github.com/apache/apisix/pull/3429)\n- :sunrise: feat(authz-keycloak): dynamic scope and resource mapping. [#3308](https://github.com/apache/apisix/pull/3308)\n- :sunrise: feat: proxy-rewrite host support host with port [#3428](https://github.com/apache/apisix/pull/3428)\n- :sunrise: feat(fault-injection): support conditional fault injection using nginx variables [#3363](https://github.com/apache/apisix/pull/3363)\n\n### Bugfix\n\n- fix(standalone): require consumer's id to be the same as username [#3394](https://github.com/apache/apisix/pull/3394)\n- fix: support upstream_id & consumer with grpc [#3387](https://github.com/apache/apisix/pull/3387)\n- fix: set conf info when global rule is hit without matched rule [#3332](https://github.com/apache/apisix/pull/3332)\n- fix: avoid caching outdated discovery upstream nodes [#3295](https://github.com/apache/apisix/pull/3295)\n- fix: create the health checker in `access` phase [#3240](https://github.com/apache/apisix/pull/3240)\n- fix: make set_more_retries() work when upstream_type is chash [#2676](https://github.com/apache/apisix/pull/2676)\n\nFor more changes, please refer to [Milestone](https://github.com/apache/apisix/milestone/12)\n\n## 2.2.0\n\n### Change\n\n- disable node-status plugin by default [#2968](https://github.com/apache/apisix/pull/2968)\n- k8s_deployment_info is no longer allowed in upstream [#3098](https://github.com/apache/apisix/pull/3098)\n- don't treat route segment with ':' as parameter by default [#3154](https://github.com/apache/apisix/pull/3154)\n\n### Core\n\n- :sunrise: allow create consumers with multiple auth plugins [#2898](https://github.com/apache/apisix/pull/2898)\n- :sunrise: increase the delay before resync etcd [#2977](https://github.com/apache/apisix/pull/2977)\n- :sunrise: support enable/disable route [#2943](https://github.com/apache/apisix/pull/2943)\n- :sunrise: route according to the graphql attributes [#2964](https://github.com/apache/apisix/pull/2964)\n- :sunrise: share etcd auth token [#2932](https://github.com/apache/apisix/pull/2932)\n- :sunrise: add control API [#3048](https://github.com/apache/apisix/pull/3048)\n\n### Plugin\n\n- :sunrise: feat(limt-count): use 'remote_addr' as default key [#2927](https://github.com/apache/apisix/pull/2927)\n- :sunrise: feat(fault-injection): support Nginx variable in abort.body [#2986](https://github.com/apache/apisix/pull/2986)\n- :sunrise: feat: implement new plugin `server-info` [#2926](https://github.com/apache/apisix/pull/2926)\n- :sunrise: feat: add batch process metrics [#3070](https://github.com/apache/apisix/pull/3070)\n- :sunrise: feat: Implement traffic splitting plugin [#2935](https://github.com/apache/apisix/pull/2935)\n- :sunrise: feat:  the proxy-rewrite plugin  support pass nginx variable within header [#3144](https://github.com/apache/apisix/pull/3144)\n- :sunrise: feat: Make headers to add to request in openid-connect plugin configurable [#2903](https://github.com/apache/apisix/pull/2903)\n- :sunrise: feat: support var in upstream_uri on proxy-rewrite plugin [#3139](https://github.com/apache/apisix/pull/3139)\n\n### Bugfix\n\n- basic-auth plugin should run in rewrite phases. [#2905](https://github.com/apache/apisix/pull/2905)\n- fixed the non effective config update in http/udp-logger [#2901](https://github.com/apache/apisix/pull/2901)\n- always necessary to save the data of the limit concurrency, and release the statistical status in the log phase [#2465](https://github.com/apache/apisix/pull/2465)\n- avoid duplicate auto-generated id [#3003](https://github.com/apache/apisix/pull/3003)\n- fix: ctx being contaminated due to a new feature of openresty 1.19. **For openresty 1.19 users, it is recommended to upgrade the APISIX version as soon as possible.** [#3105](https://github.com/apache/apisix/pull/3105)\n- fix: correct the validation of route.vars [#3124](https://github.com/apache/apisix/pull/3124)\n\nFor more changes, please refer to [Milestone](https://github.com/apache/apisix/milestone/10)\n\n## 2.1.0\n\n### Core\n\n- :sunrise: **support ENV variable in configuration.** [#2743](https://github.com/apache/apisix/pull/2743)\n- :sunrise: **support TLS connection with etcd.** [#2548](https://github.com/apache/apisix/pull/2548)\n- generate create/update_time automatically. [#2740](https://github.com/apache/apisix/pull/2740)\n- add a deprecate log for enable_websocket in upstream.[#2691](https://github.com/apache/apisix/pull/2691)\n- add a deprecate log for consumer id.[#2829](https://github.com/apache/apisix/pull/2829)\n- Added `X-APISIX-Upstream-Status` header to distinguish 5xx errors from upstream or APISIX itself. [#2817](https://github.com/apache/apisix/pull/2817)\n- support Nginx configuration snippet. [#2803](https://github.com/apache/apisix/pull/2803)\n\n### Plugin\n\n- :sunrise: **Upgrade protocol to support Apache Skywalking 8.0**[#2389](https://github.com/apache/apisix/pull/2389). So this version only supports skywalking 8.0 protocol. This plugin is disabled by default, you need to modify config.yaml to enable, which is not backward compatible.\n- :sunrise: add aliyun sls logging plugin.[#2169](https://github.com/apache/apisix/issues/2169)\n- proxy-cache: the cache_zone field in the schema should be optional.[#2776](https://github.com/apache/apisix/pull/2776)\n- fix: validate plugin configuration in the DP [#2856](https://github.com/apache/apisix/pull/2856)\n\n### Bugfix\n\n- :bug: fix(etcd): handle etcd compaction.[#2687](https://github.com/apache/apisix/pull/2687)\n- fix: move `conf/cert` to `t/certs` and disable ssl by default, which is not backward compatible. [#2112](https://github.com/apache/apisix/pull/2112)\n- fix: check decrypt key to prevent lua thread aborted [#2815](https://github.com/apache/apisix/pull/2815)\n\n### Not downward compatible features in future versions\n\n-In the 2.3 release, the consumer will only support user names and discard the id. The consumer needs to manually clean up the id field in etcd, otherwise the schema verification will report an error during use\n-In the 2.3 release, opening websocket on upstream will no longer be supported\n-In version 3.0, the data plane and control plane will be separated into two independent ports, that is, the current port 9080 will only process data plane requests, and no longer process admin API requests\n\nFor more changes, please refer to [Milestone](https://github.com/apache/apisix/milestone/8)\n\n## 2.0.0\n\nThis is release candidate.\n\n### Core\n\n- :sunrise: **Migrate from etcd v2 to v3 protocol, which is not backward compatible. Apache APISIX only supports etcd 3.4 and above versions.** [#2036](https://github.com/apache/apisix/pull/2036)\n- add labels for upstream object.[#2279](https://github.com/apache/apisix/pull/2279)\n- add managed fields in json schema for resources, such as create_time and update_time.[#2444](https://github.com/apache/apisix/pull/2444)\n- use interceptors to protect plugin's route[#2416](https://github.com/apache/apisix/pull/2416)\n- support multiple ports for http and https listen.[#2409](https://github.com/apache/apisix/pull/2409)\n- implement `core.sleep`.[#2397](https://github.com/apache/apisix/pull/2397)\n\n### Plugin\n\n- :sunrise: **add AK/SK(HMAC) auth plugin.**[#2192](https://github.com/apache/apisix/pull/2192)\n- :sunrise: add referer-restriction plugin.[#2352](https://github.com/apache/apisix/pull/2352)\n- `limit-count` support to use `redis` cluster.[#2406](https://github.com/apache/apisix/pull/2406)\n- feat(proxy-cache): store the temporary file under cache directory. [#2317](https://github.com/apache/apisix/pull/2317)\n- feat(http-logger): support for specified the log formats via admin API [#2309](https://github.com/apache/apisix/pull/2309)\n\n### Bugfix\n\n- :bug: **`high priority`** When the data plane receives an instruction to delete a resource(router or upstream etc.), it does not properly clean up the cache, resulting in the existing resources cannot be found. This problem only occurs in the case of long and frequent deletion operations.[#2168](https://github.com/apache/apisix/pull/2168)\n- fix routing priority does not take effect.[#2447](https://github.com/apache/apisix/pull/2447)\n- set random seed for each worker process at `init_worker` phase, only `init` phase is not enough.[#2357](https://github.com/apache/apisix/pull/2357)\n- remove unsupported algorithm in jwt plugin.[#2356](https://github.com/apache/apisix/pull/2356)\n- return correct response code when `http_to_https` enabled in redirect plugin.[#2311](https://github.com/apache/apisix/pull/2311)\n\nFor more changes, please refer to [Milestone](https://github.com/apache/apisix/milestone/7)\n\n### CVE\n\n- Fixed Admin API default access token vulnerability\n\n## 1.5.0\n\n### Core\n\n- Admin API: support authentication with SSL certificates. [1747](https://github.com/apache/apisix/pull/1747)\n- Admin API: support both standard `PATCH` and sub path `PATCH`. [1930](https://github.com/apache/apisix/pull/1930)\n- HealthCheck: supports custom host port. [1914](https://github.com/apache/apisix/pull/1914)\n- Upstream: supports turning off the default retry mechanism. [1919](https://github.com/apache/apisix/pull/1919)\n- URI: supports delete the '/' at the end of the `URI`. [1766](https://github.com/apache/apisix/pull/1766)\n\n### New Plugin\n\n- :sunrise: **Request Validator** [1709](https://github.com/apache/apisix/pull/1709)\n\n### Improvements\n\n- change: nginx worker_shutdown_timeout is changed from 3s to recommended value 240s. [1883](https://github.com/apache/apisix/pull/1883)\n- change: the `healthcheck` timeout time type changed from `integer` to `number`. [1892](https://github.com/apache/apisix/pull/1892)\n- change: the `request-validation` plugin input parameter supports `Schema` validation. [1920](https://github.com/apache/apisix/pull/1920)\n- change: add comments for Makefile `install` command. [1912](https://github.com/apache/apisix/pull/1912)\n- change: update comment for config.yaml `etcd.timeout` configuration. [1929](https://github.com/apache/apisix/pull/1929)\n- change: add more prometheus metrics. [1888](https://github.com/apache/apisix/pull/1888)\n- change: add more configuration options for `cors` plugin. [1963](https://github.com/apache/apisix/pull/1963)\n\n### Bugfix\n\n- fixed: failed to get `host` in health check configuration. [1871](https://github.com/apache/apisix/pull/1871)\n- fixed: should not save the runtime data of plugin into `etcd`. [1910](https://github.com/apache/apisix/pull/1910)\n- fixed: run `apisix start` several times will start multi nginx processes. [1913](https://github.com/apache/apisix/pull/1913)\n- fixed: read the request body from the temporary file if it was cached. [1863](https://github.com/apache/apisix/pull/1863)\n- fixed: batch processor name and error return type. [1927](https://github.com/apache/apisix/pull/1927)\n- fixed: failed to read redis.ttl in `limit-count` plugin. [1928](https://github.com/apache/apisix/pull/1928)\n- fixed: passive health check seems never provide a healthy report. [1918](https://github.com/apache/apisix/pull/1918)\n- fixed: avoid to modify the original plugin conf. [1958](https://github.com/apache/apisix/pull/1958)\n- fixed: the test case of `invalid-upstream` is unstable and sometimes fails to run. [1925](https://github.com/apache/apisix/pull/1925)\n\n### Doc\n\n- doc: added APISIX Lua Coding Style Guide. [1874](https://github.com/apache/apisix/pull/1874)\n- doc: fixed link syntax in README.md. [1894](https://github.com/apache/apisix/pull/1894)\n- doc: fixed image links in zh-cn benchmark. [1896](https://github.com/apache/apisix/pull/1896)\n- doc: fixed typos in `FAQ`、`admin-api`、`architecture-design`、`discovery`、`prometheus`、`proxy-rewrite`、`redirect`、`http-logger` documents. [1916](https://github.com/apache/apisix/pull/1916)\n- doc: added improvements for OSx unit tests and request validation plugin. [1926](https://github.com/apache/apisix/pull/1926)\n- doc: fixed typos in `architecture-design` document. [1938](https://github.com/apache/apisix/pull/1938)\n- doc: added the default import path of `Nginx` for unit testing in `Linux` and `macOS` systems in the `how-to-build` document. [1936](https://github.com/apache/apisix/pull/1936)\n- doc: add `request-validation` plugin chinese document. [1932](https://github.com/apache/apisix/pull/1932)\n- doc: fixed file path of `gRPC transcoding` in `README`. [1945](https://github.com/apache/apisix/pull/1945)\n- doc: fixed `uri-blocker` plugin path error in `README`. [1950](https://github.com/apache/apisix/pull/1950)\n- doc: fixed `grpc-transcode` plugin path error in `README`. [1946](https://github.com/apache/apisix/pull/1946)\n- doc: removed unnecessary configurations for `k8s` document. [1891](https://github.com/apache/apisix/pull/1891)\n\n## 1.4.1\n\n### Bugfix\n\n- Fix: multiple SSL certificates are configured, but only one certificate working fine. [1818](https://github.com/apache/incubator-apisix/pull/1818)\n\n## 1.4.0\n\n### Core\n\n- Admin API: Support unique names for routes [1655](https://github.com/apache/incubator-apisix/pull/1655)\n- Optimization of log buffer size and flush time [1570](https://github.com/apache/incubator-apisix/pull/1570)\n\n### New plugins\n\n- :sunrise: **Apache Skywalking plugin** [1241](https://github.com/apache/incubator-apisix/pull/1241)\n- :sunrise: **Keycloak Identity Server Plugin** [1701](https://github.com/apache/incubator-apisix/pull/1701)\n- :sunrise: **Echo Plugin** [1632](https://github.com/apache/incubator-apisix/pull/1632)\n- :sunrise: **Consume Restriction Plugin** [1437](https://github.com/apache/incubator-apisix/pull/1437)\n\n### Improvements\n\n- Batch Request : Copy all headers to every request [1697](https://github.com/apache/incubator-apisix/pull/1697)\n- SSL private key encryption [1678](https://github.com/apache/incubator-apisix/pull/1678)\n- Improvement of docs for multiple plugins\n\n## 1.3.0\n\nThe 1.3 version is mainly for security update.\n\n### Security\n\n- reject invalid header[#1462](https://github.com/apache/incubator-apisix/pull/1462) and uri safe encode[#1461](https://github.com/apache/incubator-apisix/pull/1461)\n- only allow 127.0.0.1 access admin API and dashboard by default. [#1458](https://github.com/apache/incubator-apisix/pull/1458)\n\n### Plugin\n\n- :sunrise: **add batch request plugin**. [#1388](https://github.com/apache/incubator-apisix/pull/1388)\n- implemented plugin `sys logger`. [#1414](https://github.com/apache/incubator-apisix/pull/1414)\n\n## 1.2.0\n\nThe 1.2 version brings many new features, including core and plugins.\n\n### Core\n\n- :sunrise: **support etcd cluster**. [#1283](https://github.com/apache/incubator-apisix/pull/1283)\n- using the local DNS resolver by default, which is friendly for k8s. [#1387](https://github.com/apache/incubator-apisix/pull/1387)\n- support to run `header_filter`, `body_filter` and `log` phases for global rules. [#1364](https://github.com/apache/incubator-apisix/pull/1364)\n- changed the `lua/apisix` dir to `apisix`(**not backward compatible**). [#1351](https://github.com/apache/incubator-apisix/pull/1351)\n- add dashboard as submodule. [#1360](https://github.com/apache/incubator-apisix/pull/1360)\n- allow adding custom shared dict. [#1367](https://github.com/apache/incubator-apisix/pull/1367)\n\n### Plugin\n\n- :sunrise: **add Apache Kafka plugin**. [#1312](https://github.com/apache/incubator-apisix/pull/1312)\n- :sunrise: **add CORS plugin**. [#1327](https://github.com/apache/incubator-apisix/pull/1327)\n- :sunrise: **add TCP logger plugin**. [#1221](https://github.com/apache/incubator-apisix/pull/1221)\n- :sunrise: **add UDP logger plugin**. [1070](https://github.com/apache/incubator-apisix/pull/1070)\n- :sunrise: **add proxy mirror plugin**. [#1288](https://github.com/apache/incubator-apisix/pull/1288)\n- :sunrise: **add proxy cache plugin**. [#1153](https://github.com/apache/incubator-apisix/pull/1153)\n- drop websocket enable control in proxy-rewrite plugin(**not backward compatible**). [1332](https://github.com/apache/incubator-apisix/pull/1332)\n- Adding support to public key based introspection for OAuth plugin. [#1266](https://github.com/apache/incubator-apisix/pull/1266)\n- response-rewrite plugin support binary data to client by base64. [#1381](https://github.com/apache/incubator-apisix/pull/1381)\n- plugin `grpc-transcode` supports grpc deadline. [#1149](https://github.com/apache/incubator-apisix/pull/1149)\n- support password auth for limit-count-redis. [#1150](https://github.com/apache/incubator-apisix/pull/1150)\n- Zipkin plugin add service name and report local server IP. [#1386](https://github.com/apache/incubator-apisix/pull/1386)\n- add `change_pwd` and `user_info` for Wolf-Rbac plugin. [#1204](https://github.com/apache/incubator-apisix/pull/1204)\n\n### Admin API\n\n- :sunrise: support key-based authentication for Admin API(**not backward compatible**). [#1169](https://github.com/apache/incubator-apisix/pull/1169)\n- hide SSL private key in admin API. [#1240](https://github.com/apache/incubator-apisix/pull/1240)\n\n### Bugfix\n\n- missing `clear` table before to reuse table (**will cause memory leak**). [#1134](https://github.com/apache/incubator-apisix/pull/1134)\n- print warning error message if the yaml route file is invalid. [#1141](https://github.com/apache/incubator-apisix/pull/1141)\n- the balancer IP may be nil, use an empty string instead. [#1166](https://github.com/apache/incubator-apisix/pull/1166)\n- plugin node-status and heartbeat don't have schema. [#1249](https://github.com/apache/incubator-apisix/pull/1249)\n- the plugin basic-auth needs required field. [#1251](https://github.com/apache/incubator-apisix/pull/1251)\n- check the count of upstream valid node. [#1292](https://github.com/apache/incubator-apisix/pull/1292)\n\n## 1.1.0\n\nThis release is mainly to strengthen the stability of the code and add more documentation.\n\n### Core\n\n- always specify perl include path when running test cases. [#1097](https://github.com/apache/incubator-apisix/pull/1097)\n- Feature: Add support for PROXY Protocol. [#1113](https://github.com/apache/incubator-apisix/pull/1113)\n- enhancement: add verify command to verify apisix configuration(nginx.conf). [#1112](https://github.com/apache/incubator-apisix/pull/1112)\n- feature: increase the default size of the core file. [#1105](https://github.com/apache/incubator-apisix/pull/1105)\n- feature: make the number of file is as configurable as the connections. [#1098](https://github.com/apache/incubator-apisix/pull/1098)\n- core: improve the core.log module. [#1093](https://github.com/apache/incubator-apisix/pull/1093)\n- Modify bin/apisix to support the SO_REUSEPORT. [#1085](https://github.com/apache/incubator-apisix/pull/1085)\n\n### Doc\n\n- doc: add link to download grafana meta data. [#1119](https://github.com/apache/incubator-apisix/pull/1119)\n- doc: Update README.md. [#1118](https://github.com/apache/incubator-apisix/pull/1118)\n- doc: doc: add wolf-rbac plugin. [#1116](https://github.com/apache/incubator-apisix/pull/1116)\n- doc: update the download link of rpm. [#1108](https://github.com/apache/incubator-apisix/pull/1108)\n- doc: add more english article. [#1092](https://github.com/apache/incubator-apisix/pull/1092)\n- Adding contribution guidelines for the documentation. [#1086](https://github.com/apache/incubator-apisix/pull/1086)\n- doc: getting-started.md check. [#1084](https://github.com/apache/incubator-apisix/pull/1084)\n- Added additional information and refactoring sentences. [#1078](https://github.com/apache/incubator-apisix/pull/1078)\n- Update admin-api-cn.md. [#1067](https://github.com/apache/incubator-apisix/pull/1067)\n- Update architecture-design-cn.md. [#1065](https://github.com/apache/incubator-apisix/pull/1065)\n\n### CI\n\n- ci: remove patch which is no longer necessary and removed in the upst. [#1090](https://github.com/apache/incubator-apisix/pull/1090)\n- fix path error when install with luarocks. [#1068](https://github.com/apache/incubator-apisix/pull/1068)\n- travis: run a apisix instance which intalled by luarocks. [#1063](https://github.com/apache/incubator-apisix/pull/1063)\n\n### Plugins\n\n- feature: Add wolf rbac plugin. [#1095](https://github.com/apache/incubator-apisix/pull/1095)\n- Adding UDP logger plugin. [#1070](https://github.com/apache/incubator-apisix/pull/1070)\n- enhancement: using internal request instead of external request in node-status plugin. [#1109](https://github.com/apache/incubator-apisix/pull/1109)\n\n## 1.0.0\n\nThis release is mainly to strengthen the stability of the code and add more documentation.\n\n### Core\n\n- :sunrise: Support routing priority. You can match different upstream services based on conditions such as header, args, priority, etc. under the same URI. [#998](https://github.com/apache/incubator-apisix/pull/998)\n- When no route is matched, an error message is returned. To distinguish it from other 404 requests. [#1013](https://github.com/apache/incubator-apisix/pull/1013)\n- The address of the dashboard `/apisix/admin` supports CORS. [#982](https://github.com/apache/incubator-apisix/pull/982)\n- The jsonschema validator returns a clearer error message. [#1011](https://github.com/apache/incubator-apisix/pull/1011)\n- Upgrade the `ngx_var` module to version 0.5. [#1005](https://github.com/apache/incubator-apisix/pull/1005)\n- Upgrade the `lua-resty-etcd` module to version 0.8. [#980](https://github.com/apache/incubator-apisix/pull/980)\n- In development mode, the number of workers is automatically adjusted to 1. [#926](https://github.com/apache/incubator-apisix/pull/926)\n- Remove the nginx.conf file from the code repository. It is automatically generated every time and cannot be modified manually. [#974](https://github.com/apache/incubator-apisix/pull/974)\n\n### Doc\n\n- Added documentation on how to customize development plugins. [#909](https://github.com/apache/incubator-apisix/pull/909)\n- fixed example's bugs in the serverless plugin documentation. [#1006](https://github.com/apache/incubator-apisix/pull/1006)\n- Added documentation for using the Oauth plugin. [#987](https://github.com/apache/incubator-apisix/pull/987)\n- Added dashboard compiled documentation. [#985](https://github.com/apache/incubator-apisix/pull/985)\n- Added documentation on how to perform a/b testing. [#957](https://github.com/apache/incubator-apisix/pull/957)\n- Added documentation on how to enable the MQTT plugin. [#916](https://github.com/apache/incubator-apisix/pull/916)\n\n### Test case\n\n- Add test cases for key-auth plugin under normal circumstances. [#964](https://github.com/apache/incubator-apisix/pull/964/)\n- Added tests for gRPC transcode pb options. [#920](https://github.com/apache/incubator-apisix/pull/920)\n\n## 0.9.0\n\nThis release brings many new features, such as support for running APISIX with Tengine,\nan advanced debugging mode that is more developer friendly, and a new URI redirection plugin.\n\n### Core\n\n- :sunrise: Supported to run APISIX with tengine. [#683](https://github.com/apache/incubator-apisix/pull/683)\n- :sunrise: Enabled HTTP2 and supported to set ssl_protocols. [#663](https://github.com/apache/incubator-apisix/pull/663)\n- :sunrise: Advanced Debug Mode, Target module function's input arguments or returned value would be printed once this option is enabled. [#614](https://github.com/apache/incubator-apisix/pull/641)\n- Support to install APISIX without dashboard. [#686](https://github.com/apache/incubator-apisix/pull/686)\n- Removed router R3 [#725](https://github.com/apache/incubator-apisix/pull/725)\n\n### Plugins\n\n- [Redirect URI](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/redirect.md): Redirect URI plugin. [#732](https://github.com/apache/incubator-apisix/pull/732)\n- [Proxy Rewrite](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/proxy-rewrite.md): Supported remove `header` feature. [#658](https://github.com/apache/incubator-apisix/pull/658)\n- [Limit Count](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/limit-count.md): Supported global limit count with `Redis Server`.[#624](https://github.com/apache/incubator-apisix/pull/624)\n\n### lua-resty-*\n\n- lua-resty-radixtree\n  - Support for `host + uri` as an index.\n- lua-resty-jsonschema\n  - This extension is a JSON data validator that replaces the existing `lua-rapidjson` extension.\n\n### Bugfix\n\n- key-auth plugin cannot run accurately in the case of multiple consumers. [#826](https://github.com/apache/incubator-apisix/pull/826)\n- Exported schema for plugin serverless. [#787](https://github.com/apache/incubator-apisix/pull/787)\n- Discard args of uri when using proxy-write plugin [#642](https://github.com/apache/incubator-apisix/pull/642)\n- Zipkin plugin not set tracing data to request header. [#715](https://github.com/apache/incubator-apisix/pull/715)\n- Skipped check cjson for luajit environment in apisix CLI. [#652](https://github.com/apache/incubator-apisix/pull/652)\n- Skipped to init etcd if use local file as config center. [#737](https://github.com/apache/incubator-apisix/pull/737)\n- Support more built-in parameters when set chash balancer. [#775](https://github.com/apache/incubator-apisix/pull/775)\n\n### Dependencies\n\n- Replace the `lua-rapidjson` module with `lua-resty-jsonschema` global,  `lua-resty-jsonschema` is faster and easier to compile.\n\n## 0.8.0\n\n> Released on 2019/09/30\n\nThis release brings many new features, such as stream proxy, support MQTT protocol proxy,\nand support for ARM platform, and proxy rewrite plugin.\n\n### Core\n\n- :sunrise: **[support standalone mode](https://github.com/apache/apisix/blob/master/docs/en/latest/deployment-modes.md#standalone)**: using yaml to update configurations of APISIX, more friendly to kubernetes. [#464](https://github.com/apache/incubator-apisix/pull/464)\n- :sunrise: **[support stream proxy](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/stream-proxy.md)**. [#513](https://github.com/apache/incubator-apisix/pull/513)\n- :sunrise: support consumer bind plugins. [#544](https://github.com/apache/incubator-apisix/pull/544)\n- support domain name in upstream, not only IP. [#522](https://github.com/apache/incubator-apisix/pull/522)\n- ignored upstream node when it's weight is 0. [#536](https://github.com/apache/incubator-apisix/pull/536)\n\n### Plugins\n\n- :sunrise: **[MQTT Proxy](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/mqtt-proxy.md)**: support to load balance MQTT by `client_id`, both support MQTT 3.1 and 5.0. [#513](https://github.com/apache/incubator-apisix/pull/513)\n- [proxy-rewrite](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/proxy-rewrite.md): rewrite uri,\n schema, host for upstream. [#594](https://github.com/apache/incubator-apisix/pull/594)\n\n### ARM\n\n- :sunrise: **APISIX can run normally under Ubuntu 18.04 of ARM64 architecture**, so you can use APISIX as IoT gateway with MQTT plugin.\n\n### lua-resty-*\n\n- lua-resty-ipmatcher\n  - support IPv6\n  - IP white/black list, route.\n- lua-resty-radixtree\n  - allow to specify multiple host, remote_addr and uri.\n  - allow to define user-function to filter request.\n  - use `lua-resty-ipmatcher` instead of `lua-resty-iputils`, `lua-resty-ipmatcher` matches fast and support IPv6.\n\n### Bugfix\n\n- healthcheck: the checker name is wrong if APISIX works under multiple processes. [#568](https://github.com/apache/incubator-apisix/issues/568)\n\n### Dependencies\n\n- removed `lua-tinyyaml` from source code base, and install through Luarocks.\n\n## 0.7.0\n\n> Released on 2019/09/06\n\nThis release brings many new features, such as IP black and white list, gPRC protocol transcoding, IPv6, IdP (identity provider) services, serverless, Change the default route to radix tree (**not downward compatible**), and more.\n\n### Core\n\n- :sunrise: **[gRPC transcoding](https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/grpc-transcode.md)**: supports protocol transcoding so that clients can access your gRPC API by using HTTP/JSON. [#395](https://github.com/apache/incubator-apisix/issues/395)\n- :sunrise: **[radix tree router](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/router-radixtree.md)**: The radix tree is used as the default router implementation. It supports the uri, host, cookie, request header, request parameters, Nginx built-in variables, etc. as the routing conditions, and supports common operators such as equal, greater than, less than, etc., more powerful and flexible.**IMPORTANT: This change is not downward compatible. All users who use historical versions need to manually modify their routing to work properly.** [#414](https://github.com/apache/incubator-apisix/issues/414)\n- Dynamic upstream supports more parameters, you can specify the upstream uri and host, and whether to enable websocket. [#451](https://github.com/apache/incubator-apisix/pull/451)\n- Support for get values from cookies directly from `ctx.var`. [#449](https://github.com/apache/incubator-apisix/pull/449)\n- Routing support IPv6. [#331](https://github.com/apache/incubator-apisix/issues/331)\n\n### Plugins\n\n- :sunrise: **[serverless](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/serverless.md)**: With serverless support, users can dynamically run any Lua function on a gateway node. Users can also use this feature as a lightweight plugin.[#86](https://github.com/apache/incubator-apisix/pull/86)\n- :sunrise: **support IdP**: Support external authentication services, such as Auth0, okta, etc., users can use this to connect to Oauth2.0 and other authentication methods. [#447](https://github.com/apache/incubator-apisix/pull/447)\n- [rate limit](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/limit-conn.md): Support for more restricted keys, such as `X-Forwarded-For` and `X-Real-IP`, and allows users to use Nginx variables, request headers, and request parameters as keys. [#228](https://github.com/apache/incubator-apisix/issues/228)\n- [IP black and white list](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/ip-restriction.md) Support IP black and white list for security. [#398](https://github.com/apache/incubator-apisix/pull/398)\n\n### CLI\n\n- Add the `version` directive to get the version number of APISIX. [#420](https://github.com/apache/incubator-apisix/issues/420)\n\n### Admin\n\n- The `PATCH` API is supported and can be modified individually for a configuration without submitting the entire configuration. [#365](https://github.com/apache/incubator-apisix/pull/365)\n\n### Dashboard\n\n- :sunrise: **Add the online version of the dashboard**，users can [experience APISIX](http://apisix.iresty.com/) without install. [#374](https://github.com/apache/incubator-apisix/issues/374)\n\n[Back to TOC](#table-of-contents)\n\n## 0.6.0\n\n> Released on 2019/08/05\n\nThis release brings many new features such as health check and circuit breaker, debug mode, opentracing and JWT auth. And add **built-in dashboard**.\n\n### Core\n\n- :sunrise: **[Health Check and Circuit Breaker](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/tutorials/health-check.md)**: Enable health check on the upstream node, and will automatically filter unhealthy nodes during load balancing to ensure system stability. [#249](https://github.com/apache/incubator-apisix/pull/249)\n- Anti-ReDoS(Regular expression Denial of Service). [#252](https://github.com/apache/incubator-apisix/pull/250)\n- supported debug mode. [#319](https://github.com/apache/incubator-apisix/pull/319)\n- allowed to use different router. [#364](https://github.com/apache/incubator-apisix/pull/364)\n- supported to match route by host + uri. [#325](https://github.com/apache/incubator-apisix/pull/325)\n- allowed plugins to handler balance phase. [#299](https://github.com/apache/incubator-apisix/pull/299)\n- added desc for upstream and service in schema. [#289](https://github.com/apache/incubator-apisix/pull/289)\n\n### Plugins\n\n- :sunrise: **[OpenTracing](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/plugins/zipkin.md)**: support Zipkin and Apache SkyWalking. [#304](https://github.com/apache/incubator-apisix/pull/304)\n- [JWT auth](https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/jwt-auth.md). [#303](https://github.com/apache/incubator-apisix/pull/303)\n\n### CLI\n\n- support multiple ips of `allow`. [#340](https://github.com/apache/incubator-apisix/pull/340)\n- supported real_ip configure in nginx.conf and added functions to get ip and remote ip. [#236](https://github.com/apache/incubator-apisix/pull/236)\n\n### Dashboard\n\n- :sunrise: **add built-in dashboard**. [#327](https://github.com/apache/incubator-apisix/pull/327)\n\n### Test\n\n- support OSX in Travis CI. [#217](https://github.com/apache/incubator-apisix/pull/217)\n- installed all of the dependencies to `deps` folder. [#248](https://github.com/apache/incubator-apisix/pull/248)\n\n[Back to TOC](#table-of-contents)\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n*The following is copied for your convenience from <https://www.apache.org/foundation/policies/conduct.html>. If there's a discrepancy between the two, let us know or submit a PR to fix it.*\n\n# Code of Conduct #\n\n## Introduction ##\n\nThis code of conduct applies to all spaces managed by the Apache\nSoftware Foundation, including IRC, all public and private mailing\nlists, issue trackers, wikis, blogs, Twitter, and any other\ncommunication channel used by our communities. A code of conduct which\nis specific to in-person events (ie., conferences) is codified in the\npublished ASF anti-harassment policy.\n\nWe expect this code of conduct to be honored by everyone who\nparticipates in the Apache community formally or informally, or claims\nany affiliation with the Foundation, in any Foundation-related\nactivities and especially when representing the ASF, in any role.\n\nThis code __is not exhaustive or complete__. It serves to distill our\ncommon understanding of a collaborative, shared environment and goals.\nWe expect it to be followed in spirit as much as in the letter, so that\nit can enrich all of us and the technical communities in which we participate.\n\n## Specific Guidelines ##\n\nWe strive to:\n\n1. __Be open.__ We invite anyone to participate in our community. We preferably use public methods of communication for project-related messages, unless discussing something sensitive. This applies to messages for help or project-related support, too; not only is a public support request much more likely to result in an answer to a question, it also makes sure that any inadvertent mistakes made by people answering will be more easily detected and corrected.\n\n2. __Be `empathetic`, welcoming, friendly, and patient.__ We work together to resolve conflict, assume good intentions, and do our best to act in an empathetic fashion. We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal attack. A community where people feel uncomfortable or threatened is not a productive one. We should be respectful when dealing with other community members as well as with people outside our community.\n\n3. __Be collaborative.__ Our work will be used by other people, and in turn we will depend on the work of others. When we make something for the benefit of the project, we are willing to explain to others how it works, so that they can build on the work to make it even better. Any decision we make will affect users and colleagues, and we take those consequences seriously when making decisions.\n\n4. __Be inquisitive.__ Nobody knows everything! Asking questions early avoids many problems later, so questions are encouraged, though they may be directed to the appropriate forum. Those who are asked should be responsive and helpful, within the context of our shared goal of improving Apache project code.\n\n5. __Be careful in the words that we choose.__ Whether we are participating as professionals or volunteers, we value professionalism in all interactions, and take responsibility for our own speech. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behaviour are not acceptable. This includes, but is not limited to:\n\n       * Violent threats or language directed against another person.\n       * Sexist, racist, or otherwise discriminatory jokes and language.\n       * Posting sexually explicit or violent material.\n       * Posting (or threatening to post) other people's personally identifying information (\"doxing\").\n       * Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history.\n       * Personal insults, especially those using racist or sexist terms.\n       * Unwelcome sexual attention.\n       * Excessive or unnecessary profanity.\n       * Repeated harassment of others. In general, if someone asks you to stop, then stop.\n       * Advocating for, or encouraging, any of the above behaviour.\n\n6. __Be concise.__ Keep in mind that what you write once will be read by hundreds of people. Writing a short email means people can understand the conversation as efficiently as possible. Short emails should always strive to be empathetic, welcoming, friendly and patient. When a long explanation is necessary, consider adding a summary.</p>\n\n      Try to bring new ideas to a conversation so that each mail adds something unique to the thread, keeping in mind that the rest of the thread still contains the other messages with arguments that have already been made.\n\n      Try to stay on topic, especially in discussions that are already fairly large.\n\n7. __Step down considerately.__ Members of every project come and go. When somebody leaves or disengages from the project they should tell people they are leaving and take the proper steps to ensure that others can pick up where they left off. In doing so, they should remain respectful of those who continue to participate in the project and should not misrepresent the project's goals or achievements. Likewise, community members should respect any individual's choice to leave the project.</p>\n\n## Diversity Statement ##\n\nApache welcomes and encourages participation by everyone. We are committed to being a community that everyone feels good about joining. Although we may not be able to satisfy everyone, we will always work to treat everyone well.\n\nNo matter how you identify yourself or how others perceive you: we welcome you. Though no list can hope to be comprehensive, we explicitly honour diversity in: age, culture, ethnicity, genotype, gender identity or expression, language, national origin, neurotype, phenotype, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, subculture and technical ability.\n\nThough we welcome people fluent in all languages, Apache development is conducted in English.\n\nStandards for behaviour in the Apache community are detailed in the Code of Conduct above. We expect participants in our community to meet these standards in all their interactions and to help others to do so as well.\n\n## Reporting Guidelines ##\n\nWhile this code of conduct should be adhered to by participants, we recognize that sometimes people may have a bad day, or be unaware of some of the guidelines in this code of conduct. When that happens, you may reply to them and point out this code of conduct. Such messages may be in public or in private, whatever is most appropriate. However, regardless of whether the message is public or not, it should still adhere to the relevant parts of this code of conduct; in particular, it should not be abusive or disrespectful.\n\nIf you believe someone is violating this code of conduct, you may reply to\nthem and point out this code of conduct. Such messages may be in public or in\nprivate, whatever is most appropriate. Assume good faith; it is more likely\nthat participants are unaware of their bad behaviour than that they\nintentionally try to degrade the quality of the discussion.  Should there be\ndifficulties in dealing with the situation, you may report your compliance\nissues in confidence to either:\n\n * President of the Apache Software Foundation: Sam Ruby (rubys at intertwingly dot net)\n\nor one of our volunteers:\n\n  * [Mark Thomas](http://home.apache.org/~markt/coc.html)\n  * [Joan Touzet](http://home.apache.org/~wohali/)\n  * [Sharan Foga](http://home.apache.org/~sharan/coc.html)\n\nIf the violation is in documentation or code, for example inappropriate pronoun usage or word choice within official documentation, we ask that people report these privately to the project in question at private@<em>project</em>.apache.org, and, if they have sufficient ability within the project, to resolve or remove the concerning material, being mindful of the perspective of the person originally reporting the issue.\n\n## End Notes ##\n\nThis Code defines __empathy__ as \"a vicarious participation in the emotions, ideas, or opinions of others; the ability to imagine oneself in the condition or predicament of another.\" __Empathetic__ is the adjectival form of empathy.\n\nThis statement thanks the following, on which it draws for content and inspiration:\n\n  * [CouchDB Project Code of conduct](http://couchdb.apache.org/conduct.html)\n  * [Fedora Project Code of Conduct](http://fedoraproject.org/code-of-conduct)\n  * [Django Code of Conduct](https://www.djangoproject.com/conduct/)\n  * [Debian Code of Conduct](http://www.debian.org/vote/2014/vote_002)\n  * [Twitter Open Source Code of Conduct](https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md)\n  * [Mozilla Code of Conduct/Draft](https://wiki.mozilla.org/Code_of_Conduct/Draft#Conflicts_of_Interest)\n  * [Python Diversity Appendix](https://www.python.org/community/diversity/)\n  * [Python Mentors Home Page](http://pythonmentors.com/)\n"
  },
  {
    "path": "CODE_STYLE.md",
    "content": "---\ntitle: APISIX Lua Coding Style Guide\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Indentation\n\nUse 4 spaces as an indent:\n\n```lua\n--No\nif a then\nngx.say(\"hello\")\nend\n```\n\n```lua\n--Yes\nif a then\n    ngx.say(\"hello\")\nend\n```\n\nYou can simplify the operation by changing the tab to 4 spaces in the editor you are using.\n\n## Space\n\nOn both sides of the operator, you need to use a space to separate:\n\n```lua\n--No\nlocal i=1\nlocal s    =    \"apisix\"\n```\n\n```lua\n--Yes\nlocal i = 1\nlocal s = \"apisix\"\n```\n\n## Blank line\n\nMany developers will add a semicolon at the end of the line:\n\n```lua\n--No\nif a then\n    ngx.say(\"hello\");\nend;\n```\n\nAdding a semicolon will make the Lua code look ugly and unnecessary. Also, don't want to save the number of lines in the code, the latter turns the multi-line code into one line in order to appear \"simple\". This will not know when the positioning error is in the end of the code:\n\n```lua\n--No\nif a then ngx.say(\"hello\") end\n```\n\n```lua\n--Yes\nif a then\n    ngx.say(\"hello\")\nend\n```\n\nThe functions needs to be separated by two blank lines:\n\n```lua\n--No\nlocal function foo()\nend\nlocal function bar()\nend\n```\n\n```lua\n--Yes\nlocal function foo()\nend\n\n\nlocal function bar()\nend\n```\n\nIf there are multiple if elseif branches, they need a blank line to separate them:\n\n```lua\n--No\nif a == 1 then\n    foo()\nelseif a== 2 then\n    bar()\nelseif a == 3 then\n    run()\nelse\n    error()\nend\n```\n\n```lua\n--Yes\nif a == 1 then\n    foo()\n\nelseif a == 2 then\n    bar()\n\nelseif a == 3 then\n    run()\n\nelse\n    error()\nend\n```\n\n## Maximum length per line\n\nEach line cannot exceed 100 characters. If it exceeds, you need to wrap and align:\n\n```lua\n--No\nreturn limit_conn_new(\"plugin-limit-conn\", conf.conn, conf.burst, conf.default_conn_delay)\n```\n\n```lua\n--Yes\nreturn limit_conn_new(\"plugin-limit-conn\", conf.conn, conf.burst,\n                      conf.default_conn_delay)\n```\n\nWhen the linefeed is aligned, the correspondence between the upper and lower lines should be reflected. For the example above, the parameters of the second line of functions are to the right of the left parenthesis of the first line.\n\nIf it is a string stitching alignment, you need to put `..` in the next line:\n\n```lua\n--No\nreturn limit_conn_new(\"plugin-limit-conn\" ..  \"plugin-limit-conn\" ..\n                      \"plugin-limit-conn\")\n```\n\n```lua\n--Yes\nreturn limit_conn_new(\"plugin-limit-conn\" .. \"plugin-limit-conn\"\n                      .. \"plugin-limit-conn\")\n```\n\n```lua\n--Yes\nreturn \"param1\", \"plugin-limit-conn\"\n                 .. \"plugin-limit-conn\"\n```\n\n## Variable\n\nLocal variables should always be used, not global variables:\n\n```lua\n--No\ni = 1\ns = \"apisix\"\n```\n\n```lua\n--Yes\nlocal i = 1\nlocal s = \"apisix\"\n```\n\nVariable naming uses the `snake_case` style:\n\n```lua\n--No\nlocal IndexArr = 1\nlocal str_Name = \"apisix\"\n```\n\n```lua\n--Yes\nlocal index_arr = 1\nlocal str_name = \"apisix\"\n```\n\nUse all capitalization for constants:\n\n```lua\n--No\nlocal max_int = 65535\nlocal server_name = \"apisix\"\n```\n\n```lua\n--Yes\nlocal MAX_INT = 65535\nlocal SERVER_NAME = \"apisix\"\n```\n\n## Table\n\nUse `table.new` to pre-allocate the table:\n\n```lua\n--No\nlocal t = {}\nfor i = 1, 100 do\n    t[i] = i\nend\n```\n\n```lua\n--Yes\nlocal new_tab = require \"table.new\"\nlocal t = new_tab(100, 0)\nfor i = 1, 100 do\n    t[i] = i\nend\n```\n\nDon't use `nil` in an array:\n\n```lua\n--No\nlocal t = {1, 2, nil, 3}\n```\n\nIf you must use null values, use `ngx.null` to indicate:\n\n```lua\n--Yes\nlocal t = {1, 2, ngx.null, 3}\n```\n\n## String\n\nDo not splicing strings on the hot code path:\n\n```lua\n--No\nlocal s = \"\"\nfor i = 1, 100000 do\n    s = s .. \"a\"\nend\n```\n\n```lua\n--Yes\nlocal new_tab = require \"table.new\"\nlocal t = new_tab(100000, 0)\nfor i = 1, 100000 do\n    t[i] = \"a\"\nend\nlocal s = table.concat(t, \"\")\n```\n\n## Function\n\nThe naming of functions also follows `snake_case`:\n\n```lua\n--No\nlocal function testNginx()\nend\n```\n\n```lua\n--Yes\nlocal function test_nginx()\nend\n```\n\nThe function should return as early as possible:\n\n```lua\n--No\nlocal function check(age, name)\n    local ret = true\n    if age < 20 then\n        ret = false\n    end\n\n    if name == \"a\" then\n        ret = false\n    end\n    -- do something else\n    return ret\nend\n```\n\n```lua\n--Yes\nlocal function check(age, name)\n    if age < 20 then\n        return false\n    end\n\n    if name == \"a\" then\n        return false\n    end\n    -- do something else\n    return true\nend\n```\n\nThe function should return `<boolean>`, `err`.\nThe first return value means successful or not, if not, the second return value specifies the error message.\nThe error message can be ignored in some cases.\n\n```lua\n--No\nlocal function check()\n    return \"failed\"\nend\n```\n\n```lua\n--Yes\nlocal function check()\n    return false, \"failed\"\nend\n```\n\n## Module\n\nAll require libraries must be localized:\n\n```lua\n--No\nlocal function foo()\n    local ok, err = ngx.timer.at(delay, handler)\nend\n```\n\n```lua\n--Yes\nlocal timer_at = ngx.timer.at\n\nlocal function foo()\n    local ok, err = timer_at(delay, handler)\nend\n```\n\nFor style unification, `require` and `ngx` also need to be localized:\n\n```lua\n--No\nlocal core = require(\"apisix.core\")\nlocal timer_at = ngx.timer.at\n\nlocal function foo()\n    local ok, err = timer_at(delay, handler)\nend\n```\n\n```lua\n--Yes\nlocal ngx = ngx\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal timer_at = ngx.timer.at\n\nlocal function foo()\n    local ok, err = timer_at(delay, handler)\nend\n```\n\n## Error handling\n\nFor functions that return with error information, the error information must be judged and processed:\n\n```lua\n--No\nlocal sock = ngx.socket.tcp()\nlocal ok = sock:connect(\"www.google.com\", 80)\nngx.say(\"successfully connected to google!\")\n```\n\n```lua\n--Yes\nlocal sock = ngx.socket.tcp()\nlocal ok, err = sock:connect(\"www.google.com\", 80)\nif not ok then\n    ngx.say(\"failed to connect to google: \", err)\n    return\nend\nngx.say(\"successfully connected to google!\")\n```\n\nThe function you wrote yourself, the error message is to be returned as a second parameter in the form of a string:\n\n```lua\n--No\nlocal function foo()\n    local ok, err = func()\n    if not ok then\n        return false\n    end\n    return true\nend\n```\n\n```lua\n--No\nlocal function foo()\n    local ok, err = func()\n    if not ok then\n        return false, {msg = err}\n    end\n    return true\nend\n```\n\n```lua\n--Yes\nlocal function foo()\n    local ok, err = func()\n    if not ok then\n        return false, \"failed to call func(): \" .. err\n    end\n    return true\nend\n```\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Contributing to APISIX\n\nFirstly, thanks for your interest in contributing! I hope that this will be a pleasant first experience for you, and that you will return to continue\ncontributing.\n\n## How to contribute?\n\nMost of the contributions that we receive are code contributions, but you can also contribute to the documentation or simply report solid bugs for us to fix. Nor is code the only way to contribute to the project. We strongly value documentation, integration with other project, and gladly accept improvements for these aspects.\n\nFor new contributors, please take a look at issues with a tag called [Good first issue](https://github.com/apache/apisix/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) or [Help wanted](https://github.com/apache/apisix/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).\n\n## How to report a bug?\n\n* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/apache/apisix/issues).\n\n* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/apache/apisix/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.\n\n## How to add a new feature or change an existing one\n\n_Before making any significant changes, please [open an issue](https://github.com/apache/apisix/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.\n\nOnce we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request. Your PR is most likely to be accepted if it:\n\n* Update the README.md with details of changes to the interface.\n* Includes tests for new functionality.\n* References the original issue in the description, e.g. \"Resolves #123\".\n* Has a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).\n* Ensure your pull request's title starts from one of the word in the `types` section of [semantic.yml](https://github.com/apache/apisix/blob/master/.github/workflows/semantic.yml).\n* Follow the [PR manners](https://raw.githubusercontent.com/apache/apisix/master/.github/PULL_REQUEST_TEMPLATE.md)\n\n## Contribution Guidelines for Documentation\n\n* Linting/Style\n\n    For linting both our Markdown and YAML files we use:\n\n    - npm based [markdownlint-cli](https://www.npmjs.com/package/markdownlint-cli)\n\n    For linting all files' license header we use:\n\n    - [license-eye](https://github.com/apache/skywalking-eyes)\n\n    For linting our shell files we use:\n\n    - [shellcheck](https://github.com/koalaman/shellcheck)\n\n    For linting our zh document files we use:\n\n    - [autocorrect](https://github.com/huacnlee/autocorrect)\n\n* Active Voice\n\n    In general, use active voice when formulating the sentence instead of passive voice. A sentence written in the active voice will emphasize\n    the person or thing who is performing an action (eg.The dog chased the ball).  In contrast, the passive voice will highlight\n    the recipient of the action (The ball was chased by the dog). Therefore use the passive voice, only when it's less important\n    who or what completed the action and more important that the action was completed. For example:\n\n    - Recommended: The key-auth plugin authenticates the requests.\n    - Not recommended: The requests are authenticated by the key-auth plugin.\n\n* Capitalization:\n\n    * For titles of a section, capitalize the first letter of each word except for the [closed-class words](https://en.wikipedia.org/wiki/Part_of_speech#Open_and_closed_classes)\n      such as determiners, pronouns, conjunctions, and prepositions. Use the following [link](https://capitalizemytitle.com/#Chicago) for guidance.\n      - Recommended: Authentication **with** APISIX\n\n    * For normal sentences, don't [capitalize](https://www.grammarly.com/blog/capitalization-rules/) random words in the middle of the sentences.\n      Use the Chicago manual for capitalization rules for the documentation.\n\n* Second Person\n\n    In general, use second person in your docs rather than first person. For example:\n\n    - Recommended: You are recommended to use the docker based deployment.\n    - Not Recommended: We recommend to use the docker based deployment.\n\n* Spellings\n\n    Use [American spellings](https://www.oxfordinternationalenglish.com/differences-in-british-and-american-spelling/) when\n    contributing to the documentation.\n\n* Voice\n\n    * Use a friendly and conversational tone. Always use simple sentences. If the sentence is lengthy try to break it in to smaller sentences.\n\n## Check code style and test case style\n\n* code style\n    * Please take a look at [APISIX Lua Coding Style Guide](CODE_STYLE.md).\n    * Use tool to check your code statically by command: `make lint`.\n\n```shell\n        # install `luacheck` first before run it\n        $ luarocks install luacheck\n        # check source code\n        $ make lint\n        ./utils/check-lua-code-style.sh\n        + luacheck -q apisix t/lib\n        Total: 0 warnings / 0 errors in 146 files\n        + find apisix -name *.lua ! -wholename apisix/cli/ngx_tpl.lua -exec ./utils/lj-releng {} +\n        + grep -E ERROR.*.lua: /tmp/check.log\n        + true\n        + [ -s /tmp/error.log ]\n        ./utils/check-test-code-style.sh\n        + find t -name '*.t' -exec grep -E '\\-\\-\\-\\s+(SKIP|ONLY|LAST|FIRST)$' '{}' +\n        + true\n        + '[' -s /tmp/error.log ']'\n        + find t -name '*.t' -exec ./utils/reindex '{}' +\n        + grep done. /tmp/check.log\n        + true\n        + '[' -s /tmp/error.log ']'\n```\n\n      The `lj-releng` and `reindex` will be downloaded automatically by `make lint` if not exists.\n\n* test case style\n    * Use tool to check your test case style statically by command, eg: `make lint`.\n    * When the test file is too large, for example > 800 lines, you should split it to a new file.\n      Please take a look at `t/plugin/limit-conn.t` and `t/plugin/limit-conn2.t`.\n    * For more details, see the [testing framework](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md)\n\n## Contributor gifts\n\nIf you have contributed to Apache APISIX, no matter it is a code contribution to fix a bug or a feature request, or a documentation change, Congratulations! You are eligible to receive the APISIX special gifts with a digital certificate! It's always been the community effort that has made Apache APISIX be understood and used by more developers.\n\n![Contributor gifts](https://static.apiseven.com/2022/12/29/63acfb2f208e1.png)\n\nContributors can request gifts by filling out this [Google form](https://forms.gle/DhPL96LnJwuaHjHU7) or [QQ Form](https://wj.qq.com/s2/11438041/7b07/). After filling in the form, please wait patiently. The community needs some time to review submissions.\n\n## Do you have questions about the source code?\n\n- **QQ Group**: 781365357(recommended), 578997126, 552030619\n- Join in `apisix` channel at [Apache Slack](http://s.apache.org/slack-invite). If the link is not working, find the latest one at [Apache INFRA WIKI](https://cwiki.apache.org/confluence/display/INFRA/Slack+Guest+Invites).\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n=======================================================================\nApache APISIX Subcomponents:\n\nThe Apache APISIX project contains subcomponents with separate copyright\nnotices and license terms. Your use of the source code for the these\nsubcomponents is subject to the terms and conditions of the following\nlicenses.\n\n========================================================================\nApache 2.0 licenses\n========================================================================\n\nThe following components are provided under the Apache License. See project link for details.\nThe text of each license is the standard Apache 2.0 license.\n\n   ewma.lua file from kubernetes/ingress-nginx: https://github.com/kubernetes/ingress-nginx Apache 2.0\n   hello.go file from OpenFunction/samples: https://github.com/OpenFunction/samples Apache 2.0\n"
  },
  {
    "path": "MAINTAIN.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Release steps\n\n### Release patch version\n\n1. Create a [pull request](https://github.com/apache/apisix/commit/7db31a1a7186b966bc0f066539d4de8011871012) (contains the changelog and version change) to master\n   > The changelog only needs to provide a link to the minor branch.\n2. Create a [pull request](https://github.com/apache/apisix/commit/21d7673c6e8ff995677456cdebc8ded5afbb3d0a) (contains the backport commits, and the change in step 1) to minor branch\n   > This should include those PRs that contain the `need backport` tag since the last patch release. Also, the title of these PRs need to be added to the changelog of the minor branch.\n3. Merge it into minor branch\n4. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created via `VERSION=x.y.z make release-src`\n5. Send the [vote email](https://lists.apache.org/thread/vq4qtwqro5zowpdqhx51oznbjy87w9d0) to dev@apisix.apache.org\n   > After executing the `VERSION=x.y.z make release-src` command, the content of the vote email will be automatically generated in the `./release` directory named `apache-apisix-${x.y.z}-vote-contents`\n6. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/k2frnvj4zj9oynsbr7h7nd6n6m3q5p89) to dev@apisix.apache.org\n7. Move the vote artifact to Apache's apisix repo\n8. Register the release info in https://reporter.apache.org/addrelease.html?apisix\n9. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.2) from the minor branch\n10. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) if the version number is the largest\n11. Update APISIX rpm package\n    > Go to [apisix-build-tools](https://github.com/api7/apisix-build-tools) repository and create a new tag named `apisix-${x.y.z}` to automatically submit the\n    package to yum repo\n12. - If the version number is the largest, update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601) in [APISIX docker repository](https://github.com/apache/apisix-docker), after PR merged, then create a new branch from master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2`.\n    - If released an LTS version and the version number less than the current largest(e.g. the current largest version number is 2.14.1, but the LTS version 2.13.2 is to be released), submit a PR like [APISIX docker](https://github.com/apache/apisix-docker/pull/322) in [APISIX docker repository](https://github.com/apache/apisix-docker) and named as `release/apisix-${version}`, e.g. `release/apisix-2.13.2`, after PR reviewed, don't need to merged PR, just close the PR and push the branch to APISIX docker repository.\n13. Update [APISIX helm chart](https://github.com/apache/apisix-helm-chart/pull/234) if the version number is the largest\n14. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org\n\n### Release minor version\n\n1. Create a minor branch, and create [pull request](https://github.com/apache/apisix/commit/bc6ddf51f15e41fffea6c5bd7d01da9838142b66) to master branch from it\n2. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created via `VERSION=x.y.z make release-src`\n3. Send the [vote email](https://lists.apache.org/thread/q8zq276o20r5r9qjkg074nfzb77xwry9) to dev@apisix.apache.org\n   > After executing the `VERSION=x.y.z make release-src` command, the content of the vote email will be automatically generated in the `./release` directory named `apache-apisix-${x.y.z}-vote-contents`\n4. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/p1m9s116rojlhb91g38cj8646393qkz7) to dev@apisix.apache.org\n5. Move the vote artifact to Apache's apisix repo\n6. Register the release info in https://reporter.apache.org/addrelease.html?apisix\n7. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.0) from the minor branch\n8. Merge the pull request into master branch\n9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/7bf0ab5a1bbd795e6571c4bb89a6e646115e7ca3)\n10. Update APISIX rpm package.\n    > Go to [apisix-build-tools](https://github.com/api7/apisix-build-tools) repository and create a new tag named `apisix-${x.y.z}` to automatically submit the rpm package to yum repo\n11. - If the version number is the largest, update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601) in [APISIX docker repository](https://github.com/apache/apisix-docker), after PR merged, then create a new branch from master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2`.\n    - If released an LTS version and the version number less than the current largest(e.g. the current largest version number is 2.14.1, but the LTS version 2.13.2 is to be released), submit a PR like [APISIX docker](https://github.com/apache/apisix-docker/pull/322) in [APISIX docker repository](https://github.com/apache/apisix-docker) and named as `release/apisix-${version}`, e.g. `release/apisix-2.13.2`, after PR reviewed, don't need to merged PR, just close the PR and push the branch to APISIX docker repository.\n12. Update [APISIX helm chart](https://github.com/apache/apisix-helm-chart/pull/234)\n13. Send the [ANNOUNCE email](https://lists.apache.org/thread/4s4msqwl1tq13p9dnv3hx7skbgpkozw1) to dev@apisix.apache.org & announce@apache.org\n"
  },
  {
    "path": "Makefile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Makefile basic env setting\n.DEFAULT_GOAL := help\n# add pipefail support for default shell\nSHELL := /bin/bash -o pipefail\n\n\n# Project basic setting\nVERSION                ?= master\nproject_name           ?= apache-apisix\nproject_release_name   ?= $(project_name)-$(VERSION)-src\n\nOTEL_CONFIG ?= ./ci/pod/otelcol-contrib/data-otlp.json\n\n# Hyperconverged Infrastructure\nENV_OS_NAME            ?= $(shell uname -s | tr '[:upper:]' '[:lower:]')\nENV_OS_ARCH            ?= $(shell uname -m | tr '[:upper:]' '[:lower:]')\nENV_APISIX             ?= $(CURDIR)/bin/apisix\nENV_GIT                ?= git\nENV_TAR                ?= tar\nENV_INSTALL            ?= install\nENV_RM                 ?= rm -vf\nENV_DOCKER             ?= docker\nENV_DOCKER_COMPOSE     ?= docker compose --project-directory $(CURDIR) -p $(project_name) -f $(project_compose_ci)\nENV_NGINX              ?= $(ENV_NGINX_EXEC) -p $(CURDIR) -c $(CURDIR)/conf/nginx.conf\nENV_NGINX_EXEC         := $(shell command -v openresty 2>/dev/null || command -v nginx 2>/dev/null)\nENV_OPENSSL_PREFIX     ?= /usr/local/openresty/openssl3\nENV_LIBYAML_INSTALL_PREFIX ?= /usr\nENV_LUAROCKS           ?= luarocks\n## These variables can be injected by luarocks\nENV_INST_PREFIX        ?= /usr\nENV_INST_LUADIR        ?= $(ENV_INST_PREFIX)/share/lua/5.1\nENV_INST_BINDIR        ?= $(ENV_INST_PREFIX)/bin\nENV_RUNTIME_VER\t     ?= $(shell $(ENV_NGINX_EXEC) -V 2>&1 | tr ' ' '\\n'  | grep 'APISIX_RUNTIME_VER' | cut -d '=' -f2)\n\nIMAGE_NAME = apache/apisix\nENV_APISIX_IMAGE_TAG_NAME  ?= $(IMAGE_NAME):$(VERSION)\n\n-include .requirements\nexport\n\nifneq ($(shell whoami), root)\n\tENV_LUAROCKS_FLAG_LOCAL := --local\nendif\n\nifdef ENV_LUAROCKS_SERVER\n\tENV_LUAROCKS_SERVER_OPT := --server $(ENV_LUAROCKS_SERVER)\nendif\n\nifneq ($(shell test -d $(ENV_OPENSSL_PREFIX) && echo -n yes), yes)\n\tENV_NGINX_PREFIX := $(shell $(ENV_NGINX_EXEC) -V 2>&1 | grep -Eo 'prefix=(.*)/nginx\\s+' | grep -Eo '/.*/')\n\tifeq ($(shell test -d $(addprefix $(ENV_NGINX_PREFIX), openssl3) && echo -n yes), yes)\n\t\tENV_OPENSSL_PREFIX := $(addprefix $(ENV_NGINX_PREFIX), openssl3)\n\tendif\nendif\n\n\n# Makefile basic extension function\n_color_red    =\\E[1;31m\n_color_green  =\\E[1;32m\n_color_yellow =\\E[1;33m\n_color_blue   =\\E[1;34m\n_color_wipe   =\\E[0m\n\n\ndefine func_echo_status\n\tprintf \"[%b info %b] %s\\n\" \"$(_color_blue)\" \"$(_color_wipe)\" $(1)\nendef\n\n\ndefine func_echo_warn_status\n\tprintf \"[%b info %b] %s\\n\" \"$(_color_yellow)\" \"$(_color_wipe)\" $(1)\nendef\n\n\ndefine func_echo_success_status\n\tprintf \"[%b info %b] %s\\n\" \"$(_color_green)\" \"$(_color_wipe)\" $(1)\nendef\n\n\ndefine func_check_folder\n\tif [[ ! -d $(1) ]]; then \\\n\t\tmkdir -p $(1); \\\n\t\t$(call func_echo_status, 'folder check -> create `$(1)`'); \\\n\telse \\\n\t\t$(call func_echo_success_status, 'folder check -> found `$(1)`'); \\\n\tfi\nendef\n\n\n# Makefile target\n.PHONY: runtime\nruntime:\nifeq ($(ENV_NGINX_EXEC), )\nifeq (\"$(wildcard /usr/local/openresty/bin/openresty)\", \"\")\n\t@$(call func_echo_warn_status, \"WARNING: OpenResty not found. You have to install OpenResty and add the binary file to PATH before install Apache APISIX.\")\n\texit 1\nelse\n\t$(eval ENV_NGINX_EXEC := /usr/local/openresty/bin/openresty)\n\t@$(call func_echo_status, \"Use openresty as default runtime\")\nendif\nendif\n\n\n### help : Show Makefile rules\n### \tIf there're awk failures, please make sure\n### \tyou are using awk or gawk\n.PHONY: help\nhelp:\n\t@$(call func_echo_success_status, \"Makefile rules:\")\n\t@awk '{ if(match($$0, /^\\s*#{3}\\s*([^:]+)\\s*:\\s*(.*)$$/, res)){ printf(\"    make %-15s : %-10s\\n\", res[1], res[2]) } }' Makefile\n\n\n### deps : Installing dependencies\n.PHONY: deps\ndeps: install-runtime\n\t$(eval ENV_LUAROCKS_VER := $(shell $(ENV_LUAROCKS) --version | grep -E -o \"luarocks [0-9]+.\"))\n\t@if [ '$(ENV_LUAROCKS_VER)' = 'luarocks 3.' ]; then \\\n\t\tmkdir -p ~/.luarocks; \\\n\t\t$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_LIBDIR $(addprefix $(ENV_OPENSSL_PREFIX), /lib); \\\n\t\t$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_INCDIR $(addprefix $(ENV_OPENSSL_PREFIX), /include); \\\n\t\t$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.YAML_DIR $(ENV_LIBYAML_INSTALL_PREFIX); \\\n\t\t$(ENV_LUAROCKS) install apisix-master-0.rockspec --tree deps --only-deps $(ENV_LUAROCKS_SERVER_OPT); \\\n\telse \\\n\t\t$(call func_echo_warn_status, \"WARNING: You're not using LuaRocks 3.x; please remove the luarocks and reinstall it via https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh\"); \\\n\t\texit 1; \\\n\tfi\n\n\n### undeps : Uninstalling dependencies\n.PHONY: undeps\nundeps: uninstall-rocks uninstall-runtime\n\n\n.PHONY: uninstall-rocks\nuninstall-rocks:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_LUAROCKS) purge --tree=deps\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### utils : Installation tools\n.PHONY: utils\nutils:\nifeq (\"$(wildcard utils/lj-releng)\", \"\")\n\twget -qP utils https://raw.githubusercontent.com/iresty/openresty-devel-utils/master/lj-releng\n\tchmod a+x utils/lj-releng\nendif\nifeq (\"$(wildcard utils/reindex)\", \"\")\n\twget -qP utils https://raw.githubusercontent.com/iresty/openresty-devel-utils/master/reindex\n\tchmod a+x utils/reindex\nendif\n\n\n### lint : Lint source code\n.PHONY: lint\nlint: utils\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t./utils/check-lua-code-style.sh\n\t./utils/check-test-code-style.sh\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### init : Initialize the runtime environment\n.PHONY: init\ninit: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_APISIX) init\n\t$(ENV_APISIX) init_etcd\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### run : Start the apisix server\n.PHONY: run\nrun: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_APISIX) start\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### quit : Stop the apisix server, exit gracefully\n.PHONY: quit\nquit: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_APISIX) quit\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### stop : Stop the apisix server, exit immediately\n.PHONY: stop\nstop: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_APISIX) stop\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### verify : Verify the configuration of apisix server\n.PHONY: verify\nverify: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_NGINX) -t\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### clean : Remove generated files\n.PHONY: clean\nclean:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\trm -rf logs/\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### reload : Reload the apisix server\n.PHONY: reload\nreload: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_APISIX) reload\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n.PHONY: install-runtime\ninstall-runtime:\nifneq ($(ENV_RUNTIME_VER), $(APISIX_RUNTIME))\n\t./utils/install-dependencies.sh\n\t@sudo $(ENV_INSTALL) /usr/local/openresty/bin/openresty $(ENV_INST_BINDIR)/openresty\nendif\n\n.PHONY: uninstall-runtime\nuninstall-runtime:\n\t./utils/install-dependencies.sh uninstall\n\trm -rf /usr/local/openresty\n\trm -f $(ENV_INST_BINDIR)/openresty\n\n### install : Install the apisix (only for luarocks)\n.PHONY: install\ninstall: runtime\n\t$(ENV_INSTALL) -d /usr/local/apisix/\n\t$(ENV_INSTALL) -d /usr/local/apisix/logs/\n\t$(ENV_INSTALL) -d /usr/local/apisix/conf/cert\n\t$(ENV_INSTALL) conf/mime.types /usr/local/apisix/conf/mime.types\n\t$(ENV_INSTALL) conf/config.yaml /usr/local/apisix/conf/config.yaml\n\t$(ENV_INSTALL) conf/debug.yaml /usr/local/apisix/conf/debug.yaml\n\t$(ENV_INSTALL) conf/cert/* /usr/local/apisix/conf/cert/\n\n\t# directories listed in alphabetical order\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix\n\t$(ENV_INSTALL) apisix/*.lua $(ENV_INST_LUADIR)/apisix/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/admin\n\t$(ENV_INSTALL) apisix/admin/*.lua $(ENV_INST_LUADIR)/apisix/admin/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/balancer\n\t$(ENV_INSTALL) apisix/balancer/*.lua $(ENV_INST_LUADIR)/apisix/balancer/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/cli\n\t$(ENV_INSTALL) apisix/cli/*.lua $(ENV_INST_LUADIR)/apisix/cli/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/control\n\t$(ENV_INSTALL) apisix/control/*.lua $(ENV_INST_LUADIR)/apisix/control/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/core\n\t$(ENV_INSTALL) apisix/core/*.lua $(ENV_INST_LUADIR)/apisix/core/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/core/dns\n\t$(ENV_INSTALL) apisix/core/dns/*.lua $(ENV_INST_LUADIR)/apisix/core/dns\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery\n\t$(ENV_INSTALL) apisix/discovery/*.lua $(ENV_INST_LUADIR)/apisix/discovery/\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery/{consul,consul_kv,dns,eureka,nacos,kubernetes,tars}\n\t$(ENV_INSTALL) apisix/discovery/consul/*.lua $(ENV_INST_LUADIR)/apisix/discovery/consul\n\t$(ENV_INSTALL) apisix/discovery/consul_kv/*.lua $(ENV_INST_LUADIR)/apisix/discovery/consul_kv\n\t$(ENV_INSTALL) apisix/discovery/dns/*.lua $(ENV_INST_LUADIR)/apisix/discovery/dns\n\t$(ENV_INSTALL) apisix/discovery/eureka/*.lua $(ENV_INST_LUADIR)/apisix/discovery/eureka\n\t$(ENV_INSTALL) apisix/discovery/kubernetes/*.lua $(ENV_INST_LUADIR)/apisix/discovery/kubernetes\n\t$(ENV_INSTALL) apisix/discovery/nacos/*.lua $(ENV_INST_LUADIR)/apisix/discovery/nacos\n\t$(ENV_INSTALL) apisix/discovery/tars/*.lua $(ENV_INST_LUADIR)/apisix/discovery/tars\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/http\n\t$(ENV_INSTALL) apisix/http/*.lua $(ENV_INST_LUADIR)/apisix/http/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/http/router\n\t$(ENV_INSTALL) apisix/http/router/*.lua $(ENV_INST_LUADIR)/apisix/http/router/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/include/apisix/model\n\t$(ENV_INSTALL) apisix/include/apisix/model/*.proto $(ENV_INST_LUADIR)/apisix/include/apisix/model/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/inspect\n\t$(ENV_INSTALL) apisix/inspect/*.lua $(ENV_INST_LUADIR)/apisix/inspect/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins\n\t$(ENV_INSTALL) apisix/plugins/*.lua $(ENV_INST_LUADIR)/apisix/plugins/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ext-plugin\n\t$(ENV_INSTALL) apisix/plugins/ext-plugin/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ext-plugin/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/grpc-transcode\n\t$(ENV_INSTALL) apisix/plugins/grpc-transcode/*.lua $(ENV_INST_LUADIR)/apisix/plugins/grpc-transcode/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ip-restriction\n\t$(ENV_INSTALL) apisix/plugins/ip-restriction/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ip-restriction/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/limit-conn\n\t$(ENV_INSTALL) apisix/plugins/limit-conn/*.lua $(ENV_INST_LUADIR)/apisix/plugins/limit-conn/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/limit-req\n\t$(ENV_INSTALL) apisix/plugins/limit-req/*.lua $(ENV_INST_LUADIR)/apisix/plugins/limit-req/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/limit-count\n\t$(ENV_INSTALL) apisix/plugins/limit-count/*.lua $(ENV_INST_LUADIR)/apisix/plugins/limit-count/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/opa\n\t$(ENV_INSTALL) apisix/plugins/opa/*.lua $(ENV_INST_LUADIR)/apisix/plugins/opa/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/prometheus\n\t$(ENV_INSTALL) apisix/plugins/prometheus/*.lua $(ENV_INST_LUADIR)/apisix/plugins/prometheus/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/proxy-cache\n\t$(ENV_INSTALL) apisix/plugins/proxy-cache/*.lua $(ENV_INST_LUADIR)/apisix/plugins/proxy-cache/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/serverless\n\t$(ENV_INSTALL) apisix/plugins/serverless/*.lua $(ENV_INST_LUADIR)/apisix/plugins/serverless/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/syslog\n\t$(ENV_INSTALL) apisix/plugins/syslog/*.lua $(ENV_INST_LUADIR)/apisix/plugins/syslog/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/tencent-cloud-cls\n\t$(ENV_INSTALL) apisix/plugins/tencent-cloud-cls/*.lua $(ENV_INST_LUADIR)/apisix/plugins/tencent-cloud-cls/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/pubsub\n\t$(ENV_INSTALL) apisix/pubsub/*.lua $(ENV_INST_LUADIR)/apisix/pubsub/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/secret\n\t$(ENV_INSTALL) apisix/secret/*.lua $(ENV_INST_LUADIR)/apisix/secret/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/zipkin\n\t$(ENV_INSTALL) apisix/plugins/zipkin/*.lua $(ENV_INST_LUADIR)/apisix/plugins/zipkin/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/ssl/router\n\t$(ENV_INSTALL) apisix/ssl/router/*.lua $(ENV_INST_LUADIR)/apisix/ssl/router/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream\n\t$(ENV_INSTALL) apisix/stream/*.lua $(ENV_INST_LUADIR)/apisix/stream/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/plugins\n\t$(ENV_INSTALL) apisix/stream/plugins/*.lua $(ENV_INST_LUADIR)/apisix/stream/plugins/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/router\n\t$(ENV_INSTALL) apisix/stream/router/*.lua $(ENV_INST_LUADIR)/apisix/stream/router/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/xrpc\n\t$(ENV_INSTALL) apisix/stream/xrpc/*.lua $(ENV_INST_LUADIR)/apisix/stream/xrpc/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/xrpc/protocols/redis\n\t$(ENV_INSTALL) apisix/stream/xrpc/protocols/redis/*.lua $(ENV_INST_LUADIR)/apisix/stream/xrpc/protocols/redis/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/xrpc/protocols/dubbo\n\t$(ENV_INSTALL) apisix/stream/xrpc/protocols/dubbo/*.lua $(ENV_INST_LUADIR)/apisix/stream/xrpc/protocols/dubbo/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/utils\n\t$(ENV_INSTALL) apisix/utils/*.lua $(ENV_INST_LUADIR)/apisix/utils/\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ai-proxy\n\t$(ENV_INSTALL) apisix/plugins/ai-proxy/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ai-proxy\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ai-drivers\n\t$(ENV_INSTALL) apisix/plugins/ai-drivers/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ai-drivers\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ai-rag/embeddings\n\t$(ENV_INSTALL) apisix/plugins/ai-rag/embeddings/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ai-rag/embeddings\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ai-rag/vector-search\n\t$(ENV_INSTALL) apisix/plugins/ai-rag/vector-search/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ai-rag/vector-search\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/mcp/broker\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/mcp/transport\n\t$(ENV_INSTALL) apisix/plugins/mcp/*.lua $(ENV_INST_LUADIR)/apisix/plugins/mcp\n\t$(ENV_INSTALL) apisix/plugins/mcp/broker/*.lua $(ENV_INST_LUADIR)/apisix/plugins/mcp/broker\n\t$(ENV_INSTALL) apisix/plugins/mcp/transport/*.lua $(ENV_INST_LUADIR)/apisix/plugins/mcp/transport\n\n\t$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/jwt-auth\n\t$(ENV_INSTALL) apisix/plugins/jwt-auth/*.lua $(ENV_INST_LUADIR)/apisix/plugins/jwt-auth\n\n\t$(ENV_INSTALL) bin/apisix $(ENV_INST_BINDIR)/apisix\n\n\n### uninstall : Uninstall the apisix\n.PHONY: uninstall\nuninstall:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_RM) -r /usr/local/apisix\n\t$(ENV_RM) -r $(ENV_INST_LUADIR)/apisix\n\t$(ENV_RM) $(ENV_INST_BINDIR)/apisix\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### test : Run the test case\n.PHONY: test\ntest: runtime\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_GIT) submodule update --init --recursive\n\tprove -I../test-nginx/lib -I./ -r -s t/\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### license-check : Check project source code for Apache License\n.PHONY: license-check\nlicense-check:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER) run -it --rm -v $(CURDIR):/github/workspace apache/skywalking-eyes header check\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n.PHONY: release-src\nrelease-src: compress-tar\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\tgpg --batch --yes --armor --detach-sig $(project_release_name).tgz\n\tshasum -a 512 $(project_release_name).tgz > $(project_release_name).tgz.sha512\n\n\t$(call func_check_folder,release)\n\tmv $(project_release_name).tgz release/$(project_release_name).tgz\n\tmv $(project_release_name).tgz.asc release/$(project_release_name).tgz.asc\n\tmv $(project_release_name).tgz.sha512 release/$(project_release_name).tgz.sha512\n\t./utils/gen-vote-contents.sh $(VERSION)\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n.PHONY: compress-tar\ncompress-tar:\n\t# The $VERSION can be major.minor.patch (from developer)\n\t# or major.minor (from the branch name in the CI)\n\t$(ENV_TAR) -zcvf $(project_release_name).tgz \\\n\t./apisix \\\n\t./bin \\\n\t./conf \\\n\t./apisix-master-0.rockspec \\\n\tLICENSE \\\n\tMakefile \\\n\tNOTICE \\\n\t*.md\n\n\n### container\n### ci-env-up : CI env launch\n.PHONY: ci-env-up\nci-env-up:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\ttouch $(OTEL_CONFIG)\n\tchmod 777 $(OTEL_CONFIG)\n\t$(ENV_DOCKER_COMPOSE) up -d\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### ci-env-ps : CI env ps\n.PHONY: ci-env-ps\nci-env-ps:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER_COMPOSE) ps\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### ci-env-rebuild : CI env image rebuild\n.PHONY: ci-env-rebuild\nci-env-rebuild:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER_COMPOSE) build\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n\n### ci-env-down : CI env destroy\n.PHONY: ci-env-down\nci-env-down:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\trm $(OTEL_CONFIG)\n\t$(ENV_DOCKER_COMPOSE) down\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n### ci-env-stop : CI env temporary stop\n.PHONY: ci-env-stop\nci-env-stop:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER_COMPOSE) stop\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n### build-on-debian-dev : Build apache/apisix:xx-debian-dev image\n.PHONY: build-on-debian-dev\nbuild-on-debian-dev:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER) build -t $(ENV_APISIX_IMAGE_TAG_NAME)-debian-dev \\\n\t\t--build-arg TARGETARCH=$(ENV_OS_ARCH) \\\n\t\t--build-arg CODE_PATH=. \\\n\t\t--build-arg ENTRYPOINT_PATH=./docker/debian-dev/docker-entrypoint.sh \\\n\t\t--build-arg INSTALL_BROTLI=./docker/debian-dev/install-brotli.sh \\\n\t\t-f ./docker/debian-dev/Dockerfile .\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n.PHONY: push-on-debian-dev\npush-on-debian-dev:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER) tag $(ENV_APISIX_IMAGE_TAG_NAME)-debian-dev $(IMAGE_NAME):dev-$(ENV_OS_ARCH)\n\t$(ENV_DOCKER) push $(IMAGE_NAME):dev-$(ENV_OS_ARCH)\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n\n### merge-dev-tags : Merge architecture-specific dev tags into a single dev tag\n.PHONY: merge-dev-tags\nmerge-dev-tags:\n\t@$(call func_echo_status, \"$@ -> [ Start ]\")\n\t$(ENV_DOCKER) manifest create $(IMAGE_NAME):dev \\\n\t\t$(IMAGE_NAME):dev-amd64 \\\n\t\t$(IMAGE_NAME):dev-arm64\n\t$(ENV_DOCKER) manifest push $(IMAGE_NAME):dev\n\t@$(call func_echo_success_status, \"$@ -> [ Done ]\")\n"
  },
  {
    "path": "NOTICE",
    "content": "Apache APISIX\nCopyright 2019-2025 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n"
  },
  {
    "path": "README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Apache APISIX API Gateway ｜ AI Gateway\n\n<img src=\"./logos/apisix-white-bg.jpg\" alt=\"APISIX logo\" height=\"150px\" align=\"right\" />\n\n[![Build Status](https://github.com/apache/apisix/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/apache/apisix/actions/workflows/build.yml)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/apache/apisix/blob/master/LICENSE)\n[![Commit activity](https://img.shields.io/github/commit-activity/m/apache/apisix)](https://github.com/apache/apisix/graphs/commit-activity)\n[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/apache/apisix.svg)](http://isitmaintained.com/project/apache/apisix \"Average time to resolve an issue\")\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/apache/apisix.svg)](http://isitmaintained.com/project/apache/apisix \"Percentage of issues still open\")\n[![Slack](https://badgen.net/badge/Slack/Join%20Apache%20APISIX?icon=slack)](https://apisix.apache.org/slack)\n\n**Apache APISIX** is a dynamic, real-time, high-performance API Gateway.\n\nAPISIX API Gateway provides rich traffic management features such as load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability, and more.\n\nAPISIX can serve as an **[AI Gateway](https://apisix.apache.org/ai-gateway/)** through its flexible plugin system, providing AI proxying, load balancing for LLMs, retries and fallbacks, token-based rate limiting, and robust security to ensure the efficiency and reliability of AI agents. APISIX also provides the [`mcp-bridge`](https://apisix.apache.org/blog/2025/04/21/host-mcp-server-with-api-gateway/) plugin to seamlessly convert stdio-based MCP servers to scalable HTTP SSE services.\n\nYou can use APISIX API Gateway to handle traditional north-south traffic, as well as east-west traffic between services. It can also be used as a [k8s ingress controller](https://github.com/apache/apisix-ingress-controller).\n\nThe technical architecture of Apache APISIX:\n\n![Technical architecture of Apache APISIX](docs/assets/images/apisix.png)\n\n## Community\n\n- [Kindly Write a Review](https://www.g2.com/products/apache-apisix/reviews) for APISIX in G2.\n- Mailing List: Mail to dev-subscribe@apisix.apache.org, follow the reply to subscribe to the mailing list.\n- Slack Workspace - [invitation link](https://apisix.apache.org/slack) (Please open an [issue](https://apisix.apache.org/docs/general/submit-issue) if this link is expired), and then join the #apisix channel (Channels -> Browse channels -> search for \"apisix\").\n- ![Twitter Follow](https://img.shields.io/twitter/follow/ApacheAPISIX?style=social) - follow and interact with us using hashtag `#ApacheAPISIX`\n- [Documentation](https://apisix.apache.org/docs/)\n- [Discussions](https://github.com/apache/apisix/discussions)\n- [Blog](https://apisix.apache.org/blog)\n\n## Features\n\nYou can use APISIX API Gateway as a traffic entrance to process all business data, including dynamic routing, dynamic upstream, dynamic certificates,\nA/B testing, canary release, blue-green deployment, limit rate, defense against malicious attacks, metrics, monitoring alarms, service observability, service governance, etc.\n\n- **All platforms**\n\n  - Cloud-Native: Platform agnostic, No vendor lock-in, APISIX API Gateway can run from bare-metal to Kubernetes.\n  - Supports ARM64: Don't worry about the lock-in of the infra technology.\n\n- **Multi protocols**\n\n  - [TCP/UDP Proxy](docs/en/latest/stream-proxy.md): Dynamic TCP/UDP proxy.\n  - [Dubbo Proxy](docs/en/latest/plugins/dubbo-proxy.md): Dynamic HTTP to Dubbo proxy.\n  - [Dynamic MQTT Proxy](docs/en/latest/plugins/mqtt-proxy.md): Supports to load balance MQTT by `client_id`, both support MQTT [3.1.\\*](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html), [5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html).\n  - [gRPC proxy](docs/en/latest/grpc-proxy.md): Proxying gRPC traffic.\n  - [gRPC Web Proxy](docs/en/latest/plugins/grpc-web.md): Proxying gRPC Web traffic to gRPC Service.\n  - [gRPC transcoding](docs/en/latest/plugins/grpc-transcode.md): Supports protocol transcoding so that clients can access your gRPC API by using HTTP/JSON.\n  - Proxy Websocket\n  - Proxy Protocol\n  - HTTP(S) Forward Proxy\n  - [SSL](docs/en/latest/certificate.md): Dynamically load an SSL certificate\n  - [HTTP/3 with QUIC](docs/en/latest/http3.md)\n\n- **Full Dynamic**\n\n  - [Hot Updates And Hot Plugins](docs/en/latest/terminology/plugin.md): Continuously updates its configurations and plugins without restarts!\n  - [Proxy Rewrite](docs/en/latest/plugins/proxy-rewrite.md): Support rewrite the `host`, `uri`, `schema`, `method`, `headers` of the request before send to upstream.\n  - [Response Rewrite](docs/en/latest/plugins/response-rewrite.md): Set customized response status code, body and header to the client.\n  - Dynamic Load Balancing: Round-robin load balancing with weight.\n  - Hash-based Load Balancing: Load balance with consistent hashing sessions.\n  - [Health Checks](docs/en/latest/tutorials/health-check.md): Enable health check on the upstream node and will automatically filter unhealthy nodes during load balancing to ensure system stability.\n  - Circuit-Breaker: Intelligent tracking of unhealthy upstream services.\n  - [Proxy Mirror](docs/en/latest/plugins/proxy-mirror.md): Provides the ability to mirror client requests.\n  - [Traffic Split](docs/en/latest/plugins/traffic-split.md): Allows users to incrementally direct percentages of traffic between various upstreams.\n\n- **Fine-grained routing**\n\n  - [Supports full path matching and prefix matching](docs/en/latest/router-radixtree.md#how-to-use-libradixtree-in-apisix)\n  - [Support all Nginx built-in variables as conditions for routing](docs/en/latest/router-radixtree.md#how-to-filter-route-by-nginx-builtin-variable), so you can use `cookie`, `args`, etc. as routing conditions to implement canary release, A/B testing, etc.\n  - Support [various operators as judgment conditions for routing](https://github.com/iresty/lua-resty-radixtree#operator-list), for example `{\"arg_age\", \">\", 24}`\n  - Support [custom route matching function](https://github.com/iresty/lua-resty-radixtree/blob/master/t/filter-fun.t#L10)\n  - IPv6: Use IPv6 to match the route.\n  - Support [TTL](docs/en/latest/admin-api.md#route)\n  - [Support priority](docs/en/latest/router-radixtree.md#3-match-priority)\n  - [Support Batch Http Requests](docs/en/latest/plugins/batch-requests.md)\n  - [Support filtering route by GraphQL attributes](docs/en/latest/router-radixtree.md#how-to-filter-route-by-graphql-attributes)\n\n- **Security**\n\n  - Rich authentication & authorization support:\n    * [key-auth](docs/en/latest/plugins/key-auth.md)\n    * [JWT](docs/en/latest/plugins/jwt-auth.md)\n    * [basic-auth](docs/en/latest/plugins/basic-auth.md)\n    * [wolf-rbac](docs/en/latest/plugins/wolf-rbac.md)\n    * [casbin](docs/en/latest/plugins/authz-casbin.md)\n    * [keycloak](docs/en/latest/plugins/authz-keycloak.md)\n    * [casdoor](docs/en/latest/plugins/authz-casdoor.md)\n  - [IP Whitelist/Blacklist](docs/en/latest/plugins/ip-restriction.md)\n  - [Referer Whitelist/Blacklist](docs/en/latest/plugins/referer-restriction.md)\n  - [IdP](docs/en/latest/plugins/openid-connect.md): Support external Identity platforms, such as Auth0, okta, etc..\n  - [Limit-req](docs/en/latest/plugins/limit-req.md)\n  - [Limit-count](docs/en/latest/plugins/limit-count.md)\n  - [Limit-concurrency](docs/en/latest/plugins/limit-conn.md)\n  - Anti-ReDoS(Regular expression Denial of Service): Built-in policies to Anti ReDoS without configuration.\n  - [CORS](docs/en/latest/plugins/cors.md) Enable CORS(Cross-origin resource sharing) for your API.\n  - [URI Blocker](docs/en/latest/plugins/uri-blocker.md): Block client request by URI.\n  - [Request Validator](docs/en/latest/plugins/request-validation.md)\n  - [CSRF](docs/en/latest/plugins/csrf.md) Based on the [`Double Submit Cookie`](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Double_Submit_Cookie) way, protect your API from CSRF attacks.\n\n- **OPS friendly**\n\n  - Zipkin tracing: [Zipkin](docs/en/latest/plugins/zipkin.md)\n  - Open source APM: support [Apache SkyWalking](docs/en/latest/plugins/skywalking.md)\n  - Works with external service discovery: In addition to the built-in etcd, it also supports [Consul](docs/en/latest/discovery/consul.md), [Consul_kv](docs/en/latest/discovery/consul_kv.md), [Nacos](docs/en/latest/discovery/nacos.md), [Eureka](docs/en/latest/discovery/eureka.md) and [Zookeeper (CP)](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md).\n  - Monitoring And Metrics: [Prometheus](docs/en/latest/plugins/prometheus.md)\n  - Clustering: APISIX nodes are stateless, creates clustering of the configuration center, please refer to [etcd Clustering Guide](https://etcd.io/docs/v3.5/op-guide/clustering/).\n  - High availability: Support to configure multiple etcd addresses in the same cluster.\n  - [Dashboard](https://github.com/apache/apisix-dashboard)\n  - Version Control: Supports rollbacks of operations.\n  - CLI: start\\stop\\reload APISIX through the command line.\n  - [Standalone](docs/en/latest/deployment-modes.md#standalone): Supports to load route rules from local YAML file, it is more friendly such as under the kubernetes(k8s).\n  - [Global Rule](docs/en/latest/terminology/global-rule.md): Allows to run any plugin for all request, eg: limit rate, IP filter etc.\n  - High performance: The single-core QPS reaches 18k with an average delay of fewer than 0.2 milliseconds.\n  - [Fault Injection](docs/en/latest/plugins/fault-injection.md)\n  - [REST Admin API](docs/en/latest/admin-api.md): Using the REST Admin API to control Apache APISIX, which only allows 127.0.0.1 access by default, you can modify the `allow_admin` field in `conf/config.yaml` to specify a list of IPs that are allowed to call the Admin API. Also, note that the Admin API uses key auth to verify the identity of the caller.\n  - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [RocketMQ Logger](docs/en/latest/plugins/rocketmq-logger.md), [SkyWalking Logger](docs/en/latest/plugins/skywalking-logger.md), [Alibaba Cloud Logging(SLS)](docs/en/latest/plugins/sls-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md), [Splunk HEC Logging](docs/en/latest/plugins/splunk-hec-logging.md), [File Logger](docs/en/latest/plugins/file-logger.md), [SolarWinds Loggly Logging](docs/en/latest/plugins/loggly.md), [TencentCloud CLS](docs/en/latest/plugins/tencent-cloud-cls.md)).\n  - [ClickHouse](docs/en/latest/plugins/clickhouse-logger.md): push logs to ClickHouse.\n  - [Elasticsearch](docs/en/latest/plugins/elasticsearch-logger.md): push logs to Elasticsearch.\n  - [Datadog](docs/en/latest/plugins/datadog.md): push custom metrics to the DogStatsD server, comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server.\n  - [Helm charts](https://github.com/apache/apisix-helm-chart)\n  - [HashiCorp Vault](https://www.vaultproject.io/): Support secret management solution for accessing secrets from Vault secure storage backed in a low trust environment. Currently, RS256 keys (public-private key pairs) or secret keys can be linked from vault in jwt-auth authentication plugin using [APISIX Secret](docs/en/latest/terminology/secret.md) resource.\n\n- **Highly scalable**\n  - [Custom plugins](docs/en/latest/plugin-develop.md): Allows hooking of common phases, such as `rewrite`, `access`, `header filter`, `body filter` and `log`, also allows to hook the `balancer` stage.\n  - [Plugin can be written in Java/Go/Python](docs/en/latest/external-plugin.md)\n  - [Plugin can be written with Proxy Wasm SDK](docs/en/latest/wasm.md)\n  - Custom load balancing algorithms: You can use custom load balancing algorithms during the `balancer` phase.\n  - Custom routing: Support users to implement routing algorithms themselves.\n\n- **Multi-Language support**\n  - Apache APISIX is a multi-language gateway for plugin development and provides support via `RPC` and `Wasm`.\n  ![Multi Language Support into Apache APISIX](docs/assets/images/external-plugin.png)\n  - The RPC way, is the current way. Developers can choose the language according to their needs and after starting an independent process with the RPC, it exchanges data with APISIX through local RPC communication. Till this moment, APISIX has support for [Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) and Node.js.\n  - The Wasm or WebAssembly, is an experimental way. APISIX can load and run Wasm bytecode via APISIX [wasm plugin](https://github.com/apache/apisix/blob/master/docs/en/latest/wasm.md) written with the [Proxy Wasm SDK](https://github.com/proxy-wasm/spec#sdks). Developers only need to write the code according to the SDK and then compile it into a Wasm bytecode that runs on Wasm VM with APISIX.\n\n- **Serverless**\n  - [Lua functions](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX.\n  - [AWS Lambda](docs/en/latest/plugins/aws-lambda.md): Integration with AWS Lambda function as a dynamic upstream to proxy all requests for a particular URI to the AWS API gateway endpoint. Supports authorization via api key and AWS IAM access secret.\n  - [Azure Functions](docs/en/latest/plugins/azure-functions.md): Seamless integration with Azure Serverless Function as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud.\n  - [Apache OpenWhisk](docs/en/latest/plugins/openwhisk.md): Seamless integration with Apache OpenWhisk as a dynamic upstream to proxy all requests for a particular URI to your own OpenWhisk cluster.\n\n## Get Started\n\n1. Installation\n\n   Please refer to [install documentation](https://apisix.apache.org/docs/apisix/installation-guide/).\n\n2. Getting started\n\n   The getting started guide is a great way to learn the basics of APISIX. Just follow the steps in [Getting Started](https://apisix.apache.org/docs/apisix/getting-started/).\n\n   Further, you can follow the documentation to try more [plugins](docs/en/latest/plugins).\n\n3. Admin API\n\n   Apache APISIX provides [REST Admin API](docs/en/latest/admin-api.md) to dynamically control the Apache APISIX cluster.\n\n4. Plugin development\n\n   You can refer to [plugin development guide](docs/en/latest/plugin-develop.md), and sample plugin `example-plugin`'s code implementation.\n   Reading [plugin concept](docs/en/latest/terminology/plugin.md) would help you learn more about the plugin.\n\nFor more documents, please refer to [Apache APISIX Documentation site](https://apisix.apache.org/docs/apisix/getting-started/)\n\n## Benchmark\n\nUsing AWS's eight-core server, APISIX's QPS reaches 140,000 with a latency of only 0.2 ms.\n\n[Benchmark script](benchmark/run.sh) has been open sourced, welcome to try and contribute.\n\n[APISIX also works perfectly in AWS graviton3 C7g.](https://apisix.apache.org/blog/2022/06/07/installation-performance-test-of-apigateway-apisix-on-aws-graviton3)\n\n## User Stories\n\n- [European eFactory Platform: API Security Gateway – Using APISIX in the eFactory Platform](https://www.efactory-project.eu/post/api-security-gateway-using-apisix-in-the-efactory-platform)\n- [Copernicus Reference System Software](https://github.com/COPRS/infrastructure/wiki/Networking-trade-off)\n- [More Stories](https://apisix.apache.org/blog/tags/case-studies/)\n\n## Who Uses APISIX API Gateway?\n\nA wide variety of companies and organizations use APISIX API Gateway for research, production and commercial product, below are some of them:\n\n- Airwallex\n- Bilibili\n- CVTE\n- European eFactory Platform\n- European Copernicus Reference System\n- Geely\n- HONOR\n- Horizon Robotics\n- iQIYI\n- Lenovo\n- NASA JPL\n- Nayuki\n- OPPO\n- QingCloud\n- Swisscom\n- Tencent Game\n- Travelsky\n- vivo\n- Sina Weibo\n- WeCity\n- WPS\n- XPENG\n- Zoom\n\n## Logos\n\n- [Apache APISIX logo(PNG)](https://github.com/apache/apisix/tree/master/logos/apache-apisix.png)\n- [Apache APISIX logo source](https://apache.org/logos/#apisix)\n\n## Acknowledgments\n\nInspired by Kong and Orange.\n\n## License\n\n[Apache 2.0 License](https://github.com/apache/apisix/tree/master/LICENSE)\n"
  },
  {
    "path": "THREAT_MODEL.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Threat Model\n\nHere is the threat model of Apache APISIX, which is relative to our developers and operators.\n\n### Where the system might be attacked\n\nAs a proxy, Apache APISIX needs to be able to run in front of untrusted downstream traffic.\n\nHowever, some features need to assume the downstream traffic is trusted. They should be either\nnot exposed to the internet by default (for example, listening to 127.0.0.1), or disclaim in\nthe doc explicitly.\n\nAs Apache APISIX is evolving rapidly, some newly added features may not be strong enough to defend against potential attacks.\nTherefore, we need to divide the features into two groups: premature and mature ones.\nFeatures that are just merged in half a year or are declared as experimental are premature.\nPremature features are not fully tested on the battlefield and are not covered by the security policy normally.\n\nAdditionally, we require the components below are trustable:\n\n1. the upstream\n2. the configuration\n3. the way we relay the configuration\n4. the 3rd party components involved in the Apache APISIX, for example, the authorization server\n\n### How can we reduce the likelihood or impact of a potential threat\n\nAs the user:\nFirst of all, don't expose the components which are required to be trustable to the internet, including the control plane (Dashboard or something else) and the configuration relay mechanism (etcd or etcd adapter or something else).\n\nThen, harden the trusted components. For example,\n\n1. if possible, enable authentication or use https for the etcd\n2. read the doc and disable plugins that are not needed, so that we can reduce the attack vector\n3. restrict and audit the change of configuration\n\nAs the developer:\nWe should keep security in mind, and validate the input from the client before use.\n\nAs the maintainer:\nWe should keep security in mind, and review the code line by line.\nWe are open to discussion from the security researchers.\n"
  },
  {
    "path": "Vision-and-Milestones.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### Vision\n\nApache APISIX is an open source API gateway designed to help developers connect any APIs securely and efficiently in any environment.\n\nManaging thousands or tens of thousands of APIs and microservices in a multi-cloud and hybrid cloud environment is not an easy task.\nThere will be many challenges as authentication, observability, security, etc.\n\nApache APISIX, a community-driven project, hopes to help everyone better manage and use APIs through the power of developers.\nEvery developer's contribution will used by thousands of companies and served by billions of users.\n\n### Milestones\n\nApache APISIX has relatively complete features for north-south traffic,\nand will be iterated around the following directions in the next 6 months (if you have any ideas, feel free to create issue to discuss):\n\n- More complete support for Gateway API on APISIX ingress controller\n- Add support for service mesh\n- User-friendly documentation\n- More plugins for public cloud and SaaS services\n- Java/Go plugins and Wasm production-ready\n- Add dynamic debugging tools for Apache APISIX\n"
  },
  {
    "path": "apisix/admin/consumer_group.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal consumers = require(\"apisix.consumer\").consumers\nlocal resource = require(\"apisix.admin.resource\")\nlocal schema_plugin = require(\"apisix.admin.plugins\").check_schema\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\nlocal type = type\nlocal tostring = tostring\nlocal ipairs = ipairs\n\n\nlocal function check_conf(id, conf, need_id, schema)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    local ok, err = schema_plugin(conf.plugins)\n    if not ok then\n        return nil, {error_msg = err}\n    end\n\n    return true\nend\n\n\nlocal function encrypt_conf(id, conf)\n    plugins_encrypt_conf(conf.plugins)\nend\n\n\nlocal function delete_checker(id)\n    local consumers, consumers_ver = consumers()\n    if consumers_ver and consumers then\n        for _, consumer in ipairs(consumers) do\n            if type(consumer) == \"table\" and consumer.value\n               and consumer.value.group_id\n               and tostring(consumer.value.group_id) == id then\n                return 400, {error_msg = \"can not delete this consumer group,\"\n                                         .. \" consumer [\" .. consumer.value.id\n                                         .. \"] is still using it now\"}\n            end\n        end\n    end\n\n    return nil, nil\nend\n\n\nreturn resource.new({\n    name = \"consumer_groups\",\n    kind = \"consumer group\",\n    schema = core.schema.consumer_group,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    unsupported_methods = {\"post\"},\n    delete_checker = delete_checker\n})\n"
  },
  {
    "path": "apisix/admin/consumers.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core    = require(\"apisix.core\")\nlocal plugins = require(\"apisix.admin.plugins\")\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\nlocal resource = require(\"apisix.admin.resource\")\n\n\nlocal function check_conf(username, conf, need_username, schema, opts)\n    opts = opts or {}\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    if username and username ~= conf.username then\n        return nil, {error_msg = \"wrong username\" }\n    end\n\n    if conf.plugins then\n        ok, err = plugins.check_schema(conf.plugins, core.schema.TYPE_CONSUMER)\n        if not ok then\n            return nil, {error_msg = \"invalid plugins configuration: \" .. err}\n        end\n    end\n\n    if conf.group_id and not opts.skip_references_check then\n        local key = \"/consumer_groups/\" .. conf.group_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch consumer group info by \"\n                                     .. \"consumer group id [\" .. conf.group_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch consumer group info by \"\n                                     .. \"consumer group id [\" .. conf.group_id .. \"], \"\n                                     .. \"response code: \" .. res.status}\n        end\n    end\n\n    return conf.username\nend\n\n\nlocal function encrypt_conf(id, conf)\n    plugins_encrypt_conf(conf.plugins, core.schema.TYPE_CONSUMER)\nend\n\n\nreturn resource.new({\n    name = \"consumers\",\n    kind = \"consumer\",\n    schema = core.schema.consumer,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    unsupported_methods = {\"post\", \"patch\"}\n})\n"
  },
  {
    "path": "apisix/admin/credentials.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core     = require(\"apisix.core\")\nlocal plugins  = require(\"apisix.admin.plugins\")\nlocal plugin   = require(\"apisix.plugin\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\nlocal pairs    = pairs\n\nlocal function check_conf(_id, conf, _need_id, schema)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    if conf.plugins then\n        ok, err = plugins.check_schema(conf.plugins, core.schema.TYPE_CONSUMER)\n        if not ok then\n            return nil, {error_msg = \"invalid plugins configuration: \" .. err}\n        end\n\n        for name, _ in pairs(conf.plugins) do\n            local plugin_obj = plugin.get(name)\n            if not plugin_obj then\n                return nil, {error_msg = \"unknown plugin \" .. name}\n            end\n            if plugin_obj.type ~= \"auth\" then\n                return nil, {error_msg = \"only supports auth type plugins in consumer credential\"}\n            end\n        end\n    end\n\n    return true, nil\nend\n\n\nlocal function encrypt_conf(id, conf)\n    plugins_encrypt_conf(conf.plugins, core.schema.TYPE_CONSUMER)\nend\n\n\n-- get_credential_etcd_key is used to splice the credential's etcd key (without prefix)\n-- from credential_id and sub_path.\n-- Parameter credential_id is from the uri or payload; sub_path is in the form of\n-- {consumer_name}/credentials or {consumer_name}/credentials/{credential_id}.\n-- Only if GET credentials list, credential_id is nil, sub_path is like {consumer_name}/credentials,\n-- so return value is /consumers/{consumer_name}/credentials.\n-- In the other methods, credential_id is not nil, return value is\n-- /consumers/{consumer_name}/credentials/{credential_id}.\nlocal function get_credential_etcd_key(credential_id, _conf, sub_path, _args)\n    if credential_id then\n        local uri_segs = core.utils.split_uri(sub_path)\n        local consumer_name = uri_segs[1]\n        return \"/consumers/\" .. consumer_name .. \"/credentials/\" .. credential_id\n    end\n\n    return \"/consumers/\" .. sub_path\nend\n\nreturn resource.new({\n    name = \"credentials\",\n    kind = \"credential\",\n    schema = core.schema.credential,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    get_resource_etcd_key = get_credential_etcd_key,\n    unsupported_methods = {\"post\", \"patch\"}\n})\n"
  },
  {
    "path": "apisix/admin/global_rules.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal schema_plugin = require(\"apisix.admin.plugins\").check_schema\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\n\nlocal pairs    = pairs\nlocal ipairs   = ipairs\nlocal tostring = tostring\n\nlocal function get_global_rules()\n    local g = core.etcd.get(\"/global_rules\", true)\n    if not g then\n        return nil\n    end\n    return core.table.try_read_attr(g, \"body\", \"list\")\nend\n\nlocal function check_conf(id, conf, need_id, schema)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    local ok, err = schema_plugin(conf.plugins)\n    if not ok then\n        return nil, {error_msg = err}\n    end\n\n    -- Check for plugin conflicts with existing global rules\n    if conf.plugins then\n        local global_rules = get_global_rules()\n        if global_rules then\n            for _, existing_rule in ipairs(global_rules) do\n                -- Skip checking against itself when updating\n                if existing_rule.value and existing_rule.value.id and\n                   tostring(existing_rule.value.id) ~= tostring(id) then\n\n                    if existing_rule.value.plugins then\n                        -- Check for any overlapping plugins\n                        for plugin_name, _ in pairs(conf.plugins) do\n                            if existing_rule.value.plugins[plugin_name] then\n                                return nil, {\n                                    error_msg = \"plugin '\" .. plugin_name ..\n                                    \"' already exists in global rule with id '\" ..\n                                    existing_rule.value.id .. \"'\"\n                                }\n                            end\n                        end\n                    end\n                end\n            end\n        end\n    end\n\n    return true\nend\n\n\nlocal function encrypt_conf(id, conf)\n    plugins_encrypt_conf(conf.plugins)\nend\n\n\nreturn resource.new({\n    name = \"global_rules\",\n    kind = \"global rule\",\n    schema = core.schema.global_rule,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    unsupported_methods = {\"post\"}\n})\n"
  },
  {
    "path": "apisix/admin/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal get_uri_args = ngx.req.get_uri_args\nlocal route = require(\"apisix.utils.router\")\nlocal plugin = require(\"apisix.plugin\")\nlocal standalone = require(\"apisix.admin.standalone\")\nlocal v3_adapter = require(\"apisix.admin.v3_adapter\")\nlocal utils = require(\"apisix.admin.utils\")\nlocal ngx = ngx\nlocal get_method = ngx.req.get_method\nlocal ngx_time = ngx.time\nlocal ngx_timer_at = ngx.timer.at\nlocal ngx_worker_id = ngx.worker.id\nlocal tonumber = tonumber\nlocal tostring = tostring\nlocal str_lower = string.lower\nlocal reload_event = \"/apisix/admin/plugins/reload\"\nlocal ipairs = ipairs\nlocal error = error\nlocal type = type\n\n\nlocal events\nlocal MAX_REQ_BODY = 1024 * 1024 * 1.5      -- 1.5 MiB\n\n\nlocal viewer_methods = {\n    get = true,\n}\n\n\nlocal resources = {\n    routes          = require(\"apisix.admin.routes\"),\n    services        = require(\"apisix.admin.services\"),\n    upstreams       = require(\"apisix.admin.upstreams\"),\n    consumers       = require(\"apisix.admin.consumers\"),\n    credentials     = require(\"apisix.admin.credentials\"),\n    schema          = require(\"apisix.admin.schema\"),\n    ssls            = require(\"apisix.admin.ssl\"),\n    plugins         = require(\"apisix.admin.plugins\"),\n    protos          = require(\"apisix.admin.proto\"),\n    global_rules    = require(\"apisix.admin.global_rules\"),\n    stream_routes   = require(\"apisix.admin.stream_routes\"),\n    plugin_metadata = require(\"apisix.admin.plugin_metadata\"),\n    plugin_configs  = require(\"apisix.admin.plugin_config\"),\n    consumer_groups = require(\"apisix.admin.consumer_group\"),\n    secrets         = require(\"apisix.admin.secrets\"),\n}\n\n\nlocal _M = {version = 0.4}\nlocal router\n\n\nlocal function check_token(ctx)\n    local local_conf = core.config.local_conf()\n\n    -- check if admin_key is required\n    if local_conf.deployment.admin.admin_key_required == false then\n        return true\n    end\n\n    local admin_key = core.table.try_read_attr(local_conf, \"deployment\", \"admin\", \"admin_key\")\n    if not admin_key then\n        return true\n    end\n\n    local req_token = ctx.var.arg_api_key or ctx.var.http_x_api_key\n                      or ctx.var.cookie_x_api_key\n    if not req_token then\n        return false, \"missing apikey\"\n    end\n\n    local admin\n    for i, row in ipairs(admin_key) do\n        if req_token == row.key then\n            admin = row\n            break\n        end\n    end\n\n    if not admin then\n        return false, \"wrong apikey\"\n    end\n\n    if admin.role == \"viewer\" and\n       not viewer_methods[str_lower(get_method())] then\n        return false, \"invalid method for role viewer\"\n    end\n\n    return true\nend\n\n-- Set the `apictx` variable and check admin api token, if the check fails, the current\n-- request will be interrupted and an error response will be returned.\n--\n-- NOTE: This is a higher wrapper for `check_token` function.\nlocal function set_ctx_and_check_token()\n    local api_ctx = {}\n    core.ctx.set_vars_meta(api_ctx)\n    ngx.ctx.api_ctx = api_ctx\n\n    local ok, err = check_token(api_ctx)\n    if not ok then\n        core.log.warn(\"failed to check token: \", err)\n        core.response.exit(401, { error_msg = \"failed to check token\", description = err })\n    end\nend\n\n\nlocal function strip_etcd_resp(data)\n    if type(data) == \"table\"\n        and data.header ~= nil\n        and data.header.revision ~= nil\n        and data.header.raft_term ~= nil\n    then\n        -- strip etcd data\n        data.header = nil\n        data.responses = nil\n        data.succeeded = nil\n\n        if data.node then\n            data.node.createdIndex = nil\n            data.node.modifiedIndex = nil\n        end\n\n        data.count = nil\n        data.more = nil\n        data.prev_kvs = nil\n\n        if data.deleted then\n            -- We used to treat the type incorrectly. But for compatibility we follow\n            -- the existing type.\n            data.deleted = tostring(data.deleted)\n        end\n    end\n\n    return data\nend\n\n\nlocal function head()\n    core.response.exit(200)\nend\n\n\nlocal function run()\n    set_ctx_and_check_token()\n\n    local uri_segs = core.utils.split_uri(ngx.var.uri)\n    core.log.info(\"uri: \", core.json.delay_encode(uri_segs))\n\n    -- /apisix/admin/schema/route\n    local seg_res, seg_id = uri_segs[4], uri_segs[5]\n    local seg_sub_path = core.table.concat(uri_segs, \"/\", 6)\n    if seg_res == \"schema\" and seg_id == \"plugins\" then\n        -- /apisix/admin/schema/plugins/limit-count\n        seg_res, seg_id = uri_segs[5], uri_segs[6]\n        seg_sub_path = core.table.concat(uri_segs, \"/\", 7)\n    end\n\n    if seg_res == \"stream_routes\" then\n        local local_conf = core.config.local_conf()\n        if local_conf.apisix.proxy_mode ~= \"stream\" and\n           local_conf.apisix.proxy_mode ~= \"http&stream\" then\n            core.log.warn(\"stream mode is disabled, can not add any stream \",\n                          \"routes\")\n            core.response.exit(400, {error_msg = \"stream mode is disabled, \" ..\n                               \"can not add stream routes\"})\n        end\n    end\n\n    if seg_res == \"consumers\" and #uri_segs >= 6 and uri_segs[6] == \"credentials\" then\n        seg_sub_path = seg_id .. \"/\" .. seg_sub_path\n        seg_res = uri_segs[6]\n        seg_id = uri_segs[7]\n    end\n\n    local resource = resources[seg_res]\n    if not resource then\n        core.response.exit(404, {error_msg = \"Unsupported resource type: \".. seg_res})\n    end\n\n    local method = str_lower(get_method())\n    if not resource[method] then\n        core.response.exit(404, {error_msg = \"not found\"})\n    end\n\n    local req_body, err = core.request.get_body(MAX_REQ_BODY)\n    if err then\n        core.log.error(\"failed to read request body: \", err)\n        core.response.exit(400, {error_msg = \"invalid request body: \" .. err})\n    end\n\n    if req_body then\n        local data, err = core.json.decode(req_body)\n        if err then\n            core.log.error(\"invalid request body: \", req_body, \" err: \", err)\n            core.response.exit(400, {error_msg = \"invalid request body: \" .. err,\n                                     req_body = req_body})\n        end\n\n        req_body = data\n    end\n\n    local uri_args = ngx.req.get_uri_args() or {}\n    if uri_args.ttl then\n        if not tonumber(uri_args.ttl) then\n            core.response.exit(400, {error_msg = \"invalid argument ttl: \"\n                                                 .. \"should be a number\"})\n        end\n    end\n\n    local code, data\n    if seg_res == \"schema\" or seg_res == \"plugins\" then\n        code, data = resource[method](seg_id, req_body, seg_sub_path, uri_args)\n    else\n        code, data = resource[method](resource, seg_id, req_body, seg_sub_path, uri_args)\n    end\n\n    if code then\n        if code == 200 and method == \"get\" and plugin.enable_gde() then\n            if seg_res == \"consumers\" or seg_res == \"credentials\" then\n                utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_CONSUMER)\n            elseif seg_res == \"plugin_metadata\" then\n                utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_METADATA)\n            else\n                utils.decrypt_params(plugin.decrypt_conf, data)\n            end\n        end\n\n        if v3_adapter.enable_v3() then\n            core.response.set_header(\"X-API-VERSION\", \"v3\")\n        else\n            core.response.set_header(\"X-API-VERSION\", \"v2\")\n        end\n\n        data = v3_adapter.filter(data, resource)\n        data = strip_etcd_resp(data)\n\n        core.response.exit(code, data)\n    end\nend\n\n\nlocal function get_plugins_list()\n    set_ctx_and_check_token()\n    local args = get_uri_args()\n    local subsystem = args[\"subsystem\"]\n    -- If subsystem is passed then it should be either http or stream.\n    -- If it is not passed/nil then http will be default.\n    subsystem = subsystem or \"http\"\n    if subsystem == \"http\" or subsystem == \"stream\" then\n        local plugins = resources.plugins.get_plugins_list(subsystem)\n        core.response.exit(200, plugins)\n    end\n    core.response.exit(400,\"invalid subsystem passed\")\nend\n\n-- Handle unsupported request methods for the virtual \"reload\" plugin\nlocal function unsupported_methods_reload_plugin()\n    set_ctx_and_check_token()\n\n    core.response.exit(405, {\n        error_msg = \"please use PUT method to reload the plugins, \"\n                    .. get_method() .. \" method is not allowed.\"\n    })\nend\n\n\nlocal function post_reload_plugins()\n    set_ctx_and_check_token()\n\n    local success, err = events:post(reload_event, get_method(), ngx_time())\n    if not success then\n        core.response.exit(503, err)\n    end\n\n    core.response.exit(200, \"done\")\nend\n\n\nlocal function plugins_eq(old, new)\n    local old_set = {}\n    for _, p in ipairs(old) do\n        old_set[p.name] = p\n    end\n\n    local new_set = {}\n    for _, p in ipairs(new) do\n        new_set[p.name] = p\n    end\n\n    return core.table.set_eq(old_set, new_set)\nend\n\n\nlocal function sync_local_conf_to_etcd(reset)\n    local local_conf = core.config.local_conf()\n\n    local plugins = {}\n    for _, name in ipairs(local_conf.plugins) do\n        core.table.insert(plugins, {\n            name = name,\n        })\n    end\n\n    for _, name in ipairs(local_conf.stream_plugins) do\n        core.table.insert(plugins, {\n            name = name,\n            stream = true,\n        })\n    end\n\n    if reset then\n        local res, err = core.etcd.get(\"/plugins\")\n        if not res then\n            core.log.error(\"failed to get current plugins: \", err)\n            return\n        end\n\n        if res.status == 404 then\n            -- nothing need to be reset\n            return\n        end\n\n        if res.status ~= 200 then\n            core.log.error(\"failed to get current plugins, status: \", res.status)\n            return\n        end\n\n        local stored_plugins = res.body.node.value\n        local revision = res.body.node.modifiedIndex\n        if plugins_eq(stored_plugins, plugins) then\n            core.log.info(\"plugins not changed, don't need to reset\")\n            return\n        end\n\n        core.log.warn(\"sync local conf to etcd\")\n\n        local res, err = core.etcd.atomic_set(\"/plugins\", plugins, nil, revision)\n        if not res then\n            core.log.error(\"failed to set plugins: \", err)\n        end\n\n        return\n    end\n\n    core.log.warn(\"sync local conf to etcd\")\n\n    -- need to store all plugins name into one key so that it can be updated atomically\n    local res, err = core.etcd.set(\"/plugins\", plugins)\n    if not res then\n        core.log.error(\"failed to set plugins: \", err)\n    end\nend\n\n\nlocal function reload_plugins(data, event, source, pid)\n    core.log.info(\"start to hot reload plugins\")\n    plugin.load()\n\n    if ngx_worker_id() == 0 then\n        sync_local_conf_to_etcd()\n    end\nend\n\n\nlocal function schema_validate()\n    local uri_segs = core.utils.split_uri(ngx.var.uri)\n    core.log.info(\"uri: \", core.json.delay_encode(uri_segs))\n\n    local seg_res = uri_segs[6]\n    local resource = resources[seg_res]\n    if not resource then\n        core.response.exit(404, {error_msg = \"Unsupported resource type: \".. seg_res})\n    end\n\n    local req_body, err = core.request.get_body(MAX_REQ_BODY)\n    if err then\n        core.log.error(\"failed to read request body: \", err)\n        core.response.exit(400, {error_msg = \"invalid request body: \" .. err})\n    end\n\n    if req_body then\n        local data, err = core.json.decode(req_body)\n        if err then\n            core.log.error(\"invalid request body: \", req_body, \" err: \", err)\n            core.response.exit(400, {error_msg = \"invalid request body: \" .. err,\n                                     req_body = req_body})\n        end\n\n        req_body = data\n    end\n\n    local ok, err = core.schema.check(resource.schema, req_body)\n    if ok then\n        core.response.exit(200)\n    end\n    core.response.exit(400, {error_msg = err})\nend\n\n\nlocal function standalone_run()\n    set_ctx_and_check_token()\n    return standalone.run()\nend\n\n\nlocal http_head_route = {\n    paths = [[/apisix/admin]],\n    methods = {\"HEAD\"},\n    handler = head,\n}\n\n\nlocal uri_route = {\n    http_head_route,\n    {\n        paths = [[/apisix/admin/*]],\n        methods = {\"GET\", \"PUT\", \"POST\", \"DELETE\", \"PATCH\"},\n        handler = run,\n    },\n    {\n        paths = [[/apisix/admin/plugins/list]],\n        methods = {\"GET\"},\n        handler = get_plugins_list,\n    },\n    {\n        paths = [[/apisix/admin/schema/validate/*]],\n        methods = {\"POST\"},\n        handler = schema_validate,\n    },\n    {\n        paths = reload_event,\n        methods = {\"PUT\"},\n        handler = post_reload_plugins,\n    },\n    -- Handle methods other than \"PUT\" on \"/plugin/reload\" to inform user\n    {\n        paths = reload_event,\n        methods = { \"GET\", \"POST\", \"DELETE\", \"PATCH\" },\n        handler = unsupported_methods_reload_plugin,\n    },\n}\n\n\nlocal standalone_uri_route = {\n    http_head_route,\n    {\n        paths = [[/apisix/admin/configs]],\n        methods = {\"GET\", \"PUT\", \"HEAD\"},\n        handler = standalone_run,\n    },\n    {\n        paths = [[/apisix/admin/configs/validate]],\n        methods = {\"POST\"},\n        handler = standalone_run,\n    },\n}\n\n\nfunction _M.init_worker()\n    local local_conf = core.config.local_conf()\n    if not local_conf.apisix or not local_conf.apisix.enable_admin then\n        return\n    end\n\n    local is_yaml_config_provider = local_conf.deployment.config_provider == \"yaml\"\n\n    if is_yaml_config_provider then\n        router = route.new(standalone_uri_route)\n        standalone.init_worker()\n    else\n        router = route.new(uri_route)\n    end\n\n    -- register reload plugin handler\n    events = require(\"apisix.events\")\n    events:register(reload_plugins, reload_event, \"PUT\")\n\n    if ngx_worker_id() == 0 then\n        -- check if admin_key is required\n        if local_conf.deployment.admin.admin_key_required == false then\n            core.log.warn(\"Admin key is bypassed! \",\n                \"If you are deploying APISIX in a production environment, \",\n                \"please enable `admin_key_required` and set a secure admin key!\")\n        end\n\n        if is_yaml_config_provider then -- standalone mode does not need sync to etcd\n            return\n        end\n\n        local ok, err = ngx_timer_at(0, function(premature)\n            if premature then\n                return\n            end\n\n            -- try to reset the /plugins to the current configuration in the admin\n            sync_local_conf_to_etcd(true)\n        end)\n\n        if not ok then\n            error(\"failed to sync local configure to etcd: \" .. err)\n        end\n    end\nend\n\n\nfunction _M.get()\n    return router\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/admin/plugin_config.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal get_routes = require(\"apisix.router\").http_routes\nlocal resource = require(\"apisix.admin.resource\")\nlocal schema_plugin = require(\"apisix.admin.plugins\").check_schema\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\nlocal type = type\nlocal tostring = tostring\nlocal ipairs = ipairs\n\n\nlocal function check_conf(id, conf, need_id, schema)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    local ok, err = schema_plugin(conf.plugins)\n    if not ok then\n        return nil, {error_msg = err}\n    end\n\n    return true\nend\n\n\nlocal function encrypt_conf(id, conf)\n    plugins_encrypt_conf(conf.plugins)\nend\n\n\nlocal function delete_checker(id)\n    local routes, routes_ver = get_routes()\n    if routes_ver and routes then\n        for _, route in ipairs(routes) do\n            if type(route) == \"table\" and route.value\n               and route.value.plugin_config_id\n               and tostring(route.value.plugin_config_id) == id then\n                return 400, {error_msg = \"can not delete this plugin config,\"\n                                         .. \" route [\" .. route.value.id\n                                         .. \"] is still using it now\"}\n            end\n        end\n    end\n\n    return nil, nil\nend\n\n\nreturn resource.new({\n    name = \"plugin_configs\",\n    kind = \"plugin config\",\n    schema = core.schema.plugin_config,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    unsupported_methods = {\"post\"},\n    delete_checker = delete_checker\n})\n"
  },
  {
    "path": "apisix/admin/plugin_metadata.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal pcall   = pcall\nlocal require = require\nlocal core    = require(\"apisix.core\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal plugin_encrypt_conf = require(\"apisix.plugin\").encrypt_conf\n\nlocal injected_mark = \"injected metadata_schema\"\n\n\nlocal function validate_plugin(name)\n    local pkg_name = \"apisix.plugins.\" .. name\n    local ok, plugin_object = pcall(require, pkg_name)\n    if ok then\n        return true, plugin_object\n    end\n\n    pkg_name = \"apisix.stream.plugins.\" .. name\n    return pcall(require, pkg_name)\nend\n\n\nlocal function inject_metadata_schema(plugin_object)\n    if not plugin_object.metadata_schema then\n        plugin_object.metadata_schema = {\n            type = \"object\",\n            ['$comment'] = injected_mark,\n            properties = {},\n        }\n    end\nend\n\n\nlocal function check_conf(plugin_name, conf)\n    if not plugin_name then\n        return nil, {error_msg = \"missing plugin name\"}\n    end\n\n    local ok, plugin_object = validate_plugin(plugin_name)\n    if not ok then\n        return nil, {error_msg = \"invalid plugin name\"}\n    end\n\n    inject_metadata_schema(plugin_object)\n\n    local schema = plugin_object.metadata_schema\n\n    local ok, err\n    if schema['$comment'] == injected_mark\n      -- check_schema is not required. If missing, fallback to check schema directly\n      or not plugin_object.check_schema\n    then\n        ok, err = core.schema.check(schema, conf)\n    else\n        ok, err = plugin_object.check_schema(conf, core.schema.TYPE_METADATA)\n    end\n\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    return plugin_name\nend\n\nlocal function encrypt_conf(plugin_name, conf)\n    if not plugin_name then\n        -- This situation shouldn't happen according to the execution order.\n        core.log.info(\"missing plugin name\")\n        return\n    end\n\n    local ok, plugin_object = validate_plugin(plugin_name)\n    if not ok then\n        -- This situation shouldn't happen according to the execution order.\n        core.log.info(\"invalid plugin name\")\n        return\n    end\n\n    inject_metadata_schema(plugin_object)\n\n    local schema = plugin_object.metadata_schema\n    if schema['$comment'] ~= injected_mark and plugin_object.check_schema then\n        plugin_encrypt_conf(plugin_name, conf, core.schema.TYPE_METADATA)\n    end\nend\n\n\nreturn resource.new({\n    name = \"plugin_metadata\",\n    kind = \"plugin_metadata\",\n    schema = core.schema.plugin_metadata,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    unsupported_methods = {\"post\", \"patch\"}\n})\n"
  },
  {
    "path": "apisix/admin/plugins.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require   = require\nlocal core = require(\"apisix.core\")\nlocal check_schema = require(\"apisix.plugin\").check_schema\nlocal ipairs    = ipairs\nlocal table_sort = table.sort\nlocal table_insert = table.insert\nlocal get_uri_args = ngx.req.get_uri_args\nlocal plugin_get_all = require(\"apisix.plugin\").get_all\nlocal plugin_get_http = require(\"apisix.plugin\").get\nlocal plugin_get_stream = require(\"apisix.plugin\").get_stream\nlocal encrypt_conf = require(\"apisix.plugin\").encrypt_conf\nlocal pairs = pairs\n\nlocal _M = {}\n\n\nfunction _M.check_schema(plugins_conf, schema_type)\n    return check_schema(plugins_conf, schema_type, false)\nend\n\n\nfunction _M.encrypt_conf(plugins_conf, schema_type)\n    if plugins_conf then\n        for name, conf in pairs(plugins_conf) do\n            encrypt_conf(name, conf, schema_type)\n        end\n    end\nend\n\n\nfunction _M.get(name)\n    local arg = get_uri_args()\n    -- If subsystem is passed inside args then it should be oneOf: http / stream.\n    local subsystem = arg[\"subsystem\"] or \"http\"\n    if subsystem ~= \"http\" and subsystem ~= \"stream\" then\n        return 400, {error_msg = \"unsupported subsystem: \"..subsystem}\n    end\n\n    -- arg all to be deprecated\n    if (arg and arg[\"all\"] == \"true\") then\n        core.log.warn(\"query parameter \\\"all\\\" will be deprecated soon.\")\n        local http_plugins, stream_plugins = plugin_get_all({\n            version = true,\n            priority = true,\n            schema = true,\n            metadata_schema = true,\n            consumer_schema = true,\n            type = true,\n            scope = true,\n        })\n\n        if arg[\"subsystem\"] == \"stream\" then\n            return 200, stream_plugins\n        end\n\n        return 200, http_plugins\n    end\n\n    local plugin\n\n    if subsystem == \"http\"  then\n        plugin = plugin_get_http(name)\n    else\n        plugin = plugin_get_stream(name)\n    end\n\n    if not plugin then\n        local err = \"plugin not found in subsystem \" .. subsystem\n        core.log.warn(err)\n        return 404, {error_msg = err}\n    end\n\n    local json_schema = plugin.schema\n    if arg and arg[\"schema_type\"] == \"consumer\" then\n        json_schema = plugin.consumer_schema\n    end\n\n    if not json_schema then\n        return 400, {error_msg = \"not found schema\"}\n    end\n\n    return 200, json_schema\nend\n\n\nfunction _M.get_plugins_list(subsystem)\n    local http_plugins\n    local stream_plugins\n    if subsystem == \"http\" then\n        http_plugins = core.config.local_conf().plugins\n    else\n        stream_plugins = core.config.local_conf().stream_plugins\n    end\n\n    local priorities = {}\n    local success = {}\n    if http_plugins then\n        for i, name in ipairs(http_plugins) do\n            local plugin = plugin_get_http(name)\n            if plugin and plugin.priority then\n                priorities[name] = plugin.priority\n                table_insert(success, name)\n            end\n        end\n    end\n\n    if stream_plugins then\n        for i, name in ipairs(stream_plugins) do\n            local plugin = plugin_get_stream(name)\n            if plugin and plugin.priority then\n                priorities[name] = plugin.priority\n                table_insert(success, name)\n            end\n        end\n    end\n\n    local function cmp(x, y)\n        return priorities[x] > priorities[y]\n    end\n\n    table_sort(success, cmp)\n    return success\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/admin/proto.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal type = type\nlocal ipairs = ipairs\nlocal core = require(\"apisix.core\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal get_routes = require(\"apisix.router\").http_routes\nlocal get_services = require(\"apisix.http.service\").services\nlocal compile_proto = require(\"apisix.plugins.grpc-transcode.proto\").compile_proto\nlocal tostring = tostring\n\n\nlocal function check_conf(id, conf, need_id, schema)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    local ok, err = compile_proto(conf.content)\n    if not ok then\n        return nil, {error_msg = \"invalid content: \" .. err}\n    end\n\n    return true\nend\n\n\nlocal function check_proto_used(plugins, deleting, ptype, pid)\n\n    --core.log.info(\"check_proto_used plugins: \", core.json.delay_encode(plugins, true))\n    --core.log.info(\"check_proto_used deleting: \", deleting)\n    --core.log.info(\"check_proto_used ptype: \", ptype)\n    --core.log.info(\"check_proto_used pid: \", pid)\n\n    if plugins then\n        if type(plugins) == \"table\" and plugins[\"grpc-transcode\"]\n           and plugins[\"grpc-transcode\"].proto_id\n           and tostring(plugins[\"grpc-transcode\"].proto_id) == deleting then\n            return false, {error_msg = \"can not delete this proto, \"\n                                     .. ptype .. \" [\" .. pid\n                                     .. \"] is still using it now\"}\n        end\n    end\n    return true\nend\n\nlocal function delete_checker(id)\n    core.log.info(\"proto delete: \", id)\n\n    local routes, routes_ver = get_routes()\n\n    core.log.info(\"routes: \", core.json.delay_encode(routes, true))\n    core.log.info(\"routes_ver: \", routes_ver)\n\n    if routes_ver and routes then\n        for _, route in ipairs(routes) do\n            core.log.info(\"proto delete route item: \", core.json.delay_encode(route, true))\n            if type(route) == \"table\" and route.value and route.value.plugins then\n                local ret, err = check_proto_used(route.value.plugins, id, \"route\",route.value.id)\n                if not ret then\n                    return 400, err\n                end\n            end\n        end\n    end\n    core.log.info(\"proto delete route ref check pass: \", id)\n\n    local services, services_ver = get_services()\n\n    core.log.info(\"services: \", core.json.delay_encode(services, true))\n    core.log.info(\"services_ver: \", services_ver)\n\n    if services_ver and services then\n        for _, service in ipairs(services) do\n            if type(service) == \"table\" and service.value and service.value.plugins then\n                local ret, err = check_proto_used(service.value.plugins, id,\n                                                \"service\", service.value.id)\n                if not ret then\n                    return 400, err\n                end\n            end\n        end\n    end\n    core.log.info(\"proto delete service ref check pass: \", id)\n\n    return nil, nil\nend\n\n\nreturn resource.new({\n    name = \"protos\",\n    kind = \"proto\",\n    schema = core.schema.proto,\n    checker = check_conf,\n    unsupported_methods = {\"patch\"},\n    delete_checker = delete_checker\n})\n"
  },
  {
    "path": "apisix/admin/resource.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal utils = require(\"apisix.admin.utils\")\nlocal apisix_ssl = require(\"apisix.ssl\")\nlocal apisix_consumer = require(\"apisix.consumer\")\nlocal tbl_deepcopy = require(\"apisix.core.table\").deepcopy\nlocal setmetatable = setmetatable\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal type = type\n\n\nlocal _M = {\n    list_filter_fields = {},\n}\nlocal mt = {\n    __index = _M\n}\n\n\nlocal no_id_res = {\n    consumers = true,\n    plugin_metadata = true\n}\n\n\nlocal function split_typ_and_id(id, sub_path)\n    local uri_segs = core.utils.split_uri(sub_path)\n    local typ = id\n    local id = nil\n    if #uri_segs > 0 then\n        id = uri_segs[1]\n    end\n    return typ, id\nend\n\n\nlocal function check_forbidden_properties(conf, forbidden_properties)\n    local not_allow_properties = \"the property is forbidden: \"\n\n    if conf then\n        for _, v in ipairs(forbidden_properties) do\n            if conf[v] then\n                return not_allow_properties .. \" \" .. v\n            end\n        end\n\n        if conf.upstream then\n            for _, v in ipairs(forbidden_properties) do\n                if conf.upstream[v] then\n                    return not_allow_properties .. \" upstream.\" .. v\n                end\n            end\n        end\n\n        if conf.plugins then\n            for _, v in ipairs(forbidden_properties) do\n                if conf.plugins[v] then\n                    return not_allow_properties .. \" plugins.\" .. v\n                end\n            end\n        end\n    end\n\n    return nil\nend\n\n\nfunction _M:check_conf(id, conf, need_id, typ, allow_time)\n    if self.name == \"secrets\" then\n        id = typ .. \"/\" .. id\n    end\n    -- check if missing configurations\n    if not conf then\n        return nil, {error_msg = \"missing configurations\"}\n    end\n\n    -- check id if need id\n    if not no_id_res[self.name] then\n        id = id or conf.id\n        if need_id and not id then\n            return nil, {error_msg = \"missing \".. self.kind .. \" id\"}\n        end\n\n        if not need_id and id then\n            return nil, {error_msg = \"wrong \".. self.kind .. \" id, do not need it\"}\n        end\n\n        if need_id and conf.id and tostring(conf.id) ~= tostring(id) then\n            return nil, {error_msg = \"wrong \".. self.kind .. \" id\"}\n        end\n\n        conf.id = id\n    end\n\n    -- check create time and update time\n    if not allow_time then\n        local forbidden_properties = {\"create_time\", \"update_time\"}\n        local err = check_forbidden_properties(conf, forbidden_properties)\n        if err then\n            return nil, {error_msg = err}\n        end\n    end\n\n    -- check the resource own rules\n    if self.name ~= \"secrets\" then\n        core.log.info(\"schema: \", core.json.delay_encode(self.schema))\n    end\n\n    local conf_for_check = tbl_deepcopy(conf)\n    local ok, err = self.checker(id, conf_for_check, need_id, self.schema, {secret_type = typ})\n\n    if self.encrypt_conf then\n        self.encrypt_conf(id, conf)\n    end\n\n    if not ok then\n        return ok, err\n    else\n        if no_id_res[self.name] then\n            return ok\n        else\n            return need_id and id or true\n        end\n    end\nend\n\n\nfunction _M:get(id, conf, sub_path)\n    if core.table.array_find(self.unsupported_methods, \"get\") then\n        return 405, {error_msg = \"not supported `GET` method for \" .. self.kind}\n    end\n\n    local key = \"/\" .. self.name\n    local typ = nil\n    if self.name == \"secrets\" then\n        key = key .. \"/\"\n        typ, id = split_typ_and_id(id, sub_path)\n    end\n\n    if id then\n        if self.name == \"secrets\" then\n            key = key .. typ\n        end\n        key = key .. \"/\" .. id\n    end\n\n    -- some resources(consumers) have sub resources(credentials),\n    -- the key format of sub resources will differ from the main resource\n    if self.get_resource_etcd_key then\n        key = self.get_resource_etcd_key(id, conf, sub_path)\n    end\n\n    local res, err = core.etcd.get(key, not id)\n    if not res then\n        core.log.error(\"failed to get \", self.kind, \"[\", key, \"] from etcd: \", err)\n        return 503, {error_msg = err}\n    end\n\n    if self.name == \"ssls\" then\n        -- not return private key for security\n        if res.body and res.body.node and res.body.node.value then\n            res.body.node.value.key = nil\n        end\n    end\n\n    -- consumers etcd range response will include credentials, so need to filter out them\n    if self.name == \"consumers\" and res.body.list then\n        res.body.list = apisix_consumer.filter_consumers_list(res.body.list)\n        res.body.total = #res.body.list\n    end\n\n    utils.fix_count(res.body, id)\n    return res.status, res.body\nend\n\n\nfunction _M:post(id, conf, sub_path, args)\n    if core.table.array_find(self.unsupported_methods, \"post\") then\n        return 405, {error_msg = \"not supported `POST` method for \" .. self.kind}\n    end\n\n    local id, err = self:check_conf(id, conf, false)\n    if not id then\n        return 400, err\n    end\n\n    if self.name == \"ssls\" then\n        -- encrypt private key\n        conf.key = apisix_ssl.aes_encrypt_pkey(conf.key)\n\n        if conf.keys then\n            for i = 1, #conf.keys do\n                conf.keys[i] = apisix_ssl.aes_encrypt_pkey(conf.keys[i])\n            end\n        end\n    end\n\n    local key = \"/\" .. self.name\n    utils.inject_timestamp(conf)\n\n    local ttl = nil\n    if args then\n        ttl = args.ttl\n    end\n\n    local res, err = core.etcd.push(key, conf, ttl)\n    if not res then\n        core.log.error(\"failed to post \", self.kind, \"[\", key, \"] to etcd: \", err)\n        return 503, {error_msg = err}\n    end\n\n    return res.status, res.body\nend\n\n\nfunction _M:put(id, conf, sub_path, args)\n    if core.table.array_find(self.unsupported_methods, \"put\") then\n        return 405, {error_msg = \"not supported `PUT` method for \" .. self.kind}\n    end\n\n    local key = \"/\" .. self.name\n    local typ = nil\n    if self.name == \"secrets\" then\n        typ, id = split_typ_and_id(id, sub_path)\n        key = key .. \"/\" .. typ\n    end\n\n    local need_id = not no_id_res[self.name]\n    local ok, err = self:check_conf(id, conf, need_id, typ)\n    if not ok then\n        return 400, err\n    end\n\n    if self.name ~= \"secrets\" then\n        id = ok\n    end\n\n    if self.name == \"ssls\" then\n        -- encrypt private key\n        conf.key = apisix_ssl.aes_encrypt_pkey(conf.key)\n\n        if conf.keys then\n            for i = 1, #conf.keys do\n                conf.keys[i] = apisix_ssl.aes_encrypt_pkey(conf.keys[i])\n            end\n        end\n    end\n\n    key = key .. \"/\" .. id\n\n    if self.get_resource_etcd_key then\n        key = self.get_resource_etcd_key(id, conf, sub_path, args)\n    end\n\n    if self.name == \"credentials\" then\n        local consumer_key = apisix_consumer.get_consumer_key_from_credential_key(key)\n        local res, err = core.etcd.get(consumer_key, false)\n        if not res then\n            return 503, {error_msg = err}\n        end\n        if res.status == 404 then\n            return res.status, {error_msg = \"consumer not found\"}\n        end\n        if res.status ~= 200 then\n            core.log.debug(\"failed to get consumer for the credential, credential key: \", key,\n                \", consumer key: \", consumer_key, \", res.status: \", res.status)\n            return res.status, {error_msg = \"failed to get the consumer\"}\n        end\n    end\n\n    if self.name ~= \"plugin_metadata\" then\n        local ok, err = utils.inject_conf_with_prev_conf(self.kind, key, conf)\n        if not ok then\n            return 503, {error_msg = err}\n        end\n    else\n        conf.id = id\n    end\n\n    local ttl = nil\n    if args then\n        ttl = args.ttl\n    end\n\n    local res, err = core.etcd.set(key, conf, ttl)\n    if not res then\n        core.log.error(\"failed to put \", self.kind, \"[\", key, \"] to etcd: \", err)\n        return 503, {error_msg = err}\n    end\n\n    return res.status, res.body\nend\n\n-- Keep the unused conf to make the args list consistent with other methods\nfunction _M:delete(id, conf, sub_path, uri_args)\n    if core.table.array_find(self.unsupported_methods, \"delete\") then\n        return 405, {error_msg = \"not supported `DELETE` method for \" .. self.kind}\n    end\n\n    local key = \"/\" .. self.name\n    local typ = nil\n    if self.name == \"secrets\" then\n        typ, id = split_typ_and_id(id, sub_path)\n    end\n\n    if not id then\n        return 400, {error_msg = \"missing \" .. self.kind .. \" id\"}\n    end\n\n    -- core.log.error(\"failed to delete \", self.kind, \"[\", key, \"] in etcd: \", err)\n\n    if self.name == \"secrets\" then\n        key = key .. \"/\" .. typ\n    end\n\n    key = key .. \"/\" .. id\n\n    if self.get_resource_etcd_key then\n        key = self.get_resource_etcd_key(id, conf, sub_path, uri_args)\n    end\n\n    if self.delete_checker and uri_args.force ~= \"true\" then\n        local code, err = self.delete_checker(id)\n        if err then\n            return code, err\n        end\n    end\n\n    if self.name == \"consumers\" then\n        local res, err = core.etcd.rmdir(key .. \"/credentials/\")\n        if not res then\n            return 503, {error_msg = err}\n        end\n    end\n\n    local res, err = core.etcd.delete(key)\n    if not res then\n        core.log.error(\"failed to delete \", self.kind, \"[\", key, \"] in etcd: \", err)\n        return 503, {error_msg = err}\n    end\n\n    return res.status, res.body\nend\n\n\nfunction _M:patch(id, conf, sub_path, args)\n    if core.table.array_find(self.unsupported_methods, \"patch\") then\n        return 405, {error_msg = \"not supported `PATCH` method for \" .. self.kind}\n    end\n\n    local key = \"/\" .. self.name\n    local typ = nil\n    if self.name == \"secrets\" then\n        local uri_segs = core.utils.split_uri(sub_path)\n        if #uri_segs < 1 then\n            return 400, {error_msg = \"no secret id\"}\n        end\n        typ = id\n        id = uri_segs[1]\n        sub_path = core.table.concat(uri_segs, \"/\", 2)\n    end\n\n    if not id then\n        return 400, {error_msg = \"missing \" .. self.kind .. \" id\"}\n    end\n\n    if self.name == \"secrets\" then\n        key = key .. \"/\" .. typ\n    end\n\n    key = key .. \"/\" .. id\n\n    if conf == nil then\n        return 400, {error_msg = \"missing new configuration\"}\n    end\n\n    if not sub_path or sub_path == \"\" then\n        if type(conf) ~= \"table\"  then\n            return 400, {error_msg = \"invalid configuration\"}\n        end\n    end\n\n    local res_old, err = core.etcd.get(key)\n    if not res_old then\n        core.log.error(\"failed to get \", self.kind, \" [\", key, \"] in etcd: \", err)\n        return 503, {error_msg = err}\n    end\n\n    if res_old.status ~= 200 then\n        return res_old.status, res_old.body\n    end\n    core.log.info(\"key: \", key, \" old value: \", core.json.delay_encode(res_old, true))\n\n    local node_value = res_old.body.node.value\n    local modified_index = res_old.body.node.modifiedIndex\n\n    if sub_path and sub_path ~= \"\" then\n        if self.name == \"ssls\" then\n            if sub_path == \"key\" then\n                conf = apisix_ssl.aes_encrypt_pkey(conf)\n            elseif sub_path == \"keys\" then\n                for i = 1, #conf do\n                    conf[i] = apisix_ssl.aes_encrypt_pkey(conf[i])\n                end\n            end\n        end\n        local code, err, node_val = core.table.patch(node_value, sub_path, conf)\n        node_value = node_val\n        if code then\n            return code, {error_msg = err}\n        end\n        utils.inject_timestamp(node_value, nil, true)\n    else\n        if self.name == \"ssls\" then\n            if conf.key then\n                conf.key = apisix_ssl.aes_encrypt_pkey(conf.key)\n            end\n\n            if conf.keys then\n                for i = 1, #conf.keys do\n                    conf.keys[i] = apisix_ssl.aes_encrypt_pkey(conf.keys[i])\n                end\n            end\n        end\n        node_value = core.table.merge(node_value, conf)\n        utils.inject_timestamp(node_value, nil, conf)\n    end\n\n    core.log.info(\"new conf: \", core.json.delay_encode(node_value, true))\n\n    local ok, err = self:check_conf(id, node_value, true, typ, true)\n    if not ok then\n        return 400, err\n    end\n\n    local ttl = nil\n    if args then\n        ttl = args.ttl\n    end\n\n    local res, err = core.etcd.atomic_set(key, node_value, ttl, modified_index)\n    if not res then\n        core.log.error(\"failed to set new \", self.kind, \"[\", key, \"] to etcd: \", err)\n        return 503, {error_msg = err}\n    end\n\n    return res.status, res.body\nend\n\n\nfunction _M.new(opt)\n    return setmetatable(opt, mt)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/admin/routes.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal expr = require(\"resty.expr.v1\")\nlocal core = require(\"apisix.core\")\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal schema_plugin = require(\"apisix.admin.plugins\").check_schema\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\nlocal type = type\nlocal loadstring = loadstring\nlocal ipairs = ipairs\nlocal jp = require(\"jsonpath\")\n\nlocal function validate_post_arg(node)\n    if type(node) ~= \"table\" then\n        return true\n    end\n\n    -- Handle post_arg conditions\n    if #node >= 3 and type(node[1]) == \"string\" and node[1]:find(\"^post_arg%.\") then\n        local key = node[1]\n        local json_path = \"$.\" .. key:sub(11)  -- Remove \"post_arg.\" prefix\n        local _, err = jp.parse(json_path)\n        if err then\n            return false, err\n        end\n        return true\n    end\n\n    for _, child in ipairs(node) do\n        local ok, err = validate_post_arg(child)\n        if not ok then\n            return false, err\n        end\n    end\n    return true\nend\n\n\nlocal function check_conf(id, conf, need_id, schema, opts)\n    opts = opts or {}\n    if conf.host and conf.hosts then\n        return nil, {error_msg = \"only one of host or hosts is allowed\"}\n    end\n\n    if conf.remote_addr and conf.remote_addrs then\n        return nil, {error_msg = \"only one of remote_addr or remote_addrs is \"\n                                 .. \"allowed\"}\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    local upstream_conf = conf.upstream\n    if upstream_conf then\n        local ok, err = apisix_upstream.check_upstream_conf(upstream_conf)\n        if not ok then\n            return nil, {error_msg = err}\n        end\n    end\n\n    local upstream_id = conf.upstream_id\n    if upstream_id and not opts.skip_references_check then\n        local key = \"/upstreams/\" .. upstream_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch upstream info by \"\n                                     .. \"upstream id [\" .. upstream_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch upstream info by \"\n                                     .. \"upstream id [\" .. upstream_id .. \"], \"\n                                     .. \"response code: \" .. res.status}\n        end\n    end\n\n    local service_id = conf.service_id\n    if service_id and not opts.skip_references_check then\n        local key = \"/services/\" .. service_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch service info by \"\n                                     .. \"service id [\" .. service_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch service info by \"\n                                     .. \"service id [\" .. service_id .. \"], \"\n                                     .. \"response code: \" .. res.status}\n        end\n    end\n\n    local plugin_config_id = conf.plugin_config_id\n    if plugin_config_id and not opts.skip_references_check then\n        local key = \"/plugin_configs/\" .. plugin_config_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch plugin config info by \"\n                                     .. \"plugin config id [\" .. plugin_config_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch plugin config info by \"\n                                     .. \"plugin config id [\" .. plugin_config_id .. \"], \"\n                                     .. \"response code: \" .. res.status}\n        end\n    end\n\n    if conf.plugins then\n        local ok, err = schema_plugin(conf.plugins)\n        if not ok then\n            return nil, {error_msg = err}\n        end\n    end\n\n    if conf.vars then\n        ok, err = expr.new(conf.vars)\n        if not ok then\n            return nil, {error_msg = \"failed to validate the 'vars' expression: \" .. err}\n        end\n    end\n\n    ok, err = validate_post_arg(conf.vars)\n    if not ok  then\n        return nil, {error_msg = \"failed to validate the 'vars' expression: \" ..\n                                    err}\n    end\n\n    if conf.filter_func then\n        local func, err = loadstring(\"return \" .. conf.filter_func)\n        if not func then\n            return nil, {error_msg = \"failed to load 'filter_func' string: \"\n                                     .. err}\n        end\n\n        if type(func()) ~= \"function\" then\n            return nil, {error_msg = \"'filter_func' should be a function\"}\n        end\n    end\n\n    if conf.script then\n        local obj, err = loadstring(conf.script)\n        if not obj then\n            return nil, {error_msg = \"failed to load 'script' string: \"\n                                     .. err}\n        end\n\n        if type(obj()) ~= \"table\" then\n            return nil, {error_msg = \"'script' should be a Lua object\"}\n        end\n    end\n\n    return true\nend\n\n\nlocal function encrypt_conf(id, conf)\n    apisix_upstream.encrypt_conf(conf.upstream)\n    plugins_encrypt_conf(conf.plugins)\nend\n\n\nreturn resource.new({\n    name = \"routes\",\n    kind = \"route\",\n    schema = core.schema.route,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    list_filter_fields = {\n        service_id = true,\n        upstream_id = true,\n    },\n})\n"
  },
  {
    "path": "apisix/admin/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\nlocal _M = {\n    version = 0.1,\n}\n\n\nfunction _M.get(name)\n    local json_schema = core.schema[name]\n    core.log.info(\"schema: \", core.json.delay_encode(core.schema, true))\n    if not json_schema then\n        return 400, {error_msg = \"not found schema: \" .. name}\n    end\n\n    return 200, json_schema\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/admin/secrets.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\n\nlocal core = require(\"apisix.core\")\nlocal resource = require(\"apisix.admin.resource\")\n\nlocal pcall = pcall\n\n\nlocal function check_conf(id, conf, need_id, schema, opts)\n    opts = opts or {}\n    if not opts.secret_type then\n        return nil, {error_msg = \"missing secret type\"}\n    end\n    local ok, secret_manager = pcall(require, \"apisix.secret.\" .. opts.secret_type)\n    if not ok then\n        return false, {error_msg = \"invalid secret manager: \" .. opts.secret_type}\n    end\n\n    local ok, err = core.schema.check(secret_manager.schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    return true\nend\n\n\nreturn resource.new({\n    name = \"secrets\",\n    kind = \"secret\",\n    checker = check_conf,\n    unsupported_methods = {\"post\"}\n})\n"
  },
  {
    "path": "apisix/admin/services.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal get_routes = require(\"apisix.router\").http_routes\nlocal get_stream_routes = require(\"apisix.router\").stream_routes\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal schema_plugin = require(\"apisix.admin.plugins\").check_schema\nlocal plugins_encrypt_conf = require(\"apisix.admin.plugins\").encrypt_conf\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal type = type\nlocal loadstring = loadstring\n\n\nlocal function check_conf(id, conf, need_id, schema, opts)\n    opts = opts or {}\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    if need_id and not id then\n        return nil, {error_msg = \"wrong type of service id\"}\n    end\n\n    local upstream_conf = conf.upstream\n    if upstream_conf then\n        local ok, err = apisix_upstream.check_upstream_conf(upstream_conf)\n        if not ok then\n            return nil, {error_msg = err}\n        end\n    end\n\n    local upstream_id = conf.upstream_id\n    if upstream_id and not opts.skip_references_check then\n        local key = \"/upstreams/\" .. upstream_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch upstream info by \"\n                                     .. \"upstream id [\" .. upstream_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch upstream info by \"\n                                     .. \"upstream id [\" .. upstream_id .. \"], \"\n                                     .. \"response code: \" .. res.status}\n        end\n    end\n\n    if conf.plugins then\n        local ok, err = schema_plugin(conf.plugins)\n        if not ok then\n            return nil, {error_msg = err}\n        end\n    end\n\n    if conf.script then\n        local obj, err = loadstring(conf.script)\n        if not obj then\n            return nil, {error_msg = \"failed to load 'script' string: \"\n                                     .. err}\n        end\n\n        if type(obj()) ~= \"table\" then\n            return nil, {error_msg = \"'script' should be a Lua object\"}\n        end\n    end\n\n    return true\nend\n\n\nlocal function delete_checker(id)\n    local routes, routes_ver = get_routes()\n    core.log.info(\"routes: \", core.json.delay_encode(routes, true))\n    core.log.info(\"routes_ver: \", routes_ver)\n    if routes_ver and routes then\n        for _, route in ipairs(routes) do\n            if type(route) == \"table\" and route.value\n               and route.value.service_id\n               and tostring(route.value.service_id) == id then\n                return 400, {error_msg = \"can not delete this service directly,\"\n                                         .. \" route [\" .. route.value.id\n                                         .. \"] is still using it now\"}\n            end\n        end\n    end\n\n    local stream_routes, stream_routes_ver = get_stream_routes()\n    core.log.info(\"stream_routes: \", core.json.delay_encode(stream_routes, true))\n    core.log.info(\"stream_routes_ver: \", stream_routes_ver)\n    if stream_routes_ver and stream_routes then\n        for _, route in ipairs(stream_routes) do\n            if type(route) == \"table\" and route.value\n               and route.value.service_id\n               and tostring(route.value.service_id) == id then\n                return 400, {error_msg = \"can not delete this service directly,\"\n                                         .. \" stream_route [\" .. route.value.id\n                                         .. \"] is still using it now\"}\n            end\n        end\n    end\n\n    return nil, nil\nend\n\n\nlocal function encrypt_conf(id, conf)\n    apisix_upstream.encrypt_conf(conf.upstream)\n    plugins_encrypt_conf(conf.plugins)\nend\n\n\nreturn resource.new({\n    name = \"services\",\n    kind = \"service\",\n    schema = core.schema.service,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    delete_checker = delete_checker,\n})\n"
  },
  {
    "path": "apisix/admin/ssl.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core              = require(\"apisix.core\")\nlocal resource          = require(\"apisix.admin.resource\")\nlocal apisix_ssl        = require(\"apisix.ssl\")\n\n\nlocal function check_conf(id, conf, need_id, schema, opts)\n    local ok, err = apisix_ssl.check_ssl_conf(false, conf)\n    if not ok then\n        return nil, {error_msg = err}\n    end\n\n    return need_id and id or true\nend\n\n\nreturn resource.new({\n    name = \"ssls\",\n    kind = \"ssl\",\n    schema = core.schema.ssl,\n    checker = check_conf\n})\n"
  },
  {
    "path": "apisix/admin/standalone.lua",
    "content": "-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal type         = type\nlocal pairs        = pairs\nlocal ipairs       = ipairs\nlocal str_lower    = string.lower\nlocal str_find     = string.find\nlocal str_sub      = string.sub\nlocal tostring     = tostring\nlocal ngx          = ngx\nlocal pcall        = pcall\nlocal ngx_time     = ngx.time\nlocal get_method   = ngx.req.get_method\nlocal shared_dict  = ngx.shared[\"standalone-config\"]\nlocal timer_every  = ngx.timer.every\nlocal exiting      = ngx.worker.exiting\nlocal table_insert = table.insert\nlocal yaml         = require(\"lyaml\")\nlocal events       = require(\"apisix.events\")\nlocal core         = require(\"apisix.core\")\nlocal config_yaml  = require(\"apisix.core.config_yaml\")\nlocal tbl_deepcopy = require(\"apisix.core.table\").deepcopy\nlocal constants    = require(\"apisix.constants\")\n\n-- combine all resources that using in http and stream substreams as one constant\nlocal CONF_VERSION_KEY_SUFFIX = \"_conf_version\"\nlocal ALL_RESOURCE_KEYS = {}\nfor dir in pairs(constants.HTTP_ETCD_DIRECTORY) do\n    local key = str_sub(dir, 2)\n    ALL_RESOURCE_KEYS[key] = key .. CONF_VERSION_KEY_SUFFIX\nend\nfor dir in pairs(constants.STREAM_ETCD_DIRECTORY) do\n    local key = str_sub(dir, 2)\n    ALL_RESOURCE_KEYS[key] = key .. CONF_VERSION_KEY_SUFFIX\nend\n\nlocal EVENT_UPDATE = \"standalone-api-configuration-update\"\nlocal NOT_FOUND_ERR = \"not found\"\n-- do not use the HTTP standard Last-Modified header to prevent affecting\n-- the caching implementation in the client\nlocal METADATA_LAST_MODIFIED = \"X-Last-Modified\"\nlocal METADATA_DIGEST = \"X-Digest\"\n\nlocal _M = {}\n\nlocal resources = {\n    routes          = require(\"apisix.admin.routes\"),\n    services        = require(\"apisix.admin.services\"),\n    upstreams       = require(\"apisix.admin.upstreams\"),\n    consumers       = require(\"apisix.admin.consumers\"),\n    credentials     = require(\"apisix.admin.credentials\"),\n    schema          = require(\"apisix.admin.schema\"),\n    ssls            = require(\"apisix.admin.ssl\"),\n    plugins         = require(\"apisix.admin.plugins\"),\n    protos          = require(\"apisix.admin.proto\"),\n    global_rules    = require(\"apisix.admin.global_rules\"),\n    stream_routes   = require(\"apisix.admin.stream_routes\"),\n    plugin_metadata = require(\"apisix.admin.plugin_metadata\"),\n    plugin_configs  = require(\"apisix.admin.plugin_config\"),\n    consumer_groups = require(\"apisix.admin.consumer_group\"),\n    secrets         = require(\"apisix.admin.secrets\"),\n}\n\nlocal function check_duplicate(item, key, id_set)\n    local identifier, identifier_type\n    if key == \"consumers\" then\n        identifier = item.id or item.username\n        identifier_type = item.id and \"credential id\" or \"username\"\n    else\n        identifier = item.id\n        identifier_type = \"id\"\n    end\n\n    if id_set[identifier] then\n        return true, \"found duplicate \" .. identifier_type .. \" \" .. identifier .. \" in \" .. key\n    end\n    id_set[identifier] = true\n    return false\nend\n\nlocal function get_config()\n    local config = shared_dict:get(\"config\")\n    if not config then\n        return nil, NOT_FOUND_ERR\n    end\n\n    local err\n    config, err = core.json.decode(config)\n    if not config then\n        return nil, \"failed to decode json: \" .. err\n    end\n    return config\nend\n\n\nlocal function update_and_broadcast_config(apisix_yaml)\n    local raw, err = core.json.encode(apisix_yaml)\n    if not raw then\n        core.log.error(\"failed to encode json: \", err)\n        return nil, \"failed to encode json: \" .. err\n    end\n\n    if shared_dict then\n        -- the worker that handles Admin API calls is responsible for writing the shared dict\n        local ok, err = shared_dict:set(\"config\", raw)\n        if not ok then\n            return nil, \"failed to save config to shared dict: \" .. err\n        end\n        core.log.info(\"standalone config updated: \", raw)\n    else\n        core.log.crit(config_yaml.ERR_NO_SHARED_DICT)\n    end\n    return events:post(EVENT_UPDATE, EVENT_UPDATE)\nend\n\nlocal function check_conf(checker, schema, item, typ)\n    if not checker then\n        return true\n    end\n    local str_id = tostring(item.id)\n    if typ == \"consumers\" and\n        core.string.find(str_id, \"/credentials/\") then\n        local credential_checker = resources.credentials.checker\n        local credential_schema = resources.credentials.schema\n        return credential_checker(item.id, item, false, credential_schema, {\n            skip_references_check = true,\n        })\n    end\n\n    local secret_type\n    if typ == \"secrets\" then\n        local idx = str_find(str_id or \"\", \"/\")\n        if not idx then\n            return false, {\n                error_msg = \"invalid secret id: \" .. (str_id or \"\")\n            }\n        end\n        secret_type = str_sub(str_id, 1, idx - 1)\n    end\n    return checker(item.id, item, false, schema, {\n        secret_type = secret_type,\n        skip_references_check = true,\n    })\nend\n\n\nlocal function validate_configuration(req_body, collect_all_errors)\n    local is_valid = true\n    local validation_results = {}\n\n    for key, conf_version_key in pairs(ALL_RESOURCE_KEYS) do\n        local items = req_body[key]\n        local resource = resources[key] or {}\n\n        -- Validate conf_version_key if present\n        local new_conf_version = req_body[conf_version_key]\n        if new_conf_version and type(new_conf_version) ~= \"number\" then\n            if not collect_all_errors then\n                return false, conf_version_key .. \" must be a number\"\n            end\n            is_valid = false\n            table_insert(validation_results, {\n                resource_type = key,\n                error = conf_version_key .. \" must be a number, got \" .. type(new_conf_version)\n            })\n        end\n\n        if items and #items > 0 then\n            local item_schema = resource.schema\n            local item_checker = resource.checker\n            local id_set = {}\n\n            for index, item in ipairs(items) do\n                local item_temp = tbl_deepcopy(item)\n                local valid, err = check_conf(item_checker, item_schema, item_temp, key)\n                if not valid then\n                    local err_prefix = \"invalid \" .. key .. \" at index \" .. (index - 1) .. \", err: \"\n                    local err_msg = type(err) == \"table\" and err.error_msg or err\n                    local error_msg = err_prefix .. err_msg\n\n                    if not collect_all_errors then\n                        return false, error_msg\n                    end\n                    is_valid = false\n                    table_insert(validation_results, {\n                        resource_type = key,\n                        index = index - 1,\n                        error = error_msg\n                    })\n                end\n\n                -- check for duplicate IDs\n                local duplicated, dup_err = check_duplicate(item, key, id_set)\n                if duplicated then\n                    if not collect_all_errors then\n                        return false, dup_err\n                    end\n                    is_valid = false\n                    table_insert(validation_results, {\n                        resource_type = key,\n                        index = index - 1,\n                        error = dup_err\n                    })\n                end\n            end\n        end\n    end\n\n    if collect_all_errors then\n        return is_valid, validation_results\n    end\n\n    return is_valid, nil\nend\n\nlocal function validate(ctx)\n    local content_type = core.request.header(nil, \"content-type\") or \"application/json\"\n    local req_body, err = core.request.get_body()\n    if err then\n        return core.response.exit(400, {error_msg = \"invalid request body: \" .. err})\n    end\n\n    if not req_body or #req_body <= 0 then\n        return core.response.exit(400, {error_msg = \"invalid request body: empty request body\"})\n    end\n\n    local data\n    if core.string.has_prefix(content_type, \"application/yaml\") then\n        local ok, result = pcall(yaml.load, req_body, { all = false })\n        if not ok or type(result) ~= \"table\" then\n            err = \"invalid yaml request body\"\n        else\n            data = result\n        end\n    else\n        data, err = core.json.decode(req_body)\n    end\n\n    if err then\n        core.log.error(\"invalid request body: \", req_body, \" err: \", err)\n        return core.response.exit(400, {error_msg = \"invalid request body: \" .. err})\n    end\n\n    local valid, validation_results = validate_configuration(data, true)\n    if not valid then\n        return core.response.exit(400, {\n            error_msg = \"Configuration validation failed\",\n            errors = validation_results\n        })\n    end\n\n    return core.response.exit(200)\nend\n\nlocal function update(ctx)\n    -- check digest header existence\n    local digest = core.request.header(nil, METADATA_DIGEST)\n    if not digest then\n        return core.response.exit(400, {\n            error_msg = \"missing digest header\"\n        })\n    end\n\n    -- read the request body\n    local content_type = core.request.header(nil, \"content-type\") or \"application/json\"\n    local req_body, err = core.request.get_body()\n    if err then\n        return core.response.exit(400, {error_msg = \"invalid request body: \" .. err})\n    end\n\n    if not req_body or #req_body <= 0 then\n        return core.response.exit(400, {error_msg = \"invalid request body: empty request body\"})\n    end\n\n    -- parse the request body\n    local data\n    if core.string.has_prefix(content_type, \"application/yaml\") then\n        data = yaml.load(req_body, { all = false })\n        if not data or type(data) ~= \"table\" then\n            err = \"invalid yaml request body\"\n        end\n    else\n        data, err = core.json.decode(req_body)\n    end\n    if err then\n        core.log.error(\"invalid request body: \", req_body, \" err: \", err)\n        core.response.exit(400, {error_msg = \"invalid request body: \" .. err})\n    end\n    req_body = data\n\n    local config, err = get_config()\n    if err and err ~= NOT_FOUND_ERR then\n        core.log.error(\"failed to get config from shared dict: \", err)\n        return core.response.exit(500, {\n            error_msg = \"failed to get config from shared dict: \" .. err\n        })\n    end\n\n    -- if the client passes in the same digest, the configuration is not updated\n    if config and config[METADATA_DIGEST] == digest then\n        -- accepted but not modified because digest is the same\n        core.log.info(\"config not changed: same digest\")\n        return core.response.exit(204)\n    end\n\n    local valid, error_msg = validate_configuration(req_body, false)\n    if not valid then\n        return core.response.exit(400, { error_msg = error_msg })\n    end\n\n    -- check input by jsonschema and build the final config\n    local apisix_yaml = {}\n\n    for key, conf_version_key in pairs(ALL_RESOURCE_KEYS) do\n        local conf_version = config and config[conf_version_key] or 0\n        local items = req_body[key]\n        local new_conf_version = req_body[conf_version_key]\n\n        if new_conf_version then\n            if new_conf_version < conf_version then\n                return core.response.exit(400, {\n                    error_msg = conf_version_key ..\n                        \" must be greater than or equal to (\" .. conf_version .. \")\",\n                })\n            end\n        else\n            new_conf_version = conf_version + 1\n        end\n\n        apisix_yaml[conf_version_key] = new_conf_version\n        if new_conf_version == conf_version then\n            apisix_yaml[key] = config and config[key]\n        elseif items and #items > 0 then\n            apisix_yaml[key] = items\n        end\n    end\n\n    -- write metadata\n    apisix_yaml[METADATA_LAST_MODIFIED] = ngx_time()\n    apisix_yaml[METADATA_DIGEST] = digest\n\n    local ok, err = update_and_broadcast_config(apisix_yaml)\n    if not ok then\n        core.response.exit(500, err)\n    end\n\n    core.response.set_header(METADATA_LAST_MODIFIED, apisix_yaml[METADATA_LAST_MODIFIED])\n    core.response.set_header(METADATA_DIGEST, apisix_yaml[METADATA_DIGEST])\n    return core.response.exit(202)\nend\n\nlocal function get(ctx)\n    local accept = core.request.header(nil, \"accept\") or \"application/json\"\n    local want_yaml_resp = core.string.has_prefix(accept, \"application/yaml\")\n\n    local config, err = get_config()\n    if not config then\n        if err ~= NOT_FOUND_ERR then\n            core.log.error(\"failed to get config from shared_dict: \", err)\n            return core.response.exit(500, {\n                error_msg = \"failed to get config from shared_dict: \" .. err\n            })\n        end\n        config = {}\n        for _, conf_version_key in pairs(ALL_RESOURCE_KEYS) do\n            config[conf_version_key] = 0\n        end\n    end\n\n    local resp, err\n    if want_yaml_resp then\n        core.response.set_header(\"Content-Type\", \"application/yaml\")\n        resp = yaml.dump({ config })\n        if not resp then\n            err = \"failed to encode yaml\"\n        end\n\n        -- remove the first line \"---\" and the last line \"...\"\n        -- because the yaml.dump() will add them for multiple documents\n        local m = ngx.re.match(resp, [[^---\\s*([\\s\\S]*?)\\s*\\.\\.\\.\\s*$]], \"jo\")\n        if m and m[1] then\n            resp = m[1]\n        end\n    else\n        core.response.set_header(\"Content-Type\", \"application/json\")\n        resp, err = core.json.encode(config, true)\n        if not resp then\n            err = \"failed to encode json: \" .. err\n        end\n    end\n\n    if not resp then\n        return core.response.exit(500, {error_msg = err})\n    end\n\n    core.response.set_header(METADATA_LAST_MODIFIED, config and config[METADATA_LAST_MODIFIED])\n    core.response.set_header(METADATA_DIGEST, config and config[METADATA_DIGEST])\n    return core.response.exit(200, resp)\nend\n\nlocal function head(ctx)\n    local config, err = get_config()\n    if not config then\n        if err ~= NOT_FOUND_ERR then\n            core.log.error(\"failed to get config from shared_dict: \", err)\n            return core.response.exit(500, {\n                error_msg = \"failed to get config from shared_dict: \" .. err\n            })\n        end\n    end\n\n    core.response.set_header(METADATA_LAST_MODIFIED, config and config[METADATA_LAST_MODIFIED])\n    core.response.set_header(METADATA_DIGEST, config and config[METADATA_DIGEST])\n    return core.response.exit(200)\nend\n\nfunction _M.run()\n    local ctx = ngx.ctx.api_ctx\n    local method = str_lower(get_method())\n    if method == \"put\" then\n        return update(ctx)\n    end\n\n    if method == \"post\" then\n        local path = ctx.var.uri\n        if path == \"/apisix/admin/configs/validate\" then\n            return validate(ctx)\n        else\n            return core.response.exit(404, {error_msg = \"Not found\"})\n        end\n    end\n\n    if method == \"head\" then\n        return head(ctx)\n    end\n\n    return get(ctx)\nend\nlocal patch_schema\ndo\n    local resource_schema = {\n        \"proto\",\n        \"global_rule\",\n        \"route\",\n        \"stream_route\",\n        \"service\",\n        \"upstream\",\n        \"consumer\",\n        \"consumer_group\",\n        \"credential\",\n        \"ssl\",\n        \"plugin_config\",\n    }\n    local function attach_modifiedIndex_schema(name)\n        local schema = core.schema[name]\n        if not schema then\n            core.log.error(\"schema for \", name, \" not found\")\n            return\n        end\n        if schema.properties and not schema.properties.modifiedIndex then\n            schema.properties.modifiedIndex = {\n                type = \"integer\",\n            }\n        end\n    end\n\n    local function patch_credential_schema()\n        local credential_schema = core.schema[\"credential\"]\n        if credential_schema and credential_schema.properties then\n            credential_schema.properties.id = {\n                type = \"string\",\n                minLength = 15,\n                maxLength = 128,\n                pattern = [[^[a-zA-Z0-9-_]+/credentials/[a-zA-Z0-9-_.]+$]],\n            }\n        end\n    end\n\n    function patch_schema()\n        -- attach modifiedIndex schema to all resource schemas\n        for _, name in ipairs(resource_schema) do\n            attach_modifiedIndex_schema(name)\n        end\n        -- patch credential schema\n        patch_credential_schema()\n    end\nend\n\n\nfunction _M.init_worker()\n    local function update_config(config)\n        if not config then\n            local err\n            config, err = get_config()\n            if not config then\n                core.log.error(\"failed to get config: \", err)\n                return\n            end\n        end\n\n        -- remove metadata key in-place\n        -- this table is generated by json decode, so there is no need to clone it\n        config[METADATA_LAST_MODIFIED] = nil\n        config[METADATA_DIGEST] = nil\n        config_yaml._update_config(config)\n    end\n    events:register(update_config, EVENT_UPDATE, EVENT_UPDATE)\n\n    -- due to the event module can not broadcast events between http and stream subsystems,\n    -- we need to poll the shared dict to keep the config in sync\n    local last_modified_per_worker\n    timer_every(1, function ()\n        if not exiting() then\n            local config, err = get_config()\n            if not config then\n                if err ~= NOT_FOUND_ERR then\n                    core.log.error(\"failed to get config: \", err)\n                end\n            else\n                local last_modified = config[METADATA_LAST_MODIFIED]\n                if last_modified_per_worker ~= last_modified then\n                    update_config(config)\n                    last_modified_per_worker = last_modified\n                end\n            end\n        end\n    end)\n\n    patch_schema()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/admin/stream_routes.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal stream_route_checker = require(\"apisix.stream.router.ip_port\").stream_route_checker\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal type = type\n\n\nlocal function check_conf(id, conf, need_id, schema, opts)\n    opts = opts or {}\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, {error_msg = \"invalid configuration: \" .. err}\n    end\n\n    local upstream_id = conf.upstream_id\n    if upstream_id and not opts.skip_references_check then\n        local key = \"/upstreams/\" .. upstream_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch upstream info by \"\n                                     .. \"upstream id [\" .. upstream_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch upstream info by \"\n                                     .. \"upstream id [\" .. upstream_id .. \"], \"\n                                     .. \"response code: \" .. res.status}\n        end\n    end\n\n    local service_id = conf.service_id\n    if service_id and not opts.skip_references_check then\n        local key = \"/services/\" .. service_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch service info by \"\n                    .. \"service id [\" .. service_id .. \"]: \"\n                    .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch service info by \"\n                    .. \"service id [\" .. service_id .. \"], \"\n                    .. \"response code: \" .. res.status}\n        end\n    end\n\n    if conf.protocol and conf.protocol.superior_id and not opts.skip_references_check then\n        local superior_id = conf.protocol.superior_id\n        local key = \"/stream_routes/\" .. superior_id\n        local res, err = core.etcd.get(key)\n        if not res then\n            return nil, {error_msg = \"failed to fetch stream routes[\" .. superior_id .. \"]: \"\n                                     .. err}\n        end\n\n        if res.status ~= 200 then\n            return nil, {error_msg = \"failed to fetch stream routes[\" .. superior_id\n                                     .. \"], response code: \" .. res.status}\n        end\n\n        local superior_route = res.body.node.value\n        if type(superior_route) == \"string\" then\n            superior_route = core.json.decode(superior_route)\n        end\n\n        if superior_route and superior_route.protocol\n           and superior_route.protocol.name ~= conf.protocol.name then\n            return nil, {error_msg = \"protocol mismatch: subordinate protocol [\"\n                                     .. conf.protocol.name .. \"] does not match superior protocol [\"\n                                     .. superior_route.protocol.name .. \"]\"}\n        end\n    end\n\n    local ok, err = stream_route_checker(conf, true)\n    if not ok then\n        return nil, {error_msg = err}\n    end\n\n    return true\nend\n\n\nlocal function delete_checker(id)\n    local key = \"/stream_routes\"\n    local res, err = core.etcd.get(key, {prefix = true})\n    if not res then\n        return nil, {error_msg = \"failed to fetch stream routes: \" .. err}\n    end\n\n    if res.status ~= 200 then\n        return nil, {error_msg = \"failed to fetch stream routes, response code: \" .. res.status}\n    end\n\n    local nodes = res.body.list\n    if not nodes then\n        if res.body.node and res.body.node.nodes then\n            nodes = res.body.node.nodes\n        end\n    end\n\n    if not nodes then\n        return true\n    end\n\n    for _, item in ipairs(nodes) do\n        local route = item.value\n        if type(route) == \"string\" then\n            route = core.json.decode(route)\n        end\n\n        if route and route.protocol and tostring(route.protocol.superior_id) == id then\n            return 400, {error_msg = \"can not delete this stream route directly, stream route [\"\n                                     .. route.id .. \"] is still using it as superior_id\"}\n        end\n    end\n\n    return true\nend\n\n\nreturn resource.new({\n    name = \"stream_routes\",\n    kind = \"stream route\",\n    schema = core.schema.stream_route,\n    checker = check_conf,\n    delete_checker = delete_checker,\n    unsupported_methods = { \"patch\" },\n    list_filter_fields = {\n        service_id = true,\n        upstream_id = true,\n    },\n})\n"
  },
  {
    "path": "apisix/admin/upstreams.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal config_util = require(\"apisix.core.config_util\")\nlocal get_routes = require(\"apisix.router\").http_routes\nlocal get_services = require(\"apisix.http.service\").services\nlocal get_plugin_configs = require(\"apisix.plugin_config\").plugin_configs\nlocal get_consumers = require(\"apisix.consumer\").consumers\nlocal get_consumer_groups = require(\"apisix.consumer_group\").consumer_groups\nlocal get_global_rules = require(\"apisix.global_rules\").global_rules\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal resource = require(\"apisix.admin.resource\")\nlocal tostring = tostring\nlocal ipairs = ipairs\n\n\nlocal function check_conf(id, conf, need_id)\n    local ok, err = apisix_upstream.check_upstream_conf(conf)\n    if not ok then\n        return nil, {error_msg = err}\n    end\n\n    return true\nend\n\n\nlocal function encrypt_conf(id, conf)\n    apisix_upstream.encrypt_conf(conf)\nend\n\n\nlocal function up_id_in_plugins(plugins, up_id)\n    if plugins and plugins[\"traffic-split\"]\n        and plugins[\"traffic-split\"].rules then\n\n        for _, rule in ipairs(plugins[\"traffic-split\"].rules) do\n            local plugin_upstreams = rule.weighted_upstreams\n            for _, plugin_upstream in ipairs(plugin_upstreams) do\n                if plugin_upstream.upstream_id\n                    and tostring(plugin_upstream.upstream_id) == up_id then\n                     return true\n                end\n            end\n        end\n\n        return false\n    end\nend\n\n\nlocal function check_resources_reference(resources, up_id,\n                                         only_check_plugin, resources_name)\n    if resources then\n        for _, resource in config_util.iterate_values(resources) do\n            if resource and resource.value then\n                if up_id_in_plugins(resource.value.plugins, up_id) then\n                    return {error_msg = \"can not delete this upstream,\"\n                                        .. \" plugin in \"\n                                        .. resources_name .. \" [\"\n                                        .. resource.value.id\n                                        .. \"] is still using it now\"}\n                end\n\n                if not only_check_plugin and resource.value.upstream_id\n                    and tostring(resource.value.upstream_id) == up_id then\n                     return {error_msg = \"can not delete this upstream, \"\n                                         .. resources_name .. \" [\" .. resource.value.id\n                                         .. \"] is still using it now\"}\n                end\n            end\n        end\n    end\nend\n\n\nlocal function delete_checker(id)\n    local routes = get_routes()\n    local err_msg = check_resources_reference(routes, id, false, \"route\")\n    if err_msg then\n        return 400, err_msg\n    end\n\n    local services, services_ver = get_services()\n    core.log.info(\"services: \", core.json.delay_encode(services, true))\n    core.log.info(\"services_ver: \", services_ver)\n    local err_msg = check_resources_reference(services, id, false, \"service\")\n    if err_msg then\n        return 400, err_msg\n    end\n\n    local plugin_configs = get_plugin_configs()\n    local err_msg = check_resources_reference(plugin_configs, id, true, \"plugin_config\")\n    if err_msg then\n        return 400, err_msg\n    end\n\n    local consumers = get_consumers()\n    local err_msg = check_resources_reference(consumers, id, true, \"consumer\")\n    if err_msg then\n        return 400, err_msg\n    end\n\n    local consumer_groups = get_consumer_groups()\n    local err_msg = check_resources_reference(consumer_groups, id, true, \"consumer_group\")\n    if err_msg then\n        return 400, err_msg\n    end\n\n    local global_rules = get_global_rules()\n    err_msg = check_resources_reference(global_rules, id, true, \"global_rules\")\n    if err_msg then\n        return 400, err_msg\n    end\n\n    return nil, nil\nend\n\n\nreturn resource.new({\n    name = \"upstreams\",\n    kind = \"upstream\",\n    schema = core.schema.upstream,\n    checker = check_conf,\n    encrypt_conf = encrypt_conf,\n    delete_checker = delete_checker\n})\n"
  },
  {
    "path": "apisix/admin/utils.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core    = require(\"apisix.core\")\nlocal ngx_time = ngx.time\nlocal tonumber = tonumber\nlocal ipairs = ipairs\nlocal pairs = pairs\n\n\nlocal _M = {}\n\n\nlocal function inject_timestamp(conf, prev_conf, patch_conf)\n    if not conf.create_time then\n        if prev_conf and (prev_conf.node or prev_conf.list).value.create_time then\n            conf.create_time = (prev_conf.node or prev_conf.list).value.create_time\n        else\n            -- As we don't know existent data's create_time, we have to pretend\n            -- they are created now.\n            conf.create_time = ngx_time()\n        end\n    end\n\n    if not conf.update_time or\n        -- For PATCH request, the modification is passed as 'patch_conf'\n        -- If the sub path is used, the 'patch_conf' will be a placeholder `true`\n        (patch_conf and (patch_conf == true or patch_conf.update_time == nil))\n    then\n        -- reset the update_time if:\n        -- 1. PATCH request, with sub path\n        -- 2. PATCH request, update_time not given\n        -- 3. Other request, update_time not given\n        conf.update_time = ngx_time()\n    end\nend\n_M.inject_timestamp = inject_timestamp\n\n\nfunction _M.inject_conf_with_prev_conf(kind, key, conf)\n    local res, err = core.etcd.get(key)\n    if not res or (res.status ~= 200 and res.status ~= 404) then\n        core.log.error(\"failed to get \" .. kind .. \"[\", key, \"] from etcd: \", err or res.status)\n        return nil, err\n    end\n\n    if res.status == 404 then\n        inject_timestamp(conf)\n    else\n        inject_timestamp(conf, res.body)\n    end\n\n    return true\nend\n\n\n-- fix_count makes the \"count\" field returned by etcd reasonable\nfunction _M.fix_count(body, id)\n    if body.count then\n        if not id then\n            -- remove the count of placeholder (init_dir)\n            body.count = tonumber(body.count) - 1\n        else\n            body.count = tonumber(body.count)\n        end\n    end\nend\n\n\nfunction _M.decrypt_params(decrypt_func, body, schema_type)\n    -- list\n    if body.list then\n        for _, route in ipairs(body.list) do\n            if route.value and route.value.plugins then\n                for name, conf in pairs(route.value.plugins) do\n                    decrypt_func(name, conf, schema_type)\n                end\n            end\n        end\n        return\n    end\n\n    -- node\n    local plugins = body.node and body.node.value\n                    and body.node.value.plugins\n\n    if plugins then\n        for name, conf in pairs(plugins) do\n            decrypt_func(name, conf, schema_type)\n        end\n    end\n\n    -- metadata\n    local conf = body.node and body.node.value\n\n    if conf and schema_type == core.schema.TYPE_METADATA then\n        decrypt_func(conf.id, conf, schema_type)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/admin/v3_adapter.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal type              = type\nlocal pairs             = pairs\nlocal tonumber          = tonumber\nlocal ngx               = ngx\nlocal re_find           = ngx.re.find\nlocal fetch_local_conf  = require(\"apisix.core.config_local\").local_conf\nlocal try_read_attr     = require(\"apisix.core.table\").try_read_attr\nlocal deepcopy          = require(\"apisix.core.table\").deepcopy\nlocal log               = require(\"apisix.core.log\")\nlocal request           = require(\"apisix.core.request\")\nlocal response          = require(\"apisix.core.response\")\nlocal table             = require(\"apisix.core.table\")\n\nlocal _M = {}\n\n\nlocal admin_api_version\nlocal function enable_v3()\n    if admin_api_version then\n        if admin_api_version == \"v3\" then\n            return true\n        end\n\n        if admin_api_version == \"default\" then\n            return false\n        end\n    end\n\n    local local_conf, err = fetch_local_conf()\n    if not local_conf then\n        admin_api_version = \"default\"\n        log.error(\"failed to fetch local conf: \", err)\n        return false\n    end\n\n    local api_ver = try_read_attr(local_conf, \"deployment\", \"admin\", \"admin_api_version\")\n    if api_ver ~= \"v3\" then\n        admin_api_version = \"default\"\n        return false\n    end\n\n    admin_api_version = api_ver\n    return true\nend\n_M.enable_v3 = enable_v3\n\n\nfunction _M.to_v3(body, action)\n    if not enable_v3() then\n        body.action = action\n    end\nend\n\n\nfunction _M.to_v3_list(body)\n    if not enable_v3() then\n        return\n    end\n\n    if body.node.dir then\n        body.list = body.node.nodes\n        body.node = nil\n    end\nend\n\n\nlocal function sort(l, r)\n    return l.createdIndex < r.createdIndex\nend\n\n\nlocal function pagination(body, args)\n    args.page = tonumber(args.page)\n    args.page_size = tonumber(args.page_size)\n    if not args.page or not args.page_size then\n        return\n    end\n\n    if args.page_size < 10 or args.page_size > 500 then\n        return response.exit(400, \"page_size must be between 10 and 500\")\n    end\n\n    if not args.page or args.page < 1 then\n        -- default page is 1\n        args.page = 1\n    end\n\n    local list = body.list\n\n    -- sort nodes by there createdIndex\n    table.sort(list, sort)\n\n    local to = args.page * args.page_size\n    local from =  to - args.page_size + 1\n\n    local res = table.new(20, 0)\n\n    for i = from, to do\n        if list[i] then\n            res[i - from + 1] = list[i]\n        end\n    end\n\n    body.list = res\nend\n\n\nlocal function _filter(item, args, resource)\n    if not args.filter then\n        return true\n    end\n\n    local filters, err = ngx.decode_args(args.filter or \"\", 100)\n    if not filters then\n        log.error(\"failed to decode filter args: \", err)\n        return false\n    end\n\n    for key, value in pairs(filters) do\n        if not resource.list_filter_fields[key] then\n            log.warn(\"filter field '\", key, \"' is not supported by resource: \", resource.name)\n            goto CONTINUE\n        end\n\n        if not item[key] then\n            return false\n        end\n\n        if type(value) == \"table\" then\n            value = value[#value] -- get the last value in the table\n        end\n\n        if item[key] ~= value then\n            return false\n        end\n\n        ::CONTINUE::\n    end\n\n    return true\nend\n\n\nlocal function filter(body, args, resource)\n    for i = #body.list, 1, -1 do\n        local name_matched = true\n        local label_matched = true\n        local uri_matched = true\n        if args.name then\n            name_matched = false\n            local matched = re_find(body.list[i].value.name, args.name, \"jo\")\n            if matched then\n                name_matched = true\n            end\n        end\n\n        if args.label then\n            label_matched = false\n            if body.list[i].value.labels then\n                for k, _ in pairs(body.list[i].value.labels) do\n                    if k == args.label then\n                        label_matched = true\n                        break\n                    end\n                end\n            end\n        end\n\n        if args.uri then\n            uri_matched = false\n            if body.list[i].value.uri then\n                local matched = re_find(body.list[i].value.uri, args.uri, \"jo\")\n                if matched then\n                    uri_matched = true\n                end\n            end\n\n            if body.list[i].value.uris then\n                for _, uri in pairs(body.list[i].value.uris) do\n                    if re_find(uri, args.uri, \"jo\") then\n                        uri_matched = true\n                        break\n                    end\n                end\n            end\n        end\n\n        if not name_matched or not label_matched or not uri_matched\n                            or not _filter(body.list[i].value, args, resource) then\n            table.remove(body.list, i)\n        end\n    end\nend\n\n\nfunction _M.filter(body, resource)\n    if not enable_v3() then\n        return body\n    end\n\n    local args = request.get_uri_args()\n    local processed_body = deepcopy(body)\n\n    if processed_body.deleted then\n        processed_body.node = nil\n    end\n\n    -- strip node wrapping for single query, create, and update scenarios.\n    if processed_body.node then\n        processed_body = processed_body.node\n    end\n\n    -- filter and paging logic for list query only\n    if processed_body.list then\n        filter(processed_body, args, resource)\n\n        -- calculate the total amount of filtered data\n        processed_body.total = processed_body.list and #processed_body.list or 0\n\n        pagination(processed_body, args)\n\n        -- remove the count field returned by etcd\n        -- we don't need a field that reflects the length of the currently returned data,\n        -- it doesn't make sense\n        processed_body.count = nil\n    end\n\n    return processed_body\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/api_router.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal router = require(\"apisix.utils.router\")\nlocal plugin_mod = require(\"apisix.plugin\")\nlocal core = require(\"apisix.core\")\nlocal ipairs = ipairs\nlocal ngx_header = ngx.header\nlocal type = type\n\n\nlocal _M = {}\nlocal match_opts = {}\nlocal has_route_not_under_apisix\n\n\nlocal fetch_api_router\ndo\n    local routes = {}\nfunction fetch_api_router()\n    core.table.clear(routes)\n\n    has_route_not_under_apisix = false\n\n    for _, plugin in ipairs(plugin_mod.plugins) do\n        local api_fun = plugin.api\n        if api_fun then\n            local api_routes = api_fun()\n            core.log.debug(\"fetched api routes: \",\n                           core.json.delay_encode(api_routes, true))\n            for _, route in ipairs(api_routes) do\n                if route.uri == nil then\n                    core.log.error(\"got nil uri in api route: \",\n                                   core.json.delay_encode(route, true))\n                    break\n                end\n\n                local typ_uri = type(route.uri)\n                if not has_route_not_under_apisix then\n                    if typ_uri == \"string\" then\n                        if not core.string.has_prefix(route.uri, \"/apisix/\") then\n                            has_route_not_under_apisix = true\n                        end\n                    else\n                        for _, uri in ipairs(route.uri) do\n                            if not core.string.has_prefix(uri, \"/apisix/\") then\n                                has_route_not_under_apisix = true\n                                break\n                            end\n                        end\n                    end\n                end\n\n                core.table.insert(routes, {\n                        methods = route.methods,\n                        paths = route.uri,\n                        handler = function (api_ctx)\n                            local code, body = route.handler(api_ctx)\n                            if code or body then\n                                if type(body) == \"table\" and ngx_header[\"Content-Type\"] == nil then\n                                    core.response.set_header(\"Content-Type\", \"application/json\")\n                                end\n\n                                core.response.exit(code, body)\n                            end\n                        end\n                    })\n            end\n        end\n    end\n\n    return router.new(routes)\nend\n\nend -- do\n\n\nfunction _M.has_route_not_under_apisix()\n    if has_route_not_under_apisix == nil then\n        return true\n    end\n\n    return has_route_not_under_apisix\nend\n\n\nfunction _M.match(api_ctx)\n    local api_router = core.lrucache.global(\"api_router\", plugin_mod.load_times, fetch_api_router)\n    if not api_router then\n        core.log.error(\"failed to fetch valid api router\")\n        return false\n    end\n\n    core.table.clear(match_opts)\n    match_opts.method = api_ctx.var.request_method\n\n    local ok = api_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)\n    return ok\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/balancer/chash.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core        = require(\"apisix.core\")\nlocal resty_chash = require(\"resty.chash\")\nlocal str_char    = string.char\nlocal str_gsub    = string.gsub\nlocal pairs = pairs\n\n\nlocal CONSISTENT_POINTS = 160   -- points per server, taken from `resty.chash`\n\n\nlocal _M = {}\n\n\nlocal function fetch_chash_hash_key(ctx, upstream)\n    local key = upstream.key\n    local hash_on = upstream.hash_on or \"vars\"\n    local chash_key\n\n    if hash_on == \"consumer\" then\n        chash_key = ctx.consumer_name\n    elseif hash_on == \"vars\" then\n        chash_key = ctx.var[key]\n    elseif hash_on == \"header\" then\n        chash_key = ctx.var[\"http_\" .. key]\n    elseif hash_on == \"cookie\" then\n        chash_key = ctx.var[\"cookie_\" .. key]\n    elseif hash_on == \"vars_combinations\" then\n        local err, n_resolved\n        chash_key, err, n_resolved = core.utils.resolve_var(key, ctx.var)\n        if err then\n            core.log.error(\"could not resolve vars in \", key, \" error: \", err)\n        end\n\n        if n_resolved == 0 then\n            chash_key = nil\n        end\n    end\n\n    if not chash_key then\n        chash_key = ctx.var[\"remote_addr\"]\n        core.log.warn(\"chash_key fetch is nil, use default chash_key \",\n                      \"remote_addr: \", chash_key)\n    end\n    core.log.info(\"upstream key: \", key)\n    core.log.info(\"hash_on: \", hash_on)\n    core.log.info(\"chash_key: \", core.json.delay_encode(chash_key))\n\n    return chash_key\nend\n\n\nfunction _M.new(up_nodes, upstream)\n    local str_null = str_char(0)\n\n    local nodes_count = 0\n    local safe_limit = 0\n    local gcd = 0\n    local servers, nodes = {}, {}\n\n    for serv, weight in pairs(up_nodes) do\n        if gcd == 0 then\n            gcd = weight\n        else\n            gcd = core.math.gcd(gcd, weight)\n        end\n    end\n\n    if gcd == 0 then\n        -- all nodes' weight are 0\n        gcd = 1\n    end\n\n    for serv, weight in pairs(up_nodes) do\n        local id = str_gsub(serv, \":\", str_null)\n\n        nodes_count = nodes_count + 1\n        weight = weight / gcd\n        safe_limit = safe_limit + weight\n        servers[id] = serv\n        nodes[id] = weight\n    end\n    safe_limit = safe_limit * CONSISTENT_POINTS\n\n    local picker = resty_chash:new(nodes)\n    return {\n        upstream = upstream,\n        get = function (ctx)\n            local id\n            if ctx.balancer_tried_servers then\n                if ctx.balancer_tried_servers_count == nodes_count then\n                    return nil, \"all upstream servers tried\"\n                end\n\n                -- the 'safe_limit' is a best effort limit to prevent infinite loop caused by bug\n                for i = 1, safe_limit do\n                    id, ctx.chash_last_server_index = picker:next(ctx.chash_last_server_index)\n                    if not ctx.balancer_tried_servers[servers[id]] then\n                        break\n                    end\n                end\n            else\n                local chash_key = fetch_chash_hash_key(ctx, upstream)\n                id, ctx.chash_last_server_index = picker:find(chash_key)\n            end\n            -- core.log.warn(\"chash id: \", id, \" val: \", servers[id])\n            return servers[id]\n        end,\n        after_balance = function (ctx, before_retry)\n            if not before_retry then\n                if ctx.balancer_tried_servers then\n                    core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                    ctx.balancer_tried_servers = nil\n                end\n\n                return nil\n            end\n\n            if not ctx.balancer_tried_servers then\n                ctx.balancer_tried_servers = core.tablepool.fetch(\"balancer_tried_servers\", 0, 2)\n            end\n\n            ctx.balancer_tried_servers[ctx.balancer_server] = true\n            ctx.balancer_tried_servers_count = (ctx.balancer_tried_servers_count or 0) + 1\n        end,\n        before_retry_next_priority = function (ctx)\n            if ctx.balancer_tried_servers then\n                core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                ctx.balancer_tried_servers = nil\n            end\n\n            ctx.balancer_tried_servers_count = 0\n        end,\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/balancer/ewma.lua",
    "content": "-- Original Authors: Shiv Nagarajan & Scott Francis\n-- Accessed: March 12, 2018\n-- Inspiration drawn from:\n-- https://github.com/twitter/finagle/blob/1bc837c4feafc0096e43c0e98516a8e1c50c4421\n--   /finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala\nlocal core = require(\"apisix.core\")\nlocal resty_lock = require(\"resty.lock\")\n\nlocal nkeys = core.table.nkeys\nlocal table_insert = core.table.insert\nlocal ngx = ngx\nlocal ngx_shared = ngx.shared\nlocal ngx_now = ngx.now\nlocal math = math\nlocal pairs = pairs\nlocal ipairs = ipairs\nlocal next = next\nlocal error = error\n\nlocal DECAY_TIME = 10 -- this value is in seconds\nlocal SHM_TTL = 60\nlocal LOCK_KEY = \":ewma_key\"\n\nlocal shm_ewma = ngx_shared[\"balancer-ewma\"]\nlocal shm_last_touched_at = ngx_shared[\"balancer-ewma-last-touched-at\"]\n\nlocal lrucache_addr = core.lrucache.new({ttl = 300, count = 1024})\nlocal lrucache_trans_format = core.lrucache.new({ttl = 300, count = 256})\n\nlocal ewma_lock, ewma_lock_err = resty_lock:new(\"balancer-ewma-locks\", {timeout = 0, exptime = 0.1})\n\nlocal _M = {name = \"ewma\"}\n\nlocal function lock(upstream)\n    local _, err = ewma_lock:lock(upstream .. LOCK_KEY)\n    if err and err ~= \"timeout\" then\n        core.log.error(\"EWMA Balancer failed to lock: \", err)\n    end\n\n    return err\nend\n\nlocal function unlock()\n    local ok, err = ewma_lock:unlock()\n    if not ok then\n        core.log.error(\"EWMA Balancer failed to unlock: \", err)\n    end\n\n    return err\nend\n\nlocal function decay_ewma(ewma, last_touched_at, rtt, now)\n    local td = now - last_touched_at\n    td = math.max(td, 0)\n    local weight = math.exp(-td / DECAY_TIME)\n\n    ewma = ewma * weight + rtt * (1.0 - weight)\n    return ewma\nend\n\nlocal function store_stats(upstream, ewma, now)\n    local success, err, forcible = shm_last_touched_at:set(upstream, now, SHM_TTL)\n    if not success then\n        core.log.error(\"shm_last_touched_at:set failed: \", err)\n    end\n    if forcible then\n        core.log.warn(\"shm_last_touched_at:set valid items forcibly overwritten\")\n    end\n\n    success, err, forcible = shm_ewma:set(upstream, ewma, SHM_TTL)\n    if not success then\n        core.log.error(\"shm_ewma:set failed: \", err)\n    end\n    if forcible then\n        core.log.warn(\"shm_ewma:set valid items forcibly overwritten\")\n    end\nend\n\nlocal function get_or_update_ewma(upstream, rtt, update)\n    if update then\n        local lock_err = lock(upstream)\n        if lock_err ~= nil then\n            return 0, lock_err\n        end\n    end\n\n    local ewma = shm_ewma:get(upstream) or 0\n\n    local now = ngx_now()\n    local last_touched_at = shm_last_touched_at:get(upstream) or 0\n    ewma = decay_ewma(ewma, last_touched_at, rtt, now)\n\n    if not update then\n        return ewma, nil\n    end\n\n    store_stats(upstream, ewma, now)\n\n    unlock()\n\n    return ewma, nil\nend\n\nlocal function get_upstream_name(upstream)\n    return upstream.host .. \":\" .. upstream.port\nend\n\nlocal function score(upstream)\n    -- Original implementation used names\n    -- Endpoints don't have names, so passing in IP:Port as key instead\n    local upstream_name = get_upstream_name(upstream)\n    return get_or_update_ewma(upstream_name, 0, false)\nend\n\nlocal function parse_addr(addr)\n    local host, port, err = core.utils.parse_addr(addr)\n    return {host = host, port = port}, err\nend\n\nlocal function _trans_format(up_nodes)\n    -- trans\n    -- {\"1.2.3.4:80\":100,\"5.6.7.8:8080\":100}\n    -- into\n    -- [{\"host\":\"1.2.3.4\",\"port\":\"80\"},{\"host\":\"5.6.7.8\",\"port\":\"8080\"}]\n    local peers = {}\n    local res, err\n\n    for addr, _ in pairs(up_nodes) do\n        res, err = lrucache_addr(addr, nil, parse_addr, addr)\n        if not err then\n            core.table.insert(peers, res)\n        else\n            core.log.error('parse_addr error: ', addr, err)\n        end\n    end\n\n    return next(peers) and peers or nil\nend\n\nlocal function _ewma_find(ctx, up_nodes)\n    local peers\n\n    if not up_nodes or nkeys(up_nodes) == 0 then\n        return nil, 'up_nodes empty'\n    end\n\n    if ctx.balancer_tried_servers and ctx.balancer_tried_servers_count == nkeys(up_nodes) then\n        return nil, \"all upstream servers tried\"\n    end\n\n    peers = lrucache_trans_format(up_nodes, ctx.upstream_version, _trans_format, up_nodes)\n    if not peers then\n        return nil, 'up_nodes trans error'\n    end\n\n    local filtered_peers\n    if ctx.balancer_tried_servers then\n        for _, peer in ipairs(peers) do\n            if not ctx.balancer_tried_servers[get_upstream_name(peer)] then\n                if not filtered_peers then\n                    filtered_peers = {}\n                end\n\n                table_insert(filtered_peers, peer)\n            end\n        end\n    else\n        filtered_peers = peers\n    end\n\n    local endpoint = filtered_peers[1]\n\n    if #filtered_peers > 1 then\n        local a, b = math.random(1, #filtered_peers), math.random(1, #filtered_peers - 1)\n        if b >= a then\n            b = b + 1\n        end\n\n        local backendpoint\n        endpoint, backendpoint = filtered_peers[a], filtered_peers[b]\n        if score(endpoint) > score(backendpoint) then\n            endpoint = backendpoint\n        end\n    end\n\n    return get_upstream_name(endpoint)\nend\n\nlocal function _ewma_after_balance(ctx, before_retry)\n    if before_retry then\n        if not ctx.balancer_tried_servers then\n            ctx.balancer_tried_servers = core.tablepool.fetch(\"balancer_tried_servers\", 0, 2)\n        end\n\n        ctx.balancer_tried_servers[ctx.balancer_server] = true\n        ctx.balancer_tried_servers_count = (ctx.balancer_tried_servers_count or 0) + 1\n\n        return nil\n    end\n\n    if ctx.balancer_tried_servers then\n        core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n        ctx.balancer_tried_servers = nil\n    end\n\n    local response_time = ctx.var.upstream_response_time or 0\n    local connect_time = ctx.var.upstream_connect_time or 0\n    local rtt = connect_time + response_time\n    local upstream = ctx.var.upstream_addr\n\n    if not upstream then\n        return nil, \"no upstream addr found\"\n    end\n\n    return get_or_update_ewma(upstream, rtt, true)\nend\n\nfunction _M.new(up_nodes, upstream)\n    if not shm_ewma or not shm_last_touched_at then\n        return nil, \"dictionary not find\"\n    end\n\n    if not ewma_lock then\n        error(ewma_lock_err)\n    end\n\n    return {\n        upstream = upstream,\n        get = function(ctx)\n            return _ewma_find(ctx, up_nodes)\n        end,\n        after_balance = _ewma_after_balance,\n        before_retry_next_priority = function (ctx)\n            if ctx.balancer_tried_servers then\n                core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                ctx.balancer_tried_servers = nil\n            end\n\n            ctx.balancer_tried_servers_count = 0\n        end,\n    }\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/balancer/least_conn.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal binaryHeap = require(\"binaryheap\")\nlocal ipairs = ipairs\nlocal pairs = pairs\n\n\nlocal _M = {}\n\n\nlocal function least_score(a, b)\n    return a.score < b.score\nend\n\n\nfunction _M.new(up_nodes, upstream)\n    local servers_heap = binaryHeap.minUnique(least_score)\n    for server, weight in pairs(up_nodes) do\n        local score = 1 / weight\n        -- Note: the argument order of insert is different from others\n        servers_heap:insert({\n            server = server,\n            effect_weight = 1 / weight,\n            score = score,\n        }, server)\n    end\n\n    return {\n        upstream = upstream,\n        get = function (ctx)\n            local server, info, err\n            if ctx.balancer_tried_servers then\n                local tried_server_list = {}\n                while true do\n                    server, info = servers_heap:peek()\n                    -- we need to let the retry > #nodes so this branch can be hit and\n                    -- the request will retry next priority of nodes\n                    if server == nil then\n                        err = \"all upstream servers tried\"\n                        break\n                    end\n\n                    if not ctx.balancer_tried_servers[server] then\n                        break\n                    end\n\n                    servers_heap:pop()\n                    core.table.insert(tried_server_list, info)\n                end\n\n                for _, info in ipairs(tried_server_list) do\n                    servers_heap:insert(info, info.server)\n                end\n            else\n                server, info = servers_heap:peek()\n            end\n\n            if not server then\n                return nil, err\n            end\n\n            info.score = info.score + info.effect_weight\n            servers_heap:update(server, info)\n            return server\n        end,\n        after_balance = function (ctx, before_retry)\n            local server = ctx.balancer_server\n            local info = servers_heap:valueByPayload(server)\n            info.score = info.score - info.effect_weight\n            servers_heap:update(server, info)\n\n            if not before_retry then\n                if ctx.balancer_tried_servers then\n                    core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                    ctx.balancer_tried_servers = nil\n                end\n\n                return nil\n            end\n\n            if not ctx.balancer_tried_servers then\n                ctx.balancer_tried_servers = core.tablepool.fetch(\"balancer_tried_servers\", 0, 2)\n            end\n\n            ctx.balancer_tried_servers[server] = true\n        end,\n        before_retry_next_priority = function (ctx)\n            if ctx.balancer_tried_servers then\n                core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                ctx.balancer_tried_servers = nil\n            end\n        end,\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/balancer/priority.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal ipairs = ipairs\n\n\nlocal _M = {}\n\n\nlocal function max_priority(a, b)\n    return a > b\nend\n\n\nfunction _M.new(up_nodes, upstream, picker_mod)\n    local priority_index = up_nodes._priority_index\n    core.table.sort(priority_index, max_priority)\n\n    local pickers = core.table.new(#priority_index, 0)\n    for i, priority in ipairs(priority_index) do\n        local picker, err = picker_mod.new(up_nodes[priority], upstream)\n        if not picker then\n            return nil, \"failed to create picker with priority \" .. priority .. \": \" .. err\n        end\n        if not picker.before_retry_next_priority then\n            return nil, \"picker should define 'before_retry_next_priority' to reset ctx\"\n        end\n\n        pickers[i] = picker\n    end\n\n    return {\n        upstream = upstream,\n        get = function (ctx)\n            for i = ctx.priority_balancer_picker_idx or 1, #pickers do\n                local picker = pickers[i]\n                local server, err = picker.get(ctx)\n                if server then\n                    ctx.priority_balancer_picker_idx = i\n                    return server\n                end\n\n                core.log.notice(\"failed to get server from current priority \",\n                                priority_index[i],\n                                \", try next one, err: \", err)\n\n                picker.before_retry_next_priority(ctx)\n            end\n\n            return nil, \"all servers tried\"\n        end,\n        after_balance = function (ctx, before_retry)\n            local priority_balancer_picker = pickers[ctx.priority_balancer_picker_idx]\n            if not priority_balancer_picker or\n                not priority_balancer_picker.after_balance\n            then\n                return\n            end\n\n            priority_balancer_picker.after_balance(ctx, before_retry)\n        end\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/balancer/roundrobin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal roundrobin  = require(\"resty.roundrobin\")\nlocal core = require(\"apisix.core\")\nlocal nkeys = core.table.nkeys\nlocal pairs = pairs\n\n\nlocal _M = {}\n\n\nfunction _M.new(up_nodes, upstream)\n    local safe_limit = 0\n    for _, weight in pairs(up_nodes) do\n        -- the weight can be zero\n        safe_limit = safe_limit + weight + 1\n    end\n\n    local picker = roundrobin:new(up_nodes)\n    local nodes_count = nkeys(up_nodes)\n    return {\n        upstream = upstream,\n        get = function (ctx)\n            if ctx.balancer_tried_servers and ctx.balancer_tried_servers_count == nodes_count then\n                return nil, \"all upstream servers tried\"\n            end\n\n            local server, err\n            for i = 1, safe_limit do\n                server, err = picker:find()\n                if not server then\n                    return nil, err\n                end\n                if ctx.balancer_tried_servers then\n                    if not ctx.balancer_tried_servers[server] then\n                        break\n                    end\n                else\n                    break\n                end\n            end\n\n            return server\n        end,\n        after_balance = function (ctx, before_retry)\n            if not before_retry then\n                if ctx.balancer_tried_servers then\n                    core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                    ctx.balancer_tried_servers = nil\n                end\n\n                return nil\n            end\n\n            if not ctx.balancer_tried_servers then\n                ctx.balancer_tried_servers = core.tablepool.fetch(\"balancer_tried_servers\", 0, 2)\n            end\n\n            ctx.balancer_tried_servers[ctx.balancer_server] = true\n            ctx.balancer_tried_servers_count = (ctx.balancer_tried_servers_count or 0) + 1\n        end,\n        before_retry_next_priority = function (ctx)\n            if ctx.balancer_tried_servers then\n                core.tablepool.release(\"balancer_tried_servers\", ctx.balancer_tried_servers)\n                ctx.balancer_tried_servers = nil\n            end\n\n            ctx.balancer_tried_servers_count = 0\n        end,\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/balancer.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require           = require\nlocal balancer          = require(\"ngx.balancer\")\nlocal core              = require(\"apisix.core\")\nlocal priority_balancer = require(\"apisix.balancer.priority\")\nlocal apisix_upstream   = require(\"apisix.upstream\")\nlocal healthcheck_manager = require(\"apisix.healthcheck_manager\")\nlocal ipairs            = ipairs\nlocal is_http           = ngx.config.subsystem == \"http\"\nlocal enable_keepalive = balancer.enable_keepalive and is_http\nlocal set_more_tries   = balancer.set_more_tries\nlocal get_last_failure = balancer.get_last_failure\nlocal set_timeouts     = balancer.set_timeouts\nlocal ngx_now          = ngx.now\n\nlocal module_name = \"balancer\"\nlocal pickers = {}\n\nlocal lrucache_server_picker = core.lrucache.new({\n    ttl = 300, count = 256\n})\nlocal lrucache_addr = core.lrucache.new({\n    ttl = 300, count = 1024 * 4\n})\n\n\nlocal _M = {\n    version = 0.2,\n    name = module_name,\n}\n\n\nlocal function transform_node(new_nodes, node)\n    if not new_nodes._priority_index then\n        new_nodes._priority_index = {}\n    end\n\n    if not new_nodes[node.priority] then\n        new_nodes[node.priority] = {}\n        core.table.insert(new_nodes._priority_index, node.priority)\n    end\n\n    new_nodes[node.priority][node.host .. \":\" .. node.port] = node.weight\n    return new_nodes\nend\n\n\nlocal function fetch_health_nodes(upstream, checker)\n    local nodes = upstream.nodes\n    if not checker then\n        local new_nodes = core.table.new(0, #nodes)\n        for _, node in ipairs(nodes) do\n            new_nodes = transform_node(new_nodes, node)\n        end\n        return new_nodes\n    end\n\n    local host = upstream.checks and upstream.checks.active and upstream.checks.active.host\n    local port = upstream.checks and upstream.checks.active and upstream.checks.active.port\n    local up_nodes = core.table.new(0, #nodes)\n    for _, node in ipairs(nodes) do\n        local ok, err = healthcheck_manager.fetch_node_status(checker,\n                                             node.host, port or node.port, host)\n        if ok then\n            up_nodes = transform_node(up_nodes, node)\n        elseif err then\n            core.log.warn(\"failed to get health check target status, addr: \",\n                node.host, \":\", port or node.port, \", host: \", host, \", err: \", err)\n        end\n    end\n\n    if core.table.nkeys(up_nodes) == 0 then\n        core.log.warn(\"all upstream nodes is unhealthy, use default\")\n        for _, node in ipairs(nodes) do\n            up_nodes = transform_node(up_nodes, node)\n        end\n    end\n\n    return up_nodes\nend\n\n\nlocal function create_server_picker(upstream, checker)\n    local picker = pickers[upstream.type]\n    if not picker then\n        pickers[upstream.type] = require(\"apisix.balancer.\" .. upstream.type)\n        picker = pickers[upstream.type]\n    end\n\n    if picker then\n        local nodes = upstream.nodes\n        local addr_to_domain = {}\n        for _, node in ipairs(nodes) do\n            if node.domain then\n                local addr = node.host .. \":\" .. node.port\n                addr_to_domain[addr] = node.domain\n            end\n        end\n\n        local up_nodes = fetch_health_nodes(upstream, checker)\n\n        if #up_nodes._priority_index > 1 then\n            core.log.info(\"upstream nodes: \", core.json.delay_encode(up_nodes))\n            local server_picker = priority_balancer.new(up_nodes, upstream, picker)\n            server_picker.addr_to_domain = addr_to_domain\n            return server_picker\n        end\n\n        core.log.info(\"upstream nodes: \",\n                      core.json.delay_encode(up_nodes[up_nodes._priority_index[1]]))\n        local server_picker = picker.new(up_nodes[up_nodes._priority_index[1]], upstream)\n        server_picker.addr_to_domain = addr_to_domain\n        return server_picker\n    end\n\n    return nil, \"invalid balancer type: \" .. upstream.type, 0\nend\n\n\nlocal function parse_addr(addr)\n    local host, port, err = core.utils.parse_addr(addr)\n    return {host = host, port = port}, err\nend\n\n\n-- set_balancer_opts will be called in balancer phase and before any tries\nlocal function set_balancer_opts(route, ctx)\n    local up_conf = ctx.upstream_conf\n\n    -- If the matched route has timeout config, prefer to use the route config.\n    local timeout = nil\n    if route and route.value and route.value.timeout then\n        timeout = route.value.timeout\n    else\n        if up_conf.timeout then\n            timeout = up_conf.timeout\n        end\n    end\n    if timeout then\n        local ok, err = set_timeouts(timeout.connect, timeout.send,\n                                     timeout.read)\n        if not ok then\n            core.log.error(\"could not set upstream timeouts: \", err)\n        end\n    end\n\n    local retries = up_conf.retries\n    if not retries or retries < 0 then\n        retries = #up_conf.nodes - 1\n    end\n\n    if retries > 0 then\n        if up_conf.retry_timeout and up_conf.retry_timeout > 0 then\n            ctx.proxy_retry_deadline = ngx_now() + up_conf.retry_timeout\n        end\n        local ok, err = set_more_tries(retries)\n        if not ok then\n            core.log.error(\"could not set upstream retries: \", err)\n        elseif err then\n            core.log.warn(\"could not set upstream retries: \", err)\n        end\n    end\nend\n\n\nlocal function parse_server_for_upstream_host(picked_server, upstream_scheme)\n    local standard_port = apisix_upstream.scheme_to_port[upstream_scheme]\n    local host = picked_server.domain or picked_server.host\n    if upstream_scheme and (not standard_port or standard_port ~= picked_server.port) then\n        host = host .. \":\" .. picked_server.port\n    end\n    return host\nend\n\n\n-- pick_server will be called:\n-- 1. in the access phase so that we can set headers according to the picked server\n-- 2. each time we need to retry upstream\nlocal function pick_server(route, ctx)\n    local up_conf = ctx.upstream_conf\n\n    local nodes_count = #up_conf.nodes\n    if nodes_count == 1 then\n        local node = up_conf.nodes[1]\n        ctx.balancer_ip = node.host\n        ctx.balancer_port = node.port\n        node.upstream_host = parse_server_for_upstream_host(node, ctx.upstream_scheme)\n        return node\n    end\n\n    local version = ctx.upstream_version\n    local key = ctx.upstream_key\n    local checker = ctx.up_checker\n\n    ctx.balancer_try_count = (ctx.balancer_try_count or 0) + 1\n    if ctx.balancer_try_count > 1 then\n        if ctx.server_picker and ctx.server_picker.after_balance then\n            ctx.server_picker.after_balance(ctx, true)\n        end\n\n        if checker then\n            local state, code = get_last_failure()\n            local host = up_conf.checks and up_conf.checks.active and up_conf.checks.active.host\n            local port = up_conf.checks and up_conf.checks.active and up_conf.checks.active.port\n            if state == \"failed\" then\n                if code == 504 then\n                    checker:report_timeout(ctx.balancer_ip, port or ctx.balancer_port, host)\n                else\n                    checker:report_tcp_failure(ctx.balancer_ip, port or ctx.balancer_port, host)\n                end\n            else\n                checker:report_http_status(ctx.balancer_ip, port or ctx.balancer_port, host, code)\n            end\n        end\n    end\n\n    if checker then\n        version = version .. \"#\" .. checker.status_ver\n    end\n\n    -- the same picker will be used in the whole request, especially during the retry\n    local server_picker = ctx.server_picker\n    if not server_picker then\n        server_picker = lrucache_server_picker(key, version,\n                                               create_server_picker, up_conf, checker)\n    end\n    if not server_picker then\n        return nil, \"failed to fetch server picker\"\n    end\n\n    local server, err = server_picker.get(ctx)\n    if not server then\n        err = err or \"no valid upstream node\"\n        return nil, \"failed to find valid upstream server, \" .. err\n    end\n    ctx.balancer_server = server\n\n    local domain = server_picker.addr_to_domain[server]\n    local res, err = lrucache_addr(server, nil, parse_addr, server)\n    if err then\n        core.log.error(\"failed to parse server addr: \", server, \" err: \", err)\n        return core.response.exit(502)\n    end\n\n    res.domain = domain\n    ctx.balancer_ip = res.host\n    ctx.balancer_port = res.port\n    ctx.server_picker = server_picker\n    res.upstream_host = parse_server_for_upstream_host(res, ctx.upstream_scheme)\n\n    return res\nend\n\n\n-- for test\n_M.pick_server = pick_server\n\n\nlocal set_current_peer\ndo\n    local pool_opt = {}\n    local default_keepalive_pool\n\n    function set_current_peer(server, ctx)\n        local up_conf = ctx.upstream_conf\n        local keepalive_pool = up_conf.keepalive_pool\n\n        if enable_keepalive then\n            if not keepalive_pool then\n                if not default_keepalive_pool then\n                    local local_conf = core.config.local_conf()\n                    local up_keepalive_conf =\n                        core.table.try_read_attr(local_conf, \"nginx_config\",\n                                                 \"http\", \"upstream\")\n                    default_keepalive_pool = {}\n                    default_keepalive_pool.idle_timeout =\n                        core.config_util.parse_time_unit(up_keepalive_conf.keepalive_timeout)\n                    default_keepalive_pool.size = up_keepalive_conf.keepalive\n                    default_keepalive_pool.requests = up_keepalive_conf.keepalive_requests\n                end\n\n                keepalive_pool = default_keepalive_pool\n            end\n\n            local idle_timeout = keepalive_pool.idle_timeout\n            local size = keepalive_pool.size\n            local requests = keepalive_pool.requests\n\n            core.table.clear(pool_opt)\n            pool_opt.pool_size = size\n\n            local scheme = up_conf.scheme\n            local pool = scheme .. \"#\" .. server.host .. \"#\" .. server.port\n            -- other TLS schemes don't use http balancer keepalive\n            if (scheme == \"https\" or scheme == \"grpcs\") then\n                local sni = ctx.var.upstream_host\n                pool = pool .. \"#\" .. sni\n\n                if up_conf.tls and up_conf.tls.client_cert then\n                    pool = pool .. \"#\" .. up_conf.tls.client_cert\n                end\n            end\n            pool_opt.pool = pool\n\n            local ok, err = balancer.set_current_peer(server.host, server.port,\n                                                      pool_opt)\n            if not ok then\n                return ok, err\n            end\n\n            return balancer.enable_keepalive(idle_timeout, requests)\n        end\n\n        return balancer.set_current_peer(server.host, server.port)\n    end\nend\n\n\nfunction _M.run(route, ctx, plugin_funcs)\n    local server, err\n\n    if ctx.picked_server then\n        -- use the server picked in the access phase\n        server = ctx.picked_server\n        ctx.picked_server = nil\n\n        set_balancer_opts(route, ctx)\n\n    else\n        if ctx.proxy_retry_deadline and ctx.proxy_retry_deadline < ngx_now() then\n            -- retry count is (try count - 1)\n            core.log.error(\"proxy retry timeout, retry count: \", (ctx.balancer_try_count or 1) - 1,\n                           \", deadline: \", ctx.proxy_retry_deadline, \" now: \", ngx_now())\n            return core.response.exit(502)\n        end\n        -- retry\n        server, err = pick_server(route, ctx)\n        if not server then\n            core.log.error(\"failed to pick server: \", err)\n            return core.response.exit(502)\n        end\n\n        local header_changed\n        local pass_host = ctx.pass_host\n        if pass_host == \"node\" then\n            local host = server.upstream_host\n            if host ~= ctx.var.upstream_host then\n                -- retried node has a different host\n                ctx.var.upstream_host = host\n                header_changed = true\n            end\n        end\n\n        local _, run = plugin_funcs(\"before_proxy\")\n        -- always recreate request as the request may be changed by plugins\n        if run or header_changed then\n            balancer.recreate_request()\n        end\n    end\n\n    core.log.info(\"proxy request to \", server.host, \":\", server.port)\n\n    local ok, err = set_current_peer(server, ctx)\n    if not ok then\n        core.log.error(\"failed to set server peer [\", server.host, \":\",\n                       server.port, \"] err: \", err)\n        return core.response.exit(502)\n    end\n\n    ctx.proxy_passed = true\nend\n\n\nfunction _M.init_worker()\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/apisix.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal pkg_cpath_org = package.cpath\nlocal pkg_path_org = package.path\n\nlocal _, find_pos_end = string.find(pkg_path_org, \";\", -1, true)\nif not find_pos_end then\n    pkg_path_org = pkg_path_org .. \";\"\nend\n\nlocal apisix_home = \"/usr/local/apisix\"\nlocal pkg_cpath = apisix_home .. \"/deps/lib64/lua/5.1/?.so;\"\n                  .. apisix_home .. \"/deps/lib/lua/5.1/?.so;\"\nlocal pkg_path_deps = apisix_home .. \"/deps/share/lua/5.1/?.lua;\"\nlocal pkg_path_env = apisix_home .. \"/?.lua;\"\n\n-- modify the load path to load our dependencies\npackage.cpath = pkg_cpath .. pkg_cpath_org\npackage.path  = pkg_path_deps .. pkg_path_org .. pkg_path_env\n\n-- pass path to construct the final result\nlocal env = require(\"apisix.cli.env\")(apisix_home, pkg_cpath_org, pkg_path_org)\nlocal ops = require(\"apisix.cli.ops\")\n\nops.execute(env, arg)\n"
  },
  {
    "path": "apisix/cli/config.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal table_conact = table.concat\n\nlocal _M = {\n  apisix = {\n    node_listen = { 9080 },\n    enable_admin = true,\n    enable_dev_mode = false,\n    enable_reuseport = true,\n    show_upstream_status_in_response_header = false,\n    enable_ipv6 = true,\n    enable_http2 = true,\n    enable_server_tokens = true,\n    extra_lua_path = \"\",\n    extra_lua_cpath = \"\",\n    proxy_cache = {\n      cache_ttl = \"10s\",\n      zones = {\n        {\n          name = \"disk_cache_one\",\n          memory_size = \"50m\",\n          disk_size = \"1G\",\n          disk_path = \"/tmp/disk_cache_one\",\n          cache_levels = \"1:2\"\n        },\n        {\n          name = \"memory_cache\",\n          memory_size = \"50m\"\n        }\n      }\n    },\n    delete_uri_tail_slash = false,\n    normalize_uri_like_servlet = false,\n    router = {\n      http = \"radixtree_host_uri\",\n      ssl = \"radixtree_sni\"\n    },\n    proxy_mode = \"http\",\n    resolver_timeout = 5,\n    enable_resolv_search_opt = true,\n    ssl = {\n      enable = true,\n      listen = { {\n        port = 9443,\n        enable_http3 = false\n      } },\n      ssl_protocols = \"TLSv1.2 TLSv1.3\",\n      ssl_ciphers = table_conact({\n        \"ECDHE-ECDSA-AES128-GCM-SHA256\", \"ECDHE-RSA-AES128-GCM-SHA256\",\n        \"ECDHE-ECDSA-AES256-GCM-SHA384\", \"ECDHE-RSA-AES256-GCM-SHA384\",\n        \"ECDHE-ECDSA-CHACHA20-POLY1305\", \"ECDHE-RSA-CHACHA20-POLY1305\",\n        \"DHE-RSA-AES128-GCM-SHA256\", \"DHE-RSA-AES256-GCM-SHA384\",\n      }, \":\"),\n      ssl_session_tickets = false,\n      ssl_trusted_certificate = \"system\"\n    },\n    enable_control = true,\n    disable_sync_configuration_during_start = false,\n    worker_startup_time_threshold = 60,\n    data_encryption = {\n      enable_encrypt_fields = true,\n      keyring = { \"qeddd145sfvddff3\", \"edd1c9f0985e76a2\" }\n    },\n    lru = {\n      secret = {\n        ttl = 300,\n        count = 512,\n        neg_ttl = 60,\n        neg_count = 512\n      }\n    },\n    tracing = false\n  },\n  nginx_config = {\n    error_log = \"logs/error.log\",\n    error_log_level = \"warn\",\n    worker_processes = \"auto\",\n    enable_cpu_affinity = false,\n    worker_rlimit_nofile = 20480,\n    worker_shutdown_timeout = \"240s\",\n    max_pending_timers = 16384,\n    max_running_timers = 4096,\n    event = {\n      worker_connections = 10620\n    },\n    meta = {\n      lua_shared_dict = {\n        [\"prometheus-metrics\"] = \"15m\",\n        [\"prometheus-cache\"] = \"10m\",\n        [\"standalone-config\"] = \"10m\",\n        [\"status-report\"] = \"1m\",\n        [\"upstream-healthcheck\"] = \"10m\",\n      }\n    },\n    stream = {\n      enable_access_log = false,\n      access_log = \"logs/access_stream.log\",\n      -- luacheck: push max code line length 300\n      access_log_format = \"$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time\",\n      -- luacheck: pop\n      access_log_format_escape = \"default\",\n      lua_shared_dict = {\n        [\"etcd-cluster-health-check-stream\"] = \"10m\",\n        [\"lrucache-lock-stream\"] = \"10m\",\n        [\"plugin-limit-conn-stream\"] = \"10m\",\n        [\"worker-events-stream\"] = \"10m\",\n        [\"tars-stream\"] = \"1m\",\n      }\n    },\n    main_configuration_snippet = \"\",\n    http_configuration_snippet = \"\",\n    http_server_configuration_snippet = \"\",\n    http_server_location_configuration_snippet = \"\",\n    http_admin_configuration_snippet = \"\",\n    http_end_configuration_snippet = \"\",\n    stream_configuration_snippet = \"\",\n    http = {\n      enable_access_log = true,\n      access_log = \"logs/access.log\",\n      access_log_buffer = 16384,\n      -- luacheck: push max code line length 300\n      access_log_format =\n      '$remote_addr - $remote_user [$time_local] $http_host \"$request\" $status $body_bytes_sent $request_time \"$http_referer\" \"$http_user_agent\" $upstream_addr $upstream_status $upstream_response_time \"$upstream_scheme://$upstream_host$upstream_uri\" \"$apisix_request_id\"',\n      -- luacheck: pop\n      access_log_format_escape = \"default\",\n      keepalive_timeout = \"60s\",\n      client_header_timeout = \"60s\",\n      client_body_timeout = \"60s\",\n      client_max_body_size = 0,\n      send_timeout = \"10s\",\n      underscores_in_headers = \"on\",\n      real_ip_header = \"X-Real-IP\",\n      real_ip_recursive = \"off\",\n      real_ip_from = { \"127.0.0.1\", \"unix:\" },\n      proxy_ssl_server_name = true,\n      upstream = {\n        keepalive = 320,\n        keepalive_requests = 1000,\n        keepalive_timeout = \"60s\"\n      },\n      charset = \"utf-8\",\n      variables_hash_max_size = 2048,\n      lua_shared_dict = {\n        [\"internal-status\"] = \"10m\",\n        [\"plugin-limit-req\"] = \"10m\",\n        [\"plugin-limit-count\"] = \"10m\",\n        [\"prometheus-metrics\"] = \"10m\",\n        [\"plugin-limit-conn\"] = \"10m\",\n        [\"worker-events\"] = \"10m\",\n        [\"lrucache-lock\"] = \"10m\",\n        [\"balancer-ewma\"] = \"10m\",\n        [\"balancer-ewma-locks\"] = \"10m\",\n        [\"balancer-ewma-last-touched-at\"] = \"10m\",\n        [\"plugin-limit-req-redis-cluster-slot-lock\"] = \"1m\",\n        [\"plugin-limit-count-redis-cluster-slot-lock\"] = \"1m\",\n        [\"plugin-limit-conn-redis-cluster-slot-lock\"] = \"1m\",\n        [\"plugin-ai-rate-limiting\"] = \"10m\",\n        [\"plugin-ai-rate-limiting-reset-header\"] = \"10m\",\n        tracing_buffer = \"10m\",\n        [\"plugin-api-breaker\"] = \"10m\",\n        [\"etcd-cluster-health-check\"] = \"10m\",\n        discovery = \"1m\",\n        jwks = \"1m\",\n        introspection = \"10m\",\n        [\"access-tokens\"] = \"1m\",\n        [\"ext-plugin\"] = \"1m\",\n        tars = \"1m\",\n        [\"cas-auth\"] = \"10m\",\n        [\"ocsp-stapling\"] = \"10m\",\n        [\"mcp-session\"] = \"10m\",\n      }\n    }\n  },\n  graphql = {\n    max_size = 1048576\n  },\n  plugins = {\n    \"real-ip\",\n    \"ai\",\n    \"client-control\",\n    \"proxy-control\",\n    \"request-id\",\n    \"zipkin\",\n    \"ext-plugin-pre-req\",\n    \"fault-injection\",\n    \"mocking\",\n    \"serverless-pre-function\",\n    \"cors\",\n    \"ip-restriction\",\n    \"ua-restriction\",\n    \"referer-restriction\",\n    \"csrf\",\n    \"uri-blocker\",\n    \"request-validation\",\n    \"chaitin-waf\",\n    \"multi-auth\",\n    \"openid-connect\",\n    \"cas-auth\",\n    \"authz-casbin\",\n    \"authz-casdoor\",\n    \"wolf-rbac\",\n    \"ldap-auth\",\n    \"hmac-auth\",\n    \"basic-auth\",\n    \"jwt-auth\",\n    \"jwe-decrypt\",\n    \"key-auth\",\n    \"consumer-restriction\",\n    \"attach-consumer-label\",\n    \"forward-auth\",\n    \"opa\",\n    \"authz-keycloak\",\n    \"proxy-cache\",\n    \"body-transformer\",\n    \"ai-prompt-template\",\n    \"ai-prompt-decorator\",\n    \"ai-prompt-guard\",\n    \"ai-rag\",\n    \"ai-rate-limiting\",\n    \"ai-proxy-multi\",\n    \"ai-proxy\",\n    \"ai-aws-content-moderation\",\n    \"ai-aliyun-content-moderation\",\n    \"proxy-mirror\",\n    \"proxy-rewrite\",\n    \"workflow\",\n    \"api-breaker\",\n    \"limit-conn\",\n    \"limit-count\",\n    \"limit-req\",\n    \"gzip\",\n    -- deprecated and will be removed in a future release\n    -- \"server-info\",\n    \"traffic-split\",\n    \"redirect\",\n    \"response-rewrite\",\n    \"mcp-bridge\",\n    \"degraphql\",\n    \"kafka-proxy\",\n    \"grpc-transcode\",\n    \"grpc-web\",\n    \"http-dubbo\",\n    \"public-api\",\n    \"prometheus\",\n    \"datadog\",\n    \"lago\",\n    \"loki-logger\",\n    \"elasticsearch-logger\",\n    \"echo\",\n    \"loggly\",\n    \"http-logger\",\n    \"splunk-hec-logging\",\n    \"skywalking-logger\",\n    \"google-cloud-logging\",\n    \"sls-logger\",\n    \"tcp-logger\",\n    \"kafka-logger\",\n    \"rocketmq-logger\",\n    \"syslog\",\n    \"udp-logger\",\n    \"file-logger\",\n    \"clickhouse-logger\",\n    \"tencent-cloud-cls\",\n    \"inspect\",\n    \"example-plugin\",\n    \"aws-lambda\",\n    \"azure-functions\",\n    \"openwhisk\",\n    \"openfunction\",\n    \"serverless-post-function\",\n    \"ext-plugin-post-req\",\n    \"ext-plugin-post-resp\",\n    \"ai-request-rewrite\",\n  },\n  stream_plugins = { \"ip-restriction\", \"limit-conn\", \"mqtt-proxy\", \"syslog\", \"traffic-split\" },\n  plugin_attr = {\n    [\"log-rotate\"] = {\n      timeout = 10000,\n      interval = 3600,\n      max_kept = 168,\n      max_size = -1,\n      enable_compression = false\n    },\n    skywalking = {\n      service_name = \"APISIX\",\n      service_instance_name = \"APISIX Instance Name\",\n      endpoint_addr = \"http://127.0.0.1:12800\",\n      report_interval = 3\n    },\n    opentelemetry = {\n      trace_id_source = \"x-request-id\",\n      resource = {\n        [\"service.name\"] = \"APISIX\"\n      },\n      collector = {\n        address = \"127.0.0.1:4318\",\n        request_timeout = 3,\n        request_headers = {\n          Authorization = \"token\"\n        }\n      },\n      batch_span_processor = {\n        drop_on_queue_full = false,\n        max_queue_size = 1024,\n        batch_timeout = 2,\n        inactive_timeout = 1,\n        max_export_batch_size = tonumber(os.getenv(\"OTEL_BSP_MAX_EXPORT_BATCH_SIZE\")) or 16\n      },\n      set_ngx_var = false\n    },\n    prometheus = {\n      export_uri = \"/apisix/prometheus/metrics\",\n      metric_prefix = \"apisix_\",\n      enable_export_server = true,\n      export_addr = {\n        ip = \"127.0.0.1\",\n        port = 9091\n      },\n      refresh_interval = 15\n    },\n    [\"server-info\"] = {\n      report_ttl = 60\n    },\n    [\"dubbo-proxy\"] = {\n      upstream_multiplex_count = 32\n    },\n    [\"proxy-mirror\"] = {\n      timeout = {\n        connect = \"60s\",\n        read = \"60s\",\n        send = \"60s\"\n      }\n    },\n    inspect = {\n      delay = 3,\n      hooks_file = \"/usr/local/apisix/plugin_inspect_hooks.lua\"\n    },\n    zipkin = {\n      set_ngx_var = false\n    }\n  },\n  deployment = {\n    role = \"traditional\",\n    role_traditional = {\n      config_provider = \"etcd\"\n    },\n    admin = {\n      admin_key_required = true,\n      admin_key = {\n        {\n          name = \"admin\",\n          key = \"\",\n          role = \"admin\"\n        }\n      },\n      enable_admin_cors = true,\n      enable_admin_ui = true,\n      allow_admin = { \"127.0.0.0/24\" },\n      admin_listen = {\n        ip = \"0.0.0.0\",\n        port = 9180\n      },\n      admin_api_version = \"v3\"\n    },\n    etcd = {\n      host = { \"http://127.0.0.1:2379\" },\n      prefix = \"/apisix\",\n      timeout = 30,\n      watch_timeout = 50,\n      startup_retry = 2,\n      tls = {\n        verify = true\n      }\n    }\n  }\n}\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/env.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal require = require\nlocal util = require(\"apisix.cli.util\")\n\nlocal pcall = pcall\nlocal error = error\nlocal exit = os.exit\nlocal stderr = io.stderr\nlocal str_find = string.find\nlocal arg = arg\nlocal package = package\nlocal tonumber = tonumber\n\nreturn function (apisix_home, pkg_cpath_org, pkg_path_org)\n    -- ulimit setting should be checked when APISIX starts\n    local res, err = util.execute_cmd(\"ulimit -n\")\n    if not res then\n        error(\"failed to exec ulimit cmd \\'ulimit -n \\', err: \" .. err)\n    end\n    local trimed_res = util.trim(res)\n    local ulimit = trimed_res == \"unlimited\" and trimed_res or tonumber(trimed_res)\n    if not ulimit then\n        error(\"failed to fetch current maximum number of open file descriptors\")\n    end\n\n    -- only for developer, use current folder as working space\n    local is_root_path = false\n    local script_path = arg[0]\n    if script_path:sub(1, 2) == './' then\n        apisix_home = util.trim(util.execute_cmd(\"pwd\"))\n        if not apisix_home then\n            error(\"failed to fetch current path\")\n        end\n\n        -- determine whether the current path is under the \"/root\" folder.\n        -- \"/root/\" is the root folder flag.\n        if str_find(apisix_home .. \"/\", '/root/', nil, true) == 1 then\n            is_root_path = true\n        end\n\n        local pkg_cpath = apisix_home .. \"/deps/lib64/lua/5.1/?.so;\"\n                          .. apisix_home .. \"/deps/lib/lua/5.1/?.so;\"\n\n        local pkg_path = apisix_home .. \"/?/init.lua;\"\n                         .. apisix_home .. \"/deps/share/lua/5.1/?/init.lua;\"\n                         .. apisix_home .. \"/deps/share/lua/5.1/?.lua;;\"\n\n        package.cpath = pkg_cpath .. package.cpath\n        package.path  = pkg_path .. package.path\n    end\n\n    do\n        -- skip luajit environment\n        local ok = pcall(require, \"table.new\")\n        if not ok then\n            local ok, json = pcall(require, \"cjson\")\n            if ok and json then\n                stderr:write(\"please remove the cjson library in Lua, it may \"\n                            .. \"conflict with the cjson library in openresty. \"\n                            .. \"\\n luarocks remove lua-cjson\\n\")\n                exit(1)\n            end\n        end\n    end\n\n    -- pre-transform openresty path\n    res, err = util.execute_cmd(\"command -v openresty\")\n    if not res then\n        error(\"failed to exec cmd \\'command -v openresty\\', err: \" .. err)\n    end\n    local openresty_path_abs = util.trim(res)\n\n    local openresty_args = openresty_path_abs .. [[ -p ]] .. apisix_home .. [[ -c ]]\n                           .. apisix_home .. [[/conf/nginx.conf]]\n\n    local or_info, err = util.execute_cmd(\"openresty -V 2>&1\")\n    if not or_info then\n        error(\"failed to exec cmd \\'openresty -V 2>&1\\', err: \" .. err)\n    end\n\n    local use_apisix_base = true\n    if not or_info:find(\"apisix-nginx-module\", 1, true) then\n        use_apisix_base = false\n    end\n\n    local min_etcd_version = \"3.4.0\"\n\n    return {\n        apisix_home = apisix_home,\n        is_root_path = is_root_path,\n        openresty_args = openresty_args,\n        openresty_info = or_info,\n        use_apisix_base = use_apisix_base,\n        pkg_cpath_org = pkg_cpath_org,\n        pkg_path_org = pkg_path_org,\n        min_etcd_version = min_etcd_version,\n        ulimit = ulimit,\n    }\nend\n"
  },
  {
    "path": "apisix/cli/etcd.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal base64_encode = require(\"base64\").encode\nlocal dkjson = require(\"dkjson\")\nlocal constants = require(\"apisix.constants\")\nlocal util = require(\"apisix.cli.util\")\nlocal file = require(\"apisix.cli.file\")\nlocal http = require(\"socket.http\")\nlocal https = require(\"ssl.https\")\nlocal ltn12 = require(\"ltn12\")\n\nlocal type = type\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal print = print\nlocal tonumber = tonumber\nlocal str_format = string.format\nlocal str_sub = string.sub\nlocal table_concat = table.concat\nlocal table_insert = table.insert\nlocal io_stderr = io.stderr\n\nlocal _M = {}\n\n-- Timeout for all I/O operations\nhttp.TIMEOUT = 3\n\nlocal function parse_semantic_version(ver)\n    local errmsg = \"invalid semantic version: \" .. ver\n\n    local parts = util.split(ver, \"-\")\n    if #parts > 2 then\n        return nil, errmsg\n    end\n\n    if #parts == 2 then\n        ver = parts[1]\n    end\n\n    local fields = util.split(ver, \".\")\n    if #fields ~= 3 then\n        return nil, errmsg\n    end\n\n    local major = tonumber(fields[1])\n    local minor = tonumber(fields[2])\n    local patch = tonumber(fields[3])\n\n    if not (major and minor and patch) then\n        return nil, errmsg\n    end\n\n    return {\n        major = major,\n        minor = minor,\n        patch = patch,\n    }\nend\n\n\nlocal function compare_semantic_version(v1, v2)\n    local ver1, err = parse_semantic_version(v1)\n    if not ver1 then\n        return nil, err\n    end\n\n    local ver2, err = parse_semantic_version(v2)\n    if not ver2 then\n        return nil, err\n    end\n\n    if ver1.major ~= ver2.major then\n        return ver1.major < ver2.major\n    end\n\n    if ver1.minor ~= ver2.minor then\n        return ver1.minor < ver2.minor\n    end\n\n    return ver1.patch < ver2.patch\nend\n\n\nlocal function request(url, yaml_conf)\n    local response_body = {}\n    local single_request = false\n    if type(url) == \"string\" then\n        url = {\n            url = url,\n            method = \"GET\",\n            sink = ltn12.sink.table(response_body),\n        }\n        single_request = true\n    end\n\n    local res, code\n\n    if str_sub(url.url, 1, 8) == \"https://\" then\n        local verify = \"peer\"\n        if yaml_conf.etcd.tls then\n            local cfg = yaml_conf.etcd.tls\n\n            if cfg.verify == false then\n                verify = \"none\"\n            end\n\n            url.certificate = cfg.cert\n            url.key = cfg.key\n\n            local apisix_ssl = yaml_conf.apisix.ssl\n            if apisix_ssl and apisix_ssl.ssl_trusted_certificate then\n                url.cafile = apisix_ssl.ssl_trusted_certificate\n            end\n        end\n\n        url.verify = verify\n        res, code = https.request(url)\n    else\n\n        res, code = http.request(url)\n    end\n\n    -- In case of failure, request returns nil followed by an error message.\n    -- Else the first return value is the response body\n    -- and followed by the response status code.\n    if single_request and res ~= nil then\n        return table_concat(response_body), code\n    end\n\n    return res, code\nend\n\n\nlocal function prepare_dirs_via_http(yaml_conf, args, index, host, host_count)\n    local is_success = true\n\n    local errmsg\n    local auth_token\n    local user = yaml_conf.etcd.user\n    local password = yaml_conf.etcd.password\n    if user and password then\n        local auth_url = host .. \"/v3/auth/authenticate\"\n        local json_auth = {\n            name = user,\n            password = password\n        }\n\n        local post_json_auth = dkjson.encode(json_auth)\n        local response_body = {}\n\n        local res, err\n        local retry_time = 0\n        while retry_time < 2 do\n            res, err = request({\n                url = auth_url,\n                method = \"POST\",\n                source = ltn12.source.string(post_json_auth),\n                sink = ltn12.sink.table(response_body),\n                headers = {\n                    [\"Content-Length\"] = #post_json_auth\n                }\n            }, yaml_conf)\n            -- In case of failure, request returns nil followed by an error message.\n            -- Else the first return value is just the number 1\n            -- and followed by the response status code.\n            if res then\n                break\n            end\n            retry_time = retry_time + 1\n            print(str_format(\"Warning! Request etcd endpoint \\'%s\\' error, %s, retry time=%s\",\n                                auth_url, err, retry_time))\n        end\n\n        if not res then\n            errmsg = str_format(\"request etcd endpoint \\\"%s\\\" error, %s\\n\", auth_url, err)\n            util.die(errmsg)\n        end\n\n        local res_auth = table_concat(response_body)\n        local body_auth, _, err_auth = dkjson.decode(res_auth)\n        if err_auth or (body_auth and not body_auth[\"token\"]) then\n            errmsg = str_format(\"got malformed auth message: \\\"%s\\\" from etcd \\\"%s\\\"\\n\",\n                                res_auth, auth_url)\n            util.die(errmsg)\n        end\n\n        auth_token = body_auth.token\n    end\n\n\n    local dirs = {}\n    for name in pairs(constants.HTTP_ETCD_DIRECTORY) do\n        dirs[name] = true\n    end\n    for name in pairs(constants.STREAM_ETCD_DIRECTORY) do\n        dirs[name] = true\n    end\n\n    for dir_name in pairs(dirs) do\n        local key =  (yaml_conf.etcd.prefix or \"\") .. dir_name .. \"/\"\n\n        local put_url = host .. \"/v3/kv/put\"\n        local post_json = '{\"value\":\"' .. base64_encode(\"init_dir\")\n                            .. '\", \"key\":\"' .. base64_encode(key) .. '\"}'\n        local response_body = {}\n        local headers = {[\"Content-Length\"] = #post_json}\n        if auth_token then\n            headers[\"Authorization\"] = auth_token\n        end\n\n        local res, err\n        local retry_time = 0\n        while retry_time < 2 do\n            res, err = request({\n                url = put_url,\n                method = \"POST\",\n                source = ltn12.source.string(post_json),\n                sink = ltn12.sink.table(response_body),\n                headers = headers\n            }, yaml_conf)\n            retry_time = retry_time + 1\n            if res then\n                break\n            end\n            print(str_format(\"Warning! Request etcd endpoint \\'%s\\' error, %s, retry time=%s\",\n                                put_url, err, retry_time))\n        end\n\n        if not res then\n            errmsg = str_format(\"request etcd endpoint \\\"%s\\\" error, %s\\n\", put_url, err)\n            util.die(errmsg)\n        end\n\n        local res_put = table_concat(response_body)\n        if res_put:find(\"404 page not found\", 1, true) then\n            errmsg = str_format(\"gRPC gateway is not enabled in etcd cluster \\\"%s\\\",\",\n                                \"which is required by Apache APISIX\\n\")\n            util.die(errmsg)\n        end\n\n        if res_put:find(\"CommonName of client sending a request against gateway\", 1, true) then\n            errmsg = str_format(\"etcd \\\"client-cert-auth\\\" cannot be used with gRPC-gateway, \"\n                                .. \"please configure the etcd username and password \"\n                                .. \"in configuration file\\n\")\n            util.die(errmsg)\n        end\n\n        if res_put:find(\"error\", 1, true) then\n            is_success = false\n            if (index == host_count) then\n                errmsg = str_format(\"got malformed key-put message: \\\"%s\\\" from etcd \\\"%s\\\"\\n\",\n                                    res_put, put_url)\n                util.die(errmsg)\n            end\n\n            break\n        end\n\n        if args and args[\"verbose\"] then\n            print(res_put)\n        end\n    end\n\n    return is_success\nend\n\n\nlocal function prepare_dirs(yaml_conf, args, index, host, host_count)\n    return prepare_dirs_via_http(yaml_conf, args, index, host, host_count)\nend\n\n\nfunction _M.init(env, args)\n    -- read_yaml_conf\n    local yaml_conf, err = file.read_yaml_conf(env.apisix_home)\n    if not yaml_conf then\n        util.die(\"failed to read local yaml config of apisix: \", err)\n    end\n\n    if not yaml_conf.apisix then\n        util.die(\"failed to read `apisix` field from yaml file when init etcd\")\n    end\n\n    if yaml_conf.deployment.config_provider ~= \"etcd\" then\n        return true\n    end\n\n    if not yaml_conf.etcd then\n        util.die(\"failed to read `etcd` field from yaml file when init etcd\")\n    end\n\n    -- convert old single etcd config to multiple etcd config\n    if type(yaml_conf.etcd.host) == \"string\" then\n        yaml_conf.etcd.host = {yaml_conf.etcd.host}\n    end\n\n    local host_count = #(yaml_conf.etcd.host)\n    local scheme\n    for i = 1, host_count do\n        local host = yaml_conf.etcd.host[i]\n        local fields = util.split(host, \"://\")\n        if not fields then\n            util.die(\"malformed etcd endpoint: \", host, \"\\n\")\n        end\n\n        if not scheme then\n            scheme = fields[1]\n        elseif scheme ~= fields[1] then\n            print([[WARNING: mixed protocols among etcd endpoints]])\n        end\n    end\n\n    -- check the etcd cluster version\n    local etcd_healthy_hosts = {}\n    for index, host in ipairs(yaml_conf.etcd.host) do\n        local version_url = host .. \"/version\"\n        local errmsg\n\n        local res, err\n        local retry_time = 0\n\n        local etcd = yaml_conf.etcd\n        local max_retry = tonumber(etcd.startup_retry) or 2\n        while retry_time < max_retry do\n            res, err = request(version_url, yaml_conf)\n            -- In case of failure, request returns nil followed by an error message.\n            -- Else the first return value is the response body\n            -- and followed by the response status code.\n            if res then\n                break\n            end\n            retry_time = retry_time + 1\n            print(str_format(\"Warning! Request etcd endpoint \\'%s\\' error, %s, retry time=%s\",\n                             version_url, err, retry_time))\n        end\n\n        if res then\n            local body, _, err = dkjson.decode(res)\n            if err or (body and not body[\"etcdcluster\"]) then\n                errmsg = str_format(\"got malformed version message: \\\"%s\\\" from etcd \\\"%s\\\"\\n\", res,\n                        version_url)\n                util.die(errmsg)\n            end\n\n            local cluster_version = body[\"etcdcluster\"]\n            if compare_semantic_version(cluster_version, env.min_etcd_version) then\n                util.die(\"etcd cluster version \", cluster_version,\n                         \" is less than the required version \", env.min_etcd_version,\n                         \", please upgrade your etcd cluster\\n\")\n            end\n\n            table_insert(etcd_healthy_hosts, host)\n        else\n            io_stderr:write(str_format(\"request etcd endpoint \\'%s\\' error, %s\\n\", version_url,\n                    err))\n        end\n    end\n\n    if #etcd_healthy_hosts <= 0 then\n        util.die(\"all etcd nodes are unavailable\\n\")\n    end\n\n    if (#etcd_healthy_hosts / host_count * 100) <= 50 then\n        util.die(\"the etcd cluster needs at least 50% and above healthy nodes\\n\")\n    end\n\n    -- access from the data plane to etcd should be read-only.\n    -- data plane writes to etcd may cause security issues.\n    if yaml_conf.deployment.role == \"data_plane\" then\n        print(\"access from the data plane to etcd should be read-only, \"\n              ..\"skip initializing the data of etcd\")\n        return true\n    end\n\n    print(\"trying to initialize the data of etcd\")\n    local etcd_ok = false\n    for index, host in ipairs(etcd_healthy_hosts) do\n        if prepare_dirs(yaml_conf, args, index, host, host_count) then\n            etcd_ok = true\n            break\n        end\n    end\n\n    if not etcd_ok then\n        util.die(\"none of the configured etcd works well\\n\")\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/file.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = ngx\nlocal yaml = require(\"lyaml\")\nlocal profile = require(\"apisix.core.profile\")\nlocal util = require(\"apisix.cli.util\")\nlocal schema = require(\"apisix.cli.schema\")\nlocal default_conf = require(\"apisix.cli.config\")\nlocal dkjson = require(\"dkjson\")\nlocal pl_path = require(\"pl.path\")\n\nlocal pairs = pairs\nlocal type = type\nlocal tonumber = tonumber\nlocal getenv = os.getenv\nlocal str_gmatch = string.gmatch\nlocal str_find = string.find\nlocal str_sub = string.sub\nlocal print = print\n\nlocal _M = {}\nlocal exported_vars\n\n\nfunction _M.get_exported_vars()\n    return exported_vars\nend\n\n\nlocal function is_empty_yaml_line(line)\n    return line == '' or str_find(line, '^%s*$') or str_find(line, '^%s*#')\nend\n\n\nlocal function tab_is_array(t)\n    local count = 0\n    for k, v in pairs(t) do\n        count = count + 1\n    end\n\n    return #t == count\nend\n\n\nlocal function var_sub(val)\n    local err\n    local var_used = false\n    -- we use '${{var}}' because '$var' and '${var}' are taken\n    -- by Nginx\n    local new_val = val:gsub(\"%$%{%{%s*([%w_]+[%:%=]?.-)%s*%}%}\", function(var)\n        local i, j = var:find(\"%:%=\")\n        local default\n        if i and j then\n            default = var:sub(i + 2, #var)\n            default = default:gsub('^%s*(.-)%s*$', '%1')\n            var = var:sub(1, i - 1)\n        end\n\n        local v = getenv(var) or default\n        if v then\n            if not exported_vars then\n                exported_vars = {}\n            end\n\n            exported_vars[var] = v\n            var_used = true\n            return v\n        end\n\n        err = \"failed to handle configuration: \" ..\n              \"can't find environment variable \" .. var\n        return \"\"\n    end)\n    return new_val, var_used, err\nend\n\n\nlocal function resolve_conf_var(conf)\n    local new_keys = {}\n    for key, val in pairs(conf) do\n        -- avoid re-iterating the table for already iterated key\n        if new_keys[key] then\n            goto continue\n        end\n        -- substitute environment variables from conf keys\n        if type(key) == \"string\" then\n            local new_key, _, err = var_sub(key)\n            if err then\n                return nil, err\n            end\n            if new_key ~= key then\n                new_keys[new_key] = \"dummy\" -- we only care about checking the key\n                conf.key = nil\n                conf[new_key] = val\n                key = new_key\n            end\n        end\n        if type(val) == \"table\" then\n            local ok, err = resolve_conf_var(val)\n            if not ok then\n                return nil, err\n            end\n\n        elseif type(val) == \"string\" then\n            local new_val, var_used, err = var_sub(val)\n\n            if err then\n                return nil, err\n            end\n\n            if var_used then\n                if tonumber(new_val) ~= nil then\n                    new_val = tonumber(new_val)\n                elseif new_val == \"true\" then\n                    new_val = true\n                elseif new_val == \"false\" then\n                    new_val = false\n                end\n            end\n\n            conf[key] = new_val\n        end\n        ::continue::\n    end\n\n    return true\nend\n\n\n_M.resolve_conf_var = resolve_conf_var\n\n\nlocal function replace_by_reserved_env_vars(conf)\n    -- TODO: support more reserved environment variables\n    local v = getenv(\"APISIX_DEPLOYMENT_ETCD_HOST\")\n    if v and conf[\"deployment\"] and conf[\"deployment\"][\"etcd\"] then\n        local val, _, err = dkjson.decode(v)\n        if err or not val then\n            print(\"parse ${APISIX_DEPLOYMENT_ETCD_HOST} failed, error:\", err)\n            return\n        end\n\n        conf[\"deployment\"][\"etcd\"][\"host\"] = val\n    end\nend\n\n\nlocal function path_is_multi_type(path, type_val)\n    if str_sub(path, 1, 14) == \"nginx_config->\" and\n            (type_val == \"number\" or type_val == \"string\") then\n        return true\n    end\n\n    if path == \"apisix->node_listen\" and type_val == \"number\" then\n        return true\n    end\n\n    if path == \"apisix->data_encryption->keyring\" then\n        return true\n    end\n\n    return false\nend\n\n\nlocal function merge_conf(base, new_tab, ppath)\n    ppath = ppath or \"\"\n\n    for key, val in pairs(new_tab) do\n        if type(val) == \"table\" then\n            if val == yaml.null then\n                base[key] = nil\n\n            elseif tab_is_array(val) then\n                base[key] = val\n\n            else\n                if base[key] == nil then\n                    base[key] = {}\n                end\n\n                local ok, err = merge_conf(\n                    base[key],\n                    val,\n                    ppath == \"\" and key or ppath .. \"->\" .. key\n                )\n                if not ok then\n                    return nil, err\n                end\n            end\n        else\n            local type_val = type(val)\n\n            if base[key] == nil then\n                base[key] = val\n            elseif type(base[key]) ~= type_val then\n                local path = ppath == \"\" and key or ppath .. \"->\" .. key\n\n                if path_is_multi_type(path, type_val) then\n                    base[key] = val\n                else\n                    return nil, \"failed to merge, path[\" .. path ..  \"] expect: \" ..\n                                type(base[key]) .. \", but got: \" .. type_val\n                end\n            else\n                base[key] = val\n            end\n        end\n    end\n\n    return base\nend\n\n\nfunction _M.read_yaml_conf(apisix_home)\n    if apisix_home then\n        profile.apisix_home = apisix_home .. \"/\"\n    end\n\n    local local_conf_path = profile:customized_yaml_path()\n    if not local_conf_path then\n        local_conf_path = profile:yaml_path(\"config\")\n    end\n    local user_conf_yaml, err = util.read_file(local_conf_path)\n    if not user_conf_yaml then\n        return nil, err\n    end\n\n    local is_empty_file = true\n    for line in str_gmatch(user_conf_yaml .. '\\n', '(.-)\\r?\\n') do\n        if not is_empty_yaml_line(line) then\n            is_empty_file = false\n            break\n        end\n    end\n\n    if not is_empty_file then\n        local user_conf = yaml.load(user_conf_yaml)\n        if not user_conf then\n            return nil, \"invalid config.yaml file\"\n        end\n\n        local ok, err = resolve_conf_var(user_conf)\n        if not ok then\n            return nil, err\n        end\n\n        ok, err = merge_conf(default_conf, user_conf)\n        if not ok then\n            return nil, err\n        end\n    end\n\n    -- fill the default value by the schema\n    local ok, err = schema.validate(default_conf)\n    if not ok then\n        return nil, err\n    end\n    if default_conf.deployment then\n        default_conf.deployment.config_provider = \"etcd\"\n        if default_conf.deployment.role == \"traditional\" then\n            default_conf.etcd = default_conf.deployment.etcd\n            if default_conf.deployment.role_traditional.config_provider == \"yaml\" then\n                default_conf.deployment.config_provider = \"yaml\"\n            end\n\n        elseif default_conf.deployment.role == \"control_plane\" then\n            default_conf.etcd = default_conf.deployment.etcd\n            default_conf.apisix.enable_admin = true\n\n        elseif default_conf.deployment.role == \"data_plane\" then\n            default_conf.etcd = default_conf.deployment.etcd\n            if default_conf.deployment.role_data_plane.config_provider == \"yaml\" then\n                default_conf.deployment.config_provider = \"yaml\"\n            elseif default_conf.deployment.role_data_plane.config_provider == \"json\" then\n                default_conf.deployment.config_provider = \"json\"\n            elseif default_conf.deployment.role_data_plane.config_provider == \"xds\" then\n                default_conf.deployment.config_provider = \"xds\"\n            end\n            default_conf.apisix.enable_admin = false\n        end\n    end\n\n    --- using `not ngx` to check whether the current execution environment is apisix cli module,\n    --- because it is only necessary to parse and validate `apisix.yaml` in apisix cli.\n    if default_conf.deployment.config_provider == \"yaml\" and not ngx then\n        local apisix_conf_path = profile:yaml_path(\"apisix\")\n        local apisix_conf_yaml, _ = util.read_file(apisix_conf_path)\n        if apisix_conf_yaml then\n            local apisix_conf = yaml.load(apisix_conf_yaml)\n            if apisix_conf then\n                local ok, err = resolve_conf_var(apisix_conf)\n                if not ok then\n                    return nil, err\n                end\n            end\n        end\n    end\n\n    local apisix_ssl = default_conf.apisix.ssl\n    if apisix_ssl and apisix_ssl.ssl_trusted_certificate then\n        -- default value is set to \"system\" during schema validation\n        if apisix_ssl.ssl_trusted_certificate == \"system\" then\n            local trusted_certs_path, err = util.get_system_trusted_certs_filepath()\n            if not trusted_certs_path then\n                util.die(err)\n            end\n\n            apisix_ssl.ssl_trusted_certificate = trusted_certs_path\n        else\n            -- During validation, the path is relative to PWD\n            -- When Nginx starts, the path is relative to conf\n            -- Therefore we need to check the absolute version instead\n            local cert_path = pl_path.abspath(apisix_ssl.ssl_trusted_certificate)\n            if not pl_path.exists(cert_path) then\n                util.die(\"certificate path\", cert_path, \"doesn't exist\\n\")\n            end\n            apisix_ssl.ssl_trusted_certificate = cert_path\n        end\n    end\n\n    replace_by_reserved_env_vars(default_conf)\n\n    return default_conf\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/ip.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- IP match and verify module.\n--\n-- @module cli.ip\n\nlocal mediador_ip = require(\"resty.mediador.ip\")\nlocal setmetatable = setmetatable\n\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\n---\n-- create a instance of module cli.ip\n--\n-- @function cli.ip:new\n-- @tparam string ip IP or CIDR.\n-- @treturn instance of module if the given ip valid, nil and error message otherwise.\nfunction _M.new(self, ip)\n    if not mediador_ip.valid(ip) then\n        return nil, \"invalid ip\"\n    end\n\n    local _ip = mediador_ip.parse(ip)\n\n    return setmetatable({ _ip = _ip }, mt)\nend\n\n\n---\n-- Is that the given ip loopback?\n--\n-- @function cli.ip:is_loopback\n-- @treturn boolean True if the given ip is the loopback, false otherwise.\nfunction _M.is_loopback(self)\n    return self._ip and \"loopback\" == self._ip:range()\nend\n\n---\n-- Is that the given ip unspecified?\n--\n-- @function cli.ip:is_unspecified\n-- @treturn boolean True if the given ip is all the unspecified, false otherwise.\nfunction _M.is_unspecified(self)\n    return self._ip and \"unspecified\" == self._ip:range()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/ngx_tpl.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn [=[\n# Configuration File - Nginx Server Configs\n# This is a read-only file, do not try to modify it.\n{% if user and user ~= '' then %}\nuser {* user *};\n{% end %}\nmaster_process on;\n\nworker_processes {* worker_processes *};\n{% if os_name == \"Linux\" and enable_cpu_affinity == true then %}\nworker_cpu_affinity auto;\n{% end %}\n\n# main configuration snippet starts\n{% if main_configuration_snippet then %}\n{* main_configuration_snippet *}\n{% end %}\n# main configuration snippet ends\n\nerror_log {* error_log *} {* error_log_level or \"warn\" *};\npid logs/nginx.pid;\n\nworker_rlimit_nofile {* worker_rlimit_nofile *};\n\nevents {\n    accept_mutex off;\n    worker_connections {* event.worker_connections *};\n}\n\nworker_rlimit_core  {* worker_rlimit_core *};\n\nworker_shutdown_timeout {* worker_shutdown_timeout *};\n\nenv APISIX_PROFILE;\nenv PATH; # for searching external plugin runner's binary\n\n# reserved environment variables for configuration\nenv APISIX_DEPLOYMENT_ETCD_HOST;\nenv GCP_SERVICE_ACCOUNT;\n\n{% if envs then %}\n{% for _, name in ipairs(envs) do %}\nenv {*name*};\n{% end %}\n{% end %}\n\n{% if use_apisix_base then %}\nthread_pool grpc-client-nginx-module threads=1;\n\nlua {\n    {% if enabled_stream_plugins[\"prometheus\"] then %}\n    lua_shared_dict prometheus-metrics {* meta.lua_shared_dict[\"prometheus-metrics\"] *};\n    {% end %}\n    {% if enabled_plugins[\"prometheus\"] or enabled_stream_plugins[\"prometheus\"] then %}\n    lua_shared_dict prometheus-cache {* meta.lua_shared_dict[\"prometheus-cache\"] *};\n    {% end %}\n    {% if standalone_with_admin_api then %}\n    lua_shared_dict standalone-config {* meta.lua_shared_dict[\"standalone-config\"] *};\n    {% end %}\n    {% if status then %}\n    lua_shared_dict status-report {* meta.lua_shared_dict[\"status-report\"] *};\n    {% end %}\n    lua_shared_dict nacos 10m;\n    lua_shared_dict upstream-healthcheck {* meta.lua_shared_dict[\"upstream-healthcheck\"] *};\n}\n\n{% if enabled_stream_plugins[\"prometheus\"] and not enable_http then %}\nhttp {\n    lua_package_path  \"{*extra_lua_path*}$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;]=]\n                       .. [=[{*apisix_lua_home*}/?.lua;{*apisix_lua_home*}/?/init.lua;;{*lua_path*};\";\n    lua_package_cpath \"{*extra_lua_cpath*}$prefix/deps/lib64/lua/5.1/?.so;]=]\n                      .. [=[$prefix/deps/lib/lua/5.1/?.so;;]=]\n                      .. [=[{*lua_cpath*};\";\n\n    {% if enabled_stream_plugins[\"prometheus\"] then %}\n\n    init_by_lua_block {\n        require \"resty.core\"\n        local process = require(\"ngx.process\")\n        local ok, err = process.enable_privileged_agent()\n        if not ok then\n            ngx.log(ngx.ERR, \"failed to enable privileged_agent: \", err)\n        end\n    }\n\n    init_worker_by_lua_block {\n        local prometheus = require(\"apisix.plugins.prometheus.exporter\")\n        prometheus.http_init(true)\n        prometheus.init_exporter_timer()\n    }\n\n    server {\n        listen {* prometheus_server_addr *} reuseport;\n\n        access_log off;\n\n        location / {\n            content_by_lua_block {\n                local prometheus = require(\"apisix.plugins.prometheus.exporter\")\n                prometheus.export_metrics()\n            }\n        }\n\n        location = /apisix/nginx_status {\n            allow 127.0.0.0/24;\n            deny all;\n            stub_status;\n        }\n    }\n    {% end %}\n}\n{% end %}\n\n{% end %}\n\n{% if enable_stream then %}\nstream {\n    lua_package_path  \"{*extra_lua_path*}$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;]=]\n                      .. [=[{*apisix_lua_home*}/?.lua;{*apisix_lua_home*}/?/init.lua;;{*lua_path*};\";\n    lua_package_cpath \"{*extra_lua_cpath*}$prefix/deps/lib64/lua/5.1/?.so;]=]\n                      .. [=[$prefix/deps/lib/lua/5.1/?.so;;]=]\n                      .. [=[{*lua_cpath*};\";\n    lua_socket_log_errors off;\n\n    {% if max_pending_timers then %}\n    lua_max_pending_timers {* max_pending_timers *};\n    {% end %}\n    {% if max_running_timers then %}\n    lua_max_running_timers {* max_running_timers *};\n    {% end %}\n\n    lua_shared_dict lrucache-lock-stream {* stream.lua_shared_dict[\"lrucache-lock-stream\"] *};\n    lua_shared_dict etcd-cluster-health-check-stream {* stream.lua_shared_dict[\"etcd-cluster-health-check-stream\"] *};\n    lua_shared_dict worker-events-stream {* stream.lua_shared_dict[\"worker-events-stream\"] *};\n\n    {% if enabled_discoveries[\"tars\"] then %}\n    lua_shared_dict tars-stream {* stream.lua_shared_dict[\"tars-stream\"] *};\n    {% end %}\n\n    {% if enabled_stream_plugins[\"limit-conn\"] then %}\n    lua_shared_dict plugin-limit-conn-stream {* stream.lua_shared_dict[\"plugin-limit-conn-stream\"] *};\n    {% end %}\n\n    # for discovery shared dict\n    {% if discovery_shared_dicts then %}\n    {% for key, size in pairs(discovery_shared_dicts) do %}\n    lua_shared_dict {*key*}-stream {*size*};\n    {% end %}\n    {% end %}\n\n    resolver {% for _, dns_addr in ipairs(dns_resolver or {}) do %} {*dns_addr*} {% end %} {% if dns_resolver_valid then %} valid={*dns_resolver_valid*}{% end %} ipv6={% if enable_ipv6 then %}on{% else %}off{% end %};\n    resolver_timeout {*resolver_timeout*};\n\n    {% if ssl.ssl_trusted_certificate ~= nil then %}\n    lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *};\n    {% end %}\n\n    # for stream logs, off by default\n    {% if stream.enable_access_log == true then %}\n    log_format main escape={* stream.access_log_format_escape *} '{* stream.access_log_format *}';\n\n    access_log {* stream.access_log *} main buffer=16384 flush=3;\n    {% end %}\n\n    # stream configuration snippet starts\n    {% if stream_configuration_snippet then %}\n    {* stream_configuration_snippet *}\n    {% end %}\n    # stream configuration snippet ends\n\n    upstream apisix_backend {\n        server 127.0.0.1:80;\n        balancer_by_lua_block {\n            apisix.stream_balancer_phase()\n        }\n    }\n\n    init_by_lua_block {\n        require \"resty.core\"\n        {% if lua_module_hook then %}\n        require \"{* lua_module_hook *}\"\n        {% end %}\n        apisix = require(\"apisix\")\n        local dns_resolver = { {% for _, dns_addr in ipairs(dns_resolver or {}) do %} \"{*dns_addr*}\", {% end %} }\n        local args = {\n            dns_resolver = dns_resolver,\n        }\n        apisix.stream_init(args)\n    }\n\n    init_worker_by_lua_block {\n        apisix.stream_init_worker()\n    }\n\n    # the server block for lua-resty-events\n    server {\n        listen unix:{*apisix_lua_home*}/logs/stream_worker_events.sock;\n        access_log off;\n        content_by_lua_block {\n            require(\"resty.events.compat\").run()\n        }\n    }\n\n    server {\n        {% for _, item in ipairs(stream_proxy.tcp or {}) do %}\n        listen {*item.addr*} {% if item.tls then %} ssl {% end %} {% if enable_reuseport then %} reuseport {% end %} {% if proxy_protocol and proxy_protocol.enable_tcp_pp then %} proxy_protocol {% end %};\n        {% end %}\n        {% for _, addr in ipairs(stream_proxy.udp or {}) do %}\n        listen {*addr*} udp {% if enable_reuseport then %} reuseport {% end %};\n        {% end %}\n\n        {% if tcp_enable_ssl then %}\n        ssl_certificate      {* ssl.ssl_cert *};\n        ssl_certificate_key  {* ssl.ssl_cert_key *};\n\n        ssl_client_hello_by_lua_block {\n            apisix.ssl_client_hello_phase()\n        }\n\n        ssl_certificate_by_lua_block {\n            apisix.ssl_phase()\n        }\n        {% end %}\n\n        {% if proxy_protocol and proxy_protocol.enable_tcp_pp_to_upstream then %}\n        proxy_protocol on;\n        {% end %}\n\n        preread_by_lua_block {\n            apisix.stream_preread_phase()\n        }\n\n        proxy_pass apisix_backend;\n\n        {% if use_apisix_base then %}\n        set $upstream_sni \"apisix_backend\";\n        proxy_ssl_server_name on;\n        proxy_ssl_name $upstream_sni;\n        {% end %}\n\n        log_by_lua_block {\n            apisix.stream_log_phase()\n        }\n    }\n}\n{% end %}\n\n{% if enable_http then %}\nhttp {\n    # put extra_lua_path in front of the builtin path\n    # so user can override the source code\n    lua_package_path  \"{*extra_lua_path*}$prefix/deps/share/lua/5.1/?.lua;$prefix/deps/share/lua/5.1/?/init.lua;]=]\n                       .. [=[{*apisix_lua_home*}/?.lua;{*apisix_lua_home*}/?/init.lua;;{*lua_path*};\";\n    lua_package_cpath \"{*extra_lua_cpath*}$prefix/deps/lib64/lua/5.1/?.so;]=]\n                      .. [=[$prefix/deps/lib/lua/5.1/?.so;;]=]\n                      .. [=[{*lua_cpath*};\";\n\n    {% if max_pending_timers then %}\n    lua_max_pending_timers {* max_pending_timers *};\n    {% end %}\n    {% if max_running_timers then %}\n    lua_max_running_timers {* max_running_timers *};\n    {% end %}\n\n    lua_shared_dict internal-status {* http.lua_shared_dict[\"internal-status\"] *};\n    lua_shared_dict worker-events {* http.lua_shared_dict[\"worker-events\"] *};\n    lua_shared_dict lrucache-lock {* http.lua_shared_dict[\"lrucache-lock\"] *};\n    lua_shared_dict balancer-ewma {* http.lua_shared_dict[\"balancer-ewma\"] *};\n    lua_shared_dict balancer-ewma-locks {* http.lua_shared_dict[\"balancer-ewma-locks\"] *};\n    lua_shared_dict balancer-ewma-last-touched-at {* http.lua_shared_dict[\"balancer-ewma-last-touched-at\"] *};\n    lua_shared_dict etcd-cluster-health-check {* http.lua_shared_dict[\"etcd-cluster-health-check\"] *}; # etcd health check\n\n    # for discovery shared dict\n    {% if discovery_shared_dicts then %}\n    {% for key, size in pairs(discovery_shared_dicts) do %}\n    lua_shared_dict {*key*} {*size*};\n    {% end %}\n    {% end %}\n\n    {% if enabled_discoveries[\"tars\"] then %}\n    lua_shared_dict tars {* http.lua_shared_dict[\"tars\"] *};\n    {% end %}\n\n\n    {% if http.lua_shared_dict[\"plugin-ai-rate-limiting\"] then %}\n    lua_shared_dict plugin-ai-rate-limiting {* http.lua_shared_dict[\"plugin-ai-rate-limiting\"] *};\n    {% else %}\n    lua_shared_dict plugin-ai-rate-limiting 10m;\n    {% end %}\n\n    {% if http.lua_shared_dict[\"plugin-ai-rate-limiting\"] then %}\n    lua_shared_dict plugin-ai-rate-limiting-reset-header {* http.lua_shared_dict[\"plugin-ai-rate-limiting-reset-header\"] *};\n    {% else %}\n    lua_shared_dict plugin-ai-rate-limiting-reset-header 10m;\n    {% end %}\n\n    {% if enabled_plugins[\"limit-conn\"] then %}\n    lua_shared_dict plugin-limit-conn {* http.lua_shared_dict[\"plugin-limit-conn\"] *};\n    lua_shared_dict plugin-limit-conn-redis-cluster-slot-lock {* http.lua_shared_dict[\"plugin-limit-conn-redis-cluster-slot-lock\"] *};\n    {% end %}\n\n    {% if enabled_plugins[\"limit-req\"] then %}\n    lua_shared_dict plugin-limit-req-redis-cluster-slot-lock {* http.lua_shared_dict[\"plugin-limit-req-redis-cluster-slot-lock\"] *};\n    lua_shared_dict plugin-limit-req {* http.lua_shared_dict[\"plugin-limit-req\"] *};\n    {% end %}\n\n    {% if enabled_plugins[\"limit-count\"] then %}\n    lua_shared_dict plugin-limit-count {* http.lua_shared_dict[\"plugin-limit-count\"] *};\n    lua_shared_dict plugin-limit-count-redis-cluster-slot-lock {* http.lua_shared_dict[\"plugin-limit-count-redis-cluster-slot-lock\"] *};\n    lua_shared_dict plugin-limit-count-reset-header {* http.lua_shared_dict[\"plugin-limit-count\"] *};\n    {% end %}\n\n    {% if enabled_plugins[\"prometheus\"] and not enabled_stream_plugins[\"prometheus\"] then %}\n    lua_shared_dict prometheus-metrics {* http.lua_shared_dict[\"prometheus-metrics\"] *};\n    {% end %}\n\n    {% if enabled_plugins[\"skywalking\"] then %}\n    lua_shared_dict tracing_buffer {* http.lua_shared_dict.tracing_buffer *}; # plugin: skywalking\n    {% end %}\n\n    {% if enabled_plugins[\"api-breaker\"] then %}\n    lua_shared_dict plugin-api-breaker {* http.lua_shared_dict[\"plugin-api-breaker\"] *};\n    {% end %}\n\n    {% if enabled_plugins[\"openid-connect\"] or enabled_plugins[\"authz-keycloak\"] then %}\n    # for openid-connect and authz-keycloak plugin\n    lua_shared_dict discovery {* http.lua_shared_dict[\"discovery\"] *}; # cache for discovery metadata documents\n    {% end %}\n\n    {% if enabled_plugins[\"openid-connect\"] then %}\n    # for openid-connect plugin\n    lua_shared_dict jwks {* http.lua_shared_dict[\"jwks\"] *}; # cache for JWKs\n    lua_shared_dict introspection {* http.lua_shared_dict[\"introspection\"] *}; # cache for JWT verification results\n    {% end %}\n\n    {% if enabled_plugins[\"cas-auth\"] then %}\n    lua_shared_dict cas_sessions {* http.lua_shared_dict[\"cas-auth\"] *};\n    {% end %}\n\n    {% if enabled_plugins[\"authz-keycloak\"] then %}\n    # for authz-keycloak\n    lua_shared_dict access-tokens {* http.lua_shared_dict[\"access-tokens\"] *}; # cache for service account access tokens\n    {% end %}\n\n    {% if enabled_plugins[\"ocsp-stapling\"] then %}\n    lua_shared_dict ocsp-stapling {* http.lua_shared_dict[\"ocsp-stapling\"] *}; # cache for ocsp-stapling\n    {% end %}\n\n    {% if enabled_plugins[\"ext-plugin-pre-req\"] or enabled_plugins[\"ext-plugin-post-req\"] then %}\n    lua_shared_dict ext-plugin {* http.lua_shared_dict[\"ext-plugin\"] *}; # cache for ext-plugin\n    {% end %}\n\n    {% if enabled_plugins[\"mcp-bridge\"] then %}\n    lua_shared_dict mcp-session {* http.lua_shared_dict[\"mcp-session\"] *}; # cache for mcp-session\n    {% end %}\n\n    {% if config_center == \"xds\" then %}\n    lua_shared_dict xds-config  10m;\n    lua_shared_dict xds-config-version  1m;\n    {% end %}\n\n    # for custom shared dict\n    {% if http.custom_lua_shared_dict then %}\n    {% for cache_key, cache_size in pairs(http.custom_lua_shared_dict) do %}\n    lua_shared_dict {*cache_key*} {*cache_size*};\n    {% end %}\n    {% end %}\n\n    {% if enabled_plugins[\"error-log-logger\"] then %}\n        lua_capture_error_log  10m;\n    {% end %}\n\n    lua_ssl_verify_depth 5;\n    ssl_session_timeout 86400;\n\n    {% if http.underscores_in_headers then %}\n    underscores_in_headers {* http.underscores_in_headers *};\n    {%end%}\n\n    lua_socket_log_errors off;\n\n    resolver {% for _, dns_addr in ipairs(dns_resolver or {}) do %} {*dns_addr*} {% end %} {% if dns_resolver_valid then %} valid={*dns_resolver_valid*}{% end %} ipv6={% if enable_ipv6 then %}on{% else %}off{% end %};\n    resolver_timeout {*resolver_timeout*};\n\n    lua_http10_buffering off;\n\n    lua_regex_match_limit 100000;\n    lua_regex_cache_max_entries 8192;\n\n    {% if http.enable_access_log == false then %}\n    access_log off;\n    {% else %}\n    log_format main escape={* http.access_log_format_escape *} '{* http.access_log_format *}';\n    uninitialized_variable_warn off;\n\n    {% if http.access_log_buffer then %}\n    access_log {* http.access_log *} main buffer={* http.access_log_buffer *} flush=3;\n    {% else %}\n    access_log {* http.access_log *} main buffer=16384 flush=3;\n    {% end %}\n    {% end %}\n    open_file_cache  max=1000 inactive=60;\n    client_max_body_size {* http.client_max_body_size *};\n    keepalive_timeout {* http.keepalive_timeout *};\n    client_header_timeout {* http.client_header_timeout *};\n    client_body_timeout {* http.client_body_timeout *};\n    send_timeout {* http.send_timeout *};\n    variables_hash_max_size {* http.variables_hash_max_size *};\n\n    server_tokens off;\n\n    include mime.types;\n    charset {* http.charset *};\n\n    {% if http.real_ip_header then %}\n    real_ip_header {* http.real_ip_header *};\n    {% end %}\n\n    {% if http.real_ip_recursive then %}\n    real_ip_recursive {* http.real_ip_recursive *};\n    {% end %}\n\n    {% if http.real_ip_from then %}\n    {% for _, real_ip in ipairs(http.real_ip_from) do %}\n    set_real_ip_from {*real_ip*};\n    {% end %}\n    {% end %}\n\n    {% if ssl.ssl_trusted_certificate ~= nil then %}\n    lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *};\n    {% end %}\n    # http configuration snippet starts\n    {% if http_configuration_snippet then %}\n    {* http_configuration_snippet *}\n    {% end %}\n    # http configuration snippet ends\n\n    upstream apisix_backend {\n        server 0.0.0.1;\n\n        {% if use_apisix_base then %}\n        keepalive {* http.upstream.keepalive *};\n        keepalive_requests {* http.upstream.keepalive_requests *};\n        keepalive_timeout {* http.upstream.keepalive_timeout *};\n        # we put the static configuration above so that we can override it in the Lua code\n\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n        {% else %}\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n\n        keepalive {* http.upstream.keepalive *};\n        keepalive_requests {* http.upstream.keepalive_requests *};\n        keepalive_timeout {* http.upstream.keepalive_timeout *};\n        {% end %}\n    }\n\n    {% if enabled_plugins[\"dubbo-proxy\"] then %}\n    upstream apisix_dubbo_backend {\n        server 0.0.0.1;\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n\n        # dynamical keepalive doesn't work with dubbo as the connection here\n        # is managed by ngx_multi_upstream_module\n        multi {* dubbo_upstream_multiplex_count *};\n        keepalive {* http.upstream.keepalive *};\n        keepalive_requests {* http.upstream.keepalive_requests *};\n        keepalive_timeout {* http.upstream.keepalive_timeout *};\n    }\n    {% end %}\n\n    {% if use_apisix_base then %}\n    apisix_delay_client_max_body_check on;\n    apisix_mirror_on_demand on;\n    {% end %}\n\n    {% if wasm then %}\n    wasm_vm wasmtime;\n    {% end %}\n\n    init_by_lua_block {\n        require \"resty.core\"\n        {% if lua_module_hook then %}\n        require \"{* lua_module_hook *}\"\n        {% end %}\n        apisix = require(\"apisix\")\n\n        local dns_resolver = { {% for _, dns_addr in ipairs(dns_resolver or {}) do %} \"{*dns_addr*}\", {% end %} }\n        local args = {\n            dns_resolver = dns_resolver,\n        }\n        apisix.http_init(args)\n\n        -- set apisix_lua_home into constants module\n        -- it may be used by plugins to determine the work path of apisix\n        local constants = require(\"apisix.constants\")\n        constants.apisix_lua_home = \"{*apisix_lua_home*}\"\n    }\n\n    init_worker_by_lua_block {\n        apisix.http_init_worker()\n    }\n\n    exit_worker_by_lua_block {\n        apisix.http_exit_worker()\n    }\n\n    # the server block for lua-resty-events\n    server {\n        listen unix:{*apisix_lua_home*}/logs/worker_events.sock;\n        access_log off;\n        location / {\n            content_by_lua_block {\n                require(\"resty.events.compat\").run()\n            }\n        }\n    }\n\n    {% if enable_control then %}\n    server {\n        listen {* control_server_addr *};\n\n        access_log off;\n\n        location / {\n            content_by_lua_block {\n                apisix.http_control()\n            }\n        }\n    }\n    {% end %}\n\n    {% if status then %}\n    server {\n        listen {* status_server_addr *} enable_process=privileged_agent;\n        access_log off;\n        location /status {\n            content_by_lua_block {\n                apisix.status()\n            }\n        }\n        location /status/ready {\n            content_by_lua_block {\n                apisix.status_ready()\n            }\n        }\n    }\n    {% end %}\n\n    {% if enabled_plugins[\"prometheus\"] and prometheus_server_addr then %}\n    server {\n        listen {* prometheus_server_addr *} reuseport;\n\n        access_log off;\n\n        location / {\n            content_by_lua_block {\n                local prometheus = require(\"apisix.plugins.prometheus.exporter\")\n                prometheus.export_metrics()\n            }\n        }\n\n        location = /apisix/nginx_status {\n            allow 127.0.0.0/24;\n            deny all;\n            stub_status;\n        }\n    }\n    {% end %}\n\n    {% if enable_admin then %}\n    server {\n        {%if https_admin then%}\n        listen {* admin_server_addr *} ssl;\n\n        ssl_certificate      {* admin_api_mtls.admin_ssl_cert *};\n        ssl_certificate_key  {* admin_api_mtls.admin_ssl_cert_key *};\n        {%if admin_api_mtls.admin_ssl_ca_cert and admin_api_mtls.admin_ssl_ca_cert ~= \"\" then%}\n        ssl_verify_client on;\n        ssl_client_certificate {* admin_api_mtls.admin_ssl_ca_cert *};\n        {% end %}\n\n        ssl_session_cache    shared:SSL:20m;\n        ssl_protocols {* ssl.ssl_protocols *};\n        ssl_ciphers {* ssl.ssl_ciphers *};\n        ssl_prefer_server_ciphers on;\n        {% if ssl.ssl_session_tickets then %}\n        ssl_session_tickets on;\n        {% else %}\n        ssl_session_tickets off;\n        {% end %}\n\n        {% else %}\n        listen {* admin_server_addr *};\n        {%end%}\n        log_not_found off;\n\n        # admin configuration snippet starts\n        {% if http_admin_configuration_snippet then %}\n        {* http_admin_configuration_snippet *}\n        {% end %}\n        # admin configuration snippet ends\n\n        set $upstream_scheme             'http';\n        set $upstream_host               $http_host;\n        set $upstream_uri                '';\n\n        {%if allow_admin then%}\n        {% for _, allow_ip in ipairs(allow_admin) do %}\n        allow {*allow_ip*};\n        {% end %}\n        deny all;\n        {%else%}\n        allow all;\n        {%end%}\n\n        {% if use_apisix_base then %}\n        set $apisix_request_id $request_id;\n        lua_error_log_request_id $apisix_request_id;\n        {% end %}\n        location /apisix/admin {\n            content_by_lua_block {\n                apisix.http_admin()\n            }\n        }\n\n        {% if enable_admin_ui then %}\n        location = /ui {\n            # Fixes incorrect redirect URLs when Nginx is behind a reverse proxy.\n            # By default, Nginx generates an absolute URL (e.g., http://backend:9180/ui/).\n            # Setting this to \"off\" generates a relative URL (e.g., /ui/), which the browser\n            # correctly resolves against the public-facing domain.\n            absolute_redirect off;\n            return 301 /ui/;\n        }\n        location ^~ /ui/ {\n            rewrite ^/ui/(.*)$ /$1 break;\n            root {* apisix_lua_home *}/ui;\n            try_files $uri /index.html =404;\n            gzip on;\n            gzip_types text/css application/javascript application/json;\n            expires 7200s;\n            add_header Cache-Control \"private,max-age=7200\";\n        }\n        {% end %}\n    }\n    {% end %}\n\n    {% if deployment_role ~= \"control_plane\" then %}\n\n    {% if enabled_plugins[\"proxy-cache\"] then %}\n    # for proxy cache\n    {% for _, cache in ipairs(proxy_cache.zones) do %}\n    {% if cache.disk_path and cache.cache_levels and cache.disk_size then %}\n    proxy_cache_path {* cache.disk_path *} levels={* cache.cache_levels *} keys_zone={* cache.name *}:{* cache.memory_size *} inactive=1d max_size={* cache.disk_size *} use_temp_path=off;\n    {% else %}\n    lua_shared_dict {* cache.name *} {* cache.memory_size *};\n    {% end %}\n    {% end %}\n\n    map $upstream_cache_zone $upstream_cache_zone_info {\n    {% for _, cache in ipairs(proxy_cache.zones) do %}\n    {% if cache.disk_path and cache.cache_levels and cache.disk_size then %}\n        {* cache.name *} {* cache.disk_path *},{* cache.cache_levels *};\n    {% end %}\n    {% end %}\n    }\n    {% end %}\n\n    server {\n        {% if enable_http2 then %}\n        http2 on;\n        {% end %}\n        {% if enable_http3_in_server_context then %}\n        http3 on;\n        {% end %}\n        {% for _, item in ipairs(node_listen) do %}\n        listen {* item.ip *}:{* item.port *} default_server {% if enable_reuseport then %} reuseport {% end %};\n        {% end %}\n        {% if ssl.enable then %}\n        {% for _, item in ipairs(ssl.listen) do %}\n        {% if item.enable_http3 then %}\n        listen {* item.ip *}:{* item.port *} quic default_server {% if enable_reuseport then %} reuseport {% end %};\n        listen {* item.ip *}:{* item.port *} ssl default_server;\n        {% else %}\n        listen {* item.ip *}:{* item.port *} ssl default_server {% if enable_reuseport then %} reuseport {% end %};\n        {% end %}\n        {% end %}\n        {% end %}\n        {% if proxy_protocol and proxy_protocol.listen_http_port then %}\n        listen {* proxy_protocol.listen_http_port *} default_server proxy_protocol;\n        {% end %}\n        {% if proxy_protocol and proxy_protocol.listen_https_port then %}\n        listen {* proxy_protocol.listen_https_port *} ssl default_server proxy_protocol;\n        {% end %}\n\n        server_name _;\n\n        {% if ssl.enable then %}\n        ssl_certificate      {* ssl.ssl_cert *};\n        ssl_certificate_key  {* ssl.ssl_cert_key *};\n        ssl_session_cache    shared:SSL:20m;\n        ssl_session_timeout 10m;\n\n        ssl_protocols {* ssl.ssl_protocols *};\n        ssl_ciphers {* ssl.ssl_ciphers *};\n        ssl_prefer_server_ciphers on;\n        {% if ssl.ssl_session_tickets then %}\n        ssl_session_tickets on;\n        {% else %}\n        ssl_session_tickets off;\n        {% end %}\n        {% end %}\n\n        {% if ssl.ssl_trusted_certificate ~= nil then %}\n        proxy_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *};\n        {% end %}\n\n        # opentelemetry_set_ngx_var starts\n        set $opentelemetry_context_traceparent          '';\n        set $opentelemetry_trace_id                     '';\n        set $opentelemetry_span_id                      '';\n        # opentelemetry_set_ngx_var ends\n\n        # zipkin_set_ngx_var starts\n        {% if zipkin_set_ngx_var then %}\n        set $zipkin_context_traceparent          '';\n        set $zipkin_trace_id                     '';\n        set $zipkin_span_id                      '';\n        {% end %}\n        # zipkin_set_ngx_var ends\n\n        # http server configuration snippet starts\n        {% if http_server_configuration_snippet then %}\n        {* http_server_configuration_snippet *}\n        {% end %}\n        # http server configuration snippet ends\n\n        location = /apisix/nginx_status {\n            allow 127.0.0.0/24;\n            deny all;\n            access_log off;\n            stub_status;\n        }\n\n        {% if ssl.enable then %}\n        ssl_client_hello_by_lua_block {\n            apisix.ssl_client_hello_phase()\n        }\n\n        ssl_certificate_by_lua_block {\n            apisix.ssl_phase()\n        }\n        {% end %}\n\n        {% if http.proxy_ssl_server_name then %}\n        proxy_ssl_name $upstream_host;\n        proxy_ssl_server_name on;\n        {% end %}\n\n        location / {\n            set $upstream_mirror_host        '';\n            set $upstream_mirror_uri         '';\n            set $upstream_upgrade            '';\n            set $upstream_connection         '';\n\n            set $upstream_scheme             'http';\n            set $upstream_host               $http_host;\n            set $upstream_uri                '';\n            set $ctx_ref                     '';\n\n            {% if wasm then %}\n            set $wasm_process_req_body       '';\n            set $wasm_process_resp_body      '';\n            {% end %}\n\n            # http server location configuration snippet starts\n            {% if http_server_location_configuration_snippet then %}\n            {* http_server_location_configuration_snippet *}\n            {% end %}\n            # http server location configuration snippet ends\n\n            {% if enabled_plugins[\"dubbo-proxy\"] then %}\n            set $dubbo_service_name          '';\n            set $dubbo_service_version       '';\n            set $dubbo_method                '';\n            {% end %}\n\n            set $llm_content_risk_level         '';\n            set $apisix_upstream_response_time  $upstream_response_time;\n            set $request_type               'traditional_http';\n            set $request_llm_model              '';\n\n            set $llm_time_to_first_token        '0';\n            set $llm_model                      '';\n            set $llm_prompt_tokens              '0';\n            set $llm_completion_tokens          '0';\n\n\n            {% if use_apisix_base then %}\n            set $apisix_request_id $request_id;\n            lua_error_log_request_id $apisix_request_id;\n            {% end %}\n\n            access_by_lua_block {\n                apisix.http_access_phase()\n            }\n\n            proxy_http_version 1.1;\n            proxy_set_header   Host              $upstream_host;\n            proxy_set_header   Upgrade           $upstream_upgrade;\n            proxy_set_header   Connection        $upstream_connection;\n            proxy_set_header   X-Real-IP         $remote_addr;\n            proxy_pass_header  Date;\n\n            ### the following x-forwarded-* headers is to send to upstream server\n\n            set $var_x_forwarded_proto      $scheme;\n            set $var_x_forwarded_host       $host;\n            set $var_x_forwarded_port       $server_port;\n\n            proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;\n            proxy_set_header   X-Forwarded-Proto    $var_x_forwarded_proto;\n            proxy_set_header   X-Forwarded-Host     $var_x_forwarded_host;\n            proxy_set_header   X-Forwarded-Port     $var_x_forwarded_port;\n\n            {% if enabled_plugins[\"proxy-cache\"] then %}\n            ###  the following configuration is to cache response content from upstream server\n\n            set $upstream_cache_zone            off;\n            set $upstream_cache_key             '';\n            set $upstream_cache_bypass          '';\n            set $upstream_no_cache              '';\n\n            proxy_cache                         $upstream_cache_zone;\n            proxy_cache_valid                   any {% if proxy_cache.cache_ttl then %} {* proxy_cache.cache_ttl *} {% else %} 10s {% end %};\n            proxy_cache_min_uses                1;\n            proxy_cache_methods                 GET HEAD POST;\n            proxy_cache_lock_timeout            5s;\n            proxy_cache_use_stale               off;\n            proxy_cache_key                     $upstream_cache_key;\n            proxy_no_cache                      $upstream_no_cache;\n            proxy_cache_bypass                  $upstream_cache_bypass;\n\n            {% end %}\n\n            proxy_pass      $upstream_scheme://apisix_backend$upstream_uri;\n\n            {% if enabled_plugins[\"proxy-mirror\"] then %}\n            mirror          /proxy_mirror;\n            {% end %}\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n\n        location @grpc_pass {\n\n            access_by_lua_block {\n                apisix.grpc_access_phase()\n            }\n\n            {% if use_apisix_base then %}\n            # For servers which obey the standard, when `:authority` is missing,\n            # `host` will be used instead. When used with apisix-runtime, we can do\n            # better by setting `:authority` directly\n            grpc_set_header   \":authority\" $upstream_host;\n            {% else %}\n            grpc_set_header   \"Host\" $upstream_host;\n            {% end %}\n            grpc_set_header   Content-Type application/grpc;\n            grpc_set_header   TE trailers;\n            grpc_socket_keepalive on;\n            grpc_pass         $upstream_scheme://apisix_backend;\n\n            {% if enabled_plugins[\"proxy-mirror\"] then %}\n            mirror           /proxy_mirror_grpc;\n            {% end %}\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n\n        {% if enabled_plugins[\"dubbo-proxy\"] then %}\n        location @dubbo_pass {\n            access_by_lua_block {\n                apisix.dubbo_access_phase()\n            }\n\n            dubbo_pass_all_headers on;\n            dubbo_pass_body on;\n            dubbo_pass $dubbo_service_name $dubbo_service_version $dubbo_method apisix_dubbo_backend;\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n        {% end %}\n\n        {% if enabled_plugins[\"proxy-mirror\"] then %}\n        location = /proxy_mirror {\n            internal;\n\n            {% if not use_apisix_base then %}\n            if ($upstream_mirror_uri = \"\") {\n                return 200;\n            }\n            {% end %}\n\n\n            {% if proxy_mirror_timeouts then %}\n                {% if proxy_mirror_timeouts.connect then %}\n            proxy_connect_timeout {* proxy_mirror_timeouts.connect *};\n                {% end %}\n                {% if proxy_mirror_timeouts.read then %}\n            proxy_read_timeout {* proxy_mirror_timeouts.read *};\n                {% end %}\n                {% if proxy_mirror_timeouts.send then %}\n            proxy_send_timeout {* proxy_mirror_timeouts.send *};\n                {% end %}\n            {% end %}\n            proxy_http_version 1.1;\n            proxy_set_header Host $upstream_host;\n            proxy_pass $upstream_mirror_uri;\n        }\n        {% end %}\n\n        {% if enabled_plugins[\"proxy-mirror\"] then %}\n        location = /proxy_mirror_grpc {\n            internal;\n\n            {% if not use_apisix_base then %}\n            if ($upstream_mirror_uri = \"\") {\n                return 200;\n            }\n            {% end %}\n\n\n            {% if proxy_mirror_timeouts then %}\n                {% if proxy_mirror_timeouts.connect then %}\n            grpc_connect_timeout {* proxy_mirror_timeouts.connect *};\n                {% end %}\n                {% if proxy_mirror_timeouts.read then %}\n            grpc_read_timeout {* proxy_mirror_timeouts.read *};\n                {% end %}\n                {% if proxy_mirror_timeouts.send then %}\n            grpc_send_timeout {* proxy_mirror_timeouts.send *};\n                {% end %}\n            {% end %}\n            grpc_pass $upstream_mirror_host;\n        }\n        {% end %}\n    }\n    {% end %}\n\n    # http end configuration snippet starts\n    {% if http_end_configuration_snippet then %}\n    {* http_end_configuration_snippet *}\n    {% end %}\n    # http end configuration snippet ends\n}\n{% end %}\n]=]\n"
  },
  {
    "path": "apisix/cli/ops.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ver = require(\"apisix.core.version\")\nlocal etcd = require(\"apisix.cli.etcd\")\nlocal util = require(\"apisix.cli.util\")\nlocal file = require(\"apisix.cli.file\")\nlocal schema = require(\"apisix.cli.schema\")\nlocal ngx_tpl = require(\"apisix.cli.ngx_tpl\")\nlocal cli_ip = require(\"apisix.cli.ip\")\nlocal profile = require(\"apisix.core.profile\")\nlocal template = require(\"resty.template\")\nlocal argparse = require(\"argparse\")\nlocal pl_path = require(\"pl.path\")\nlocal lfs = require(\"lfs\")\nlocal signal = require(\"posix.signal\")\nlocal errno = require(\"posix.errno\")\n\nlocal stderr = io.stderr\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal print = print\nlocal type = type\nlocal tostring = tostring\nlocal tonumber = tonumber\nlocal io_open = io.open\nlocal execute = os.execute\nlocal os_rename = os.rename\nlocal os_remove = os.remove\nlocal table_insert = table.insert\nlocal table_remove = table.remove\nlocal getenv = os.getenv\nlocal max = math.max\nlocal floor = math.floor\nlocal str_find = string.find\nlocal str_byte = string.byte\nlocal str_sub = string.sub\nlocal str_format = string.format\n\n\nlocal _M = {}\n\n\nlocal function help()\n    print([[\nUsage: apisix [action] <argument>\n\nhelp:       print the apisix cli help message\ninit:       initialize the local nginx.conf\ninit_etcd:  initialize the data of etcd\nstart:      start the apisix server\nstop:       stop the apisix server\nquit:       stop the apisix server gracefully\nrestart:    restart the apisix server\nreload:     reload the apisix server\ntest:       test the generated nginx.conf\nversion:    print the version of apisix\n]])\nend\n\n\nlocal function version_greater_equal(cur_ver_s, need_ver_s)\n    local cur_vers = util.split(cur_ver_s, [[.]])\n    local need_vers = util.split(need_ver_s, [[.]])\n    local len = max(#cur_vers, #need_vers)\n\n    for i = 1, len do\n        local cur_ver = tonumber(cur_vers[i]) or 0\n        local need_ver = tonumber(need_vers[i]) or 0\n        if cur_ver > need_ver then\n            return true\n        end\n\n        if cur_ver < need_ver then\n            return false\n        end\n    end\n\n    return true\nend\n\n\nlocal function get_openresty_version()\n    local str = \"nginx version: openresty/\"\n    local ret = util.execute_cmd(\"openresty -v 2>&1\")\n    local pos = str_find(ret, str, 1, true)\n    if pos then\n        return str_sub(ret, pos + #str)\n    end\n\n    str = \"nginx version: nginx/\"\n    pos = str_find(ret, str, 1, true)\n    if pos then\n        return str_sub(ret, pos + #str)\n    end\nend\n\n\nlocal function local_dns_resolver(file_path)\n    local file, err = io_open(file_path, \"rb\")\n    if not file then\n        return false, \"failed to open file: \" .. file_path .. \", error info:\" .. err\n    end\n\n    local dns_addrs = {}\n    for line in file:lines() do\n        local addr, n = line:gsub(\"^nameserver%s+([^%s]+)%s*$\", \"%1\")\n        if n == 1 then\n            table_insert(dns_addrs, addr)\n        end\n    end\n\n    file:close()\n    return dns_addrs\nend\n-- exported for test\n_M.local_dns_resolver = local_dns_resolver\n\n\nlocal function version()\n    print(ver['VERSION'])\nend\n\n\nlocal function get_lua_path(conf)\n    -- we use \"\" as the placeholder to enforce the type to be string\n    if conf and conf ~= \"\" then\n        if #conf < 2 then\n            -- the shortest valid path is ';;'\n            util.die(\"invalid extra_lua_path/extra_lua_cpath: \\\"\", conf, \"\\\"\\n\")\n        end\n\n        local path = conf\n        if path:byte(-1) ~= str_byte(';') then\n            path = path .. ';'\n        end\n        return path\n    end\n\n    return \"\"\nend\n\n\nlocal function init(env)\n    if env.is_root_path then\n        print('Warning! Running apisix under /root is only suitable for '\n              .. 'development environments and it is dangerous to do so. '\n              .. 'It is recommended to run APISIX in a directory '\n              .. 'other than /root.')\n    end\n\n    local min_ulimit = 1024\n    if env.ulimit ~= \"unlimited\" and env.ulimit <= min_ulimit then\n        print(str_format(\"Warning! Current maximum number of open file \"\n                .. \"descriptors [%d] is not greater than %d, please increase user limits by \"\n                .. \"execute \\'ulimit -n <new user limits>\\' , otherwise the performance\"\n                .. \" is low.\", env.ulimit, min_ulimit))\n    end\n\n    -- read_yaml_conf\n    local yaml_conf, err = file.read_yaml_conf(env.apisix_home)\n    if not yaml_conf then\n        util.die(\"failed to read local yaml config of apisix: \", err, \"\\n\")\n    end\n\n    local ok, err = schema.validate(yaml_conf)\n    if not ok then\n        util.die(err, \"\\n\")\n    end\n\n    -- validate standalone mode config\n    local standalone_env = getenv(\"APISIX_STAND_ALONE\")\n    if standalone_env == \"true\" then\n        local role = yaml_conf.deployment.role\n        local config_provider\n\n        if role == \"data_plane\" then\n            config_provider = yaml_conf.deployment.role_data_plane.config_provider\n        elseif role == \"traditional\" then\n            config_provider = yaml_conf.deployment.role_traditional and\n                yaml_conf.deployment.role_traditional.config_provider\n        end\n\n        if config_provider ~= \"yaml\" and config_provider ~= \"json\" then\n            util.die(\"APISIX_STAND_ALONE is set to 'true' but config_provider is '\"\n                .. tostring(config_provider) .. \"'\\n\"\n                .. \"For standalone mode, config_provider must be 'yaml' or 'json'\\n\"\n                .. \"Current role: \" .. tostring(role) .. \"\\n\"\n                .. \"See: https://apisix.apache.org/docs/apisix/deployment-modes/\\n\")\n        end\n    end\n\n    -- check the Admin API token\n    local checked_admin_key = false\n    local allow_admin = yaml_conf.deployment.admin and\n        yaml_conf.deployment.admin.allow_admin\n    if yaml_conf.apisix.enable_admin and allow_admin\n       and #allow_admin == 1 and allow_admin[1] == \"127.0.0.0/24\" then\n        checked_admin_key = true\n    end\n    -- check if admin_key is required\n    if yaml_conf.deployment.admin.admin_key_required == false then\n        checked_admin_key = true\n        print(\"Warning! Admin key is bypassed! \"\n                .. \"If you are deploying APISIX in a production environment, \"\n                .. \"please enable `admin_key_required` and set a secure admin key!\")\n    end\n\n    if yaml_conf.apisix.enable_admin and not checked_admin_key then\n        local help = [[\n\n%s\nPlease modify \"admin_key\" in conf/config.yaml .\n\n]]\n        local admin_key = yaml_conf.deployment.admin\n        if admin_key then\n            admin_key = admin_key.admin_key\n        end\n\n        if type(admin_key) ~= \"table\" or #admin_key == 0\n        then\n            util.die(help:format(\"ERROR: missing valid Admin API token.\"))\n        end\n\n        for _, admin in ipairs(admin_key) do\n            if type(admin.key) == \"table\" then\n                admin.key = \"\"\n            else\n                admin.key = tostring(admin.key)\n            end\n\n            if admin.key == \"\" then\n                stderr:write(\n                    help:format([[WARNING: using empty Admin API.\n                    This will trigger APISIX to automatically generate a random Admin API token.]]),\n                    \"\\n\"\n                )\n            end\n        end\n    end\n\n    if yaml_conf.deployment.admin then\n        local admin_api_mtls = yaml_conf.deployment.admin.admin_api_mtls\n        local https_admin = yaml_conf.deployment.admin.https_admin\n        if https_admin and not (admin_api_mtls and\n            admin_api_mtls.admin_ssl_cert and\n            admin_api_mtls.admin_ssl_cert ~= \"\" and\n            admin_api_mtls.admin_ssl_cert_key and\n            admin_api_mtls.admin_ssl_cert_key ~= \"\")\n        then\n            util.die(\"missing ssl cert for https admin\")\n        end\n    end\n\n    local or_ver = get_openresty_version()\n    if or_ver == nil then\n        util.die(\"can not find openresty\\n\")\n    end\n\n    local need_ver = \"1.21.4\"\n    if not version_greater_equal(or_ver, need_ver) then\n        util.die(\"openresty version must >=\", need_ver, \" current \", or_ver, \"\\n\")\n    end\n\n    local or_info = env.openresty_info\n    if not or_info:find(\"http_stub_status_module\", 1, true) then\n        util.die(\"'http_stub_status_module' module is missing in \",\n                 \"your openresty, please check it out.\\n\")\n    end\n\n    --- http is enabled by default\n    local enable_http = true\n    --- stream is disabled by default\n    local enable_stream = false\n    if yaml_conf.apisix.proxy_mode then\n        --- check for \"http\"\n        if yaml_conf.apisix.proxy_mode == \"http\" then\n            enable_http = true\n            enable_stream = false\n        --- check for \"stream\"\n        elseif yaml_conf.apisix.proxy_mode == \"stream\" then\n            enable_stream = true\n            enable_http = false\n        --- check for \"http&stream\"\n        elseif yaml_conf.apisix.proxy_mode == \"http&stream\" then\n            enable_stream = true\n            enable_http = true\n        end\n    end\n\n    local enabled_discoveries = {}\n    for name in pairs(yaml_conf.discovery or {}) do\n        enabled_discoveries[name] = true\n    end\n\n    local enabled_plugins = {}\n    for i, name in ipairs(yaml_conf.plugins or {}) do\n        enabled_plugins[name] = true\n    end\n\n    local enabled_stream_plugins = {}\n    for i, name in ipairs(yaml_conf.stream_plugins or {}) do\n        enabled_stream_plugins[name] = true\n    end\n\n    if enabled_plugins[\"proxy-cache\"] and not yaml_conf.apisix.proxy_cache then\n        util.die(\"missing apisix.proxy_cache for plugin proxy-cache\\n\")\n    end\n\n    if enabled_plugins[\"batch-requests\"] then\n        local pass_real_client_ip = false\n        local real_ip_from = yaml_conf.nginx_config.http.real_ip_from\n        -- the real_ip_from is enabled by default, we just need to make sure it's\n        -- not disabled by the users\n        if real_ip_from then\n            for _, ip in ipairs(real_ip_from) do\n                local _ip = cli_ip:new(ip)\n                if _ip then\n                    if _ip:is_loopback() or _ip:is_unspecified() then\n                        pass_real_client_ip = true\n                    end\n                end\n            end\n        end\n\n        if not pass_real_client_ip then\n            util.die(\"missing loopback or unspecified in the nginx_config.http.real_ip_from\" ..\n                     \" for plugin batch-requests\\n\")\n        end\n    end\n\n    local ports_to_check = {}\n\n    local function validate_and_get_listen_addr(port_name, default_ip, configured_ip,\n                                                default_port, configured_port)\n        local ip = configured_ip or default_ip\n        local port = tonumber(configured_port) or default_port\n        if ports_to_check[port] ~= nil then\n            util.die(port_name .. \" \", port, \" conflicts with \", ports_to_check[port], \"\\n\")\n        end\n        ports_to_check[port] = port_name\n        return ip .. \":\" .. port\n    end\n\n    -- listen in admin use a separate port, support specific IP, compatible with the original style\n    local admin_server_addr\n    if yaml_conf.apisix.enable_admin then\n        local ip = yaml_conf.deployment.admin.admin_listen.ip\n        local port = yaml_conf.deployment.admin.admin_listen.port\n        admin_server_addr = validate_and_get_listen_addr(\"admin port\", \"0.0.0.0\", ip,\n                                                          9180, port)\n    end\n\n    local status_server_addr\n    if yaml_conf.apisix.status then\n        status_server_addr = validate_and_get_listen_addr(\"status port\", \"127.0.0.1\",\n                             yaml_conf.apisix.status.ip, 7085,\n                             yaml_conf.apisix.status.port)\n    end\n\n    local control_server_addr\n    if yaml_conf.apisix.enable_control then\n        if not yaml_conf.apisix.control then\n            control_server_addr = validate_and_get_listen_addr(\"control port\", \"127.0.0.1\", nil,\n                                          9090, nil)\n        else\n            control_server_addr = validate_and_get_listen_addr(\"control port\", \"127.0.0.1\",\n                                          yaml_conf.apisix.control.ip,\n                                          9090, yaml_conf.apisix.control.port)\n        end\n    end\n\n    local prometheus_server_addr\n    if yaml_conf.plugin_attr.prometheus then\n        local prometheus = yaml_conf.plugin_attr.prometheus\n        if prometheus.enable_export_server then\n            prometheus_server_addr = validate_and_get_listen_addr(\"prometheus port\", \"127.0.0.1\",\n                                             prometheus.export_addr.ip,\n                                             9091, prometheus.export_addr.port)\n        end\n    end\n\n    if enabled_stream_plugins[\"prometheus\"] and not prometheus_server_addr then\n        util.die(\"L4 prometheus metric should be exposed via export server\\n\")\n    end\n\n    local ip_port_to_check = {}\n\n    local function listen_table_insert(listen_table, scheme, ip, port,\n                                enable_http3, enable_ipv6)\n        if type(ip) ~= \"string\" then\n            util.die(scheme, \" listen ip format error, must be string\", \"\\n\")\n        end\n\n        if type(port) ~= \"number\" then\n            util.die(scheme, \" listen port format error, must be number\", \"\\n\")\n        end\n\n        if ports_to_check[port] ~= nil then\n            util.die(scheme, \" listen port \", port, \" conflicts with \",\n                ports_to_check[port], \"\\n\")\n        end\n\n        local addr = ip .. \":\" .. port\n\n        if ip_port_to_check[addr] == nil then\n            table_insert(listen_table,\n                    {\n                        ip = ip,\n                        port = port,\n                        enable_http3 = enable_http3\n                    })\n            ip_port_to_check[addr] = scheme\n        end\n\n        if enable_ipv6 then\n            ip = \"[::]\"\n            addr = ip .. \":\" .. port\n\n            if ip_port_to_check[addr] == nil then\n                table_insert(listen_table,\n                        {\n                            ip = ip,\n                            port = port,\n                            enable_http3 = enable_http3\n                        })\n                ip_port_to_check[addr] = scheme\n            end\n        end\n    end\n\n    local node_listen = {}\n    -- listen in http, support multiple ports and specific IP, compatible with the original style\n    if type(yaml_conf.apisix.node_listen) == \"number\" then\n        listen_table_insert(node_listen, \"http\", \"0.0.0.0\", yaml_conf.apisix.node_listen,\n                false, yaml_conf.apisix.enable_ipv6)\n    elseif type(yaml_conf.apisix.node_listen) == \"table\" then\n        for _, value in ipairs(yaml_conf.apisix.node_listen) do\n            if type(value) == \"number\" then\n                listen_table_insert(node_listen, \"http\", \"0.0.0.0\", value,\n                        false, yaml_conf.apisix.enable_ipv6)\n            elseif type(value) == \"table\" then\n                local ip = value.ip\n                local port = value.port\n                local enable_ipv6 = false\n                local enable_http2 = value.enable_http2\n\n                if ip == nil then\n                    ip = \"0.0.0.0\"\n                    if yaml_conf.apisix.enable_ipv6 then\n                        enable_ipv6 = true\n                    end\n                end\n\n                if port == nil then\n                    port = 9080\n                end\n\n                if enable_http2 ~= nil then\n                    util.die(\"ERROR: port level enable_http2 in node_listen is deprecated\"\n                            .. \"from 3.9 version, and you should use enable_http2 in \"\n                            .. \"apisix level.\", \"\\n\")\n                end\n\n                listen_table_insert(node_listen, \"http\", ip, port,\n                        false, enable_ipv6)\n            end\n        end\n    end\n    yaml_conf.apisix.node_listen = node_listen\n\n    local enable_http3_in_server_context = false\n    local ssl_listen = {}\n    -- listen in https, support multiple ports, support specific IP\n    for _, value in ipairs(yaml_conf.apisix.ssl.listen) do\n        local ip = value.ip\n        local port = value.port\n        local enable_ipv6 = false\n        local enable_http2 = value.enable_http2\n        local enable_http3 = value.enable_http3\n\n        if ip == nil then\n            ip = \"0.0.0.0\"\n            if yaml_conf.apisix.enable_ipv6 then\n                enable_ipv6 = true\n            end\n        end\n\n        if port == nil then\n            port = 9443\n        end\n\n        if enable_http2 ~= nil then\n            util.die(\"ERROR: port level enable_http2 in ssl.listen is deprecated\"\n                      .. \"from 3.9 version, and you should use enable_http2 in \"\n                      .. \"apisix level.\", \"\\n\")\n        end\n\n        if enable_http3 == nil then\n            enable_http3 = false\n        end\n        if enable_http3 == true then\n            enable_http3_in_server_context = true\n        end\n\n        listen_table_insert(ssl_listen, \"https\", ip, port,\n                enable_http3, enable_ipv6)\n    end\n\n    yaml_conf.apisix.ssl.listen = ssl_listen\n    yaml_conf.apisix.enable_http3_in_server_context = enable_http3_in_server_context\n\n    -- enable ssl with place holder crt&key\n    yaml_conf.apisix.ssl.ssl_cert = \"cert/ssl_PLACE_HOLDER.crt\"\n    yaml_conf.apisix.ssl.ssl_cert_key = \"cert/ssl_PLACE_HOLDER.key\"\n\n    local tcp_enable_ssl\n    -- compatible with the original style which only has the addr\n    if enable_stream and yaml_conf.apisix.stream_proxy and yaml_conf.apisix.stream_proxy.tcp then\n        local tcp = yaml_conf.apisix.stream_proxy.tcp\n        for i, item in ipairs(tcp) do\n            if type(item) ~= \"table\" then\n                tcp[i] = {addr = item}\n            else\n                if item.tls then\n                    tcp_enable_ssl = true\n                end\n            end\n        end\n    end\n\n    local dubbo_upstream_multiplex_count = 32\n    if yaml_conf.plugin_attr and yaml_conf.plugin_attr[\"dubbo-proxy\"] then\n        local dubbo_conf = yaml_conf.plugin_attr[\"dubbo-proxy\"]\n        if tonumber(dubbo_conf.upstream_multiplex_count) >= 1 then\n            dubbo_upstream_multiplex_count = dubbo_conf.upstream_multiplex_count\n        end\n    end\n\n    if yaml_conf.apisix.dns_resolver_valid then\n        if tonumber(yaml_conf.apisix.dns_resolver_valid) == nil then\n            util.die(\"apisix->dns_resolver_valid should be a number\")\n        end\n    end\n\n    local proxy_mirror_timeouts\n    if yaml_conf.plugin_attr[\"proxy-mirror\"] then\n        proxy_mirror_timeouts = yaml_conf.plugin_attr[\"proxy-mirror\"].timeout\n    end\n\n    if yaml_conf.deployment and yaml_conf.deployment.role then\n        local role = yaml_conf.deployment.role\n        env.deployment_role = role\n\n        if role == \"control_plane\" and not admin_server_addr then\n            local listen = node_listen[1]\n            admin_server_addr = str_format(\"%s:%s\", listen.ip, listen.port)\n        end\n    end\n\n    local zipkin_set_ngx_var\n    if enabled_plugins[\"zipkin\"] and yaml_conf.plugin_attr[\"zipkin\"] then\n        zipkin_set_ngx_var = yaml_conf.plugin_attr[\"zipkin\"].set_ngx_var\n    end\n\n    -- Using template.render\n    local sys_conf = {\n        lua_path = env.pkg_path_org,\n        lua_cpath = env.pkg_cpath_org,\n        os_name = util.trim(util.execute_cmd(\"uname\")),\n        apisix_lua_home = env.apisix_home,\n        deployment_role = env.deployment_role,\n        use_apisix_base = env.use_apisix_base,\n        error_log = {level = \"warn\"},\n        enable_http = enable_http,\n        enable_stream = enable_stream,\n        enabled_discoveries = enabled_discoveries,\n        enabled_plugins = enabled_plugins,\n        enabled_stream_plugins = enabled_stream_plugins,\n        dubbo_upstream_multiplex_count = dubbo_upstream_multiplex_count,\n        status_server_addr = status_server_addr,\n        tcp_enable_ssl = tcp_enable_ssl,\n        admin_server_addr = admin_server_addr,\n        control_server_addr = control_server_addr,\n        prometheus_server_addr = prometheus_server_addr,\n        proxy_mirror_timeouts = proxy_mirror_timeouts,\n        zipkin_set_ngx_var = zipkin_set_ngx_var\n    }\n\n    if not yaml_conf.apisix then\n        util.die(\"failed to read `apisix` field from yaml file\")\n    end\n\n    if not yaml_conf.nginx_config then\n        util.die(\"failed to read `nginx_config` field from yaml file\")\n    end\n\n    if util.is_32bit_arch() then\n        sys_conf[\"worker_rlimit_core\"] = \"4G\"\n    else\n        sys_conf[\"worker_rlimit_core\"] = \"16G\"\n    end\n\n    for k,v in pairs(yaml_conf.apisix) do\n        sys_conf[k] = v\n    end\n    for k,v in pairs(yaml_conf.nginx_config) do\n        sys_conf[k] = v\n    end\n    if yaml_conf.deployment.admin then\n        for k,v in pairs(yaml_conf.deployment.admin) do\n            sys_conf[k] = v\n        end\n    end\n\n    sys_conf.standalone_with_admin_api = env.deployment_role == \"traditional\" and\n        yaml_conf.apisix.enable_admin and yaml_conf.deployment.config_provider == \"yaml\"\n\n    sys_conf[\"wasm\"] = yaml_conf.wasm\n\n\n    local wrn = sys_conf[\"worker_rlimit_nofile\"]\n    local wc = sys_conf[\"event\"][\"worker_connections\"]\n    if not wrn or wrn <= wc then\n        -- ensure the number of fds is slightly larger than the number of conn\n        sys_conf[\"worker_rlimit_nofile\"] = wc + 128\n    end\n\n    if sys_conf[\"enable_dev_mode\"] == true then\n        sys_conf[\"worker_processes\"] = 1\n        sys_conf[\"enable_reuseport\"] = false\n\n    elseif tonumber(sys_conf[\"worker_processes\"]) == nil then\n        sys_conf[\"worker_processes\"] = \"auto\"\n    end\n\n    local dns_resolver = sys_conf[\"dns_resolver\"]\n    if not dns_resolver or #dns_resolver == 0 then\n        local dns_addrs, err = local_dns_resolver(\"/etc/resolv.conf\")\n        if not dns_addrs then\n            util.die(\"failed to import local DNS: \", err, \"\\n\")\n        end\n\n        if #dns_addrs == 0 then\n            util.die(\"local DNS is empty\\n\")\n        end\n\n        sys_conf[\"dns_resolver\"] = dns_addrs\n    end\n\n    for i, r in ipairs(sys_conf[\"dns_resolver\"]) do\n        if r:match(\":[^:]*:\") then\n            -- more than one colon, is IPv6\n            if r:byte(1) ~= str_byte('[') then\n                -- ensure IPv6 address is always wrapped in []\n                sys_conf[\"dns_resolver\"][i] = \"[\" .. r .. \"]\"\n            end\n        end\n\n        -- check if the dns_resolver is ipv6 address with zone_id\n        -- Nginx does not support this form\n        if r:find(\"%%\") then\n            stderr:write(\"unsupported DNS resolver: \" .. r ..\n                         \", would ignore this item\\n\")\n            table_remove(sys_conf[\"dns_resolver\"], i)\n        end\n    end\n\n    local env_worker_processes = getenv(\"APISIX_WORKER_PROCESSES\")\n    if env_worker_processes then\n        sys_conf[\"worker_processes\"] = floor(tonumber(env_worker_processes))\n    end\n\n    local exported_vars = file.get_exported_vars()\n    if exported_vars then\n        if not sys_conf[\"envs\"] then\n            sys_conf[\"envs\"]= {}\n        end\n        for _, cfg_env in ipairs(sys_conf[\"envs\"]) do\n            local cfg_name\n            local from = str_find(cfg_env, \"=\", 1, true)\n            if from then\n                cfg_name = str_sub(cfg_env, 1, from - 1)\n            else\n                cfg_name = cfg_env\n            end\n\n            exported_vars[cfg_name] = false\n        end\n\n        for name, value in pairs(exported_vars) do\n            if value then\n                table_insert(sys_conf[\"envs\"], name)\n            end\n        end\n    end\n\n    -- inject kubernetes discovery shared dict and environment variable\n    if enabled_discoveries[\"kubernetes\"] then\n\n        if not sys_conf[\"discovery_shared_dicts\"] then\n            sys_conf[\"discovery_shared_dicts\"] = {}\n        end\n\n        local kubernetes_conf = yaml_conf.discovery[\"kubernetes\"]\n\n        local inject_environment = function(conf, envs)\n            local keys = {\n                conf.service.host,\n                conf.service.port,\n            }\n\n            if conf.client.token then\n                table_insert(keys, conf.client.token)\n            end\n\n            if conf.client.token_file then\n                table_insert(keys, conf.client.token_file)\n            end\n\n            for _, key in ipairs(keys) do\n                if #key > 3 then\n                    local first, second = str_byte(key, 1, 2)\n                    if first == str_byte('$') and second == str_byte('{') then\n                        local last = str_byte(key, #key)\n                        if last == str_byte('}') then\n                            envs[str_sub(key, 3, #key - 1)] = \"\"\n                        end\n                    end\n                end\n            end\n\n        end\n\n        local envs = {}\n        if #kubernetes_conf == 0 then\n            sys_conf[\"discovery_shared_dicts\"][\"kubernetes\"] = kubernetes_conf.shared_size\n            inject_environment(kubernetes_conf, envs)\n        else\n            for _, item in ipairs(kubernetes_conf) do\n                sys_conf[\"discovery_shared_dicts\"][\"kubernetes-\" .. item.id] = item.shared_size\n                inject_environment(item, envs)\n            end\n        end\n\n        if not sys_conf[\"envs\"] then\n            sys_conf[\"envs\"] = {}\n        end\n\n        for item in pairs(envs) do\n            table_insert(sys_conf[\"envs\"], item)\n        end\n\n    end\n\n    -- inject consul discovery shared dict\n    if enabled_discoveries[\"consul\"] then\n        if not sys_conf[\"discovery_shared_dicts\"] then\n            sys_conf[\"discovery_shared_dicts\"] = {}\n        end\n\n        local consul_conf = yaml_conf.discovery[\"consul\"]\n        sys_conf[\"discovery_shared_dicts\"][\"consul\"] = consul_conf.shared_size or \"10m\"\n    end\n\n    -- fix up lua path\n    sys_conf[\"extra_lua_path\"] = get_lua_path(yaml_conf.apisix.extra_lua_path)\n    sys_conf[\"extra_lua_cpath\"] = get_lua_path(yaml_conf.apisix.extra_lua_cpath)\n\n    local conf_render = template.compile(ngx_tpl)\n    local ngxconf = conf_render(sys_conf)\n\n    local ok, err = util.write_file(env.apisix_home .. \"/conf/nginx.conf\",\n                                    ngxconf)\n    if not ok then\n        util.die(\"failed to update nginx.conf: \", err, \"\\n\")\n    end\nend\n\n\nlocal function init_etcd(env, args)\n    etcd.init(env, args)\nend\n\n\nlocal function cleanup(env)\n    if env.apisix_home then\n        profile.apisix_home = env.apisix_home\n    end\n\n    os_remove(profile:customized_yaml_index())\nend\n\n\nlocal function sleep(n)\n  execute(\"sleep \" .. tonumber(n))\nend\n\n\nlocal function check_running(env)\n    local pid_path = env.apisix_home .. \"/logs/nginx.pid\"\n    local pid = util.read_file(pid_path)\n    pid = tonumber(pid)\n    if not pid then\n        return false, nil\n    end\n    return true, pid\nend\n\n\nlocal function start(env, ...)\n    cleanup(env)\n\n    -- Because the worker process started by apisix has \"nobody\" permission,\n    -- it cannot access the `/root` directory. Therefore, it is necessary to\n    -- prohibit APISIX from running in the /root directory.\n    if env.is_root_path then\n        util.die(\"Error: It is forbidden to run APISIX in the /root directory.\\n\")\n    end\n\n    local logs_path = env.apisix_home .. \"/logs\"\n    if not pl_path.exists(logs_path) then\n        local _, err = pl_path.mkdir(logs_path)\n        if err ~= nil then\n            util.die(\"failed to mkdir \", logs_path, \", error: \", err)\n        end\n    elseif not pl_path.isdir(logs_path) and not pl_path.islink(logs_path) then\n        util.die(logs_path, \" is not directory nor symbol link\")\n    end\n\n    -- check running and wait old apisix stop\n    local pid = nil\n    for i = 1, 30 do\n        local running\n        running, pid = check_running(env)\n        if not running then\n            break\n        else\n            sleep(0.1)\n        end\n    end\n\n    if pid then\n        if pid <= 0 then\n            print(\"invalid pid\")\n            return\n        end\n\n        local signone = 0\n\n        local ok, err, err_no = signal.kill(pid, signone)\n        if ok then\n            print(\"the old APISIX is still running, the new one will not start\")\n            return\n        -- no such process\n        elseif err_no ~= errno.ESRCH then\n            print(err)\n            return\n        end\n\n        print(\"nginx.pid exists but there's no corresponding process with pid \", pid,\n              \", the file will be overwritten\")\n    end\n\n    -- start a new APISIX instance\n\n    local parser = argparse()\n    parser:argument(\"_\", \"Placeholder\")\n    parser:option(\"-c --config\", \"location of customized config.yaml\")\n    -- TODO: more logs for APISIX cli could be added using this feature\n    parser:flag(\"-v --verbose\", \"show init_etcd debug information\")\n    local args = parser:parse()\n\n    local customized_yaml = args[\"config\"]\n    if customized_yaml then\n        local customized_yaml_path\n        local idx = str_find(customized_yaml, \"/\")\n        if idx and idx == 1 then\n            customized_yaml_path = customized_yaml\n        else\n            local cur_dir, err = lfs.currentdir()\n            if err then\n                util.die(\"failed to get current directory\")\n            end\n            customized_yaml_path = cur_dir .. \"/\" .. customized_yaml\n        end\n\n        if not util.file_exists(customized_yaml_path) then\n           util.die(\"customized config file not exists, path: \" .. customized_yaml_path)\n        end\n\n        local ok, err = util.write_file(profile:customized_yaml_index(), customized_yaml_path)\n        if not ok then\n            util.die(\"write customized config index failed, err: \" .. err)\n        end\n\n        print(\"Use customized yaml: \", customized_yaml)\n    end\n\n    init(env)\n\n    if env.deployment_role ~= \"data_plane\" then\n        init_etcd(env, args)\n    end\n\n    util.execute_cmd(env.openresty_args)\nend\n\n\nlocal function test(env, backup_ngx_conf)\n    -- backup nginx.conf\n    local ngx_conf_path = env.apisix_home .. \"/conf/nginx.conf\"\n    local ngx_conf_path_bak = ngx_conf_path .. \".bak\"\n    local ngx_conf_exist = pl_path.exists(ngx_conf_path)\n    if ngx_conf_exist then\n        local ok, err = os_rename(ngx_conf_path, ngx_conf_path_bak)\n        if not ok then\n            util.die(\"failed to backup nginx.conf, error: \", err)\n        end\n    end\n\n    -- reinit nginx.conf\n    init(env)\n\n    local test_cmd = env.openresty_args .. [[ -t -q ]]\n    local test_ret = execute((test_cmd))\n\n    -- restore nginx.conf\n    if ngx_conf_exist then\n        local ok, err = os_rename(ngx_conf_path_bak, ngx_conf_path)\n        if not ok then\n            util.die(\"failed to restore original nginx.conf, error: \", err)\n        end\n    end\n\n    -- When success,\n    -- On linux, os.execute returns 0,\n    -- On macos, os.execute returns 3 values: true, exit, 0, and we need the first.\n    if (test_ret == 0 or test_ret == true) then\n        print(\"configuration test is successful\")\n        return\n    end\n\n    util.die(\"configuration test failed\")\nend\n\n\nlocal function quit(env)\n    cleanup(env)\n\n    local cmd = env.openresty_args .. [[ -s quit]]\n    util.execute_cmd(cmd)\nend\n\n\nlocal function stop(env)\n    cleanup(env)\n\n    local cmd = env.openresty_args .. [[ -s stop]]\n    util.execute_cmd(cmd)\nend\n\n\nlocal function restart(env)\n  -- test configuration\n  test(env)\n  stop(env)\n  start(env)\nend\n\n\nlocal function reload(env)\n    -- reinit nginx.conf\n    init(env)\n\n    local test_cmd = env.openresty_args .. [[ -t -q ]]\n    -- When success,\n    -- On linux, os.execute returns 0,\n    -- On macos, os.execute returns 3 values: true, exit, 0, and we need the first.\n    local test_ret = execute((test_cmd))\n    if (test_ret == 0 or test_ret == true) then\n        local cmd = env.openresty_args .. [[ -s reload]]\n        execute(cmd)\n        return\n    end\n\n    print(\"test openresty failed\")\nend\n\n\n\nlocal action = {\n    help = help,\n    version = version,\n    init = init,\n    init_etcd = etcd.init,\n    start = start,\n    stop = stop,\n    quit = quit,\n    restart = restart,\n    reload = reload,\n    test = test,\n}\n\n\nfunction _M.execute(env, arg)\n    local cmd_action = arg[1]\n    if not cmd_action then\n        return help()\n    end\n\n    if not action[cmd_action] then\n        stderr:write(\"invalid argument: \", cmd_action, \"\\n\")\n        return help()\n    end\n\n    action[cmd_action](env, arg[2])\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal jsonschema = require(\"jsonschema\")\nlocal pairs = pairs\nlocal pcall = pcall\nlocal require = require\nlocal schema_def = require(\"apisix.schema_def\")\n\n\nlocal _M = {}\nlocal etcd_schema = {\n    type = \"object\",\n    properties = {\n        resync_delay = {\n            type = \"integer\",\n        },\n        user = {\n            type = \"string\",\n        },\n        password = {\n            type = \"string\",\n        },\n        tls = {\n            type = \"object\",\n            properties = {\n                cert = {\n                    type = \"string\",\n                },\n                key = {\n                    type = \"string\",\n                },\n            },\n        },\n        prefix = {\n            type = \"string\",\n        },\n        host = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                pattern = [[^https?://]]\n            },\n            minItems = 1,\n        },\n        timeout = {\n            type = \"integer\",\n            default = 30,\n            minimum = 1,\n            description = \"etcd connection timeout in seconds\",\n        },\n    },\n    required = {\"prefix\", \"host\"}\n}\n\nlocal config_schema = {\n    type = \"object\",\n    properties = {\n        apisix = {\n            properties = {\n                lua_module_hook = {\n                    pattern = \"^[a-zA-Z._-]+$\",\n                },\n                proxy_protocol = {\n                    type = \"object\",\n                    properties = {\n                        listen_http_port = {\n                            type = \"integer\",\n                        },\n                        listen_https_port = {\n                            type = \"integer\",\n                        },\n                        enable_tcp_pp = {\n                            type = \"boolean\",\n                        },\n                        enable_tcp_pp_to_upstream = {\n                            type = \"boolean\",\n                        },\n                    }\n                },\n                proxy_cache = {\n                    type = \"object\",\n                    properties = {\n                        zones = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                type = \"object\",\n                                properties = {\n                                    name = {\n                                        type = \"string\",\n                                    },\n                                    memory_size = {\n                                        type = \"string\",\n                                    },\n                                    disk_size = {\n                                        type = \"string\",\n                                    },\n                                    disk_path = {\n                                        type = \"string\",\n                                    },\n                                    cache_levels = {\n                                        type = \"string\",\n                                    },\n                                },\n                                oneOf = {\n                                    {\n                                        required = {\"name\", \"memory_size\"},\n                                        maxProperties = 2,\n                                    },\n                                    {\n                                        required = {\"name\", \"memory_size\", \"disk_size\",\n                                            \"disk_path\", \"cache_levels\"},\n                                    }\n                                },\n                            },\n                            uniqueItems = true,\n                        }\n                    }\n                },\n                proxy_mode = {\n                    type = \"string\",\n                    enum = {\"http\", \"stream\", \"http&stream\"},\n                },\n                stream_proxy = {\n                    type = \"object\",\n                    properties = {\n                        tcp = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                anyOf = {\n                                    {\n                                        type = \"integer\",\n                                    },\n                                    {\n                                        type = \"string\",\n                                    },\n                                    {\n                                        type = \"object\",\n                                        properties = {\n                                            addr = {\n                                                anyOf = {\n                                                    {\n                                                        type = \"integer\",\n                                                    },\n                                                    {\n                                                        type = \"string\",\n                                                    },\n                                                }\n                                            },\n                                            tls = {\n                                                type = \"boolean\",\n                                            }\n                                        },\n                                        required = {\"addr\"}\n                                    },\n                                },\n                            },\n                            uniqueItems = true,\n                        },\n                        udp = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                anyOf = {\n                                    {\n                                        type = \"integer\",\n                                    },\n                                    {\n                                        type = \"string\",\n                                    },\n                                },\n                            },\n                            uniqueItems = true,\n                        },\n                    }\n                },\n                dns_resolver = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"string\",\n                    }\n                },\n                dns_resolver_valid = {\n                    type = \"integer\",\n                },\n                enable_http2 = {\n                    type = \"boolean\",\n                    default = true\n                },\n                ssl = {\n                    type = \"object\",\n                    properties = {\n                        ssl_trusted_certificate = {\n                            type = \"string\",\n                            default = \"system\"\n                        },\n                        listen = {\n                            type = \"array\",\n                            items = {\n                                type = \"object\",\n                                properties = {\n                                    ip = {\n                                        type = \"string\",\n                                    },\n                                    port = {\n                                        type = \"integer\",\n                                        minimum = 1,\n                                        maximum = 65535\n                                    },\n                                    enable_http3 = {\n                                        type = \"boolean\",\n                                    },\n                                }\n                            }\n                        },\n                    }\n                },\n                data_encryption = {\n                    type = \"object\",\n                    properties = {\n                        keyring = {\n                            anyOf = {\n                                {\n                                    type = \"array\",\n                                    minItems = 1,\n                                    items = {\n                                        type = \"string\",\n                                        minLength = 16,\n                                        maxLength = 16\n                                    }\n                                },\n                                {\n                                    type = \"string\",\n                                    minLength = 16,\n                                    maxLength = 16\n                                }\n                            }\n                        },\n                    }\n                },\n                disable_upstream_healthcheck = {\n                    type = \"boolean\",\n                    default = false,\n                    description = \"a global switch to disable upstream health checks\",\n                },\n                trusted_addresses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        anyOf = schema_def.ip_def,\n                    },\n                    uniqueItems = true\n                },\n            }\n        },\n        nginx_config = {\n            type = \"object\",\n            properties = {\n                envs = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"string\",\n                    }\n                }\n            },\n        },\n        http = {\n            type = \"object\",\n            properties = {\n                custom_lua_shared_dict = {\n                    type = \"object\",\n                }\n            }\n        },\n        etcd = etcd_schema,\n        plugins = {\n            type = \"array\",\n            default = {},\n            minItems = 0,\n            items = {\n                type = \"string\"\n            }\n        },\n        stream_plugins = {\n            type = \"array\",\n            default = {},\n            minItems = 0,\n            items = {\n                type = \"string\"\n            }\n        },\n        wasm = {\n            type = \"object\",\n            properties = {\n                plugins = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"object\",\n                        properties = {\n                            name = {\n                                type = \"string\"\n                            },\n                            file = {\n                                type = \"string\"\n                            },\n                            priority = {\n                                type = \"integer\"\n                            },\n                            http_request_phase = {\n                                enum = {\"access\", \"rewrite\"},\n                                default = \"access\",\n                            },\n                        },\n                        required = {\"name\", \"file\", \"priority\"}\n                    }\n                }\n            }\n        },\n        deployment = {\n            type = \"object\",\n            properties = {\n                role = {\n                    enum = {\"traditional\", \"control_plane\", \"data_plane\", \"standalone\"},\n                    default = \"traditional\"\n                }\n            },\n        },\n    },\n    required = {\"apisix\", \"deployment\"},\n}\n\nlocal admin_schema = {\n    type = \"object\",\n    properties = {\n        admin_key = {\n            type = \"array\",\n            properties = {\n                items = {\n                    properties = {\n                        name = {type = \"string\"},\n                        key = {type = \"string\"},\n                        role = {type = \"string\"},\n                    }\n                }\n            }\n        },\n        admin_listen = {\n            properties = {\n                listen = { type = \"string\" },\n                port = { type = \"integer\" },\n            },\n            default = {\n                listen = \"0.0.0.0\",\n                port = 9180,\n            }\n        },\n        https_admin = {\n            type = \"boolean\",\n        },\n        admin_key_required = {\n            type = \"boolean\",\n        },\n    }\n}\n\nlocal deployment_schema = {\n    traditional = {\n        properties = {\n            etcd = etcd_schema,\n            admin = admin_schema,\n            role_traditional = {\n                properties = {\n                    config_provider = {\n                        enum = {\"etcd\", \"yaml\"}\n                    },\n                },\n                required = {\"config_provider\"}\n            }\n        },\n        required = {\"etcd\"}\n    },\n    control_plane = {\n        properties = {\n            etcd = etcd_schema,\n            admin = admin_schema,\n            role_control_plane = {\n                properties = {\n                    config_provider = {\n                        enum = {\"etcd\"}\n                    },\n                },\n                required = {\"config_provider\"}\n            },\n        },\n        required = {\"etcd\", \"role_control_plane\"}\n    },\n    data_plane = {\n        properties = {\n            etcd = etcd_schema,\n            role_data_plane = {\n                properties = {\n                    config_provider = {\n                        enum = {\"etcd\", \"yaml\", \"json\", \"xds\"}\n                    },\n                },\n                required = {\"config_provider\"}\n            },\n        },\n        required = {\"role_data_plane\"}\n    }\n}\n\n\nfunction _M.validate(yaml_conf)\n    local validator = jsonschema.generate_validator(config_schema)\n    local ok, err = validator(yaml_conf)\n    if not ok then\n        return false, \"failed to validate config: \" .. err\n    end\n\n    if yaml_conf.discovery then\n        for kind, conf in pairs(yaml_conf.discovery) do\n            local ok, schema = pcall(require, \"apisix.discovery.\" .. kind .. \".schema\")\n            if ok then\n                local validator = jsonschema.generate_validator(schema)\n                local ok, err = validator(conf)\n                if not ok then\n                    return false, \"invalid discovery \" .. kind .. \" configuration: \" .. err\n                end\n            end\n        end\n    end\n\n    local role = yaml_conf.deployment.role\n    local validator = jsonschema.generate_validator(deployment_schema[role])\n    local ok, err = validator(yaml_conf.deployment)\n    if not ok then\n        return false, \"invalid deployment \" .. role .. \" configuration: \" .. err\n    end\n\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/cli/util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal require = require\nlocal pcall = pcall\nlocal open = io.open\nlocal popen = io.popen\nlocal close = io.close\nlocal exit = os.exit\nlocal stderr = io.stderr\nlocal str_format = string.format\nlocal tonumber = tonumber\nlocal io = io\nlocal ipairs = ipairs\nlocal assert = assert\n\nlocal _M = {}\n\n\n-- Note: The `execute_cmd` return value will have a line break at the end,\n-- it is recommended to use the `trim` function to handle the return value.\nlocal function execute_cmd(cmd)\n    local t, err = popen(cmd)\n    if not t then\n        return nil, \"failed to execute command: \"\n                    .. cmd .. \", error info: \" .. err\n    end\n\n    local data, err = t:read(\"*all\")\n    t:close()\n\n    if not data then\n        return nil, \"failed to read execution result of: \"\n                    .. cmd .. \", error info: \" .. err\n    end\n\n    return data\nend\n_M.execute_cmd = execute_cmd\n\n\n-- For commands which stdout would be always be empty,\n-- forward stderr to stdout to get the error msg\nfunction _M.execute_cmd_with_error(cmd)\n    return execute_cmd(cmd .. \" 2>&1\")\nend\n\n\nfunction _M.trim(s)\n    return (s:gsub(\"^%s*(.-)%s*$\", \"%1\"))\nend\n\n\nfunction _M.split(self, sep)\n    local sep, fields = sep or \":\", {}\n    local pattern = str_format(\"([^%s]+)\", sep)\n\n    self:gsub(pattern, function(c) fields[#fields + 1] = c end)\n\n    return fields\nend\n\n\nfunction _M.read_file(file_path)\n    local file, err = open(file_path, \"rb\")\n    if not file then\n        return false, \"failed to open file: \" .. file_path .. \", error info:\" .. err\n    end\n\n    local data, err = file:read(\"*all\")\n    file:close()\n    if not data then\n        return false, \"failed to read file: \" .. file_path .. \", error info:\" .. err\n    end\n\n    return data\nend\n\n\nfunction _M.die(...)\n    stderr:write(...)\n    exit(1)\nend\n\n\nfunction _M.is_32bit_arch()\n    local ok, ffi = pcall(require, \"ffi\")\n    if ok then\n        -- LuaJIT\n        return ffi.abi(\"32bit\")\n    end\n\n    local ret = _M.execute_cmd(\"getconf LONG_BIT\")\n    local bits = tonumber(ret)\n    return bits <= 32\nend\n\n\nfunction _M.write_file(file_path, data)\n    local file, err = open(file_path, \"w+\")\n    if not file then\n        return false, \"failed to open file: \"\n                      .. file_path\n                      .. \", error info:\"\n                      .. err\n    end\n\n    local ok, err = file:write(data)\n    file:close()\n    if not ok then\n        return false, \"failed to write file: \"\n                      .. file_path\n                      .. \", error info:\"\n                      .. err\n    end\n    return true\nend\n\n\nfunction _M.file_exists(file_path)\n    local f = open(file_path, \"r\")\n    return f ~= nil and close(f)\nend\n\ndo\n    local trusted_certs_paths = {\n        \"/etc/ssl/certs/ca-certificates.crt\",                -- Debian/Ubuntu/Gentoo\n        \"/etc/pki/tls/certs/ca-bundle.crt\",                  -- Fedora/RHEL 6\n        \"/etc/ssl/ca-bundle.pem\",                            -- OpenSUSE\n        \"/etc/pki/tls/cacert.pem\",                           -- OpenELEC\n        \"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem\", -- CentOS/RHEL 7\n        \"/etc/ssl/cert.pem\",                                 -- OpenBSD, Alpine\n    }\n\n    -- Check if a file exists using Lua's built-in `io.open`\n    local function file_exists(path)\n        local file = io.open(path, \"r\")\n        if file then\n            file:close()\n            return true\n        else\n            return false\n        end\n    end\n\n    function _M.get_system_trusted_certs_filepath()\n        for _, path in ipairs(trusted_certs_paths) do\n            if file_exists(path) then\n                return path\n            end\n        end\n\n        return nil,\n            \"Could not find trusted certs file in \" ..\n            \"any of the `system`-predefined locations. \" ..\n            \"Please install a certs file there or set \" ..\n            \"`lua_ssl_trusted_certificate` to a \" ..\n            \"specific file path instead of `system`\"\n    end\nend\n\n\nfunction _M.gen_trusted_certs_combined_file(combined_filepath, paths)\n    local combined_file = assert(io.open(combined_filepath, \"w\"))\n    for _, path in ipairs(paths) do\n        local cert_file = assert(io.open(path, \"r\"))\n        combined_file:write(cert_file:read(\"*a\"))\n        combined_file:write(\"\\n\")\n        cert_file:close()\n    end\n    combined_file:close()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/constants.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn {\n    RPC_ERROR = 0,\n    RPC_PREPARE_CONF = 1,\n    RPC_HTTP_REQ_CALL = 2,\n    RPC_EXTRA_INFO = 3,\n    RPC_HTTP_RESP_CALL = 4,\n    HTTP_ETCD_DIRECTORY = {\n        [\"/upstreams\"] = true,\n        [\"/plugins\"] = true,\n        [\"/ssls\"] = true,\n        [\"/stream_routes\"] = true,\n        [\"/plugin_metadata\"] = true,\n        [\"/routes\"] = true,\n        [\"/services\"] = true,\n        [\"/consumers\"] = true,\n        [\"/global_rules\"] = true,\n        [\"/protos\"] = true,\n        [\"/plugin_configs\"] = true,\n        [\"/consumer_groups\"] = true,\n        [\"/secrets\"] = true,\n    },\n    STREAM_ETCD_DIRECTORY = {\n        [\"/upstreams\"] = true,\n        [\"/services\"] = true,\n        [\"/plugins\"] = true,\n        [\"/ssls\"] = true,\n        [\"/stream_routes\"] = true,\n        [\"/plugin_metadata\"] = true,\n    },\n}\n"
  },
  {
    "path": "apisix/consumer.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core           = require(\"apisix.core\")\nlocal config_local   = require(\"apisix.core.config_local\")\nlocal secret         = require(\"apisix.secret\")\nlocal plugin         = require(\"apisix.plugin\")\nlocal plugin_checker = require(\"apisix.plugin\").plugin_checker\nlocal check_schema   = require(\"apisix.core.schema\").check\nlocal error          = error\nlocal ipairs         = ipairs\nlocal pairs          = pairs\nlocal type           = type\nlocal string_sub     = string.sub\nlocal consumers\n\n\nlocal _M = {\n    version = 0.3,\n}\n\nlocal lrucache = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\n-- Please calculate and set the value of the \"consumers_count_for_lrucache\"\n-- variable based on the number of consumers in the current environment,\n-- taking into account the appropriate adjustment coefficient.\nlocal consumers_count_for_lrucache = 4096\n\nlocal function remove_etcd_prefix(key)\n    local prefix = \"\"\n    local local_conf = config_local.local_conf()\n    local role = core.table.try_read_attr(local_conf, \"deployment\", \"role\")\n    local provider = core.table.try_read_attr(local_conf, \"deployment\", \"role_\" ..\n    role, \"config_provider\")\n    if provider == \"etcd\" and local_conf.etcd and local_conf.etcd.prefix then\n        prefix = local_conf.etcd.prefix\n    end\n    return string_sub(key, #prefix + 1)\nend\n\n-- /{etcd.prefix}/consumers/{consumer_name}/credentials/{credential_id} --> {consumer_name}\nlocal function get_consumer_name_from_credential_etcd_key(key)\n    local uri_segs = core.utils.split_uri(remove_etcd_prefix(key))\n    return uri_segs[3]\nend\n\nlocal function is_credential_etcd_key(key)\n    if not key then\n        return false\n    end\n\n    local uri_segs = core.utils.split_uri(remove_etcd_prefix(key))\n    return uri_segs[2] == \"consumers\" and uri_segs[4] == \"credentials\"\nend\n\nlocal function get_credential_id_from_etcd_key(key)\n    local uri_segs = core.utils.split_uri(remove_etcd_prefix(key))\n    return uri_segs[5]\nend\n\nlocal function filter_consumers_list(data_list)\n    if #data_list == 0 then\n        return data_list\n    end\n\n    local list = {}\n    for _, item in ipairs(data_list) do\n        if not (type(item) == \"table\" and is_credential_etcd_key(item.key)) then\n            core.table.insert(list, item)\n        end\n    end\n\n    return list\nend\n\nlocal plugin_consumer\ndo\n    local consumers_id_lrucache = core.lrucache.new({\n            count = consumers_count_for_lrucache\n        })\n\nlocal function construct_consumer_data(val, name, plugin_config)\n    -- if the val is a Consumer, clone it to the local consumer;\n    -- if the val is a Credential, to get the Consumer by consumer_name and then clone\n    -- it to the local consumer.\n    local consumer\n    if is_credential_etcd_key(val.key) then\n        local consumer_name = get_consumer_name_from_credential_etcd_key(val.key)\n        local the_consumer = consumers:get(consumer_name)\n        if the_consumer and the_consumer.value then\n            consumer = consumers_id_lrucache(val.value.id .. name, val.modifiedIndex..\n                                                the_consumer.modifiedIndex,\n                function (val, the_consumer)\n                    consumer = core.table.clone(the_consumer.value)\n                    consumer.modifiedIndex = the_consumer.modifiedIndex\n                    consumer.credential_id = get_credential_id_from_etcd_key(val.key)\n                    return consumer\n                end, val, the_consumer)\n        else\n            -- Normally wouldn't get here:\n            -- it should belong to a consumer for any credential.\n            return nil, \"failed to get the consumer for the credential,\",\n                \" a wild credential has appeared!\",\n                \" credential key: \", val.key, \", consumer name: \", consumer_name\n        end\n    else\n        consumer = consumers_id_lrucache(val.value.id .. name, val.modifiedIndex,\n            function (val)\n                consumer = core.table.clone(val.value)\n                consumer.modifiedIndex = val.modifiedIndex\n                return consumer\n            end, val)\n    end\n\n    -- if the consumer has labels, set the field custom_id to it.\n    -- the custom_id is used to set in the request headers to the upstream.\n    if consumer.labels then\n        consumer.custom_id = consumer.labels[\"custom_id\"]\n    end\n\n    -- Note: the id here is the key of consumer data, which\n    -- is 'username' field in admin\n    consumer.consumer_name = consumer.id\n    consumer.auth_conf = plugin_config\n\n    return consumer\nend\n\n\nfunction plugin_consumer()\n    local plugins = {}\n\n    if consumers.values == nil then\n        return plugins\n    end\n\n    -- consumers.values is the list that got from etcd by prefix key {etcd_prefix}/consumers.\n    -- So it contains consumers and credentials.\n    -- The val in the for-loop may be a Consumer or a Credential.\n    for _, val in ipairs(consumers.values) do\n        if type(val) ~= \"table\" then\n            goto CONTINUE\n        end\n\n        for name, config in pairs(val.value.plugins or {}) do\n            local plugin_obj = plugin.get(name)\n            if plugin_obj and plugin_obj.type == \"auth\" then\n                if not plugins[name] then\n                    plugins[name] = {\n                        nodes = {},\n                        len = 0,\n                        conf_version = consumers.conf_version\n                    }\n                end\n\n                local consumer, err = construct_consumer_data(val, name, config)\n                if not consumer then\n                    core.log.error(\"failed to construct consumer data for plugin \",\n                                   name, \": \", err)\n                    goto CONTINUE\n                end\n\n                plugins[name].len = plugins[name].len + 1\n                core.table.insert(plugins[name].nodes, plugins[name].len,\n                                    consumer)\n            end\n        end\n\n        ::CONTINUE::\n    end\n\n    return plugins\nend\n\nend\n\n\n_M.filter_consumers_list = filter_consumers_list\n\nfunction _M.get_consumer_key_from_credential_key(key)\n    local uri_segs = core.utils.split_uri(key)\n    return \"/consumers/\" .. uri_segs[3]\nend\n\nfunction _M.plugin(plugin_name)\n    local plugin_conf = core.lrucache.global(\"/consumers\",\n                            consumers.conf_version, plugin_consumer)\n    return plugin_conf[plugin_name]\nend\n\nfunction _M.consumers_conf(plugin_name)\n    return _M.plugin(plugin_name)\nend\n\n\n-- attach chosen consumer to the ctx, used in auth plugin\nfunction _M.attach_consumer(ctx, consumer, conf)\n    ctx.consumer = consumer\n    ctx.consumer_name = consumer.consumer_name\n    ctx.consumer_group_id = consumer.group_id\n    ctx.consumer_ver = conf.conf_version\n\n    core.request.set_header(ctx, \"X-Consumer-Username\", consumer.username)\n    core.request.set_header(ctx, \"X-Credential-Identifier\", consumer.credential_id)\n    core.request.set_header(ctx, \"X-Consumer-Custom-ID\", consumer.custom_id)\nend\n\n\nfunction _M.consumers()\n    if not consumers then\n        return nil, nil\n    end\n\n    return filter_consumers_list(consumers.values), consumers.conf_version\nend\n\n\nlocal create_consume_cache\ndo\n    local consumer_lrucache = core.lrucache.new({\n            count = consumers_count_for_lrucache\n        })\n\nlocal function fill_consumer_secret(consumer)\n    local new_consumer = core.table.clone(consumer)\n    new_consumer.auth_conf = secret.fetch_secrets(new_consumer.auth_conf, false)\n    return new_consumer\nend\n\n\nfunction create_consume_cache(consumers_conf, key_attr)\n    local consumer_names = {}\n\n    for _, consumer in ipairs(consumers_conf.nodes) do\n        local new_consumer = consumer_lrucache(consumer, nil,\n                                fill_consumer_secret, consumer)\n        consumer_names[new_consumer.auth_conf[key_attr]] = new_consumer\n    end\n\n    return consumer_names\nend\n\nend\n\n\nfunction _M.consumers_kv(plugin_name, consumer_conf, key_attr)\n    local consumers = lrucache(\"consumers_key#\" .. plugin_name, consumer_conf.conf_version,\n        create_consume_cache, consumer_conf, key_attr)\n\n    return consumers\nend\n\n\nfunction _M.find_consumer(plugin_name, key, key_value)\n    local consumer\n    local consumer_conf\n    consumer_conf = _M.plugin(plugin_name)\n    if not consumer_conf then\n        return nil, nil, \"Missing related consumer\"\n    end\n    local consumers = _M.consumers_kv(plugin_name, consumer_conf, key)\n    consumer = consumers[key_value]\n    return consumer, consumer_conf\nend\n\n\nlocal function check_consumer(consumer, key)\n    local data_valid\n    local err\n    if is_credential_etcd_key(key) then\n        data_valid, err = check_schema(core.schema.credential, consumer)\n    else\n        data_valid, err = check_schema(core.schema.consumer, consumer)\n    end\n    if not data_valid then\n        return data_valid, err\n    end\n\n    return plugin_checker(consumer, core.schema.TYPE_CONSUMER)\nend\n\n\nlocal function filter(consumer)\n    if not consumer.value or not consumer.value.plugins then\n        return\n    end\n    plugin.set_plugins_meta_parent(consumer.value.plugins, consumer)\nend\n\n\nfunction _M.init_worker()\n    local err\n    local cfg = {\n        automatic = true,\n        checker = check_consumer,\n        filter = filter\n    }\n\n    consumers, err = core.config.new(\"/consumers\", cfg)\n    if not consumers then\n        error(\"failed to create etcd instance for fetching consumers: \" .. err)\n        return\n    end\nend\n\nlocal function get_anonymous_consumer_from_local_cache(name)\n    local anon_consumer_raw = consumers:get(name)\n\n    if not anon_consumer_raw or not anon_consumer_raw.value or\n    not anon_consumer_raw.value.id or not anon_consumer_raw.modifiedIndex then\n        return nil, nil, \"failed to get anonymous consumer \" .. name\n    end\n\n    -- make structure of anon_consumer similar to that of consumer_mod.consumers_kv's response\n    local anon_consumer = anon_consumer_raw.value\n    anon_consumer.consumer_name = anon_consumer_raw.value.id\n    anon_consumer.modifiedIndex = anon_consumer_raw.modifiedIndex\n\n    local anon_consumer_conf = {\n        conf_version = anon_consumer_raw.modifiedIndex\n    }\n\n    return anon_consumer, anon_consumer_conf\nend\n\n\nfunction _M.get_anonymous_consumer(name)\n    local anon_consumer, anon_consumer_conf, err\n    anon_consumer, anon_consumer_conf, err = get_anonymous_consumer_from_local_cache(name)\n\n    return anon_consumer, anon_consumer_conf, err\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/consumer_group.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin_checker = require(\"apisix.plugin\").plugin_checker\nlocal plugin         = require(\"apisix.plugin\")\nlocal error = error\n\n\nlocal consumer_groups\n\n\nlocal _M = {\n}\n\n\nlocal function filter(consumer_grp)\n    if not consumer_grp.value or not consumer_grp.value.plugins then\n        return\n    end\n    plugin.set_plugins_meta_parent(consumer_grp.value.plugins, consumer_grp)\nend\n\n\nfunction _M.init_worker()\n    local err\n    consumer_groups, err = core.config.new(\"/consumer_groups\", {\n        automatic = true,\n        item_schema = core.schema.consumer_group,\n        checker = plugin_checker,\n        filter = filter,\n    })\n    if not consumer_groups then\n        error(\"failed to sync /consumer_groups: \" .. err)\n    end\nend\n\n\nfunction _M.consumer_groups()\n    if not consumer_groups then\n        return nil, nil\n    end\n    return consumer_groups.values, consumer_groups.conf_version\nend\n\n\nfunction _M.get(id)\n    return consumer_groups:get(id)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/control/router.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal router = require(\"apisix.utils.router\")\nlocal radixtree = require(\"resty.radixtree\")\nlocal builtin_v1_routes = require(\"apisix.control.v1\")\nlocal plugin_mod = require(\"apisix.plugin\")\nlocal core = require(\"apisix.core\")\n\nlocal str_sub = string.sub\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal type = type\nlocal ngx = ngx\nlocal get_method = ngx.req.get_method\nlocal events = require(\"apisix.events\")\n\nlocal _M = {}\n\n\nlocal function format_dismod_uri(mod_name, uri)\n    if core.string.has_prefix(uri, \"/v1/\") then\n        return uri\n    end\n\n    local tmp = {\"/v1/discovery/\", mod_name}\n    if not core.string.has_prefix(uri, \"/\") then\n        core.table.insert(tmp, \"/\")\n    end\n    core.table.insert(tmp, uri)\n\n    return core.table.concat(tmp, \"\")\nend\n\n-- we do not hardcode the discovery module's control api uri\nlocal function format_dismod_control_api_uris(mod_name, api_route)\n    if not api_route or #api_route == 0 then\n        return api_route\n    end\n\n    local clone_route = core.table.clone(api_route)\n    for _, v in ipairs(clone_route) do\n        local uris = v.uris\n        local target_uris = core.table.new(#uris, 0)\n        for _, uri in ipairs(uris) do\n            local target_uri = format_dismod_uri(mod_name, uri)\n            core.table.insert(target_uris, target_uri)\n        end\n        v.uris = target_uris\n    end\n\n    return clone_route\nend\n\n\nlocal fetch_control_api_router\ndo\n    local function register_api_routes(routes, api_routes)\n        for _, route in ipairs(api_routes) do\n            core.table.insert(routes, {\n                methods = route.methods,\n                -- note that it is 'uris' for control API, which is an array of strings\n                paths = route.uris,\n                handler = function (api_ctx)\n                    local code, body = route.handler(api_ctx)\n                    if code or body then\n                        if type(body) == \"table\" and ngx.header[\"Content-Type\"] == nil then\n                            core.response.set_header(\"Content-Type\", \"application/json\")\n                        end\n\n                        core.response.exit(code, body)\n                    end\n                end\n            })\n        end\n    end\n\n    local routes = {}\n    local v1_routes = {}\n    local function empty_func() end\n\nfunction fetch_control_api_router()\n    core.table.clear(routes)\n\n    for _, plugin in ipairs(plugin_mod.plugins) do\n        local api_fun = plugin.control_api\n        if api_fun then\n            local api_route = api_fun()\n            register_api_routes(routes, api_route)\n        end\n    end\n\n    local discovery_type = require(\"apisix.core.config_local\").local_conf().discovery\n    if discovery_type then\n        local discovery = require(\"apisix.discovery.init\").discovery\n        local dump_apis = {}\n        for key, _ in pairs(discovery_type) do\n            local dis_mod = discovery[key]\n            -- if discovery module has control_api method, support it\n            local api_fun = dis_mod.control_api\n            if api_fun then\n                local api_route = api_fun()\n                local format_route = format_dismod_control_api_uris(key, api_route)\n                register_api_routes(routes, format_route)\n            end\n\n            local dump_data = dis_mod.dump_data\n            if dump_data then\n                local target_uri = format_dismod_uri(key, \"/dump\")\n                local item = {\n                    methods = {\"GET\"},\n                    uris = {target_uri},\n                    handler = function()\n                        return 200, dump_data()\n                    end\n                }\n                core.table.insert(dump_apis, item)\n            end\n        end\n\n        if #dump_apis > 0 then\n            core.log.notice(\"dump_apis: \", core.json.encode(dump_apis, true))\n            register_api_routes(routes, dump_apis)\n        end\n    end\n\n    core.table.clear(v1_routes)\n    register_api_routes(v1_routes, builtin_v1_routes)\n\n    local v1_router, err = router.new(v1_routes)\n    if not v1_router then\n        return nil, err\n    end\n\n    core.table.insert(routes, {\n        paths = {\"/v1/*\"},\n        filter_fun = function(vars, opts, ...)\n            local uri = str_sub(vars.uri, #\"/v1\" + 1)\n            return v1_router:dispatch(uri, opts, ...)\n        end,\n        handler = empty_func,\n    })\n\n    local with_parameter = false\n    local conf = core.config.local_conf()\n    if conf.apisix.enable_control and conf.apisix.control then\n        if conf.apisix.control.router == \"radixtree_uri_with_parameter\" then\n            with_parameter = true\n        end\n    end\n\n    if with_parameter then\n        return radixtree.new(routes)\n    else\n        return router.new(routes)\n    end\nend\n\nend -- do\n\n\ndo\n    local match_opts = {}\n    local cached_version\n    local router\n\nfunction _M.match(uri)\n    if cached_version ~= plugin_mod.load_times then\n        local err\n        router, err = fetch_control_api_router()\n        if router == nil then\n            core.log.error(\"failed to fetch valid api router: \", err)\n            return false\n        end\n\n        cached_version = plugin_mod.load_times\n    end\n\n    core.table.clear(match_opts)\n    match_opts.method = get_method()\n\n    return router:dispatch(uri, match_opts)\nend\n\nend -- do\n\nlocal function reload_plugins()\n    core.log.info(\"start to hot reload plugins\")\n    plugin_mod.load()\nend\n\n\nfunction _M.init_worker()\n    -- register reload plugin handler\n    events:register(reload_plugins, builtin_v1_routes.reload_event, \"PUT\")\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/control/v1.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal get_routes = require(\"apisix.router\").http_routes\nlocal get_stream_routes = require(\"apisix.router\").stream_routes\nlocal get_services = require(\"apisix.http.service\").services\nlocal upstream_mod = require(\"apisix.upstream\")\nlocal healthcheck_manager = require(\"apisix.healthcheck_manager\")\nlocal get_upstreams = upstream_mod.upstreams\nlocal collectgarbage = collectgarbage\nlocal ipairs = ipairs\nlocal pcall = pcall\nlocal str_format = string.format\nlocal ngx = ngx\nlocal ngx_var = ngx.var\nlocal events = require(\"apisix.events\")\n\n\nlocal _M = {}\n\n_M.RELOAD_EVENT = 'control-api-plugin-reload'\n\nfunction _M.schema()\n    local http_plugins, stream_plugins = plugin.get_all({\n        version = true,\n        priority = true,\n        schema = true,\n        metadata_schema = true,\n        consumer_schema = true,\n        type = true,\n        scope = true,\n    })\n    local schema = {\n        main = {\n            consumer = core.schema.consumer,\n            consumer_group = core.schema.consumer_group,\n            global_rule = core.schema.global_rule,\n            plugin_config = core.schema.plugin_config,\n            plugins = core.schema.plugins,\n            proto = core.schema.proto,\n            route = core.schema.route,\n            service = core.schema.service,\n            ssl = core.schema.ssl,\n            stream_route = core.schema.stream_route,\n            upstream = core.schema.upstream,\n            upstream_hash_header_schema = core.schema.upstream_hash_header_schema,\n            upstream_hash_vars_schema = core.schema.upstream_hash_vars_schema,\n        },\n        plugins = http_plugins,\n        stream_plugins = stream_plugins,\n    }\n    return 200, schema\nend\n\nlocal healthcheck\nlocal function extra_checker_info(value)\n    if not healthcheck then\n        healthcheck = require(\"resty.healthcheck\")\n    end\n\n    local name = healthcheck_manager.get_healthchecker_name(value.value)\n    local nodes, err = healthcheck.get_target_list(name, \"upstream-healthcheck\")\n    if err then\n        core.log.error(\"healthcheck.get_target_list failed: \", err)\n    end\n    return {\n        name = value.key,\n        nodes = nodes,\n    }\nend\n\n\nlocal function get_checker_type(checks)\n    if checks.active and checks.active.type then\n        return checks.active.type\n    elseif checks.passive and checks.passive.type then\n        return checks.passive.type\n    end\nend\n\n\nlocal function iter_and_add_healthcheck_info(infos, values)\n    if not values then\n        return\n    end\n\n    for _, value in core.config_util.iterate_values(values) do\n        local checks = value.value.checks or (value.value.upstream and value.value.upstream.checks)\n        if checks then\n            local info = extra_checker_info(value)\n            info.type = get_checker_type(checks)\n            core.table.insert(infos, info)\n        end\n    end\nend\n\n\nlocal HTML_TEMPLATE = [[\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n  <title>APISIX upstream check status</title>\n</head>\n<body>\n<h1>APISIX upstream check status</h1>\n<table style=\"background-color:white\" cellspacing=\"0\" cellpadding=\"3\" border=\"1\">\n  <tr bgcolor=\"#C0C0C0\">\n    <th>Index</th>\n    <th>Upstream</th>\n    <th>Check type</th>\n    <th>Host</th>\n    <th>Status</th>\n    <th>Success counts</th>\n    <th>TCP Failures</th>\n    <th>HTTP Failures</th>\n    <th>TIMEOUT Failures</th>\n  </tr>\n{% local i = 0 %}\n{% for _, stat in ipairs(stats) do %}\n{% for _, node in ipairs(stat.nodes) do %}\n{% i = i + 1 %}\n  {% if node.status == \"healthy\" or node.status == \"mostly_healthy\" then %}\n  <tr>\n  {% else %}\n  <tr bgcolor=\"#FF0000\">\n  {% end %}\n    <td>{* i *}</td>\n    <td>{* stat.name *}</td>\n    <td>{* stat.type *}</td>\n    <td>{* node.ip .. \":\" .. node.port *}</td>\n    <td>{* node.status *}</td>\n    <td>{* node.counter.success *}</td>\n    <td>{* node.counter.tcp_failure *}</td>\n    <td>{* node.counter.http_failure *}</td>\n    <td>{* node.counter.timeout_failure *}</td>\n  </tr>\n{% end %}\n{% end %}\n</table>\n</body>\n</html>\n]]\n\nlocal html_render\n\nlocal function try_render_html(data)\n    if not html_render then\n        local template = require(\"resty.template\")\n        html_render = template.compile(HTML_TEMPLATE)\n    end\n    local accept = ngx_var.http_accept\n    if accept and accept:find(\"text/html\") then\n        local ok, out = pcall(html_render, data)\n        if not ok then\n            local err = str_format(\"HTML template rendering: %s\", out)\n            core.log.error(err)\n            return nil, err\n        end\n        return out\n    end\nend\n\n\nlocal function _get_health_checkers()\n    local infos = {}\n    local routes = get_routes()\n    iter_and_add_healthcheck_info(infos, routes)\n    local stream_routes = get_stream_routes()\n    iter_and_add_healthcheck_info(infos, stream_routes)\n    local services = get_services()\n    iter_and_add_healthcheck_info(infos, services)\n    local upstreams = get_upstreams()\n    iter_and_add_healthcheck_info(infos, upstreams)\n    return infos\nend\n\n\nfunction _M.get_health_checkers()\n    local infos = _get_health_checkers()\n    local out, err = try_render_html({stats=infos})\n    if out then\n        core.response.set_header(\"Content-Type\", \"text/html\")\n        return 200, out\n    end\n    if err then\n        return 503, {error_msg = err}\n    end\n\n    return 200, infos\nend\n\n\nlocal function iter_and_find_healthcheck_info(values, src_type, src_id)\n    if not values then\n        return nil, str_format(\"%s[%s] not found\", src_type, src_id)\n    end\n\n    for _, value in core.config_util.iterate_values(values) do\n        if value.value.id == src_id then\n            local checks = value.value.checks or\n                (value.value.upstream and value.value.upstream.checks)\n            if not checks then\n                return nil, str_format(\"no checker for %s[%s]\", src_type, src_id)\n            end\n            local info = extra_checker_info(value)\n            info.type = get_checker_type(checks)\n            return info\n        end\n    end\n\n    return nil, str_format(\"%s[%s] not found\", src_type, src_id)\nend\n\n\nfunction _M.get_health_checker()\n    local uri_segs = core.utils.split_uri(ngx_var.uri)\n    core.log.info(\"healthcheck uri: \", core.json.delay_encode(uri_segs))\n\n    local src_type, src_id = uri_segs[4], uri_segs[5]\n    if not src_id then\n        return 404, {error_msg = str_format(\"missing src id for src type %s\", src_type)}\n    end\n\n    local values\n    if src_type == \"routes\" then\n        values = get_routes()\n    elseif src_type == \"services\" then\n        values = get_services()\n    elseif src_type == \"upstreams\" then\n        values = get_upstreams()\n    elseif src_type == \"stream_routes\" then\n        values = get_stream_routes()\n    else\n        return 400, {error_msg = str_format(\"invalid src type %s\", src_type)}\n    end\n\n    local info, err = iter_and_find_healthcheck_info(values, src_type, src_id)\n    if not info then\n        return 404, {error_msg = err}\n    end\n    local out, err = try_render_html({stats={info}})\n    if out then\n        core.response.set_header(\"Content-Type\", \"text/html\")\n        return 200, out\n    end\n    if err then\n        return 503, {error_msg = err}\n    end\n\n    return 200, info\nend\n\nlocal function iter_add_get_routes_info(values, route_id)\n    local infos = {}\n    for _, route in core.config_util.iterate_values(values) do\n        local new_route = core.table.deepcopy(route)\n        -- remove healthcheck info\n        new_route.checker = nil\n        new_route.checker_idx = nil\n        new_route.checker_upstream = nil\n        new_route.clean_handlers = nil\n        core.table.insert(infos, new_route)\n        -- check the route id\n        if route_id and route.value.id == route_id then\n            return new_route\n        end\n    end\n    if not route_id then\n        return infos\n    end\n    return nil\nend\n\nfunction _M.dump_all_routes_info()\n    local routes = get_routes()\n    local infos = iter_add_get_routes_info(routes, nil)\n    return 200, infos\nend\n\nfunction _M.dump_route_info()\n    local routes = get_routes()\n    local uri_segs = core.utils.split_uri(ngx_var.uri)\n    local route_id = uri_segs[4]\n    local route = iter_add_get_routes_info(routes, route_id)\n    if not route then\n        return 404, {error_msg = str_format(\"route[%s] not found\", route_id)}\n    end\n    return 200, route\nend\n\nlocal function iter_add_get_upstream_info(values, upstream_id)\n    if not values then\n        return nil\n    end\n\n    local infos = {}\n    for _, upstream in core.config_util.iterate_values(values) do\n        local new_upstream = core.table.deepcopy(upstream)\n        core.table.insert(infos, new_upstream)\n        -- check the upstream id\n        if upstream_id and upstream.value.id == upstream_id then\n            return new_upstream\n        end\n    end\n    if not upstream_id then\n        return infos\n    end\n    return nil\nend\n\nfunction _M.dump_all_upstreams_info()\n    local upstreams = get_upstreams()\n    local infos = iter_add_get_upstream_info(upstreams, nil)\n    return 200, infos\nend\n\n\nfunction _M.dump_upstream_info()\n    local upstreams = get_upstreams()\n    local uri_segs = core.utils.split_uri(ngx_var.uri)\n    local upstream_id = uri_segs[4]\n    local upstream = iter_add_get_upstream_info(upstreams, upstream_id)\n    if not upstream then\n        return 404, {error_msg = str_format(\"upstream[%s] not found\", upstream_id)}\n    end\n    return 200, upstream\nend\n\nfunction _M.trigger_gc()\n    -- TODO: find a way to trigger GC in the stream subsystem\n    collectgarbage()\n    return 200\nend\n\n\nlocal function iter_add_get_services_info(values, svc_id)\n    local infos = {}\n    for _, svc in core.config_util.iterate_values(values) do\n        local new_svc = core.table.deepcopy(svc)\n        -- remove healthcheck info\n        new_svc.checker = nil\n        new_svc.checker_idx = nil\n        new_svc.checker_upstream = nil\n        new_svc.clean_handlers = nil\n        core.table.insert(infos, new_svc)\n        -- check the service id\n        if svc_id and svc.value.id == svc_id then\n            return new_svc\n        end\n    end\n    if not svc_id then\n        return infos\n    end\n    return nil\nend\n\nfunction _M.dump_all_services_info()\n    local services = get_services()\n    local infos = iter_add_get_services_info(services, nil)\n    return 200, infos\nend\n\nfunction _M.dump_service_info()\n    local services = get_services()\n    local uri_segs = core.utils.split_uri(ngx_var.uri)\n    local svc_id = uri_segs[4]\n    local info = iter_add_get_services_info(services, svc_id)\n    if not info then\n        return 404, {error_msg = str_format(\"service[%s] not found\", svc_id)}\n    end\n    return 200, info\nend\n\nfunction _M.dump_all_plugin_metadata()\n    local names = core.config.local_conf().plugins\n    local metadatas = core.table.new(0, #names)\n    for _, name in ipairs(names) do\n        local metadata = plugin.plugin_metadata(name)\n        if metadata then\n            core.table.insert(metadatas, metadata.value)\n        end\n    end\n    return 200, metadatas\nend\n\nfunction _M.dump_plugin_metadata()\n    local uri_segs = core.utils.split_uri(ngx_var.uri)\n    local name = uri_segs[4]\n    local metadata = plugin.plugin_metadata(name)\n    if not metadata then\n        return 404, {error_msg = str_format(\"plugin metadata[%s] not found\", name)}\n    end\n    return 200, metadata.value\nend\n\nfunction _M.post_reload_plugins()\n    local success, err = events:post(_M.RELOAD_EVENT, ngx.req.get_method(), ngx.time())\n    if not success then\n        core.response.exit(503, err)\n    end\n\n    core.response.exit(200, \"done\")\nend\n\nreturn {\n    -- /v1/schema\n    {\n        methods = {\"GET\"},\n        uris = {\"/schema\"},\n        handler = _M.schema,\n    },\n    -- /v1/healthcheck\n    {\n        methods = {\"GET\"},\n        uris = {\"/healthcheck\"},\n        handler = _M.get_health_checkers,\n    },\n    -- /v1/healthcheck/{src_type}/{src_id}\n    {\n        methods = {\"GET\"},\n        uris = {\"/healthcheck/*\"},\n        handler = _M.get_health_checker,\n    },\n    -- /v1/gc\n    {\n        methods = {\"POST\"},\n        uris = {\"/gc\"},\n        handler = _M.trigger_gc,\n    },\n    -- /v1/routes\n    {\n        methods = {\"GET\"},\n        uris = {\"/routes\"},\n        handler = _M.dump_all_routes_info,\n    },\n    -- /v1/route/*\n    {\n        methods = {\"GET\"},\n        uris = {\"/route/*\"},\n        handler = _M.dump_route_info,\n    },\n    -- /v1/services\n    {\n        methods = {\"GET\"},\n        uris = {\"/services\"},\n        handler = _M.dump_all_services_info\n    },\n    -- /v1/service/*\n    {\n        methods = {\"GET\"},\n        uris = {\"/service/*\"},\n        handler = _M.dump_service_info\n    },\n    -- /v1/upstreams\n    {\n        methods = {\"GET\"},\n        uris = {\"/upstreams\"},\n        handler = _M.dump_all_upstreams_info,\n    },\n    -- /v1/upstream/*\n    {\n        methods = {\"GET\"},\n        uris = {\"/upstream/*\"},\n        handler = _M.dump_upstream_info,\n    },\n    -- /v1/plugin_metadatas\n    {\n        methods = {\"GET\"},\n        uris = {\"/plugin_metadatas\"},\n        handler = _M.dump_all_plugin_metadata,\n    },\n    -- /v1/plugin_metadata/*\n    {\n        methods = {\"GET\"},\n        uris = {\"/plugin_metadata/*\"},\n        handler = _M.dump_plugin_metadata,\n    },\n    -- /v1/plugins/reload\n    {\n        methods = {\"PUT\"},\n        uris = {\"/plugins/reload\"},\n        handler = _M.post_reload_plugins,\n    },\n    get_health_checkers = _get_health_checkers,\n    reload_event = _M.RELOAD_EVENT,\n}\n"
  },
  {
    "path": "apisix/core/config_etcd.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Get configuration information.\n--\n-- @module core.config_etcd\n\nlocal table        = require(\"apisix.core.table\")\nlocal config_local = require(\"apisix.core.config_local\")\nlocal config_util  = require(\"apisix.core.config_util\")\nlocal log          = require(\"apisix.core.log\")\nlocal json         = require(\"apisix.core.json\")\nlocal etcd_apisix  = require(\"apisix.core.etcd\")\nlocal core_str     = require(\"apisix.core.string\")\nlocal new_tab      = require(\"table.new\")\nlocal inspect      = require(\"inspect\")\nlocal process      = require(\"ngx.process\")\nlocal check_schema = require(\"apisix.core.schema\").check\nlocal exiting      = ngx.worker.exiting\nlocal worker_id    = ngx.worker.id\nlocal insert_tab   = table.insert\nlocal type         = type\nlocal ipairs       = ipairs\nlocal setmetatable = setmetatable\nlocal ngx_sleep    = require(\"apisix.core.utils\").sleep\nlocal ngx_timer_at = ngx.timer.at\nlocal ngx_time     = ngx.time\nlocal ngx          = ngx\nlocal sub_str      = string.sub\nlocal tostring     = tostring\nlocal tonumber     = tonumber\nlocal xpcall       = xpcall\nlocal debug        = debug\nlocal string       = string\nlocal error        = error\nlocal pairs        = pairs\nlocal next         = next\nlocal assert       = assert\nlocal rand         = math.random\nlocal constants    = require(\"apisix.constants\")\nlocal health_check = require(\"resty.etcd.health_check\")\nlocal semaphore    = require(\"ngx.semaphore\")\nlocal tablex       = require(\"pl.tablex\")\nlocal ngx_thread_spawn = ngx.thread.spawn\nlocal ngx_thread_kill = ngx.thread.kill\nlocal ngx_thread_wait = ngx.thread.wait\n\n\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal err_etcd_grpc_engine_timeout = \"context deadline exceeded\"\nlocal err_etcd_grpc_ngx_timeout = \"timeout\"\nlocal err_etcd_unhealthy_all = \"has no healthy etcd endpoint available\"\nlocal health_check_shm_name = \"etcd-cluster-health-check\"\nlocal status_report_shared_dict_name = \"status-report\"\nif not is_http then\n    health_check_shm_name = health_check_shm_name .. \"-stream\"\nend\nlocal created_obj  = {}\nlocal loaded_configuration = {}\nlocal configuration_loaded_time\nlocal watch_ctx\n\n\nlocal _M = {\n    version = 0.3,\n    local_conf = config_local.local_conf,\n    clear_local_cache = config_local.clear_cache,\n}\n\n\nlocal mt = {\n    __index = _M,\n    __tostring = function(self)\n        return \" etcd key: \" .. self.key\n    end\n}\n\n\nlocal get_etcd\ndo\n    local etcd_cli\n\n    function get_etcd()\n        if etcd_cli ~= nil then\n            return etcd_cli\n        end\n\n        local _, err\n        etcd_cli, _, err = etcd_apisix.get_etcd_syncer()\n        return etcd_cli, err\n    end\nend\n\n\nlocal function cancel_watch(http_cli)\n    local res, err = watch_ctx.cli:watchcancel(http_cli)\n    if res == 1 then\n        log.info(\"cancel watch connection success\")\n    else\n        log.error(\"cancel watch failed: \", err)\n    end\nend\n\n\n-- append res to the queue and notify pending watchers\nlocal function produce_res(res, err)\n    insert_tab(watch_ctx.res, {res=res, err=err})\n    for _, sema in pairs(watch_ctx.sema) do\n        sema:post()\n    end\n    table.clear(watch_ctx.sema)\nend\n\n\nlocal function do_run_watch(premature)\n    if premature then\n        return\n    end\n\n    -- the main watcher first start\n    if watch_ctx.started == false then\n        local local_conf, err = config_local.local_conf()\n        if not local_conf then\n            error(\"no local conf: \" .. err)\n        end\n        watch_ctx.prefix = local_conf.etcd.prefix .. \"/\"\n        watch_ctx.timeout = local_conf.etcd.watch_timeout\n\n        watch_ctx.cli, err = get_etcd()\n        if not watch_ctx.cli then\n            error(\"failed to create etcd instance: \" .. string(err))\n        end\n\n        local rev = 0\n        if loaded_configuration then\n            local _, res = next(loaded_configuration)\n            if res then\n                rev = tonumber(res.headers[\"X-Etcd-Index\"])\n                assert(rev > 0, 'invalid res.headers[\"X-Etcd-Index\"]')\n            end\n        end\n\n        if rev == 0 then\n            while true do\n                local res, err = watch_ctx.cli:get(watch_ctx.prefix)\n                if not res then\n                    log.error(\"etcd get: \", err)\n                    ngx_sleep(3)\n                else\n                    rev = tonumber(res.body.header.revision)\n                    break\n                end\n            end\n        end\n\n        watch_ctx.rev = rev + 1\n        watch_ctx.started = true\n\n        log.info(\"main etcd watcher initialised, revision=\", watch_ctx.rev)\n\n        if watch_ctx.wait_init then\n            for _, sema in pairs(watch_ctx.wait_init) do\n                sema:post()\n            end\n            watch_ctx.wait_init = nil\n        end\n    end\n\n    local opts = {}\n    opts.timeout = watch_ctx.timeout or 50 -- second\n    opts.need_cancel = true\n    opts.start_revision = watch_ctx.rev\n\n    -- get latest revision\n    local res, err = watch_ctx.cli:readdir(watch_ctx.prefix .. \"/phantomkey\")\n    if err then\n        log.error(\"failed to get latest revision, err: \", err)\n    end\n    local latest_rev\n    if res and res.body and res.body.header and res.body.header.revision then\n        latest_rev = tonumber(res.body.header.revision)\n    else\n        log.error(\"failed to get latest revision, res: \", json.delay_encode(res))\n    end\n\n    log.info(\"restart watchdir: start_revision=\", opts.start_revision)\n\n    local res_func, err, http_cli = watch_ctx.cli:watchdir(watch_ctx.prefix, opts)\n    if not res_func then\n        log.error(\"watchdir err: \", err)\n        ngx_sleep(3)\n        return\n    end\n\n    ::watch_event::\n    while true do\n        local res, err = res_func()\n        if not res then\n            if err ~= \"closed\" and\n                err ~= \"timeout\" and\n                err ~= \"broken pipe\"\n            then\n                log.error(\"wait watch event: \", err)\n            end\n            if err == \"timeout\" then\n                if latest_rev and watch_ctx.rev < latest_rev + 1 then\n                    watch_ctx.rev = latest_rev + 1\n                    log.info(\"etcd watch timeout, upgrade revision to \", watch_ctx.rev)\n                end\n            end\n            cancel_watch(http_cli)\n            break\n        end\n\n        if res.error then\n            log.error(\"wait watch event: \", inspect(res.error))\n            cancel_watch(http_cli)\n            break\n        end\n\n        --[[\n        when etcd response permission denied, both result.canceled and result.created are true,\n        so we need to check result.canceled first, for example:\n        result = {\n          cancel_reason = \"rpc error: code = PermissionDenied desc = etcdserver: permission denied\",\n          canceled = true,\n          created = true,\n          header = {\n            cluster_id = \"14841639068965178418\",\n            member_id = \"10276657743932975437\",\n            raft_term = \"4\",\n            revision = \"33\"\n          }\n        }\n        --]]\n        if res.result.canceled then\n            log.warn(\"watch canceled by etcd, res: \", inspect(res))\n            if res.result.compact_revision then\n                watch_ctx.rev = tonumber(res.result.compact_revision)\n                log.error(\"etcd compacted, compact_revision=\", watch_ctx.rev)\n                produce_res(nil, \"compacted\")\n            end\n            cancel_watch(http_cli)\n            break\n        end\n\n        if res.result.created then\n            goto watch_event\n        end\n\n        -- cleanup\n        local min_idx = 0\n        for _, idx in pairs(watch_ctx.idx) do\n            if (min_idx == 0) or (idx < min_idx) then\n                min_idx = idx\n            end\n        end\n\n        for i = 1, min_idx - 1 do\n            watch_ctx.res[i] = false\n        end\n\n        if min_idx > 100 then\n            for k, idx in pairs(watch_ctx.idx) do\n                watch_ctx.idx[k] = idx - min_idx + 1\n            end\n            -- trim the res table\n            for i = 1, min_idx - 1 do\n                table.remove(watch_ctx.res, 1)\n            end\n        end\n\n        local rev = tonumber(res.result.header.revision)\n        if rev == nil then\n            log.warn(\"receive a invalid revision header, header: \", inspect(res.result.header))\n            cancel_watch(http_cli)\n            break\n        end\n\n        if rev < watch_ctx.rev then\n            log.error(\"received smaller revision, rev=\", rev, \", watch_ctx.rev=\",\n                      watch_ctx.rev,\". etcd may be restarted. resyncing....\")\n            watch_ctx.rev = rev\n            produce_res(nil, \"restarted\")\n            cancel_watch(http_cli)\n            break\n        end\n        if rev > watch_ctx.rev then\n            watch_ctx.rev = rev + 1\n        end\n        produce_res(res)\n    end\nend\n\n\nlocal function run_watch(premature)\n    local run_watch_th, err = ngx_thread_spawn(do_run_watch, premature)\n    if not run_watch_th then\n        log.error(\"failed to spawn thread do_run_watch: \", err)\n        return\n    end\n\n    local check_worker_th, err = ngx_thread_spawn(function ()\n        while not exiting() do\n            ngx_sleep(0.1)\n        end\n    end)\n    if not check_worker_th then\n        log.error(\"failed to spawn thread check_worker: \", err)\n        return\n    end\n\n    local ok, err = ngx_thread_wait(run_watch_th, check_worker_th)\n    if not ok then\n        log.error(\"run_watch or check_worker thread terminates failed\",\n                        \" restart those threads, error: \", inspect(err))\n    end\n\n    ngx_thread_kill(run_watch_th)\n    ngx_thread_kill(check_worker_th)\n\n    if not exiting() then\n        ngx_timer_at(0, run_watch)\n    else\n        -- notify child watchers\n        produce_res(nil, \"worker exited\")\n    end\nend\n\n\nlocal function init_watch_ctx(key)\n    if not watch_ctx then\n        watch_ctx = {\n            idx = {},\n            res = {},\n            sema = {},\n            wait_init = {},\n            started = false,\n        }\n        ngx_timer_at(0, run_watch)\n    end\n\n    if watch_ctx.started == false then\n        -- wait until the main watcher is started\n        local sema, err = semaphore.new()\n        if not sema then\n            error(err)\n        end\n        watch_ctx.wait_init[key] = sema\n        while true do\n            local ok, err = sema:wait(60)\n            if ok then\n                break\n            end\n            log.error(\"wait main watcher to start, key: \", key, \", err: \", err)\n        end\n    end\nend\n\n\nlocal function getkey(etcd_cli, key)\n    if not etcd_cli then\n        return nil, \"not inited\"\n    end\n\n    local res, err = etcd_cli:readdir(key)\n    if not res then\n        -- log.error(\"failed to get key from etcd: \", err)\n        return nil, err\n    end\n\n    if type(res.body) ~= \"table\" then\n        return nil, \"failed to get key from etcd\"\n    end\n\n    res, err = etcd_apisix.get_format(res, key, true)\n    if not res then\n        return nil, err\n    end\n\n    return res\nend\n\n\nlocal function readdir(etcd_cli, key, formatter)\n    if not etcd_cli then\n        return nil, \"not inited\"\n    end\n\n    local res, err = etcd_cli:readdir(key)\n    if not res then\n        -- log.error(\"failed to get key from etcd: \", err)\n        return nil, err\n    end\n\n    if type(res.body) ~= \"table\" then\n        return nil, \"failed to read etcd dir\"\n    end\n\n    res, err = etcd_apisix.get_format(res, key .. '/', true, formatter)\n    if not res then\n        return nil, err\n    end\n\n    return res\nend\n\n\nlocal function http_waitdir(self, etcd_cli, key, modified_index, timeout)\n    if not watch_ctx.idx[key] then\n        watch_ctx.idx[key] = 1\n    end\n\n    ::iterate_events::\n    for i = watch_ctx.idx[key], #watch_ctx.res do\n        watch_ctx.idx[key] = i + 1\n\n        local item = watch_ctx.res[i]\n        if item == false then\n            goto iterate_events\n        end\n\n        local res, err = item.res, item.err\n        if err then\n            return res, err\n        end\n\n        -- ignore res with revision smaller then self.prev_index\n        if tonumber(res.result.header.revision) > self.prev_index then\n            local res2\n            for _, evt in ipairs(res.result.events) do\n                if core_str.find(evt.kv.key, key) == 1 then\n                    if not res2 then\n                        res2 = tablex.deepcopy(res)\n                        table.clear(res2.result.events)\n                    end\n                    insert_tab(res2.result.events, evt)\n                end\n            end\n\n            if res2 then\n                return res2\n            end\n        end\n    end\n\n    -- if no events, wait via semaphore\n    if not self.watch_sema then\n        local sema, err = semaphore.new()\n        if not sema then\n            error(err)\n        end\n        self.watch_sema = sema\n    end\n\n    watch_ctx.sema[key] = self.watch_sema\n    local ok, err = self.watch_sema:wait(timeout or 60)\n    watch_ctx.sema[key] = nil\n    if ok then\n        goto iterate_events\n    else\n        if err ~= \"timeout\" then\n            log.error(\"wait watch event, key=\", key, \", err: \", err)\n        end\n        return nil, err\n    end\nend\n\n\nlocal function is_bulk_operation(dir_res)\n    if not dir_res or not dir_res.body or not dir_res.body.node then\n        return false\n    end\n    if #dir_res.body.node > 1 then\n        return true\n    end\n    return false\nend\n\nlocal function waitdir(self)\n    local etcd_cli = self.etcd_cli\n    local key = self.key\n    local modified_index = self.prev_index + 1\n    local timeout = self.timeout\n\n    if not etcd_cli then\n        return nil, \"not inited\"\n    end\n\n    local res, err = http_waitdir(self, etcd_cli, key, modified_index, timeout)\n\n    if not res then\n        -- log.error(\"failed to get key from etcd: \", err)\n        return nil, err\n    end\n\n    return etcd_apisix.watch_format(res)\nend\n\n\nlocal function short_key(self, str)\n    return sub_str(str, #self.key + 2)\nend\n\n\nlocal function sync_status_to_shdict(status)\n    local local_conf = config_local.local_conf()\n    if not local_conf.apisix.status then\n        return\n    end\n    if process.type() ~= \"worker\" then\n        return\n    end\n    local status_shdict = ngx.shared[status_report_shared_dict_name]\n    if not status_shdict then\n        return\n    end\n    local id = worker_id()\n    status_shdict:set(id, status)\nend\n\n\nlocal function load_full_data(self, dir_res, headers)\n    local err\n    local changed = false\n\n    if self.single_item then\n        self.values = new_tab(1, 0)\n        self.values_hash = new_tab(0, 1)\n\n        local item = dir_res\n        local data_valid = item.value ~= nil\n\n        if data_valid and self.item_schema then\n            data_valid, err = check_schema(self.item_schema, item.value)\n            if not data_valid then\n                log.error(\"failed to check item data of [\", self.key,\n                          \"] err:\", err, \" ,val: \", json.encode(item.value))\n            end\n        end\n\n        if data_valid and self.checker then\n            data_valid, err = self.checker(item.value)\n            if not data_valid then\n                log.error(\"failed to check item data of [\", self.key,\n                          \"] err:\", err, \" ,val: \", json.delay_encode(item.value))\n            end\n        end\n\n        if data_valid then\n            changed = true\n            insert_tab(self.values, item)\n            self.values_hash[self.key] = #self.values\n\n            item.clean_handlers = {}\n\n            if self.filter then\n                self.filter(item)\n            end\n        end\n\n        self:upgrade_version(item.modifiedIndex)\n\n    else\n        -- here dir_res maybe res.body.node or res.body.list\n        -- we need make values equals to res.body.node.nodes or res.body.list\n        local values = (dir_res and dir_res.nodes) or dir_res\n        if not values then\n            values = {}\n        end\n\n        self.values = new_tab(#values, 0)\n        self.values_hash = new_tab(0, #values)\n\n        for _, item in ipairs(values) do\n            local key = short_key(self, item.key)\n            local data_valid = true\n            if type(item.value) ~= \"table\" then\n                data_valid = false\n                log.error(\"invalid item data of [\", self.key .. \"/\" .. key,\n                          \"], val: \", item.value,\n                          \", it should be an object\")\n            end\n\n            if data_valid and self.item_schema then\n                data_valid, err = check_schema(self.item_schema, item.value)\n                if not data_valid then\n                    log.error(\"failed to check item data of [\", self.key,\n                              \"] err:\", err, \" ,val: \", json.encode(item.value))\n                end\n            end\n\n            if data_valid and self.checker then\n                -- TODO: An opts table should be used\n                -- as different checkers may use different parameters\n                data_valid, err = self.checker(item.value, item.key)\n                if not data_valid then\n                    log.error(\"failed to check item data of [\", self.key,\n                              \"] err:\", err, \" ,val: \", json.delay_encode(item.value))\n                end\n            end\n\n            if data_valid then\n                changed = true\n                insert_tab(self.values, item)\n                self.values_hash[key] = #self.values\n\n                item.value.id = key\n                item.clean_handlers = {}\n\n                if self.filter then\n                    self.filter(item)\n                end\n            end\n\n            self:upgrade_version(item.modifiedIndex)\n        end\n    end\n\n    if headers then\n        self.prev_index = tonumber(headers[\"X-Etcd-Index\"]) or 0\n        self:upgrade_version(headers[\"X-Etcd-Index\"])\n    end\n\n    if changed then\n        self.conf_version = self.conf_version + 1\n    end\n\n    self.need_reload = false\n    sync_status_to_shdict(true)\nend\n\n\nfunction _M.upgrade_version(self, new_ver)\n    new_ver = tonumber(new_ver)\n    if not new_ver then\n        return\n    end\n\n    local pre_index = self.prev_index\n\n    if new_ver <= pre_index then\n        return\n    end\n\n    self.prev_index = new_ver\n    return\nend\n\n\nlocal function sync_data(self)\n    if not self.key then\n        return nil, \"missing 'key' arguments\"\n    end\n\n    init_watch_ctx(self.key)\n\n    if self.need_reload then\n        local res, err = readdir(self.etcd_cli, self.key)\n        if not res then\n            return false, err\n        end\n\n        local dir_res, headers = res.body.list or res.body.node or {}, res.headers\n        log.debug(\"readdir key: \", self.key, \" res: \",\n                  json.delay_encode(dir_res))\n\n        if self.values then\n            for i, val in ipairs(self.values) do\n                config_util.fire_all_clean_handlers(val)\n            end\n\n            self.values = nil\n            self.values_hash = nil\n        end\n\n        load_full_data(self, dir_res, headers)\n\n        return true\n    end\n\n    local dir_res, err = waitdir(self)\n    log.info(\"waitdir key: \", self.key, \" prev_index: \", self.prev_index + 1)\n    if is_bulk_operation(dir_res) then\n        log.info(\"etcd events sent in bulk\")\n    end\n    if not dir_res then\n        if err == \"compacted\" or err == \"restarted\" then\n            self.need_reload = true\n            log.error(\"waitdir [\", self.key, \"] err: \", err,\n                     \", will read the configuration again via readdir\")\n            return false\n        end\n\n        return false, err\n    end\n\n    local res = dir_res.body.node\n    local err_msg = dir_res.body.message\n    if err_msg then\n        return false, err\n    end\n\n    if not res then\n        return false, err\n    end\n\n    local res_copy = res\n    -- waitdir will return [res] even for self.single_item = true\n    for _, res in ipairs(res_copy) do\n        local key\n        local data_valid = true\n        if self.single_item then\n            key = self.key\n        else\n            key = short_key(self, res.key)\n        end\n\n        if res.value and not self.single_item and type(res.value) ~= \"table\" then\n            data_valid = false\n            log.error(\"invalid item data of [\", self.key .. \"/\" .. key,\n                      \"], val: \", res.value,\n                      \", it should be an object\")\n        end\n\n        if data_valid and res.value and self.item_schema then\n            data_valid, err = check_schema(self.item_schema, res.value)\n            if not data_valid then\n                log.error(\"failed to check item data of [\", self.key,\n                          \"] err:\", err, \" ,val: \", json.encode(res.value))\n            end\n        end\n\n        if data_valid and res.value and self.checker then\n            data_valid, err = self.checker(res.value, res.key)\n            if not data_valid then\n                log.error(\"failed to check item data of [\", self.key,\n                          \"] err:\", err, \" ,val: \", json.delay_encode(res.value))\n            end\n        end\n\n        -- the modifiedIndex tracking should be updated regardless of the validity of the config\n        self:upgrade_version(res.modifiedIndex)\n\n        if not data_valid then\n            -- do not update the config cache when the data is invalid\n            -- invalid data should only cancel this config item update, not discard\n            -- the remaining events, use continue instead of loop break and return\n            goto CONTINUE\n        end\n\n        if res.dir then\n            if res.value then\n                return false, \"todo: support for parsing `dir` response \"\n                                .. \"structures. \" .. json.encode(res)\n            end\n            return false\n        end\n\n        local pre_index = self.values_hash[key]\n        if pre_index then\n            local pre_val = self.values[pre_index]\n            if pre_val then\n                config_util.fire_all_clean_handlers(pre_val)\n            end\n\n            if res.value then\n                if not self.single_item then\n                    res.value.id = key\n                end\n\n                self.values[pre_index] = res\n                res.clean_handlers = {}\n                log.info(\"update data by key: \", key)\n\n            else\n                self.sync_times = self.sync_times + 1\n                self.values[pre_index] = false\n                self.values_hash[key] = nil\n                log.info(\"delete data by key: \", key)\n            end\n\n        elseif res.value then\n            res.clean_handlers = {}\n            insert_tab(self.values, res)\n            self.values_hash[key] = #self.values\n            if not self.single_item then\n                res.value.id = key\n            end\n\n            log.info(\"insert data by key: \", key)\n        end\n\n        -- avoid space waste\n        if self.sync_times > 100 then\n            local values_original = table.clone(self.values)\n            table.clear(self.values)\n\n            for i = 1, #values_original do\n                local val = values_original[i]\n                if val then\n                    table.insert(self.values, val)\n                end\n            end\n\n            table.clear(self.values_hash)\n            log.info(\"clear stale data in `values_hash` for key: \", key)\n\n            for i = 1, #self.values do\n                key = short_key(self, self.values[i].key)\n                self.values_hash[key] = i\n            end\n\n            self.sync_times = 0\n        end\n\n        -- /plugins' filter need to known self.values when it is called\n        -- so the filter should be called after self.values set.\n        if self.filter then\n            self.filter(res)\n        end\n\n        self.conf_version = self.conf_version + 1\n\n        ::CONTINUE::\n    end\n\n    return self.values\nend\n\n\nfunction _M.get(self, key)\n    if not self.values_hash then\n        return\n    end\n\n    local arr_idx = self.values_hash[tostring(key)]\n    if not arr_idx then\n        return nil\n    end\n\n    return self.values[arr_idx]\nend\n\n\nfunction _M.getkey(self, key)\n    if not self.running then\n        return nil, \"stopped\"\n    end\n\n    local local_conf = config_local.local_conf()\n    if local_conf and local_conf.etcd and local_conf.etcd.prefix then\n        key = local_conf.etcd.prefix .. key\n    end\n\n    return getkey(self.etcd_cli, key)\nend\n\n\nlocal function _automatic_fetch(premature, self)\n    if premature then\n        return\n    end\n\n    if not (health_check.conf and health_check.conf.shm_name) then\n        -- used for worker processes to synchronize configuration\n        local _, err = health_check.init({\n            shm_name = health_check_shm_name,\n            fail_timeout = self.health_check_timeout,\n            max_fails = 3,\n            retry = true,\n        })\n        if err then\n            log.warn(\"fail to create health_check: \" .. err)\n        end\n    end\n\n    local i = 0\n    while not exiting() and self.running and i <= 32 do\n        i = i + 1\n\n        local ok, err = xpcall(function()\n            if not self.etcd_cli then\n                local etcd_cli, err = get_etcd()\n                if not etcd_cli then\n                    error(\"failed to create etcd instance for key [\"\n                          .. self.key .. \"]: \" .. (err or \"unknown\"))\n                end\n                self.etcd_cli = etcd_cli\n            end\n\n            local ok, err = sync_data(self)\n            if err then\n                if core_str.find(err, err_etcd_grpc_engine_timeout) or\n                   core_str.find(err, err_etcd_grpc_ngx_timeout)\n                then\n                    err = \"timeout\"\n                end\n\n                if core_str.find(err, err_etcd_unhealthy_all) then\n                    local reconnected = false\n                    while err and not reconnected and i <= 32 do\n                        local backoff_duration, backoff_factor, backoff_step = 1, 2, 6\n                        for _ = 1, backoff_step do\n                            i = i + 1\n                            ngx_sleep(backoff_duration)\n                            _, err = sync_data(self)\n                            if not err or not core_str.find(err, err_etcd_unhealthy_all) then\n                                log.warn(\"reconnected to etcd\")\n                                reconnected = true\n                                break\n                            end\n                            backoff_duration = backoff_duration * backoff_factor\n                            log.error(\"no healthy etcd endpoint available, next retry after \"\n                                       .. backoff_duration .. \"s\")\n                        end\n                    end\n                elseif err == \"worker exited\" then\n                    log.info(\"worker exited.\")\n                    return\n                elseif err ~= \"timeout\" and err ~= \"Key not found\"\n                    and self.last_err ~= err then\n                    log.error(\"failed to fetch data from etcd: \", err, \", \",\n                              tostring(self))\n                end\n\n                if err ~= self.last_err then\n                    self.last_err = err\n                    self.last_err_time = ngx_time()\n                elseif self.last_err then\n                    if ngx_time() - self.last_err_time >= 30 then\n                        self.last_err = nil\n                    end\n                end\n\n                -- etcd watch timeout is an expected error, so there is no need for resync_delay\n                if err ~= \"timeout\" then\n                    ngx_sleep(self.resync_delay + rand() * 0.5 * self.resync_delay)\n                end\n            elseif not ok then\n                -- no error. reentry the sync with different state\n                ngx_sleep(0.05)\n            end\n\n        end, debug.traceback)\n\n        if not ok then\n            log.error(\"failed to fetch data from etcd: \", err, \", \",\n                      tostring(self))\n            ngx_sleep(self.resync_delay + rand() * 0.5 * self.resync_delay)\n            break\n        end\n    end\n\n    if not exiting() and self.running then\n        ngx_timer_at(0, _automatic_fetch, self)\n    end\nend\n\n-- for test\n_M.test_sync_data = sync_data\n_M.test_automatic_fetch = _automatic_fetch\nfunction _M.inject_sync_data(f)\n    sync_data = f\nend\n\n\n---\n-- Create a new connection to communicate with the control plane.\n-- This function should be used in the `init_worker_by_lua` phase.\n--\n-- @function core.config.new\n-- @tparam string key etcd directory to be monitored, e.g. \"/routes\".\n-- @tparam table opts Parameters related to the etcd client connection.\n-- The keys in `opts` are as follows:\n--  * automatic: whether to get the latest etcd data automatically\n--  * item_schema: the jsonschema that checks the value of each item under the **key** directory\n--  * filter: the custom function to filter the value of each item under the **key** directory\n--  * timeout: the timeout for watch operation, default is 30s\n--  * single_item: whether only one item under the **key** directory\n--  * checker: the custom function to check the value of each item under the **key** directory\n-- @treturn table The etcd client connection.\n-- @usage\n-- local plugins_conf, err = core.config.new(\"/custom_dir\", {\n--    automatic = true,\n--    filter = function(item)\n--        -- called once before reload for sync data from admin\n--    end,\n--})\nfunction _M.new(key, opts)\n    local local_conf, err = config_local.local_conf()\n    if not local_conf then\n        return nil, err\n    end\n\n    local etcd_conf = local_conf.etcd\n    local prefix = etcd_conf.prefix\n    local resync_delay = etcd_conf.resync_delay\n    if not resync_delay or resync_delay < 0 then\n        resync_delay = 5\n    end\n    local health_check_timeout = etcd_conf.health_check_timeout\n    if not health_check_timeout or health_check_timeout < 0 then\n        health_check_timeout = 10\n    end\n    local automatic = opts and opts.automatic\n    local item_schema = opts and opts.item_schema\n    local filter_fun = opts and opts.filter\n    local timeout = opts and opts.timeout\n    local single_item = opts and opts.single_item\n    local checker = opts and opts.checker\n\n    local obj = setmetatable({\n        etcd_cli = nil,\n        key = key and prefix .. key,\n        automatic = automatic,\n        item_schema = item_schema,\n        checker = checker,\n        sync_times = 0,\n        running = true,\n        conf_version = 0,\n        values = {},\n        need_reload = true,\n        watching_stream = nil,\n        routes_hash = nil,\n        prev_index = 0,\n        last_err = nil,\n        last_err_time = nil,\n        resync_delay = resync_delay,\n        health_check_timeout = health_check_timeout,\n        timeout = timeout,\n        single_item = single_item,\n        filter = filter_fun,\n    }, mt)\n\n    if automatic then\n        if not key then\n            return nil, \"missing `key` argument\"\n        end\n\n        if loaded_configuration[key] then\n            local res = loaded_configuration[key]\n            loaded_configuration[key] = nil -- tried to load\n\n            log.notice(\"use loaded configuration \", key)\n\n            local dir_res, headers = res.body, res.headers\n            load_full_data(obj, dir_res, headers)\n        end\n\n        ngx_timer_at(0, _automatic_fetch, obj)\n\n    else\n        local etcd_cli, err = get_etcd()\n        if not etcd_cli then\n            return nil, \"failed to start an etcd instance: \" .. err\n        end\n        obj.etcd_cli = etcd_cli\n    end\n\n    if key then\n        created_obj[key] = obj\n    end\n\n    return obj\nend\n\n\nfunction _M.close(self)\n    self.running = false\nend\n\n\nfunction _M.fetch_created_obj(key)\n    return created_obj[key]\nend\n\n\nfunction _M.server_version(self)\n    if not self.running then\n        return nil, \"stopped\"\n    end\n\n    local res, err = etcd_apisix.server_version()\n    if not res then\n        return nil, err\n    end\n\n    return res.body\nend\n\n\nlocal function create_formatter(prefix)\n    return function (res)\n        res.body.nodes = {}\n\n        local dirs\n        if is_http then\n            dirs = constants.HTTP_ETCD_DIRECTORY\n        else\n            dirs = constants.STREAM_ETCD_DIRECTORY\n        end\n\n        local curr_dir_data\n        local curr_key\n        for _, item in ipairs(res.body.kvs) do\n            if curr_dir_data then\n                if core_str.has_prefix(item.key, curr_key) then\n                    table.insert(curr_dir_data, etcd_apisix.kvs_to_node(item))\n                    goto CONTINUE\n                end\n\n                curr_dir_data = nil\n            end\n\n            local key = sub_str(item.key, #prefix + 1)\n            if dirs[key] then\n                -- single item\n                loaded_configuration[key] = {\n                    body = etcd_apisix.kvs_to_node(item),\n                    headers = res.headers,\n                }\n            else\n                local key = sub_str(item.key, #prefix + 1, #item.key - 1)\n                -- ensure the same key hasn't been handled as single item\n                if dirs[key] and not loaded_configuration[key] then\n                    loaded_configuration[key] = {\n                        body = {\n                            nodes = {},\n                        },\n                        headers = res.headers,\n                    }\n                    curr_dir_data = loaded_configuration[key].body.nodes\n                    curr_key = item.key\n                end\n            end\n\n            ::CONTINUE::\n        end\n\n        return res\n    end\nend\n\n\nlocal function init_loaded_configuration()\n    loaded_configuration = {}\n    local etcd_cli, prefix, err = etcd_apisix.new_without_proxy()\n    if not etcd_cli then\n        return \"failed to start a etcd instance: \" .. err\n    end\n\n    local res, err = readdir(etcd_cli, prefix, create_formatter(prefix))\n    if not res then\n        return err\n    end\n\n    configuration_loaded_time = ngx_time()\nend\n\n\nfunction _M.init()\n    local local_conf, err = config_local.local_conf()\n    if not local_conf then\n        return nil, err\n    end\n\n    if table.try_read_attr(local_conf, \"apisix\", \"disable_sync_configuration_during_start\") then\n        return true\n    end\n\n    local err = init_loaded_configuration()\n    if err then\n        return nil, err\n    end\n\n    return true\nend\n\n\nfunction _M.init_worker()\n    sync_status_to_shdict(false)\n    local local_conf, err = config_local.local_conf()\n    if not local_conf then\n        return nil, err\n    end\n\n    local threshold = table.try_read_attr(local_conf, \"apisix\",\n                                    \"worker_startup_time_threshold\") or 60\n    -- if the startup time of a worker differs significantly from that of the master process,\n    -- we consider it to have restarted, and at this point,\n    -- it is necessary to reload the full configuration from etcd.\n    if configuration_loaded_time and ngx_time() - configuration_loaded_time > threshold then\n        log.warn(\"master process has been running for a long time, \",\n                     \"reloading the full configuration from etcd for this new worker\")\n        local err = init_loaded_configuration()\n        if err then\n            return nil, err\n        end\n    end\n\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/config_local.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Get configuration information.\n--\n-- @module core.config_local\n\nlocal file   = require(\"apisix.cli.file\")\n\n\nlocal _M = {}\n\n\nlocal config_data\n\n\nfunction _M.clear_cache()\n    config_data = nil\nend\n\n---\n-- Get the local config info.\n-- The configuration information consists of two parts, user-defined configuration in\n-- `conf/config.yaml` and default configuration in `conf/config-default.yaml`. The configuration\n-- of the same name present in `conf/config.yaml` will overwrite `conf/config-default.yaml`.\n-- The final full configuration is `conf/config.yaml` and the default configuration in\n-- `conf/config-default.yaml` that is not overwritten.\n--\n-- @function core.config_local.local_conf\n-- @treturn table The configuration information.\n-- @usage\n-- -- Given a config item in `conf/config.yaml`:\n-- --\n-- -- apisix:\n-- --   ssl:\n-- --     fallback_sni: \"a.test2.com\"\n-- --\n-- -- you can get the value of `fallback_sni` by:\n-- local local_conf = core.config.local_conf()\n-- local fallback_sni = core.table.try_read_attr(\n--                        local_conf, \"apisix\", \"ssl\", \"fallback_sni\") -- \"a.test2.com\"\nfunction _M.local_conf(force)\n    if not force and config_data then\n        return config_data\n    end\n\n    local default_conf, err = file.read_yaml_conf()\n    if not default_conf then\n        return nil, err\n    end\n\n    config_data = default_conf\n    return config_data\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/config_util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Collection of util functions\n--\n-- @module core.config_util\n\nlocal core_tab = require(\"apisix.core.table\")\nlocal log = require(\"apisix.core.log\")\nlocal str_byte = string.byte\nlocal str_char = string.char\nlocal ipairs = ipairs\nlocal setmetatable = setmetatable\nlocal tostring = tostring\nlocal type = type\nlocal ngx_re = require('ngx.re')\nlocal _M = {}\n\n\nlocal function _iterate_values(self, tab)\n    while true do\n        self.idx = self.idx + 1\n        local v = tab[self.idx]\n        if type(v) == \"table\" then\n            return self.idx, v\n        end\n        if v == nil then\n            return nil, nil\n        end\n        -- skip the tombstone\n    end\nend\n\n\nfunction _M.iterate_values(tab)\n    local iter = setmetatable({idx = 0}, {__call = _iterate_values})\n    return iter, tab, 0\nend\n\n\n-- Add a clean handler to a runtime configuration item.\n-- The clean handler will be called when the item is deleted from configuration\n-- or cancelled. Note that Nginx worker exit doesn't trigger the clean handler.\n-- Return an index so that we can cancel it later.\nfunction _M.add_clean_handler(item, func)\n    if not item.clean_handlers then\n        return nil, \"clean handlers for the item are nil\"\n    end\n\n    if not item.clean_handlers._id then\n        item.clean_handlers._id = 1\n    end\n\n    local id = item.clean_handlers._id\n    item.clean_handlers._id = item.clean_handlers._id + 1\n    core_tab.insert(item.clean_handlers, {f = func, id = id})\n    return id\nend\n\n\n-- cancel a clean handler added by add_clean_handler.\n-- If `fire` is true, call the clean handler.\nfunction _M.cancel_clean_handler(item, idx, fire)\n    local pos, f\n    -- the number of pending clean handler is small so we can cancel them in O(n)\n    for i, clean_handler in ipairs(item.clean_handlers) do\n        if clean_handler.id == idx then\n            pos = i\n            f = clean_handler.f\n            break\n        end\n    end\n\n    if not pos then\n        log.error(\"failed to find clean_handler with idx \", idx)\n        return\n    end\n\n    core_tab.remove(item.clean_handlers, pos)\n    if not fire then\n        return\n    end\n\n    if f then\n        f(item)\n    else\n        log.error(\"The function used to clear the health checker is nil, please check\")\n    end\nend\n\n\n-- fire all clean handlers added by add_clean_handler.\nfunction _M.fire_all_clean_handlers(item)\n    -- When the key is deleted, the item will be set to false.\n    if not item then\n        return\n    end\n    if not item.clean_handlers then\n        return\n    end\n\n    for _, clean_handler in ipairs(item.clean_handlers) do\n        clean_handler.f(item)\n    end\n\n    item.clean_handlers = {}\nend\n\n\n---\n-- Convert different time units to seconds as time units.\n-- Time intervals can be specified in milliseconds, seconds, minutes, hours, days and so on,\n-- using the following suffixes:\n-- ms\tmilliseconds\n-- s\tseconds\n-- m\tminutes\n-- h\thours\n-- d\tdays\n-- w\tweeks\n-- M\tmonths, 30 days\n-- y\tyears, 365 days\n-- Multiple units can be combined in a single value by specifying them in the order from the most\n-- to the least significant, and optionally separated by whitespace.\n-- A value without a suffix means seconds.\n--\n-- @function core.config_util.parse_time_unit\n-- @tparam number|string s Strings with time units, e.g. \"60m\".\n-- @treturn number Number of seconds after conversion\n-- @usage\n-- local seconds = core.config_util.parse_time_unit(\"60m\") -- 3600\nfunction _M.parse_time_unit(s)\n    local typ = type(s)\n    if typ == \"number\" then\n        return s\n    end\n\n    if typ ~= \"string\" or #s == 0 then\n        return nil, \"invalid data: \" .. tostring(s)\n    end\n\n    local size = 0\n    local size_in_unit = 0\n    local step = 60 * 60 * 24 * 365\n    local with_ms = false\n    for i = 1, #s do\n        local scale\n        local unit = str_byte(s, i)\n        if unit == 121 then -- y\n            scale = 60 * 60 * 24 * 365\n        elseif unit == 77 then -- M\n            scale = 60 * 60 * 24 * 30\n        elseif unit == 119 then -- w\n            scale = 60 * 60 * 24 * 7\n        elseif unit == 100 then -- d\n            scale = 60 * 60 * 24\n        elseif unit == 104 then -- h\n            scale = 60 * 60\n        elseif unit == 109 then -- m\n            unit = str_byte(s, i + 1)\n            if unit == 115 then -- ms\n                size = size * 1000\n                with_ms = true\n                step = 0\n                break\n            end\n\n            scale = 60\n\n        elseif unit == 115 then -- s\n            scale = 1\n        elseif 48 <= unit and unit <= 57 then\n            size_in_unit = size_in_unit * 10 + unit - 48\n        elseif unit ~= 32 then\n            return nil, \"invalid data: \" .. str_char(unit)\n        end\n\n        if scale ~= nil then\n            if scale > step then\n                return nil, \"unexpected unit: \" .. str_char(unit)\n            end\n\n            step = scale\n            size = size + scale * size_in_unit\n            size_in_unit = 0\n        end\n    end\n\n    if size_in_unit > 0 then\n        if step == 1 then\n            return nil, \"specific unit conflicts with the default unit second\"\n        end\n\n        size = size + size_in_unit\n    end\n\n    if with_ms then\n        size = size / 1000\n    end\n\n    return size\nend\n\n\nfunction _M.parse_path(resource_full_path)\n    local resource_path_parts = ngx_re.split(resource_full_path, \"#\")\n    local resource_path = resource_path_parts[1] or resource_full_path\n    local resource_sub_path = resource_path_parts[2] or \"\"\n    return resource_path, resource_sub_path\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/config_xds.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Get configuration form ngx.shared.DICT\n--\n-- @module core.config_xds\n\nlocal config_local      = require(\"apisix.core.config_local\")\nlocal config_util       = require(\"apisix.core.config_util\")\nlocal string            = require(\"apisix.core.string\")\nlocal log               = require(\"apisix.core.log\")\nlocal json              = require(\"apisix.core.json\")\nlocal os                = require(\"apisix.core.os\")\nlocal ngx_sleep         = require(\"apisix.core.utils\").sleep\nlocal check_schema      = require(\"apisix.core.schema\").check\nlocal new_tab           = require(\"table.new\")\nlocal table             = table\nlocal insert_tab        = table.insert\nlocal error             = error\nlocal pcall             = pcall\nlocal tostring          = tostring\nlocal setmetatable      = setmetatable\nlocal io                = io\nlocal io_open           = io.open\nlocal io_close          = io.close\nlocal package           = package\nlocal ipairs            = ipairs\nlocal type              = type\nlocal sub_str           = string.sub\nlocal ffi               = require (\"ffi\")\nlocal C                 = ffi.C\nlocal config            = ngx.shared[\"xds-config\"]\nlocal conf_ver          = ngx.shared[\"xds-config-version\"]\nlocal is_http           = ngx.config.subsystem == \"http\"\nlocal ngx_re_match      = ngx.re.match\nlocal ngx_re_gmatch     = ngx.re.gmatch\nlocal ngx_timer_every   = ngx.timer.every\nlocal ngx_timer_at      = ngx.timer.at\nlocal exiting           = ngx.worker.exiting\nlocal ngx_time          = ngx.time\n\nlocal xds_lib_name      = \"libxds.so\"\n\nlocal process\nif is_http then\n    process = require(\"ngx.process\")\nend\n\nlocal shdict_udata_to_zone\nif not pcall(function() return C.ngx_http_lua_ffi_shdict_udata_to_zone end) then\n    shdict_udata_to_zone = C.ngx_meta_lua_ffi_shdict_udata_to_zone\nelse\n    shdict_udata_to_zone = C.ngx_http_lua_ffi_shdict_udata_to_zone\nend\n\n\nffi.cdef[[\nextern void initial(void* config_zone, void* version_zone);\n]]\n\nlocal created_obj  = {}\n\nlocal _M = {\n    version = 0.1,\n    local_conf = config_local.local_conf,\n}\n\n\nlocal mt = {\n    __index = _M,\n    __tostring = function(self)\n        return \" xds key: \" .. self.key\n    end\n}\n\n\n-- todo: refactor this function in chash.lua and radixtree.lua\nlocal function load_shared_lib(lib_name)\n    local cpath = package.cpath\n    local tried_paths = new_tab(32, 0)\n    local i = 1\n\n    local iter, err = ngx_re_gmatch(cpath, \"[^;]+\", \"jo\")\n    if not iter then\n        error(\"failed to gmatch: \" .. err)\n    end\n\n    while true do\n        local it = iter()\n        local fpath\n        fpath, err = ngx_re_match(it[0], \"(.*/)\",  \"jo\")\n        if err then\n            error(\"failed to match: \" .. err)\n        end\n        local spath = fpath[0] .. lib_name\n\n        local f = io_open(spath)\n        if f ~= nil then\n            io_close(f)\n            return ffi.load(spath)\n        end\n        tried_paths[i] = spath\n        i = i + 1\n\n        if not it then\n            break\n        end\n    end\n\n    return nil, tried_paths\nend\n\n\nlocal function load_libxds(lib_name)\n    local xdsagent, tried_paths = load_shared_lib(lib_name)\n\n    if not xdsagent then\n        tried_paths[#tried_paths + 1] = 'tried above paths but can not load ' .. lib_name\n        error(\"can not load xds library, tried paths: \" ..\n              table.concat(tried_paths, '\\r\\n', 1, #tried_paths))\n    end\n\n    local config_zone = shdict_udata_to_zone(config[1])\n    local config_shd_cdata = ffi.cast(\"void*\", config_zone)\n\n    local conf_ver_zone = shdict_udata_to_zone(conf_ver[1])\n    local conf_ver_shd_cdata = ffi.cast(\"void*\", conf_ver_zone)\n\n    xdsagent.initial(config_shd_cdata, conf_ver_shd_cdata)\nend\n\n\nlocal latest_version\nlocal function sync_data(self)\n    if self.conf_version == latest_version then\n        return true\n    end\n\n    if self.values then\n        for _, val in ipairs(self.values) do\n            config_util.fire_all_clean_handlers(val)\n        end\n        self.values = nil\n        self.values_hash = nil\n    end\n\n    local keys = config:get_keys(0)\n\n    if not keys or #keys <= 0 then\n        -- xds did not write any data to shdict\n        return false, \"no keys\"\n    end\n\n    self.values = new_tab(#keys, 0)\n    self.values_hash = new_tab(0, #keys)\n\n    for _, key in ipairs(keys) do\n        if string.has_prefix(key, self.key) then\n            local data_valid = true\n            local conf_str = config:get(key, 0)\n            local conf, err = json.decode(conf_str)\n            if not conf then\n                data_valid = false\n                log.error(\"decode the conf of [\", key, \"] failed, err: \", err,\n                          \", conf_str: \", conf_str)\n            end\n\n            if not self.single_item and type(conf) ~= \"table\" then\n                data_valid = false\n                log.error(\"invalid conf of [\", key, \"], conf: \", conf,\n                          \", it should be an object\")\n            end\n\n            if data_valid and self.item_schema then\n                local ok, err = check_schema(self.item_schema, conf)\n                if not ok then\n                    data_valid = false\n                    log.error(\"failed to check the conf of [\", key, \"] err:\", err)\n                end\n            end\n\n            if data_valid and self.checker then\n                local ok, err = self.checker(conf)\n                if not ok then\n                    data_valid = false\n                    log.error(\"failed to check the conf of [\", key, \"] err:\", err)\n                end\n            end\n\n            if data_valid then\n                if not conf.id then\n                    conf.id = sub_str(key, #self.key + 2, #key + 1)\n                    log.warn(\"the id of [\", key, \"] is nil, use the id: \", conf.id)\n                end\n\n                local conf_item = {value = conf, modifiedIndex = latest_version,\n                                   key = key}\n                insert_tab(self.values, conf_item)\n                self.values_hash[conf.id] = #self.values\n                conf_item.clean_handlers = {}\n\n                if self.filter then\n                    self.filter(conf_item)\n                end\n            end\n        end\n    end\n\n    self.conf_version = latest_version\n    return true\nend\n\n\nlocal function _automatic_fetch(premature, self)\n    if premature then\n        return\n    end\n\n    local i = 0\n    while not exiting() and self.running and i <= 32 do\n        i = i + 1\n        local ok, ok2, err = pcall(sync_data, self)\n        if not ok then\n            err = ok2\n            log.error(\"failed to fetch data from xds: \",\n                      err, \", \", tostring(self))\n            ngx_sleep(3)\n            break\n        elseif not ok2 and err then\n            -- todo: handler other error\n            if err ~= \"wait for more time\" and err ~= \"no keys\" and self.last_err ~= err then\n                log.error(\"failed to fetch data from xds, \", err, \", \", tostring(self))\n            end\n\n            if err ~= self.last_err then\n                self.last_err = err\n                self.last_err_time = ngx_time()\n            else\n                if ngx_time() - self.last_err_time >= 30 then\n                    self.last_err = nil\n                end\n            end\n            ngx_sleep(0.5)\n        elseif not ok2 then\n            ngx_sleep(0.05)\n        else\n            ngx_sleep(0.1)\n        end\n    end\n\n    if not exiting() and self.running then\n        ngx_timer_at(0, _automatic_fetch, self)\n    end\nend\n\n\nlocal function fetch_version(premature)\n    if premature then\n        return\n    end\n\n    local version = conf_ver:get(\"version\")\n\n    if not version then\n        return\n    end\n\n    if version ~= latest_version then\n        latest_version = version\n    end\nend\n\n\nfunction _M.new(key, opts)\n    local automatic = opts and opts.automatic\n    local item_schema = opts and opts.item_schema\n    local filter_fun = opts and opts.filter\n    local single_item = opts and opts.single_item\n    local checker = opts and opts.checker\n\n\n    local obj = setmetatable({\n        automatic = automatic,\n        item_schema = item_schema,\n        checker = checker,\n        sync_times = 0,\n        running = true,\n        conf_version = 0,\n        values = nil,\n        routes_hash = nil,\n        prev_index = nil,\n        last_err = nil,\n        last_err_time = nil,\n        key = key,\n        single_item = single_item,\n        filter = filter_fun,\n    }, mt)\n\n    if automatic then\n        if not key then\n            return nil, \"missing `key` argument\"\n        end\n\n        -- blocking until xds completes initial configuration\n        while true do\n            os.usleep(1000)\n            fetch_version()\n            if latest_version then\n                break\n            end\n        end\n\n        local ok, ok2, err = pcall(sync_data, obj)\n        if not ok then\n            err = ok2\n        end\n\n        if err then\n            log.error(\"failed to fetch data from xds \",\n                      err, \", \", key)\n        end\n\n        ngx_timer_at(0, _automatic_fetch, obj)\n    end\n\n    if key then\n        created_obj[key] = obj\n    end\n\n    return obj\nend\n\n\nfunction _M.get(self, key)\n    if not self.values_hash then\n        return\n    end\n\n    local arr_idx = self.values_hash[tostring(key)]\n    if not arr_idx then\n        return nil\n    end\n\n    return self.values[arr_idx]\nend\n\n\nfunction _M.fetch_created_obj(key)\n    return created_obj[key]\nend\n\n\nfunction _M.init_worker()\n    if process.type() == \"privileged agent\" then\n        load_libxds(xds_lib_name)\n    end\n\n    ngx_timer_every(1, fetch_version)\n\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/config_yaml.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Get configuration information in Stand-alone mode.\n--\n-- @module core.config_yaml\n\nlocal config_local = require(\"apisix.core.config_local\")\nlocal config_util  = require(\"apisix.core.config_util\")\nlocal yaml         = require(\"lyaml\")\nlocal log          = require(\"apisix.core.log\")\nlocal json         = require(\"apisix.core.json\")\nlocal new_tab      = require(\"table.new\")\nlocal check_schema = require(\"apisix.core.schema\").check\nlocal profile      = require(\"apisix.core.profile\")\nlocal lfs          = require(\"lfs\")\nlocal file         = require(\"apisix.cli.file\")\nlocal exiting      = ngx.worker.exiting\nlocal insert_tab   = table.insert\nlocal type         = type\nlocal ipairs       = ipairs\nlocal setmetatable = setmetatable\nlocal ngx_sleep    = require(\"apisix.core.utils\").sleep\nlocal ngx_timer_at = ngx.timer.at\nlocal ngx_time     = ngx.time\nlocal ngx_shared   = ngx.shared\nlocal sub_str      = string.sub\nlocal tostring     = tostring\nlocal pcall        = pcall\nlocal io           = io\nlocal ngx          = ngx\nlocal re_find      = ngx.re.find\nlocal process      = require(\"ngx.process\")\nlocal worker_id    = ngx.worker.id\nlocal created_obj  = {}\nlocal shared_dict\nlocal status_report_shared_dict_name = \"status-report\"\n\nlocal _M = {\n    version = 0.2,\n    local_conf = config_local.local_conf,\n    clear_local_cache = config_local.clear_cache,\n\n    -- yaml or json\n    file_type = \"yaml\",\n\n    ERR_NO_SHARED_DICT = \"failed prepare standalone config shared dict, this will degrade \"..\n                    \"to event broadcasting, and if a worker crashes, the configuration \"..\n                    \"cannot be restored from other workers and shared dict\"\n}\n\n\nlocal mt = {\n    __index = _M,\n    __tostring = function(self)\n        return \"apisix.yaml key: \" .. (self.key or \"\")\n    end\n}\n\nlocal apisix_yaml\nlocal apisix_yaml_mtime\n\nlocal config_yaml = {\n    path = profile:yaml_path(\"apisix\"),\n    type = \"yaml\",\n    parse = function(self)\n        local f, err = io.open(self.path, \"r\")\n        if not f then\n            return nil, \"failed to open file \" .. self.path .. \" : \" .. err\n        end\n\n        f:seek('end', -10)\n        local end_flag = f:read(\"*a\")\n        local found_end_flag = re_find(end_flag, [[#END\\s*$]], \"jo\")\n\n        if not found_end_flag then\n            f:close()\n            return nil, \"missing valid end flag in file \" .. self.path\n        end\n\n        f:seek('set')\n        local raw_config = f:read(\"*a\")\n        f:close()\n\n        return yaml.load(raw_config), nil\n    end\n}\n\nlocal config_json = {\n    -- `-5` to remove the \"yaml\" suffix\n    path = config_yaml.path:sub(1, -5) .. \"json\",\n    type = \"json\",\n    parse = function(self)\n        local f, err = io.open(self.path, \"r\")\n        if not f then\n            return nil, \"failed to open file \" .. self.path .. \" : \" .. err\n        end\n        local raw_config = f:read(\"*a\")\n        f:close()\n\n        local config, err = json.decode(raw_config)\n        if err then\n            return nil, \"failed to decode json: \" .. err\n        end\n        return config, nil\n    end\n}\n\nlocal config_file_table = {\n    yaml = config_yaml,\n    json = config_json\n}\n\n\nlocal config_file = setmetatable({}, {\n    __index = function(_, key)\n        return config_file_table[_M.file_type][key]\n    end\n})\n\n\nlocal function sync_status_to_shdict(status)\n    if process.type() ~= \"worker\" then\n        return\n    end\n    local status_shdict = ngx.shared[status_report_shared_dict_name]\n    if not status_shdict then\n        return\n    end\n    local id = worker_id()\n    log.info(\"sync status to shared dict, id: \", id, \" status: \", status)\n    status_shdict:set(id, status)\nend\n\n\nlocal function update_config(table, conf_version)\n    if not table then\n        log.error(\"failed update config: empty table\")\n        return\n    end\n\n    local ok, err = file.resolve_conf_var(table)\n    if not ok then\n        log.error(\"failed to resolve variables:\" .. err)\n        return\n    end\n\n    apisix_yaml = table\n    sync_status_to_shdict(true)\n    apisix_yaml_mtime = conf_version\nend\n_M._update_config = update_config\n\n\nlocal function is_use_admin_api()\n    local local_conf, _ = config_local.local_conf()\n    return local_conf and local_conf.apisix and local_conf.apisix.enable_admin\nend\n\n\nlocal function read_apisix_config(premature, pre_mtime)\n    if premature then\n        return\n    end\n    local attributes, err = lfs.attributes(config_file.path)\n    if not attributes then\n        log.error(\"failed to fetch \", config_file.path, \" attributes: \", err)\n        return\n    end\n\n    local last_modification_time = attributes.modification\n    if apisix_yaml_mtime == last_modification_time then\n        return\n    end\n\n    local config_new, err = config_file:parse()\n    if err then\n        log.error(\"failed to parse the content of file \", config_file.path, \": \", err)\n        return\n    end\n\n    update_config(config_new, last_modification_time)\n\n    log.warn(\"config file \", config_file.path, \" reloaded.\")\nend\n\n\nlocal function sync_data(self)\n    if not self.key then\n        return nil, \"missing 'key' arguments\"\n    end\n\n    local conf_version\n    if is_use_admin_api() then\n        conf_version = apisix_yaml[self.conf_version_key] or 0\n    else\n        if not apisix_yaml_mtime then\n            log.warn(\"wait for more time\")\n            return nil, \"failed to read local file \" .. config_file.path\n        end\n        conf_version = apisix_yaml_mtime\n    end\n\n    if not conf_version or conf_version == self.conf_version then\n        return true\n    end\n\n    local items = apisix_yaml[self.key]\n    if not items then\n        self.values = new_tab(8, 0)\n        self.values_hash = new_tab(0, 8)\n        self.conf_version = conf_version\n        return true\n    end\n\n    if self.values and #self.values > 0 then\n        if is_use_admin_api() then\n            -- filter self.values to retain only those whose IDs exist in the new items list.\n            local exist_values = new_tab(8, 0)\n            self.values_hash = new_tab(0, 8)\n\n            local exist_items = {}\n            for _, item in ipairs(items) do\n                exist_items[tostring(item.id)] = true\n            end\n            -- remove objects that exist in the self.values but do not exist in the new items.\n            -- for removed items, trigger cleanup handlers.\n            for _, item in ipairs(self.values) do\n                local id = item.value.id\n                if not exist_items[id]  then\n                    config_util.fire_all_clean_handlers(item)\n                else\n                    insert_tab(exist_values, item)\n                    self.values_hash[id] = #exist_values\n                end\n            end\n            self.values = exist_values\n        else\n            for _, item in ipairs(self.values) do\n                config_util.fire_all_clean_handlers(item)\n            end\n            self.values = nil\n        end\n    end\n\n    if self.single_item then\n        -- treat items as a single item\n        self.values = new_tab(1, 0)\n        self.values_hash = new_tab(0, 1)\n\n        local item = items\n        local modifiedIndex = item.modifiedIndex or conf_version\n        local conf_item = {value = item, modifiedIndex = modifiedIndex,\n                           key = \"/\" .. self.key}\n\n        local data_valid = true\n        local err\n        if self.item_schema then\n            data_valid, err = check_schema(self.item_schema, item)\n            if not data_valid then\n                log.error(\"failed to check item data of [\", self.key,\n                          \"] err:\", err, \" ,val: \", json.delay_encode(item))\n            end\n\n            if data_valid and self.checker then\n                -- TODO: An opts table should be used\n                -- as different checkers may use different parameters\n                data_valid, err = self.checker(item, conf_item.key)\n                if not data_valid then\n                    log.error(\"failed to check item data of [\", self.key,\n                              \"] err:\", err, \" ,val: \", json.delay_encode(item))\n                end\n            end\n        end\n\n        if data_valid then\n            insert_tab(self.values, conf_item)\n            self.values_hash[self.key] = #self.values\n            conf_item.clean_handlers = {}\n\n            if self.filter then\n                self.filter(conf_item)\n            end\n        end\n\n    else\n        if not self.values then\n            self.values = new_tab(8, 0)\n            self.values_hash = new_tab(0, 8)\n        end\n\n        local err\n        for i, item in ipairs(items) do\n            local idx = tostring(i)\n            local data_valid = true\n            if type(item) ~= \"table\" then\n                data_valid = false\n                log.error(\"invalid item data of [\", self.key .. \"/\" .. idx,\n                          \"], val: \", json.delay_encode(item),\n                          \", it should be an object\")\n            end\n\n            local id = item.id or item.username or (\"arr_\" .. idx)\n            local modifiedIndex = item.modifiedIndex or conf_version\n            local conf_item = {value = item, modifiedIndex = modifiedIndex,\n                            key = \"/\" .. self.key .. \"/\" .. id}\n\n            if data_valid and self.item_schema then\n                data_valid, err = check_schema(self.item_schema, item)\n                if not data_valid then\n                    log.error(\"failed to check item data of [\", self.key,\n                              \"] err:\", err, \" ,val: \", json.delay_encode(item))\n                end\n            end\n\n            if data_valid and self.checker then\n                data_valid, err = self.checker(item, conf_item.key)\n                if not data_valid then\n                    log.error(\"failed to check item data of [\", self.key,\n                              \"] err:\", err, \" ,val: \", json.delay_encode(item))\n                end\n            end\n\n            if data_valid then\n                local item_id = tostring(id)\n                local pre_index = self.values_hash[item_id]\n                if pre_index then\n                    -- remove the old item\n                    local pre_val = self.values[pre_index]\n                    if pre_val and\n                        (not item.modifiedIndex or pre_val.modifiedIndex ~= item.modifiedIndex) then\n                        config_util.fire_all_clean_handlers(pre_val)\n                        self.values[pre_index] = conf_item\n                        conf_item.value.id = item_id\n                        conf_item.clean_handlers = {}\n                    end\n                else\n                    insert_tab(self.values, conf_item)\n                    self.values_hash[item_id] = #self.values\n                    conf_item.value.id = item_id\n                    conf_item.clean_handlers = {}\n                end\n\n                if self.filter then\n                    self.filter(conf_item)\n                end\n            end\n        end\n    end\n\n    self.conf_version = conf_version\n    return true\nend\n\n\nfunction _M.get(self, key)\n    if not self.values_hash then\n        return\n    end\n\n    local arr_idx = self.values_hash[tostring(key)]\n    if not arr_idx then\n        return nil\n    end\n\n    return self.values[arr_idx]\nend\n\n\nlocal function _automatic_fetch(premature, self)\n    if premature then\n        return\n    end\n\n    -- the _automatic_fetch is only called in the timer, and according to the\n    -- documentation, ngx.shared.DICT.get can be executed there.\n    -- if the file's global variables have not yet been assigned values,\n    -- we can assume that the worker has not been initialized yet and try to\n    -- read any old data that may be present from the shared dict\n    -- try load from shared dict only on first startup, otherwise use event mechanism\n    if is_use_admin_api() and not shared_dict then\n        log.info(\"try to load config from shared dict\")\n\n        local config, err\n        shared_dict = ngx_shared[\"standalone-config\"] -- init shared dict in current worker\n        if not shared_dict then\n            log.error(\"failed to read config from shared dict: shared dict not found\")\n            goto SKIP_SHARED_DICT\n        end\n        config, err = shared_dict:get(\"config\")\n        if not config then\n            if err then -- if the key does not exist, the return values are both nil\n                log.error(\"failed to read config from shared dict: \", err)\n            end\n            log.info(\"no config found in shared dict\")\n            goto SKIP_SHARED_DICT\n        end\n        log.info(\"startup config loaded from shared dict: \", config)\n\n        config, err = json.decode(tostring(config))\n        if not config then\n            log.error(\"failed to decode config from shared dict: \", err)\n            goto SKIP_SHARED_DICT\n        end\n        _M._update_config(config)\n        log.info(\"config loaded from shared dict\")\n\n        ::SKIP_SHARED_DICT::\n        if not shared_dict then\n            log.crit(_M.ERR_NO_SHARED_DICT)\n\n            -- fill that value to make the worker not try to read from shared dict again\n            shared_dict = \"error\"\n        end\n    end\n\n    local i = 0\n    while not exiting() and self.running and i <= 32 do\n        i = i + 1\n        local ok, ok2, err = pcall(sync_data, self)\n        if not ok then\n            err = ok2\n            log.error(\"failed to fetch data from local file \" .. config_file.path .. \": \",\n                      err, \", \", tostring(self))\n            ngx_sleep(3)\n            break\n\n        elseif not ok2 and err then\n            if err ~= \"timeout\" and err ~= \"Key not found\"\n               and self.last_err ~= err then\n                log.error(\"failed to fetch data from local file \" .. config_file.path .. \": \",\n                          err, \", \", tostring(self))\n            end\n\n            if err ~= self.last_err then\n                self.last_err = err\n                self.last_err_time = ngx_time()\n            else\n                if ngx_time() - self.last_err_time >= 30 then\n                    self.last_err = nil\n                end\n            end\n            ngx_sleep(0.5)\n\n        elseif not ok2 then\n            ngx_sleep(0.05)\n\n        else\n            ngx_sleep(0.1)\n        end\n    end\n\n    if not exiting() and self.running then\n        ngx_timer_at(0, _automatic_fetch, self)\n    end\nend\n\n\nfunction _M.new(key, opts)\n    local local_conf, err = config_local.local_conf()\n    if not local_conf then\n        return nil, err\n    end\n\n    local automatic = opts and opts.automatic\n    local item_schema = opts and opts.item_schema\n    local filter_fun = opts and opts.filter\n    local single_item = opts and opts.single_item\n    local checker = opts and opts.checker\n\n    -- like /routes and /upstreams, remove first char `/`\n    if key then\n        key = sub_str(key, 2)\n    end\n\n    local obj = setmetatable({\n        automatic = automatic,\n        item_schema = item_schema,\n        checker = checker,\n        sync_times = 0,\n        running = true,\n        conf_version = 0,\n        values = nil,\n        routes_hash = nil,\n        prev_index = nil,\n        last_err = nil,\n        last_err_time = nil,\n        key = key,\n        conf_version_key = key and key .. \"_conf_version\",\n        single_item = single_item,\n        filter = filter_fun,\n    }, mt)\n\n    if automatic then\n        if not key then\n            return nil, \"missing `key` argument\"\n        end\n\n        local ok, ok2, err = pcall(sync_data, obj)\n        if not ok then\n            err = ok2\n        end\n\n        if err then\n            log.error(\"failed to fetch data from local file \", config_file.path, \": \",\n                      err, \", \", key)\n        end\n\n        ngx_timer_at(0, _automatic_fetch, obj)\n    end\n\n    if key then\n        created_obj[key] = obj\n    end\n\n    return obj\nend\n\n\nfunction _M.close(self)\n    self.running = false\nend\n\n\nfunction _M.server_version(self)\n    return \"apisix.yaml \" .. _M.version\nend\n\n\nfunction _M.fetch_created_obj(key)\n    return created_obj[sub_str(key, 2)]\nend\n\n\nfunction _M.init()\n    if is_use_admin_api() then\n        return true\n    end\n\n    read_apisix_config()\n    return true\nend\n\n\nfunction _M.init_worker()\n    sync_status_to_shdict(false)\n    if is_use_admin_api() then\n        apisix_yaml = {}\n        apisix_yaml_mtime = 0\n        return true\n    end\n\n    -- sync data in each non-master process\n    ngx.timer.every(1, read_apisix_config)\n\n    if apisix_yaml then\n        update_config(apisix_yaml, apisix_yaml_mtime)\n    end\n\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/ctx.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Define the request context.\n--\n-- @module core.ctx\n\nlocal core_str     = require(\"apisix.core.string\")\nlocal core_tab     = require(\"apisix.core.table\")\nlocal request      = require(\"apisix.core.request\")\nlocal log          = require(\"apisix.core.log\")\nlocal json         = require(\"apisix.core.json\")\nlocal config_local = require(\"apisix.core.config_local\")\nlocal tablepool    = require(\"tablepool\")\nlocal get_var      = require(\"resty.ngxvar\").fetch\nlocal get_request  = require(\"resty.ngxvar\").request\nlocal ck           = require \"resty.cookie\"\nlocal multipart    = require(\"multipart\")\nlocal util         = require(\"apisix.cli.util\")\nlocal gq_parse     = require(\"graphql\").parse\nlocal jp           = require(\"jsonpath\")\nlocal setmetatable = setmetatable\nlocal sub_str      = string.sub\nlocal ngx          = ngx\nlocal ngx_var      = ngx.var\nlocal re_gsub      = ngx.re.gsub\nlocal ipairs       = ipairs\nlocal type         = type\nlocal error        = error\nlocal pcall        = pcall\n\n\nlocal _M = {version = 0.2}\nlocal GRAPHQL_DEFAULT_MAX_SIZE       = 1048576               -- 1MiB\nlocal GRAPHQL_REQ_DATA_KEY           = \"query\"\nlocal GRAPHQL_REQ_METHOD_HTTP_GET    = \"GET\"\nlocal GRAPHQL_REQ_METHOD_HTTP_POST   = \"POST\"\nlocal GRAPHQL_REQ_MIME_JSON          = \"application/json\"\n\n\nlocal fetch_graphql_data = {\n    [GRAPHQL_REQ_METHOD_HTTP_GET] = function(ctx, max_size)\n        local body = request.get_uri_args(ctx)[GRAPHQL_REQ_DATA_KEY]\n        if not body then\n            return nil, \"failed to read graphql data, args[\" ..\n                        GRAPHQL_REQ_DATA_KEY .. \"] is nil\"\n        end\n\n        if type(body) == \"table\" then\n            body = body[1]\n        end\n\n        return body\n    end,\n\n    [GRAPHQL_REQ_METHOD_HTTP_POST] = function(ctx, max_size)\n        local body, err = request.get_body(max_size, ctx)\n        if not body then\n            return nil, \"failed to read graphql data, \" .. (err or \"request body has zero size\")\n        end\n\n        if request.header(ctx, \"Content-Type\") == GRAPHQL_REQ_MIME_JSON then\n            local res\n            res, err = json.decode(body)\n            if not res then\n                return nil, \"failed to read graphql data, \" .. err\n            end\n\n            if not res[GRAPHQL_REQ_DATA_KEY] then\n                return nil, \"failed to read graphql data, json body[\" ..\n                            GRAPHQL_REQ_DATA_KEY .. \"] is nil\"\n            end\n\n            body = res[GRAPHQL_REQ_DATA_KEY]\n        end\n\n        return body\n    end\n}\n\n\nlocal function parse_graphql(ctx)\n    local local_conf, err = config_local.local_conf()\n    if not local_conf then\n        return nil, \"failed to get local conf: \" .. err\n    end\n\n    local max_size = GRAPHQL_DEFAULT_MAX_SIZE\n    local size = core_tab.try_read_attr(local_conf, \"graphql\", \"max_size\")\n    if size then\n        max_size = size\n    end\n\n    local method = request.get_method()\n    local func = fetch_graphql_data[method]\n    if not func then\n        return nil, \"graphql not support `\" .. method .. \"` request\"\n    end\n\n    local body\n    body, err = func(ctx, max_size)\n    if not body then\n        return nil, err\n    end\n\n    local ok, res = pcall(gq_parse, body)\n    if not ok then\n        return nil, \"failed to parse graphql: \" .. res .. \" body: \" .. body\n    end\n\n    if #res.definitions == 0 then\n        return nil, \"empty graphql: \" .. body\n    end\n\n    return res\nend\n\n\nlocal function get_parsed_graphql()\n    local ctx = ngx.ctx.api_ctx\n    if ctx._graphql then\n        return ctx._graphql\n    end\n\n    local res, err = parse_graphql(ctx)\n    if not res then\n        log.error(err)\n        ctx._graphql = {}\n        return ctx._graphql\n    end\n\n    if #res.definitions > 1 then\n        log.warn(\"Multiple operations are not supported.\",\n                    \"Only the first one is handled\")\n    end\n\n    local def = res.definitions[1]\n    local fields = def.selectionSet.selections\n    local root_fields = core_tab.new(#fields, 0)\n    for i, f in ipairs(fields) do\n        root_fields[i] = f.name.value\n    end\n\n    local name = \"\"\n    if def.name and def.name.value then\n        name = def.name.value\n    end\n\n    ctx._graphql = {\n        name = name,\n        operation = def.operation,\n        root_fields = root_fields,\n    }\n\n    return ctx._graphql\nend\n\n\nlocal CONTENT_TYPE_JSON = \"application/json\"\nlocal CONTENT_TYPE_FORM_URLENCODED = \"application/x-www-form-urlencoded\"\nlocal CONTENT_TYPE_MULTIPART_FORM = \"multipart/form-data\"\n\nlocal function get_parsed_request_body(ctx)\n    local ct_header = request.header(ctx, \"Content-Type\") or \"\"\n\n    if core_str.find(ct_header, CONTENT_TYPE_JSON) then\n        local request_table, err = request.get_json_request_body_table()\n        if not request_table then\n            return nil, \"failed to parse JSON body: \" .. err\n        end\n        return request_table\n    end\n\n    if core_str.find(ct_header, CONTENT_TYPE_FORM_URLENCODED) then\n        local args, err = request.get_post_args()\n        if not args then\n            return nil, \"failed to parse form data: \" .. (err or \"unknown error\")\n        end\n        return args\n    end\n\n    if core_str.find(ct_header, CONTENT_TYPE_MULTIPART_FORM) then\n        local body = request.get_body()\n        local res = multipart(body, ct_header)\n        if not res then\n            return nil, \"failed to parse multipart form data\"\n        end\n        return res:get_all()\n    end\n\n    local err = \"unsupported content-type in header: \" .. ct_header ..\n                \", supported types are: \" ..\n                CONTENT_TYPE_JSON .. \", \" ..\n                CONTENT_TYPE_FORM_URLENCODED .. \", \" ..\n                CONTENT_TYPE_MULTIPART_FORM\n    return nil, err\nend\n\n\ndo\n    local var_methods = {\n        method = ngx.req.get_method,\n        cookie = function ()\n            if ngx.var.http_cookie then\n                return ck:new()\n            end\n        end\n    }\n\n    local no_cacheable_var_names = {\n        -- var.args should not be cached as it can be changed via set_uri_args\n        args = true,\n        is_args = true,\n    }\n\n    local ngx_var_names = {\n        upstream_scheme            = true,\n        upstream_host              = true,\n        upstream_upgrade           = true,\n        upstream_connection        = true,\n        upstream_uri               = true,\n        llm_content_risk_level     = true,\n        apisix_request_id          = true,\n\n        request_type               = true,\n        apisix_upstream_response_time = true,\n        llm_time_to_first_token    = true,\n        request_llm_model          = true,\n        llm_model                  = true,\n        llm_prompt_tokens          = true,\n        llm_completion_tokens      = true,\n\n        upstream_mirror_host       = true,\n        upstream_mirror_uri        = true,\n\n        upstream_cache_zone        = true,\n        upstream_cache_zone_info   = true,\n        upstream_no_cache          = true,\n        upstream_cache_key         = true,\n        upstream_cache_bypass      = true,\n\n        var_x_forwarded_proto      = true,\n        var_x_forwarded_port       = true,\n        var_x_forwarded_host       = true,\n    }\n\n    -- sort in alphabetical\n    local apisix_var_names = {\n        balancer_ip = true,\n        balancer_port = true,\n        consumer_group_id = true,\n        consumer_name = true,\n        resp_body = function(ctx)\n            -- only for logger and requires the logger to have a special configuration\n            return ctx.resp_body or ''\n        end,\n        route_id = true,\n        route_name = true,\n        service_id = true,\n        service_name = true,\n    }\n\n    local mt = {\n        __index = function(t, key)\n            local cached = t._cache[key]\n            if cached ~= nil then\n                log.debug(\"serving ctx value from cache for key: \", key)\n                return cached\n            end\n\n            if type(key) ~= \"string\" then\n                error(\"invalid argument, expect string value\", 2)\n            end\n\n            local val\n            local method = var_methods[key]\n            if method then\n                val = method()\n\n            elseif core_str.has_prefix(key, \"cookie_\") then\n                local cookie = t.cookie\n                if cookie then\n                    local err\n                    val, err = cookie:get(sub_str(key, 8))\n                    if err then\n                        log.warn(\"failed to fetch cookie value by key: \",\n                                 key, \" error: \", err)\n                    end\n                end\n\n            elseif core_str.has_prefix(key, \"arg_\") then\n                local arg_key = sub_str(key, 5)\n                local args = request.get_uri_args()[arg_key]\n                if args then\n                    if type(args) == \"table\" then\n                        val = args[1]\n                    else\n                        val = args\n                    end\n                end\n\n            elseif core_str.has_prefix(key, \"post_arg_\") then\n                -- only match default post form\n                local content_type = request.header(nil, \"Content-Type\")\n                if content_type ~= nil and core_str.has_prefix(content_type,\n                        \"application/x-www-form-urlencoded\") then\n                    local arg_key = sub_str(key, 10)\n                    local args = request.get_post_args()[arg_key]\n                    if args then\n                        if type(args) == \"table\" then\n                            val = args[1]\n                        else\n                            val = args\n                        end\n                    end\n                end\n\n            elseif core_str.has_prefix(key, \"uri_param_\") then\n                -- `uri_param_<name>` provides access to the uri parameters when using\n                -- radixtree_uri_with_parameter\n                if t._ctx.curr_req_matched then\n                    local arg_key = sub_str(key, 11)\n                    val = t._ctx.curr_req_matched[arg_key]\n                end\n\n            elseif core_str.has_prefix(key, \"http_\") then\n                local arg_key = key:lower()\n                arg_key = re_gsub(arg_key, \"-\", \"_\", \"jo\")\n                val = get_var(arg_key, t._request)\n\n            elseif core_str.has_prefix(key, \"graphql_\") then\n                -- trim the \"graphql_\" prefix\n                local arg_key = sub_str(key, 9)\n                val = get_parsed_graphql()[arg_key]\n            elseif core_str.has_prefix(key, \"post_arg.\") then\n                -- trim the \"post_arg.\" prefix (10 characters)\n                local arg_key = sub_str(key, 10)\n                local parsed_body, err = get_parsed_request_body(t._ctx)\n                if not parsed_body then\n                    log.warn(\"failed to fetch post args value by key: \", arg_key, \" error: \", err)\n                    return nil\n                end\n                if arg_key:find(\"[%[%*]\") or arg_key:find(\"..\", 1, true) then\n                    arg_key = \"$.\" .. arg_key\n                    local results = jp.query(parsed_body, arg_key)\n                    if #results == 0 then\n                        val = nil\n                    else\n                        val = results\n                    end\n                else\n                    local parts = util.split(arg_key, \"(.)\")\n                    local current = parsed_body\n                    for _, part in ipairs(parts) do\n                        if type(current) ~= \"table\" then\n                            current = nil\n                            break\n                        end\n                        current = current[part]\n                    end\n                    val = current\n                end\n\n            else\n                local getter = apisix_var_names[key]\n                if getter then\n                    local ctx = t._ctx\n                    if getter == true then\n                        val = ctx and ctx[key]\n                    else\n                        -- the getter is registered by ctx.register_var\n                        val = getter(ctx)\n                    end\n\n                else\n                    val = get_var(key, t._request)\n                end\n            end\n\n            if val ~= nil and not no_cacheable_var_names[key] then\n                t._cache[key] = val\n            end\n\n            return val\n        end,\n\n        __newindex = function(t, key, val)\n            if ngx_var_names[key] then\n                ngx_var[key] = val\n            end\n\n            -- log.info(\"key: \", key, \" new val: \", val)\n            t._cache[key] = val\n        end,\n    }\n\n---\n-- Register custom variables.\n-- Register variables globally, and use them as normal builtin variables.\n-- Note that the custom variables can't be used in features that depend\n-- on the Nginx directive, like `access_log_format`.\n--\n-- @function core.ctx.register_var\n-- @tparam string name custom variable name\n-- @tparam function getter The fetch function for custom variables.\n-- @tparam table opts An optional options table which controls the behavior about the variable\n-- @usage\n-- local core = require \"apisix.core\"\n--\n-- core.ctx.register_var(\"a6_labels_zone\", function(ctx)\n--     local route = ctx.matched_route and ctx.matched_route.value\n--     if route and route.labels then\n--         return route.labels.zone\n--     end\n--     return nil\n-- end)\n--\n-- We support the options below in the `opts`:\n-- * no_cacheable: if the result of getter is cacheable or not. Default to `false`.\nfunction _M.register_var(name, getter, opts)\n    if type(getter) ~= \"function\" then\n        error(\"the getter of registered var should be a function\")\n    end\n\n    apisix_var_names[name] = getter\n\n    if opts then\n        if opts.no_cacheable then\n            no_cacheable_var_names[name] = true\n        end\n    end\nend\n\nfunction _M.set_vars_meta(ctx)\n    local var = tablepool.fetch(\"ctx_var\", 0, 32)\n    if not var._cache then\n        var._cache = {}\n    end\n\n    var._request = get_request()\n    var._ctx = ctx\n    setmetatable(var, mt)\n    ctx.var = var\nend\n\nfunction _M.release_vars(ctx)\n    if ctx.var == nil then\n        return\n    end\n\n    core_tab.clear(ctx.var._cache)\n    tablepool.release(\"ctx_var\", ctx.var, true)\n    ctx.var = nil\nend\n\nend -- do\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/dns/client.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Wrapped dns search client.\n--\n-- @module core.dns.client\n\nlocal require = require\nlocal config_local = require(\"apisix.core.config_local\")\nlocal log = require(\"apisix.core.log\")\nlocal json = require(\"apisix.core.json\")\nlocal table = require(\"apisix.core.table\")\nlocal gcd = require(\"apisix.core.math\").gcd\nlocal insert_tab = table.insert\nlocal math_random = math.random\nlocal package_loaded = package.loaded\nlocal ipairs = ipairs\nlocal table_remove = table.remove\nlocal setmetatable = setmetatable\n\n\nlocal _M = {\n    RETURN_RANDOM = 1,\n    RETURN_ALL = 2,\n}\n\n\nlocal function resolve_srv(client, answers)\n    if #answers == 0 then\n        return nil, \"empty SRV record\"\n    end\n\n    local resolved_answers = {}\n    local answer_to_count = {}\n    for _, answer in ipairs(answers) do\n        if answer.type ~= client.TYPE_SRV then\n            return nil, \"mess SRV with other record\"\n        end\n\n        local resolved, err = client.resolve(answer.target)\n        if not resolved then\n            local msg = \"failed to resolve SRV record \" .. answer.target .. \": \" .. err\n            return nil, msg\n        end\n\n        log.info(\"dns resolve SRV \", answer.target, \", result: \",\n                 json.delay_encode(resolved))\n\n        local weight = answer.weight\n        if weight == 0 then\n            weight = 1\n        end\n\n        local count = #resolved\n        answer_to_count[answer] = count\n        -- one target may have multiple resolved results\n        for _, res in ipairs(resolved) do\n            local copy = table.deepcopy(res)\n            copy.weight = weight / count\n            copy.port = answer.port\n            copy.priority = answer.priority\n            insert_tab(resolved_answers, copy)\n        end\n    end\n\n    -- find the least common multiple of the counts\n    local lcm = answer_to_count[answers[1]]\n    for i = 2, #answers do\n        local count = answer_to_count[answers[i]]\n        lcm = count * lcm / gcd(count, lcm)\n    end\n    -- fix the weight as the weight should be integer\n    for _, res in ipairs(resolved_answers) do\n        res.weight = res.weight * lcm\n    end\n\n    return resolved_answers\nend\n\n\nfunction _M.resolve(self, domain, selector)\n    local client = self.client\n\n    -- this function will dereference the CNAME records\n    local answers, err = client.resolve(domain)\n    if not answers then\n        return nil, \"failed to query the DNS server: \" .. err\n    end\n\n    if answers.errcode then\n        return nil, \"server returned error code: \" .. answers.errcode\n                    .. \": \" .. answers.errstr\n    end\n\n    if selector == _M.RETURN_ALL then\n        log.info(\"dns resolve \", domain, \", result: \", json.delay_encode(answers))\n        for _, answer in ipairs(answers) do\n            if answer.type == client.TYPE_SRV then\n                return resolve_srv(client, answers)\n            end\n        end\n        return table.deepcopy(answers)\n    end\n\n    local idx = math_random(1, #answers)\n    local answer = answers[idx]\n    local dns_type = answer.type\n    if dns_type == client.TYPE_A or dns_type == client.TYPE_AAAA then\n        log.info(\"dns resolve \", domain, \", result: \", json.delay_encode(answer))\n        return table.deepcopy(answer)\n    end\n\n    return nil, \"unsupported DNS answer\"\nend\n\n\nfunction _M.new(opts)\n    local local_conf = config_local.local_conf()\n\n    if opts.enable_ipv6 == nil then\n        opts.enable_ipv6 = local_conf.apisix.enable_ipv6\n    end\n\n    -- ensure the resolver throws an error when ipv6 is disabled\n    if not opts.enable_ipv6 then\n        for i, v in ipairs(opts.order) do\n            if v == \"AAAA\" then\n                table_remove(opts.order, i)\n                break\n            end\n        end\n    end\n\n    opts.timeout = 2000 -- 2 sec\n    opts.retrans = 5 -- 5 retransmissions on receive timeout\n    opts.finalCacheOnly = true -- only cache final records\n\n    -- make sure each client has its separate room\n    package_loaded[\"resty.dns.client\"] = nil\n    local dns_client_mod = require(\"resty.dns.client\")\n\n    local ok, err = dns_client_mod.init(opts)\n    if not ok then\n        return nil, \"failed to init the dns client: \" .. err\n    end\n\n    return setmetatable({client = dns_client_mod}, {__index = _M})\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/env.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ffi         = require \"ffi\"\n\nlocal json        = require(\"apisix.core.json\")\nlocal log         = require(\"apisix.core.log\")\nlocal string      = require(\"apisix.core.string\")\n\nlocal os          = os\nlocal type        = type\nlocal upper       = string.upper\nlocal find        = string.find\nlocal sub         = string.sub\nlocal str         = ffi.string\n\nlocal ENV_PREFIX = \"$ENV://\"\n\nlocal _M = {\n    PREFIX = ENV_PREFIX\n}\n\n\nlocal apisix_env_vars = {}\n\nffi.cdef [[\n  extern char **environ;\n]]\n\n\nfunction _M.init()\n    local e = ffi.C.environ\n    if not e then\n        log.warn(\"could not access environment variables\")\n        return\n    end\n\n    local i = 0\n    while e[i] ~= nil do\n        local var = str(e[i])\n        local p = find(var, \"=\")\n        if p then\n            apisix_env_vars[sub(var, 1, p - 1)] = sub(var, p + 1)\n        end\n\n        i = i + 1\n    end\nend\n\n\nlocal function parse_env_uri(env_uri)\n    -- Avoid the error caused by has_prefix to cause a crash.\n    if type(env_uri) ~= \"string\" then\n        return nil, \"error env_uri type: \" .. type(env_uri)\n    end\n\n    if not string.has_prefix(upper(env_uri), ENV_PREFIX) then\n        return nil, \"error env_uri prefix: \" .. env_uri\n    end\n\n    local path = sub(env_uri, #ENV_PREFIX + 1)\n    local idx = find(path, \"/\")\n    if not idx then\n        return {key = path, sub_key = \"\"}\n    end\n    local key = sub(path, 1, idx - 1)\n    local sub_key = sub(path, idx + 1)\n\n    return {\n      key = key,\n      sub_key = sub_key\n    }\nend\n\n\nfunction _M.fetch_by_uri(env_uri)\n    log.info(\"fetching data from env uri: \", env_uri)\n    local opts, err = parse_env_uri(env_uri)\n    if not opts then\n        return nil, err\n    end\n\n    local main_value = apisix_env_vars[opts.key] or os.getenv(opts.key)\n    if main_value and opts.sub_key ~= \"\" then\n        local vt, err = json.decode(main_value)\n        if not vt then\n            return nil, \"decode failed, err: \" .. (err or \"\") .. \", value: \" .. main_value\n        end\n        return vt[opts.sub_key]\n    end\n\n    return main_value\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/etcd.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Etcd API.\n--\n-- @module core.etcd\n\nlocal require           = require\nlocal fetch_local_conf  = require(\"apisix.core.config_local\").local_conf\nlocal array_mt          = require(\"apisix.core.json\").array_mt\nlocal log               = require(\"apisix.core.log\")\nlocal try_read_attr     = require(\"apisix.core.table\").try_read_attr\nlocal v3_adapter        = require(\"apisix.admin.v3_adapter\")\nlocal etcd              = require(\"resty.etcd\")\nlocal clone_tab         = require(\"table.clone\")\nlocal health_check      = require(\"resty.etcd.health_check\")\nlocal pl_path           = require(\"pl.path\")\nlocal ipairs            = ipairs\nlocal setmetatable      = setmetatable\nlocal string            = string\nlocal tonumber          = tonumber\nlocal ngx_get_phase     = ngx.get_phase\n\n\nlocal _M = {}\n\n\nlocal NOT_ALLOW_WRITE_ETCD_WARN = 'Data plane role should not write to etcd. ' ..\n    'This operation will be deprecated in future releases.'\n\nlocal function is_data_plane()\n    local local_conf, err = fetch_local_conf()\n    if not local_conf then\n        return nil, err\n    end\n\n    local role = try_read_attr(local_conf, \"deployment\", \"role\")\n    if role == \"data_plane\" then\n      return true\n    end\n\n    return false\nend\n\n\n\nlocal function disable_write_if_data_plane()\n    local data_plane, err = is_data_plane()\n    if err then\n        log.error(\"failed to check data plane role: \", err)\n        return true, err\n    end\n\n    if data_plane then\n        -- current only warn, will be return false in future releases\n        -- to block etcd write\n        log.warn(NOT_ALLOW_WRITE_ETCD_WARN)\n        return false\n    end\n\n    return false, nil\nend\n\n\nlocal function wrap_etcd_client(etcd_cli)\n    -- note: methods txn can read and write, don't use txn to write when data plane role\n    local methods_to_wrap = {\n        \"set\",\n        \"setnx\",\n        \"setx\",\n        \"delete\",\n        \"rmdir\",\n        \"grant\",\n        \"revoke\",\n        \"keepalive\"\n    }\n\n    local original_methods = {}\n    for _, method in ipairs(methods_to_wrap) do\n        if not etcd_cli[method] then\n            log.error(\"method \", method, \" not found in etcd client\")\n            return nil, \"method \" .. method .. \" not found in etcd client\"\n        end\n\n        original_methods[method] = etcd_cli[method]\n    end\n\n    for _, method in ipairs(methods_to_wrap) do\n        etcd_cli[method] = function(self, ...)\n            local disable, err = disable_write_if_data_plane()\n            if disable then\n                return nil, err\n            end\n\n            return original_methods[method](self, ...)\n        end\n    end\n\n    return etcd_cli\nend\n\n\nlocal function _new(etcd_conf)\n    local prefix = etcd_conf.prefix\n    etcd_conf.http_host = etcd_conf.host\n    etcd_conf.host = nil\n    etcd_conf.prefix = nil\n    etcd_conf.protocol = \"v3\"\n    etcd_conf.api_prefix = \"/v3\"\n\n    -- default to verify etcd cluster certificate\n    etcd_conf.ssl_verify = true\n    if etcd_conf.tls then\n        if etcd_conf.tls.verify == false then\n            etcd_conf.ssl_verify = false\n        end\n\n        if etcd_conf.tls.cert then\n            etcd_conf.ssl_cert_path = etcd_conf.tls.cert\n            etcd_conf.ssl_key_path = etcd_conf.tls.key\n        end\n\n        if etcd_conf.tls.sni then\n            etcd_conf.sni = etcd_conf.tls.sni\n        end\n    end\n\n    local etcd_cli, err = etcd.new(etcd_conf)\n    if not etcd_cli then\n        return nil, nil, err\n    end\n\n    etcd_cli = wrap_etcd_client(etcd_cli)\n\n    return etcd_cli, prefix\nend\n\n\n---\n-- Create an etcd client which will connect to etcd without being proxyed by conf server.\n-- This method is used in init_worker phase when the conf server is not ready.\n--\n-- @function core.etcd.new_without_proxy\n-- @treturn table|nil the etcd client, or nil if failed.\n-- @treturn string|nil the configured prefix of etcd keys, or nil if failed.\n-- @treturn nil|string the error message.\nlocal function new_without_proxy()\n    local local_conf, err = fetch_local_conf()\n    if not local_conf then\n        return nil, nil, err\n    end\n\n    local etcd_conf = clone_tab(local_conf.etcd)\n\n    if local_conf.apisix.ssl and local_conf.apisix.ssl.ssl_trusted_certificate then\n        etcd_conf.trusted_ca = local_conf.apisix.ssl.ssl_trusted_certificate\n    end\n\n    return _new(etcd_conf)\nend\n_M.new_without_proxy = new_without_proxy\n\n\nlocal function new()\n    local local_conf, err = fetch_local_conf()\n    if not local_conf then\n        return nil, nil, err\n    end\n\n    local etcd_conf = clone_tab(local_conf.etcd)\n\n    if local_conf.apisix.ssl and local_conf.apisix.ssl.ssl_trusted_certificate then\n        etcd_conf.trusted_ca = local_conf.apisix.ssl.ssl_trusted_certificate\n    end\n\n    if not health_check.conf then\n        health_check.init({\n            max_fails = 1,\n            retry = true,\n        })\n    end\n\n    return _new(etcd_conf)\nend\n_M.new = new\n\n\nlocal function switch_proxy()\n    if ngx_get_phase() == \"init\" or ngx_get_phase() == \"init_worker\" then\n        return new_without_proxy()\n    end\n\n    local etcd_cli, prefix, err = new()\n    if not etcd_cli or err then\n        return etcd_cli, prefix, err\n    end\n\n    if not etcd_cli.unix_socket_proxy then\n        return etcd_cli, prefix, err\n    end\n    local sock_path = etcd_cli.unix_socket_proxy:sub(#\"unix:\" + 1)\n    local ok = pl_path.exists(sock_path)\n    if not ok then\n        return new_without_proxy()\n    end\n\n    return etcd_cli, prefix, err\nend\n_M.get_etcd_syncer = switch_proxy\n\n-- convert ETCD v3 entry to v2 one\nlocal function kvs_to_node(kvs)\n    local node = {}\n    node.key = kvs.key\n    node.value = kvs.value\n    node.createdIndex = tonumber(kvs.create_revision)\n    node.modifiedIndex = tonumber(kvs.mod_revision)\n    return node\nend\n_M.kvs_to_node = kvs_to_node\n\nlocal function kvs_to_nodes(res, exclude_dir)\n    res.body.node.dir = true\n    res.body.node.nodes = setmetatable({}, array_mt)\n    if exclude_dir then\n        for i=2, #res.body.kvs do\n            res.body.node.nodes[i-1] = kvs_to_node(res.body.kvs[i])\n        end\n    else\n        for i=1, #res.body.kvs do\n            res.body.node.nodes[i] = kvs_to_node(res.body.kvs[i])\n        end\n    end\n    return res\nend\n\n\nlocal function not_found(res)\n    res.body.message = \"Key not found\"\n    res.reason = \"Not found\"\n    res.status = 404\n    return res\nend\n\n\n-- When `is_dir` is true, returns the value of both the dir key and its descendants.\n-- Otherwise, return the value of key only.\nfunction _M.get_format(res, real_key, is_dir, formatter)\n    if res.body.error == \"etcdserver: user name is empty\" then\n        return nil, \"insufficient credentials code: 401\"\n    end\n\n    if res.body.error == \"etcdserver: permission denied\" then\n        return nil, \"etcd forbidden code: 403\"\n    end\n\n    if res.body.error then\n        -- other errors, like \"grpc: received message larger than max\"\n        return nil, res.body.error\n    end\n\n    res.headers[\"X-Etcd-Index\"] = res.body.header.revision\n\n    if not res.body.kvs then\n        return not_found(res)\n    end\n\n    v3_adapter.to_v3(res.body, \"get\")\n\n    if formatter then\n        return formatter(res)\n    end\n\n    if not is_dir then\n        local key = res.body.kvs[1].key\n        if key ~= real_key then\n            return not_found(res)\n        end\n\n        res.body.node = kvs_to_node(res.body.kvs[1])\n\n    else\n        -- In etcd v2, the direct key asked for is `node`, others which under this dir are `nodes`\n        -- While in v3, this structure is flatten and all keys related the key asked for are `kvs`\n        res.body.node = kvs_to_node(res.body.kvs[1])\n        -- we have a init_dir (for etcd v2) value that can't be deserialized with json,\n        -- but we don't put init_dir for new resource type like consumer credential\n        if not res.body.kvs[1].value then\n            -- remove last \"/\" when necessary\n            if string.byte(res.body.node.key, -1) == 47 then\n                res.body.node.key = string.sub(res.body.node.key, 1, #res.body.node.key-1)\n            end\n            res = kvs_to_nodes(res, true)\n        else\n            -- get dir key by remove last part of node key,\n            -- for example: /apisix/consumers/jack -> /apisix/consumers\n            local last_slash_index = string.find(res.body.node.key, \"/[^/]*$\")\n            if last_slash_index then\n                res.body.node.key = string.sub(res.body.node.key, 1, last_slash_index-1)\n            end\n            res = kvs_to_nodes(res, false)\n        end\n    end\n\n    res.body.kvs = nil\n    v3_adapter.to_v3_list(res.body)\n    return res\nend\n\n\nfunction _M.watch_format(v3res)\n    local v2res = {}\n    v2res.headers = {\n        [\"X-Etcd-Index\"] = v3res.result.header.revision\n    }\n    v2res.body = {\n        node = {}\n    }\n\n    local compact_revision = v3res.result.compact_revision\n    if compact_revision and tonumber(compact_revision) > 0 then\n        -- When the revisions are compacted, there might be compacted changes\n        -- which are unsynced. So we need to do a fully sync.\n        -- TODO: cover this branch in CI\n        return nil, \"compacted\"\n    end\n\n    for i, event in ipairs(v3res.result.events) do\n        v2res.body.node[i] = kvs_to_node(event.kv)\n        if event.type == \"DELETE\" then\n            v2res.body.action = \"delete\"\n        end\n    end\n\n    return v2res\nend\n\n\nlocal get_etcd_cli\ndo\n    local prefix\n    local etcd_cli_init_phase\n    local etcd_cli\n    local tmp_etcd_cli\n\n    function get_etcd_cli()\n        local err\n        if ngx_get_phase() == \"init\" or ngx_get_phase() == \"init_worker\" then\n            if etcd_cli_init_phase == nil then\n                tmp_etcd_cli, prefix, err = new_without_proxy()\n                if not tmp_etcd_cli then\n                    return nil, nil, err\n                end\n\n                return tmp_etcd_cli, prefix\n            end\n\n            return etcd_cli_init_phase, prefix\n        end\n\n        if etcd_cli_init_phase ~= nil then\n            -- we can't share the etcd instance created in init* phase\n            -- they have different configuration\n            etcd_cli_init_phase:close()\n            etcd_cli_init_phase = nil\n        end\n\n        if etcd_cli == nil then\n            tmp_etcd_cli, prefix, err = switch_proxy()\n            if not tmp_etcd_cli then\n                return nil, nil, err\n            end\n\n            etcd_cli = tmp_etcd_cli\n\n            return tmp_etcd_cli, prefix\n        end\n\n        return etcd_cli, prefix\n    end\nend\n-- export it so we can mock the etcd cli in test\n_M.get_etcd_cli = get_etcd_cli\n\n\nfunction _M.get(key, is_dir)\n    local etcd_cli, prefix, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    key = prefix .. key\n\n    -- in etcd v2, get could implicitly turn into readdir\n    -- while in v3, we need to do it explicitly\n    local res, err = etcd_cli:readdir(key)\n    if not res then\n        return nil, err\n    end\n    return _M.get_format(res, key, is_dir)\nend\n\n\nlocal function set(key, value, ttl)\n    local disable, err = disable_write_if_data_plane()\n    if disable then\n        return nil, err\n    end\n\n\n    local etcd_cli, prefix, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    -- lease substitute ttl in v3\n    local res, err\n    if ttl then\n        local data, grant_err = etcd_cli:grant(tonumber(ttl))\n        if not data then\n            return nil, grant_err\n        end\n\n        res, err = etcd_cli:set(prefix .. key, value, {prev_kv = true, lease = data.body.ID})\n        if not res then\n            return nil, err\n        end\n\n        res.body.lease_id = data.body.ID\n    else\n        res, err = etcd_cli:set(prefix .. key, value, {prev_kv = true})\n    end\n    if not res then\n        return nil, err\n    end\n\n    if res.body.error then\n        return nil, res.body.error\n    end\n\n    res.headers[\"X-Etcd-Index\"] = res.body.header.revision\n\n    -- etcd v3 set would not return kv info\n    v3_adapter.to_v3(res.body, \"set\")\n    res.body.node = {}\n    res.body.node.key = prefix .. key\n    res.body.node.value = value\n    res.status = 201\n    if res.body.prev_kv then\n        res.status = 200\n        res.body.prev_kv = nil\n    end\n\n    return res, nil\nend\n_M.set = set\n\n\nfunction _M.atomic_set(key, value, ttl, mod_revision)\n    local disable, err = disable_write_if_data_plane()\n    if disable then\n        return nil, err\n    end\n\n    local etcd_cli, prefix, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    local lease_id\n    if ttl then\n        local data, grant_err = etcd_cli:grant(tonumber(ttl))\n        if not data then\n            return nil, grant_err\n        end\n\n        lease_id = data.body.ID\n    end\n\n    key = prefix .. key\n\n    local compare = {\n        {\n            key = key,\n            target = \"MOD\",\n            result = \"EQUAL\",\n            mod_revision = mod_revision,\n        }\n    }\n\n    local success = {\n        {\n            requestPut = {\n                key = key,\n                value = value,\n                lease = lease_id,\n            }\n        }\n    }\n\n    local res, err = etcd_cli:txn(compare, success)\n    if not res then\n        return nil, err\n    end\n\n    if not res.body.succeeded then\n        return nil, \"value changed before overwritten\"\n    end\n\n    res.headers[\"X-Etcd-Index\"] = res.body.header.revision\n    -- etcd v3 set would not return kv info\n    v3_adapter.to_v3(res.body, \"compareAndSwap\")\n    res.body.node = {\n        key = key,\n        value = value,\n    }\n\n    return res, nil\nend\n\n\n\nfunction _M.push(key, value, ttl)\n    local disable, err = disable_write_if_data_plane()\n    if disable then\n        return nil, err\n    end\n\n    local etcd_cli, _, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    -- Create a new revision and use it as the id.\n    -- It will be better if we use snowflake algorithm like manager-api,\n    -- but we haven't found a good library. It costs too much to write\n    -- our own one as the admin-api will be replaced by manager-api finally.\n    local res, err = set(\"/gen_id\", 1)\n    if not res then\n        return nil, err\n    end\n\n    -- manually add suffix\n    local index = res.body.header.revision\n    index = string.format(\"%020d\", index)\n\n    -- set the basic id attribute\n    value.id = index\n\n    res, err = set(key .. \"/\" .. index, value, ttl)\n    if not res then\n        return nil, err\n    end\n\n    v3_adapter.to_v3(res.body, \"create\")\n    return res, nil\nend\n\n\nfunction _M.delete(key)\n    local disable, err = disable_write_if_data_plane()\n    if disable then\n        return nil, err\n    end\n\n    local etcd_cli, prefix, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    local res, err = etcd_cli:delete(prefix .. key)\n\n    if not res then\n        return nil, err\n    end\n\n    res.headers[\"X-Etcd-Index\"] = res.body.header.revision\n\n    if not res.body.deleted then\n        return not_found(res), nil\n    end\n\n    -- etcd v3 set would not return kv info\n    v3_adapter.to_v3(res.body, \"delete\")\n    res.body.node = {}\n    res.body.key = prefix .. key\n\n    return res, nil\nend\n\nfunction _M.rmdir(key, opts)\n    local disable, err = disable_write_if_data_plane()\n    if disable then\n        return nil, err\n    end\n\n    local etcd_cli, prefix, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    local res, err = etcd_cli:rmdir(prefix .. key, opts)\n    if not res then\n        return nil, err\n    end\n\n    res.headers[\"X-Etcd-Index\"] = res.body.header.revision\n\n    if not res.body.deleted then\n        return not_found(res), nil\n    end\n\n    v3_adapter.to_v3(res.body, \"delete\")\n    res.body.node = {}\n    res.body.key = prefix .. key\n\n    return res, nil\nend\n\n---\n-- Get etcd cluster and server version.\n--\n-- @function core.etcd.server_version\n-- @treturn table The response of query etcd server version.\n-- @usage\n-- local res, err = core.etcd.server_version()\n-- -- the res.body is as follows:\n-- -- {\n-- --   etcdcluster = \"3.5.0\",\n-- --   etcdserver = \"3.5.0\"\n-- -- }\nfunction _M.server_version()\n    local etcd_cli, _, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    return etcd_cli:version()\nend\n\n\nfunction _M.keepalive(id)\n    local disable, err = disable_write_if_data_plane()\n    if disable then\n        return nil, err\n    end\n\n    local etcd_cli, _, err = get_etcd_cli()\n    if not etcd_cli then\n        return nil, err\n    end\n\n    local res, err = etcd_cli:keepalive(id)\n    if not res then\n        return nil, err\n    end\n\n    return res, nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/event.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal CONST = {\n    BUILD_ROUTER = 1,\n}\n\nlocal _M = {\n    CONST = CONST,\n}\n\nlocal events = {}\n\n\nfunction _M.push(type, ...)\n    local handler = events[type]\n    if handler then\n        handler(...)\n    end\nend\n\nfunction _M.register(type, handler)\n    -- TODO: we can register more than one handler\n    events[type] = handler\nend\n\nfunction _M.unregister(type)\n    events[type] = nil\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/id.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Instance id of APISIX\n--\n-- @module core.id\n\nlocal fetch_local_conf = require(\"apisix.core.config_local\").local_conf\nlocal try_read_attr    = require(\"apisix.core.table\").try_read_attr\nlocal profile          = require(\"apisix.core.profile\")\nlocal log              = require(\"apisix.core.log\")\nlocal uuid             = require(\"resty.jit-uuid\")\nlocal lyaml            = require(\"lyaml\")\nlocal smatch           = string.match\nlocal open             = io.open\nlocal type             = type\nlocal ipairs           = ipairs\nlocal string           = string\nlocal math             = math\nlocal prefix           = ngx.config.prefix()\nlocal pairs            = pairs\nlocal ngx_exit         = ngx.exit\nlocal apisix_uid\n\nlocal _M = {version = 0.1}\n\n\nlocal function rtrim(str)\n    return smatch(str, \"^(.-)%s*$\")\nend\n\n\nlocal function read_file(path)\n    local file = open(path, \"rb\") -- r read mode and b binary mode\n    if not file then\n        return nil\n    end\n\n    local content = file:read(\"*a\")  -- *a or *all reads the whole file\n    file:close()\n    return rtrim(content)\nend\n\n\nlocal function write_file(path, data)\n    local file = open(path, \"w+\")\n    if not file then\n        return nil, \"failed to open file[\" .. path .. \"] for writing\"\n    end\n\n    file:write(data)\n    file:close()\n    return true\nend\n\n\nlocal function generate_yaml(table)\n    -- By default lyaml will parse null values as []\n    -- The following logic is a workaround so that null values are parsed as null\n    local function replace_null(tbl)\n        for k, v in pairs(tbl) do\n            if type(v) == \"table\" then\n                replace_null(v)\n            elseif v == nil then\n                tbl[k] = \"<PLACEHOLDER>\"\n            end\n        end\n    end\n\n    -- Replace null values with \"<PLACEHOLDER>\"\n    replace_null(table)\n    local yaml = lyaml.dump({ table })\n    yaml = yaml:gsub(\"<PLACEHOLDER>\", \"null\"):gsub(\"%[%s*%]\", \"null\")\n    return yaml\nend\n\n\n_M.gen_uuid_v4 = uuid.generate_v4\n\n\n--- This will autogenerate the admin key if it's passed as an empty string in the configuration.\nlocal function autogenerate_admin_key(default_conf)\n    local changed = false\n   -- Check if deployment.role is either traditional or control_plane\n    local deployment_role = default_conf.deployment and default_conf.deployment.role\n    if deployment_role and (deployment_role == \"traditional\" or\n       deployment_role == \"control_plane\") then\n        -- Check if deployment.admin.admin_key is not nil and it's an empty string\n        local admin_keys = try_read_attr(default_conf, \"deployment\", \"admin\", \"admin_key\")\n        if admin_keys and type(admin_keys) == \"table\" then\n            for i, admin_key in ipairs(admin_keys) do\n                if admin_key.role == \"admin\" and admin_key.key == \"\" then\n                    changed = true\n                    admin_keys[i].key = \"\"\n                    for _ = 1, 32 do\n                        admin_keys[i].key = admin_keys[i].key ..\n                        string.char(math.random(65, 90) + math.random(0, 1) * 32)\n                    end\n                end\n            end\n        end\n    end\n    return default_conf,changed\nend\n\n\nfunction _M.init()\n    local local_conf = fetch_local_conf()\n\n    local local_conf, changed = autogenerate_admin_key(local_conf)\n    if changed then\n        local yaml_conf = generate_yaml(local_conf)\n        local local_conf_path = profile:yaml_path(\"config\")\n        local ok, err = write_file(local_conf_path, yaml_conf)\n        if not ok then\n            log.error(\"failed to write updated local configuration: \", err)\n            ngx_exit(-1)\n        end\n    end\n\n    --allow user to specify a meaningful id as apisix instance id\n    local uid_file_path = prefix .. \"/conf/apisix.uid\"\n    apisix_uid = read_file(uid_file_path)\n    if apisix_uid then\n        return\n    end\n\n    local id = try_read_attr(local_conf, \"apisix\", \"id\")\n    if id then\n        apisix_uid = local_conf.apisix.id\n    else\n        uuid.seed()\n        apisix_uid = uuid.generate_v4()\n        log.notice(\"not found apisix uid, generate a new one: \", apisix_uid)\n    end\n\n    local ok, err = write_file(uid_file_path, apisix_uid)\n    if not ok then\n        log.error(err)\n    end\nend\n\n\n---\n-- Returns the instance id of the running APISIX\n--\n-- @function core.id.get\n-- @treturn string the instance id\n-- @usage\n-- local apisix_id = core.id.get()\nfunction _M.get()\n    return apisix_uid\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/io.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- I/O operations on files.\n--\n-- @module core.io\n\nlocal open = io.open\n\n\nlocal _M = {}\n\n---\n-- Read the contents of a file.\n--\n-- @function core.io.get_file\n-- @tparam string file_name either an absolute path or\n-- a relative path based on the APISIX working directory.\n-- @treturn string The file content.\n-- @usage\n-- local file_content, err = core.io.get_file(\"conf/apisix.uid\")\n-- -- the `file_content` maybe the APISIX instance id in uuid format,\n-- -- like \"3f0e827b-5f26-440e-8074-c101c8eb0174\"\nfunction _M.get_file(file_name)\n    local f, err = open(file_name, 'r')\n    if not f then\n        return nil, err\n    end\n\n    local req_body = f:read(\"*all\")\n    f:close()\n    return req_body\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/ip.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- IP match and verify module.\n--\n-- @module core.ip\n\nlocal json = require(\"apisix.core.json\")\nlocal log = require(\"apisix.core.log\")\nlocal ipmatcher = require(\"resty.ipmatcher\")\nlocal str_sub   = string.sub\nlocal str_find  = require(\"apisix.core.string\").find\nlocal tonumber  = tonumber\n\n\nlocal _M = {}\n\n\nfunction _M.create_ip_matcher(ip_list)\n    local ip, err = ipmatcher.new(ip_list)\n    if not ip then\n        log.error(\"failed to create ip matcher: \", err,\n                  \" ip list: \", json.delay_encode(ip_list))\n        return nil\n    end\n\n    return ip\nend\n\n---\n-- Verify that the given ip is a valid ip or cidr.\n--\n-- @function core.ip.validate_cidr_or_ip\n-- @tparam string ip IP or cidr.\n-- @treturn boolean True if the given ip is a valid ip or cidr, false otherwise.\n-- @usage\n-- local ip1 = core.ip.validate_cidr_or_ip(\"127.0.0.1\") -- true\n-- local cidr = core.ip.validate_cidr_or_ip(\"113.74.26.106/24\") -- true\n-- local ip2 = core.ip.validate_cidr_or_ip(\"113.74.26.666\") -- false\nfunction _M.validate_cidr_or_ip(ip)\n    local mask = 0\n    local sep_pos = str_find(ip, \"/\")\n    if sep_pos then\n        mask = str_sub(ip, sep_pos + 1)\n        mask = tonumber(mask)\n        if mask < 0 or mask > 128 then\n            return false\n        end\n        ip = str_sub(ip, 1, sep_pos - 1)\n    end\n\n    if ipmatcher.parse_ipv4(ip) then\n        if mask < 0 or mask > 32 then\n            return false\n        end\n        return true\n    end\n\n    if mask < 0 or mask > 128 then\n        return false\n    end\n    return ipmatcher.parse_ipv6(ip)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/json.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Wrapped serialization and deserialization modules for json and lua tables.\n--\n-- @module core.json\n\nlocal cjson = require(\"cjson.safe\")\nlocal json_encode = cjson.encode\nlocal clear_tab = require(\"table.clear\")\nlocal ngx = ngx\nlocal tostring = tostring\nlocal type = type\nlocal pairs = pairs\nlocal cached_tab = {}\n\n\ncjson.encode_escape_forward_slash(false)\ncjson.decode_array_with_array_mt(true)\nlocal _M = {\n    version = 0.1,\n    array_mt = cjson.array_mt,\n    decode = cjson.decode,\n    -- This method produces the same encoded string when the input is not changed.\n    -- Different calls with cjson.encode will produce different string because\n    -- it doesn't maintain the object key order.\n    stably_encode = require(\"dkjson\").encode\n}\n\n\nlocal function serialise_obj(data)\n    if type(data) == \"function\" or type(data) == \"userdata\"\n       or type(data) == \"cdata\"\n       or type(data) == \"table\" then\n        return tostring(data)\n    end\n\n    return data\nend\n\n\nlocal function tab_clone_with_serialise(data)\n    if type(data) ~= \"table\" then\n        return serialise_obj(data)\n    end\n\n    local t = {}\n    for k, v in pairs(data) do\n        if type(v) == \"table\" then\n            if cached_tab[v] then\n                t[serialise_obj(k)] = tostring(v)\n            else\n                cached_tab[v] = true\n                t[serialise_obj(k)] = tab_clone_with_serialise(v)\n            end\n\n        else\n            t[serialise_obj(k)] = serialise_obj(v)\n        end\n    end\n\n    return t\nend\n\n\nlocal function encode(data, force)\n    if force then\n        clear_tab(cached_tab)\n        data = tab_clone_with_serialise(data)\n    end\n\n    return json_encode(data)\nend\n_M.encode = encode\n\nlocal max_delay_encode_items = 16\nlocal delay_tab_idx = 0\nlocal delay_tab_arr = {}\nfor i = 1, max_delay_encode_items do\n    delay_tab_arr[i] = setmetatable({data = \"\", force = false}, {\n        __tostring = function(self)\n            local res, err = encode(self.data, self.force)\n            if not res then\n                ngx.log(ngx.WARN, \"failed to encode: \", err,\n                        \" force: \", self.force)\n            end\n\n            return res\n        end\n    })\nend\n\n\n\n---\n-- Delayed encoding of input data, avoid unnecessary encode operations.\n-- When really writing logs, if the given parameter is table, it will be converted to string in\n-- OpenResty by checking if there is a metamethod registered for `__tostring`, and if so,\n-- calling this method to convert it to string.\n--\n-- @function core.json.delay_encode\n-- @tparam string|table data The data to be encoded.\n-- @tparam boolean force encode data can't be encoded as JSON with tostring\n-- @treturn table The table with the __tostring function overridden.\n-- @usage\n-- core.log.info(\"conf  : \", core.json.delay_encode(conf))\nfunction _M.delay_encode(data, force)\n    delay_tab_idx = delay_tab_idx+1\n    if delay_tab_idx > max_delay_encode_items then\n        delay_tab_idx = 1\n    end\n    delay_tab_arr[delay_tab_idx].data = data\n    delay_tab_arr[delay_tab_idx].force = force\n    return delay_tab_arr[delay_tab_idx]\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/log.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Wrapped `ngx.log`.\n--\n-- @module core.log\n\nlocal ngx = ngx\nlocal ngx_log  = ngx.log\nlocal require  = require\nlocal select = select\nlocal setmetatable = setmetatable\nlocal tostring = tostring\nlocal unpack = unpack\n-- avoid loading other module since core.log is the most foundational one\nlocal tab_clear = require(\"table.clear\")\nlocal ngx_errlog = require(\"ngx.errlog\")\nlocal ngx_get_phase = ngx.get_phase\n\n\nlocal _M = {version = 0.4}\n\n\nlocal log_levels = {\n    stderr = ngx.STDERR,\n    emerg  = ngx.EMERG,\n    alert  = ngx.ALERT,\n    crit   = ngx.CRIT,\n    error  = ngx.ERR,\n    warn   = ngx.WARN,\n    notice = ngx.NOTICE,\n    info   = ngx.INFO,\n    debug  = ngx.DEBUG,\n}\n\n\nlocal cur_level\n\nlocal do_nothing = function() end\n\n\nlocal function update_log_level()\n    -- Nginx use `notice` level in init phase instead of error_log directive config\n    -- Ref to src/core/ngx_log.c's ngx_log_init\n    if ngx_get_phase() ~= \"init\" then\n        cur_level = ngx.config.subsystem == \"http\" and ngx_errlog.get_sys_filter_level()\n    end\nend\n\n\nfunction _M.new(prefix)\n    local m = {version = _M.version}\n    setmetatable(m, {__index = function(self, cmd)\n        local log_level = log_levels[cmd]\n        local method\n        update_log_level()\n\n        if cur_level and (log_level > cur_level)\n        then\n            method = do_nothing\n        else\n            method = function(...)\n                return ngx_log(log_level, prefix, ...)\n            end\n        end\n\n        -- cache the lazily generated method in our\n        -- module table\n        if ngx_get_phase() ~= \"init\" then\n            self[cmd] = method\n        end\n\n        return method\n    end})\n\n    return m\nend\n\n\nsetmetatable(_M, {__index = function(self, cmd)\n    local log_level = log_levels[cmd]\n    local method\n    update_log_level()\n\n    if cur_level and (log_level > cur_level)\n    then\n        method = do_nothing\n    else\n        method = function(...)\n            return ngx_log(log_level, ...)\n        end\n    end\n\n    -- cache the lazily generated method in our\n    -- module table\n    if ngx_get_phase() ~= \"init\" then\n        self[cmd] = method\n    end\n\n    return method\nend})\n\n\nlocal delay_tab = setmetatable({\n    func = function() end,\n    args = {},\n    res = nil,\n    }, {\n    __tostring = function(self)\n        -- the `__tostring` will be called twice, the first to get the length and\n        -- the second to get the data\n        if self.res then\n            local res = self.res\n            -- avoid unexpected reference\n            self.res = nil\n            return res\n        end\n\n        local res, err = self.func(unpack(self.args))\n        if err then\n            ngx.log(ngx.WARN, \"failed to exec: \", err)\n        end\n\n        -- avoid unexpected reference\n        tab_clear(self.args)\n        self.res = tostring(res)\n        return self.res\n    end\n})\n\n\n---\n-- Delayed execute log printing.\n-- It works well with log.$level, eg: log.info(..., log.delay_exec(func, ...))\n-- Should not use it elsewhere.\n--\n-- @function core.log.delay_exec\n-- @tparam function func Functions that need to be delayed during log printing.\n-- @treturn table The table with the res attribute overridden.\n-- @usage\n-- local function delay_func(param1, param2)\n--     return param1 .. \" \" .. param2\n-- end\n-- core.log.info(\"delay log print: \", core.log.delay_exec(delay_func, \"hello\", \"world))\n-- -- then the log will be: \"delay log print: hello world\"\nfunction _M.delay_exec(func, ...)\n    delay_tab.func = func\n\n    tab_clear(delay_tab.args)\n    for i = 1, select('#', ...) do\n        delay_tab.args[i] = select(i, ...)\n    end\n\n    delay_tab.res = nil\n    return delay_tab\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/lrucache.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- LRU Caching Implementation.\n--\n-- @module core.lrucache\n\nlocal lru_new = require(\"resty.lrucache\").new\nlocal resty_lock = require(\"resty.lock\")\nlocal log = require(\"apisix.core.log\")\nlocal pairs = pairs\nlocal pcall = pcall\nlocal unpack = unpack\nlocal tostring = tostring\nlocal ngx = ngx\nlocal get_phase = ngx.get_phase\nlocal timer_every = ngx.timer.every\nlocal exiting = ngx.worker.exiting\n\n\nlocal lock_shdict_name = \"lrucache-lock\"\nif ngx.config.subsystem == \"stream\" then\n    lock_shdict_name = lock_shdict_name .. \"-\" .. ngx.config.subsystem\nend\n\n\nlocal can_yield_phases = {\n    ssl_session_fetch = true,\n    ssl_session_store = true,\n    rewrite = true,\n    access = true,\n    content = true,\n    timer = true\n}\n\nlocal stale_obj_pool = {}\n\nlocal GLOBAL_ITEMS_COUNT = 1024\nlocal GLOBAL_TTL         = 60 * 60          -- 60 min\nlocal PLUGIN_TTL         = 5 * 60           -- 5 min\nlocal PLUGIN_ITEMS_COUNT = 8\nlocal global_lru_fun\n\n\nlocal function fetch_valid_cache(lru_obj, invalid_stale, refresh_stale, item_ttl,\n                                 key, version, create_obj_fun, ...)\n    local obj, stale_obj = lru_obj:get(key)\n    if obj and obj.ver == version then\n        return obj\n    end\n\n    if stale_obj and stale_obj.ver == version then\n        if not invalid_stale then\n            lru_obj:set(key, stale_obj, item_ttl)\n            return stale_obj\n        end\n\n        if refresh_stale then\n            stale_obj_pool[lru_obj][key] = {\n                fn = create_obj_fun,\n                args = {...},\n                ver = version,\n                ttl = item_ttl,\n            }\n            return stale_obj\n        end\n    end\n\n    return nil\nend\n\n\nlocal function new_lru_fun(opts)\n    local item_count, item_ttl\n    if opts and opts.type == 'plugin' then\n        item_count = opts.count or PLUGIN_ITEMS_COUNT\n        item_ttl = opts.ttl or PLUGIN_TTL\n    else\n        item_count = opts and opts.count or GLOBAL_ITEMS_COUNT\n        item_ttl = opts and opts.ttl or GLOBAL_TTL\n    end\n\n    local invalid_stale = opts and opts.invalid_stale\n    local refresh_stale = opts and opts.refresh_stale\n    local serial_creating = opts and opts.serial_creating\n    local lru_obj = lru_new(item_count)\n\n    local neg_lru_obj\n    if opts and opts.neg_ttl and opts.neg_count then\n        neg_lru_obj = lru_new(opts.neg_count)\n    end\n\n    stale_obj_pool[lru_obj] = {}\n\n    return function (key, version, create_obj_fun, ...)\n        -- check negative cache first\n        if neg_lru_obj then\n            local neg_obj = neg_lru_obj:get(key)\n            if neg_obj and neg_obj.ver == version then\n                return nil, neg_obj.err\n            end\n        end\n\n        if not serial_creating or not can_yield_phases[get_phase()] then\n            local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, refresh_stale,\n                                item_ttl, key, version, create_obj_fun, ...)\n            if cache_obj then\n                return cache_obj.val\n            end\n\n            local obj, err = create_obj_fun(...)\n            if obj ~= nil then\n                lru_obj:set(key, {val = obj, ver = version}, item_ttl)\n            elseif neg_lru_obj then\n                -- cache the failure in negative cache\n                neg_lru_obj:set(key, {err = err, ver = version}, opts.neg_ttl)\n            end\n\n            return obj, err\n        end\n\n        local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, refresh_stale, item_ttl,\n                            key, version, create_obj_fun, ...)\n        if cache_obj then\n            return cache_obj.val\n        end\n\n        local lock, err = resty_lock:new(lock_shdict_name)\n        if not lock then\n            return nil, \"failed to create lock: \" .. err\n        end\n\n        local key_s = tostring(key)\n        log.info(\"try to lock with key \", key_s)\n\n        local elapsed, err = lock:lock(key_s)\n        if not elapsed then\n            return nil, \"failed to acquire the lock: \" .. err\n        end\n\n        cache_obj = fetch_valid_cache(lru_obj, invalid_stale, refresh_stale, item_ttl,\n                        key, version, create_obj_fun, ...)\n        if cache_obj then\n            lock:unlock()\n            log.info(\"unlock with key \", key_s)\n            return cache_obj.val\n        end\n\n        local obj, err = create_obj_fun(...)\n        if obj ~= nil then\n            lru_obj:set(key, {val = obj, ver = version}, item_ttl)\n        elseif neg_lru_obj then\n            -- cache the failure in negative cache\n            neg_lru_obj:set(key, {err = err, ver = version}, opts.neg_ttl)\n        end\n        lock:unlock()\n        log.info(\"unlock with key \", key_s)\n\n        return obj, err\n    end\nend\n\n\nglobal_lru_fun = new_lru_fun()\n\n\nlocal function plugin_ctx_key_and_ver(api_ctx, extra_key)\n    local key = api_ctx.conf_type .. \"#\" .. api_ctx.conf_id\n\n    if extra_key then\n        key = key .. \"#\" .. extra_key\n    end\n\n    return key, api_ctx.conf_version\nend\n\n---\n--  Cache some objects for plugins to avoid duplicate resources creation.\n--\n-- @function core.lrucache.plugin_ctx\n-- @tparam table lrucache LRUCache object instance.\n-- @tparam table api_ctx The request context.\n-- @tparam string extra_key Additional parameters for generating the lrucache identification key.\n-- @tparam function create_obj_func Functions for creating cache objects.\n-- If the object does not exist in the lrucache, this function is\n-- called to create it and cache it in the lrucache.\n-- @treturn table The object cached in lrucache.\n-- @usage\n-- local function create_obj() {\n-- --   create the object\n-- --   return the object\n-- }\n-- local obj, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_obj)\n-- -- obj is the object cached in lrucache\nlocal function plugin_ctx(lrucache, api_ctx, extra_key, create_obj_func, ...)\n    local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key)\n    return lrucache(key, ver, create_obj_func, ...)\nend\n\nlocal function plugin_ctx_id(api_ctx, extra_key)\n    local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key)\n    return key .. \"#\" .. ver\nend\n\n\nlocal function refresh_stale_objs()\n    for lru_obj, keys in pairs(stale_obj_pool) do\n        for key, new_obj in pairs(keys) do\n            local obj, err = new_obj.fn(unpack(new_obj.args))\n            if obj ~= nil then\n                lru_obj:set(key, {val = obj, ver = new_obj.ver}, new_obj.ttl)\n                keys[key] = nil\n                log.info(\"successfully refresh stale obj for key \",\n                            tostring(key), \" to ver \", new_obj.ver)\n            else\n                log.error(\"failed to refresh stale obj for key \", key, \": \", err)\n            end\n        end\n    end\nend\n\n\nlocal function init_worker()\n    local running = false\n    timer_every(1, function ()\n        if not exiting() then\n            if running then\n                log.info(\"timer_refresh_stale is already running, skipping this iteration\")\n                return\n            end\n            running = true\n            local ok, err = pcall(refresh_stale_objs)\n            if not ok then\n                log.error(\"failed to run timer_refresh_stale: \", err)\n            end\n            running = false\n        end\n    end)\nend\n\n\nlocal _M = {\n    version = 0.1,\n    init_worker = init_worker,\n    new = new_lru_fun,\n    global = global_lru_fun,\n    plugin_ctx = plugin_ctx,\n    plugin_ctx_id = plugin_ctx_id,\n}\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/math.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Common library about math\n--\n-- @module core.math\nlocal _M = {}\n\n\n---\n-- Calculate the greatest common divisor (GCD) of two numbers\n--\n-- @function core.math.gcd\n-- @tparam number a\n-- @tparam number b\n-- @treturn number the GCD of a and b\nlocal function gcd(a, b)\n    if b == 0 then\n        return a\n    end\n\n    return gcd(b, a % b)\nend\n_M.gcd = gcd\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/os.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- OS module.\n--\n-- @module core.os\n\nlocal ffi = require(\"ffi\")\nlocal ffi_str = ffi.string\nlocal ffi_errno = ffi.errno\nlocal C = ffi.C\nlocal ceil = math.ceil\nlocal floor = math.floor\nlocal error = error\nlocal tostring = tostring\nlocal type = type\n\n\nlocal _M = {}\nlocal WNOHANG = 1\n\n\nffi.cdef[[\n    typedef int32_t pid_t;\n    typedef unsigned int  useconds_t;\n\n    int setenv(const char *name, const char *value, int overwrite);\n    char *strerror(int errnum);\n\n    int usleep(useconds_t usec);\n    pid_t waitpid(pid_t pid, int *wstatus, int options);\n]]\n\n\nlocal function err()\n    return ffi_str(C.strerror(ffi_errno()))\nend\n\n---\n--  Sets the value of the environment variable.\n--\n-- @function core.os.setenv\n-- @tparam string name The name of environment variable.\n-- @tparam string value The value of environment variable.\n-- @treturn boolean Results of setting environment variables, true on success.\n-- @usage\n-- local ok, err = core.os.setenv(\"foo\", \"bar\")\nfunction _M.setenv(name, value)\n    local tv = type(value)\n    if type(name) ~= \"string\" or (tv ~= \"string\" and tv ~= \"number\") then\n        return false, \"invalid argument\"\n    end\n\n    value = tostring(value)\n    local ok = C.setenv(name, value, 1) == 0\n    if not ok then\n        return false, err()\n    end\n    return true\nend\n\n\n---\n--  sleep blockingly in microseconds\n--\n-- @function core.os.usleep\n-- @tparam number us The number of microseconds.\nlocal function usleep(us)\n    if ceil(us) ~= floor(us) then\n        error(\"bad microseconds: \" .. us)\n    end\n    C.usleep(us)\nend\n_M.usleep = usleep\n\n\nlocal function waitpid_nohang(pid)\n    local res = C.waitpid(pid, nil, WNOHANG)\n    if res == -1 then\n        return nil, err()\n    end\n    return res > 0\nend\n\n\nfunction _M.waitpid(pid, timeout)\n    local count = 0\n    local step = 1000 * 10\n    local total = timeout * 1000 * 1000\n    while step * count < total do\n        count = count + 1\n        usleep(step)\n        local ok, err = waitpid_nohang(pid)\n        if err then\n            return nil, err\n        end\n        if ok then\n            return true\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/profile.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Profile module.\n--\n-- @module core.profile\n\nlocal util = require(\"apisix.cli.util\")\n\nlocal _M = {\n    version = 0.1,\n    profile = os.getenv(\"APISIX_PROFILE\") or \"\",\n    apisix_home = (ngx and ngx.config.prefix()) or \"\"\n}\n\n---\n--  Get yaml file path by filename under the `conf/`.\n--\n-- @function core.profile.yaml_path\n-- @tparam self self The profile module itself.\n-- @tparam string file_name Name of the yaml file to search.\n-- @treturn string The path of yaml file searched.\n-- @usage\n-- local profile = require(\"apisix.core.profile\")\n-- ......\n-- -- set the working directory of APISIX\n-- profile.apisix_home = env.apisix_home .. \"/\"\n-- local local_conf_path = profile:yaml_path(\"config\")\nfunction _M.yaml_path(self, file_name)\n    local file_path = self.apisix_home  .. \"conf/\" .. file_name\n    if self.profile ~= \"\" and file_name ~= \"config-default\" then\n        file_path = file_path .. \"-\" .. self.profile\n    end\n\n    return file_path .. \".yaml\"\nend\n\n\nfunction _M.customized_yaml_index(self)\n    return self.apisix_home .. \"/conf/.customized_config_path\"\nend\n\n\nfunction _M.customized_yaml_path(self)\n    local customized_config_index = self:customized_yaml_index()\n    if util.file_exists(customized_config_index) then\n        return util.read_file(customized_config_index)\n    end\n    return nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/pubsub.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Extensible framework to support publish-and-subscribe scenarios\n--\n-- @module core.pubsub\n\nlocal log          = require(\"apisix.core.log\")\nlocal ws_server    = require(\"resty.websocket.server\")\nlocal protoc       = require(\"protoc\")\nlocal pb           = require(\"pb\")\nlocal ngx          = ngx\nlocal setmetatable = setmetatable\nlocal pcall        = pcall\n\n\nlocal _M = { version = 0.1 }\nlocal mt = { __index = _M }\n\nlocal pb_state\nlocal function init_pb_state()\n    -- clear current pb state\n    local old_pb_state = pb.state(nil)\n\n    -- set int64 rule for pubsub module\n    pb.option(\"int64_as_string\")\n\n    -- initialize protoc compiler\n    protoc.reload()\n    local pubsub_protoc = protoc.new()\n    pubsub_protoc:addpath(ngx.config.prefix() .. \"apisix/include/apisix/model\")\n    local ok, err = pcall(pubsub_protoc.loadfile, pubsub_protoc, \"pubsub.proto\")\n    if not ok then\n        pubsub_protoc:reset()\n        pb.state(old_pb_state)\n        return \"failed to load pubsub protocol: \" .. err\n    end\n\n    pb_state = pb.state(old_pb_state)\nend\n\n\n-- parse command name and parameters from client message\nlocal function get_cmd(data)\n    -- There are sequence and command properties in the data,\n    -- select the handler according to the command value.\n    local key = data.req\n    return key, data[key]\nend\n\n\n-- send generic response to client\nlocal function send_resp(ws, sequence, data)\n    data.sequence = sequence\n    -- only restore state if it has changed\n    if pb_state ~= pb.state() then\n        pb.state(pb_state)\n    end\n    local ok, encoded = pcall(pb.encode, \"PubSubResp\", data)\n    if not ok or not encoded then\n        log.error(\"failed to encode response message, err: \", encoded)\n        return\n    end\n\n    local _, err = ws:send_binary(encoded)\n    if err then\n        log.error(\"failed to send response to client, err: \", err)\n    end\nend\n\n\n-- send error response to client\nlocal function send_error(ws, sequence, err_msg)\n    return send_resp(ws, sequence, {\n        error_resp = {\n            code = 0,\n            message = err_msg,\n        },\n    })\nend\n\n\n---\n-- Create pubsub module instance\n--\n-- @function core.pubsub.new\n-- @treturn pubsub module instance\n-- @treturn string|nil error message if present\n-- @usage\n-- local pubsub, err = core.pubsub.new()\nfunction _M.new()\n    if not pb_state then\n        local err = init_pb_state()\n        if err then\n            return nil, err\n        end\n    end\n\n    local ws, err = ws_server:new()\n    if not ws then\n        return nil, err\n    end\n\n    local obj = setmetatable({\n        ws_server = ws,\n        cmd_handler = {},\n    }, mt)\n\n    -- add default ping handler\n    obj:on(\"cmd_ping\", function (params)\n        return { pong_resp = params }\n    end)\n\n    return obj\nend\n\n\n---\n-- Add command callbacks to pubsub module instances\n--\n-- The callback function prototype: function (params)\n-- The params in the parameters contain the data defined in the requested command.\n-- Its first return value is the data, which needs to contain the data needed for\n-- the particular resp, returns nil if an error exists.\n-- Its second return value is a string type error message, no need to return when\n-- no error exists.\n--\n-- @function core.pubsub.on\n-- @tparam string command The command to add callback.\n-- @tparam func handler The callback function on receipt of command.\n-- @usage\n-- pubsub:on(command, function (params)\n--     return data, err\n-- end)\nfunction _M.on(self, command, handler)\n    self.cmd_handler[command] = handler\nend\n\n\n---\n-- Put the pubsub instance into an event loop, waiting to process client commands\n--\n-- @function core.pubsub.wait\n-- @usage\n-- local err = pubsub:wait()\nfunction _M.wait(self)\n    local fatal_err\n    local ws = self.ws_server\n    while true do\n        -- read raw data frames from websocket connection\n        local raw_data, raw_type, err = ws:recv_frame()\n        if err then\n            -- terminate the event loop when a fatal error occurs\n            if ws.fatal then\n                fatal_err = err\n                break\n            end\n\n            -- skip this loop for non-fatal errors\n            log.error(\"failed to receive websocket frame: \", err)\n            goto continue\n        end\n\n        -- handle client close connection\n        if raw_type == \"close\" then\n            break\n        end\n\n        -- the pubsub messages use binary, if the message is not\n        -- binary, skip this message\n        if raw_type ~= \"binary\" then\n            log.warn(\"pubsub server receive non-binary data, type: \",\n                raw_type, \", data: \", raw_data)\n            goto continue\n        end\n\n        -- only recover state if it has changed\n        if pb.state() ~= pb_state then\n            pb.state(pb_state)\n        end\n        local data, err = pb.decode(\"PubSubReq\", raw_data)\n        if not data then\n            log.error(\"pubsub server receives undecodable data, err: \", err)\n            send_error(ws, 0, \"wrong command\")\n            goto continue\n        end\n\n        -- command sequence code\n        local sequence = data.sequence\n\n        local cmd, params = get_cmd(data)\n        if not cmd and not params then\n            log.warn(\"pubsub server receives empty command\")\n            goto continue\n        end\n\n        -- find the handler for the current command\n        local handler = self.cmd_handler[cmd]\n        if not handler then\n            log.error(\"pubsub callback handler not registered for the\",\n                \" command, command: \", cmd)\n            send_error(ws, sequence, \"unknown command\")\n            goto continue\n        end\n\n        -- call command handler to generate response data\n        local resp, err = handler(params)\n        if not resp then\n            send_error(ws, sequence, err)\n            goto continue\n        end\n        send_resp(ws, sequence, resp)\n\n        ::continue::\n    end\n\n    if fatal_err then\n        log.error(\"fatal error in pubsub websocket server, err: \", fatal_err)\n    end\n    ws:send_close()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/request.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Get or set the information of the client request.\n--\n-- @module core.request\n\nlocal lfs = require(\"lfs\")\nlocal log = require(\"apisix.core.log\")\nlocal json = require(\"apisix.core.json\")\nlocal io = require(\"apisix.core.io\")\nlocal req_add_header\nif ngx.config.subsystem == \"http\" then\n    local ngx_req = require \"ngx.req\"\n    req_add_header = ngx_req.add_header\nend\nlocal is_apisix_or, a6_request = pcall(require, \"resty.apisix.request\")\nlocal ngx = ngx\nlocal get_headers = ngx.req.get_headers\nlocal clear_header = ngx.req.clear_header\nlocal tonumber  = tonumber\nlocal error     = error\nlocal type      = type\nlocal str_fmt   = string.format\nlocal str_lower = string.lower\nlocal req_read_body = ngx.req.read_body\nlocal req_get_body_data = ngx.req.get_body_data\nlocal req_get_body_file = ngx.req.get_body_file\nlocal req_get_post_args = ngx.req.get_post_args\nlocal req_get_uri_args = ngx.req.get_uri_args\nlocal req_set_uri_args = ngx.req.set_uri_args\nlocal table_insert = table.insert\nlocal req_set_header = ngx.req.set_header\n\n\nlocal _M = {}\n\n\nlocal function _headers(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n\n    if not is_apisix_or then\n        return get_headers()\n    end\n\n    if a6_request.is_request_header_set() then\n        a6_request.clear_request_header()\n        ctx.headers = get_headers()\n    end\n\n    local headers = ctx.headers\n    if not headers then\n        headers = get_headers()\n        ctx.headers = headers\n    end\n\n    return headers\nend\n\nlocal function _validate_header_name(name)\n    local tname = type(name)\n    if tname ~= \"string\" then\n        return nil, str_fmt(\"invalid header name %q: got %s, \" ..\n                \"expected string\", name, tname)\n    end\n\n    return name\nend\n\n---\n-- Returns all headers of the current request.\n-- The name and value of the header in return table is in lower case.\n--\n-- @function core.request.headers\n-- @tparam table ctx The context of the current request.\n-- @treturn table all headers\n-- @usage\n-- local headers = core.request.headers(ctx)\n_M.headers = _headers\n\n---\n-- Returns the value of the header with the specified name.\n--\n-- @function core.request.header\n-- @tparam table ctx The context of the current request.\n-- @tparam string name The header name, example: \"Content-Type\".\n-- @treturn string|nil the value of the header, or nil if not found.\n-- @usage\n-- -- You can use upper case for header \"Content-Type\" here to get the value.\n-- local content_type = core.request.header(ctx, \"Content-Type\") -- \"application/json\"\nfunction _M.header(ctx, name)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n\n    local value = _headers(ctx)[name]\n    return type(value) == \"table\" and value[1] or value\nend\n\nlocal function modify_header(ctx, header_name, header_value, override)\n    if type(ctx) == \"string\" then\n        -- It would be simpler to keep compatibility if we put 'ctx'\n        -- after 'header_value', but the style is too ugly!\n        header_value = header_name\n        header_name = ctx\n        ctx = nil\n\n        if override then\n            log.warn(\"DEPRECATED: use set_header(ctx, header_name, header_value) instead\")\n        else\n            log.warn(\"DEPRECATED: use add_header(ctx, header_name, header_value) instead\")\n        end\n    end\n\n    local err\n    header_name, err = _validate_header_name(header_name)\n    if err then\n        error(err)\n    end\n\n    local changed = false\n    if is_apisix_or then\n        changed = a6_request.is_request_header_set()\n    end\n\n    if override then\n        req_set_header(header_name, header_value)\n    else\n        req_add_header(header_name, header_value)\n    end\n\n    if ctx and ctx.var then\n        -- when the header is updated, clear cache of ctx.var\n        ctx.var[\"http_\" .. str_lower(header_name)] = nil\n    end\n\n    if is_apisix_or and not changed then\n        -- if the headers are not changed before,\n        -- we can only update part of the cache instead of invalidating the whole\n        a6_request.clear_request_header()\n        if ctx and ctx.headers then\n            if override or not ctx.headers[header_name] then\n                ctx.headers[header_name] = header_value\n            else\n                local values = ctx.headers[header_name]\n                if type(values) == \"table\" then\n                    table_insert(values, header_value)\n                else\n                    ctx.headers[header_name] = {values, header_value}\n                end\n            end\n        end\n    end\nend\n\nfunction _M.set_header(ctx, header_name, header_value)\n    modify_header(ctx, header_name, header_value, true)\nend\n\nfunction _M.add_header(ctx, header_name, header_value)\n    modify_header(ctx, header_name, header_value, false)\nend\n\n-- return the remote address of client which directly connecting to APISIX.\n-- so if there is a load balancer between downstream client and APISIX,\n-- this function will return the ip of load balancer.\nfunction _M.get_ip(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n    return ctx.var.realip_remote_addr or ctx.var.remote_addr or ''\nend\n\n\n-- get remote address of downstream client,\n-- in cases there is a load balancer between downstream client and APISIX.\nfunction _M.get_remote_client_ip(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n    return ctx.var.remote_addr or ''\nend\n\n\nfunction _M.get_remote_client_port(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n    return tonumber(ctx.var.remote_port)\nend\n\n\nfunction _M.get_uri_args(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n\n    if not ctx.req_uri_args then\n        -- use 0 to avoid truncated result and keep the behavior as the\n        -- same as other platforms\n        local args = req_get_uri_args(0)\n        ctx.req_uri_args = args\n    end\n\n    return ctx.req_uri_args\nend\n\n\nfunction _M.set_uri_args(ctx, args)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n\n    ctx.req_uri_args = nil\n    return req_set_uri_args(args)\nend\n\n\nfunction _M.get_post_args(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n\n    if not ctx.req_post_args then\n        req_read_body()\n\n        -- use 0 to avoid truncated result and keep the behavior as the\n        -- same as other platforms\n        local args, err = req_get_post_args(0)\n        if not args then\n            -- do we need a way to handle huge post forms?\n            log.error(\"the post form is too large: \", err)\n            args = {}\n        end\n        ctx.req_post_args = args\n    end\n\n    return ctx.req_post_args\nend\n\n\nlocal function check_size(size, max_size)\n    if max_size and size > max_size then\n        return nil, \"request size \" .. size .. \" is greater than the \"\n                    .. \"maximum size \" .. max_size .. \" allowed\"\n    end\n\n    return true\nend\n\n\nlocal function test_expect(var)\n    local expect = var.http_expect\n    return expect and str_lower(expect) == \"100-continue\"\nend\n\n\nfunction _M.get_body(max_size, ctx)\n    if max_size then\n        local var = ctx and ctx.var or ngx.var\n        local content_length = tonumber(var.http_content_length)\n        if content_length then\n            local ok, err = check_size(content_length, max_size)\n            if not ok then\n                -- When client_max_body_size is exceeded, Nginx will set r->expect_tested = 1 to\n                -- avoid sending the 100 CONTINUE.\n                -- We use trick below to imitate this behavior.\n                if test_expect(var) then\n                    clear_header(\"expect\")\n                end\n\n                return nil, err\n            end\n        end\n    end\n\n    -- check content-length header for http2/http3\n    do\n        local var = ctx and ctx.var or ngx.var\n        local content_length = tonumber(var.http_content_length)\n        if (var.server_protocol == \"HTTP/2.0\" or var.server_protocol == \"HTTP/3.0\")\n            and not content_length then\n            return nil, \"HTTP2/HTTP3 request without a Content-Length header\"\n        end\n    end\n    req_read_body()\n\n    local req_body = req_get_body_data()\n    if req_body then\n        local ok, err = check_size(#req_body, max_size)\n        if not ok then\n            return nil, err\n        end\n\n        return req_body\n    end\n\n    local file_name = req_get_body_file()\n    if not file_name then\n        return nil\n    end\n\n    log.info(\"attempt to read body from file: \", file_name)\n\n    if max_size then\n        local size, err = lfs.attributes (file_name, \"size\")\n        if not size then\n            return nil, err\n        end\n\n        local ok, err = check_size(size, max_size)\n        if not ok then\n            return nil, err\n        end\n    end\n\n    local req_body, err = io.get_file(file_name)\n    return req_body, err\nend\n\n\nfunction _M.get_json_request_body_table()\n    local body, err = _M.get_body()\n    if not body then\n        return nil, { message = \"could not get body: \" .. (err or \"request body is empty\") }\n    end\n\n    local body_tab, err = json.decode(body)\n    if not body_tab then\n        return nil, { message = \"could not get parse JSON request body: \" .. err }\n    end\n\n    return body_tab\nend\n\n\nfunction _M.get_scheme(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n    return ctx.var.scheme or ''\nend\n\n\nfunction _M.get_host(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n    return ctx.var.host or ''\nend\n\n\nfunction _M.get_port(ctx)\n    if not ctx then\n        ctx = ngx.ctx.api_ctx\n    end\n    return tonumber(ctx.var.server_port)\nend\n\n\n_M.get_http_version = ngx.req.http_version\n\n\n_M.get_method = ngx.req.get_method\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/resolver.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Domain Resolver.\n--\n-- @module core.resolver\n\nlocal json           = require(\"apisix.core.json\")\nlocal log            = require(\"apisix.core.log\")\nlocal utils          = require(\"apisix.core.utils\")\nlocal dns_utils      = require(\"resty.dns.utils\")\nlocal config_local   = require(\"apisix.core.config_local\")\n\n\nlocal HOSTS_IP_MATCH_CACHE = {}\n\n\nlocal _M = {}\n\n\nlocal function init_hosts_ip()\n    local hosts, err = dns_utils.parseHosts()\n    if not hosts then\n        return hosts, err\n    end\n    HOSTS_IP_MATCH_CACHE = hosts\nend\n\n\nfunction _M.init_resolver(args)\n    --  initialize /etc/hosts\n    init_hosts_ip()\n\n    local dns_resolver = args and args[\"dns_resolver\"]\n    utils.set_resolver(dns_resolver)\n    log.info(\"dns resolver \", json.delay_encode(dns_resolver, true))\nend\n\n---\n--  Resolve domain name to ip.\n--\n-- @function core.resolver.parse_domain\n-- @tparam string host Domain name that need to be resolved.\n-- @treturn string The IP of the domain name after being resolved.\n-- @usage\n-- local ip, err = core.resolver.parse_domain(\"apache.org\") -- \"198.18.10.114\"\nfunction _M.parse_domain(host)\n    local rev = HOSTS_IP_MATCH_CACHE[host]\n    local enable_ipv6 = config_local.local_conf().apisix.enable_ipv6\n    if rev then\n        -- use ipv4 in high priority\n        local ip = rev[\"ipv4\"]\n        if enable_ipv6 and not ip then\n            ip = rev[\"ipv6\"]\n        end\n        if ip then\n            -- meet test case\n            log.info(\"dns resolve \", host, \", result: \", json.delay_encode(ip))\n            log.info(\"dns resolver domain: \", host, \" to \", ip)\n            return ip\n        end\n    end\n\n    local ip_info, err = utils.dns_parse(host)\n    if not ip_info then\n        log.error(\"failed to parse domain: \", host, \", error: \",err)\n        return nil, err\n    end\n\n    log.info(\"parse addr: \", json.delay_encode(ip_info))\n    log.info(\"resolver: \", json.delay_encode(utils.get_resolver()))\n    log.info(\"host: \", host)\n    if ip_info.address then\n        log.info(\"dns resolver domain: \", host, \" to \", ip_info.address)\n        return ip_info.address\n    end\n\n    return nil, \"failed to parse domain\"\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/response.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Get the information form upstream response, or set the information to client response.\n--\n-- @module core.response\n\nlocal tracer = require(\"apisix.tracer\")\nlocal encode_json = require(\"cjson.safe\").encode\nlocal ngx = ngx\nlocal arg = ngx.arg\nlocal ngx_print = ngx.print\nlocal ngx_header = ngx.header\nlocal ngx_add_header\nif ngx.config.subsystem == \"http\" then\n    local ngx_resp = require \"ngx.resp\"\n    ngx_add_header = ngx_resp.add_header\nend\n\nlocal error = error\nlocal select = select\nlocal type = type\nlocal ngx_exit = ngx.exit\nlocal concat_tab = table.concat\nlocal str_sub = string.sub\nlocal tonumber = tonumber\nlocal clear_tab = require(\"table.clear\")\nlocal pairs = pairs\n\nlocal _M = {version = 0.1}\n\n\nlocal resp_exit\ndo\n    local t = {}\n    local idx = 1\n\nfunction resp_exit(code, ...)\n    clear_tab(t)\n    idx = 0\n\n    if code and type(code) ~= \"number\" then\n        idx = idx + 1\n        t[idx] = code\n        code = nil\n    end\n\n    if code then\n        ngx.status = code\n    end\n\n    for i = 1, select('#', ...) do\n        local v = select(i, ...)\n        if type(v) == \"table\" then\n            local body, err = encode_json(v)\n            if err then\n                error(\"failed to encode data: \" .. err, -2)\n            else\n                idx = idx + 1\n                t[idx] = body\n                idx = idx + 1\n                t[idx] = \"\\n\"\n            end\n\n        elseif v ~= nil then\n            idx = idx + 1\n            t[idx] = v\n        end\n    end\n\n    if idx > 0 then\n        ngx_print(t)\n    end\n\n    if code then\n        if code >= 400 then\n            tracer.finish_all(ngx.ctx, tracer.status.ERROR, \"response code \" .. code)\n        end\n        return ngx_exit(code)\n    end\nend\n\nend -- do\n_M.exit = resp_exit\n\n\nfunction _M.say(...)\n    resp_exit(nil, ...)\nend\n\n\nlocal function set_header(append, ...)\n    if ngx.headers_sent then\n      error(\"headers have already been sent\", 2)\n    end\n\n    local count = select('#', ...)\n    if count == 1 then\n        local headers = select(1, ...)\n        if type(headers) ~= \"table\" then\n            -- response.set_header(name, nil)\n            ngx_header[headers] = nil\n            return\n        end\n\n        for k, v in pairs(headers) do\n            if append then\n                ngx_add_header(k, v)\n            else\n                ngx_header[k] = v\n            end\n        end\n\n        return\n    end\n\n    for i = 1, count, 2 do\n        if append then\n            ngx_add_header(select(i, ...), select(i + 1, ...))\n        else\n            ngx_header[select(i, ...)] = select(i + 1, ...)\n        end\n    end\nend\n\n\nfunction _M.set_header(...)\n    set_header(false, ...)\nend\n\n---\n-- Add a header to the client response.\n--\n-- @function core.response.add_header\n-- @usage\n-- core.response.add_header(\"Apisix-Plugins\", \"no plugin\")\nfunction _M.add_header(...)\n    set_header(true, ...)\nend\n\n\nfunction _M.get_upstream_status(ctx)\n    -- $upstream_status maybe including multiple status, only need the last one\n    return tonumber(str_sub(ctx.var.upstream_status or \"\", -3))\nend\n\n\nfunction _M.clear_header_as_body_modified()\n    ngx.header.content_length = nil\n    -- in case of upstream content is compressed content\n    ngx.header.content_encoding = nil\n\n    -- clear cache identifier\n    ngx.header.last_modified = nil\n    ngx.header.etag = nil\nend\n\n\n-- Hold body chunks and return the final body once all chunks have been read.\n-- Usage:\n-- function _M.body_filter(conf, ctx)\n--  local final_body = core.response.hold_body_chunk(ctx)\n--  if not final_body then\n--      return\n--  end\n--  final_body = transform(final_body)\n--  ngx.arg[1] = final_body\n--  ...\nfunction _M.hold_body_chunk(ctx, hold_the_copy, max_resp_body_bytes)\n    local body_buffer\n    local chunk, eof = arg[1], arg[2]\n\n    if not ctx._body_buffer then\n        ctx._body_buffer = {}\n    end\n\n    if type(chunk) == \"string\" and chunk ~= \"\" then\n        body_buffer = ctx._body_buffer[ctx._plugin_name]\n        if not body_buffer then\n            body_buffer = {\n                chunk,\n                n = 1\n            }\n            ctx._body_buffer[ctx._plugin_name] = body_buffer\n            ctx._resp_body_bytes = #chunk\n        else\n            local n = body_buffer.n + 1\n            body_buffer.n = n\n            body_buffer[n] = chunk\n            ctx._resp_body_bytes = ctx._resp_body_bytes + #chunk\n        end\n        if max_resp_body_bytes and ctx._resp_body_bytes >= max_resp_body_bytes then\n            local body_data = concat_tab(body_buffer, \"\", 1, body_buffer.n)\n            body_data = str_sub(body_data, 1, max_resp_body_bytes)\n            return body_data\n        end\n    end\n\n    if eof then\n        body_buffer = ctx._body_buffer[ctx._plugin_name]\n        if not body_buffer then\n            if max_resp_body_bytes and #chunk >= max_resp_body_bytes then\n                chunk = str_sub(chunk, 1, max_resp_body_bytes)\n            end\n            return chunk\n        end\n\n        local body_data = concat_tab(body_buffer, \"\", 1, body_buffer.n)\n        ctx._body_buffer[ctx._plugin_name] = nil\n        return body_data\n    end\n\n    if not hold_the_copy then\n        -- flush the origin body chunk\n        arg[1] = nil\n    end\n    return nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Json schema validation module.\n--\n-- @module core.schema\n\nlocal jsonschema = require('jsonschema')\nlocal lrucache = require(\"apisix.core.lrucache\")\nlocal schema_def = require(\"apisix.schema_def\")\nlocal cached_validator = lrucache.new({count = 1000, ttl = 0})\nlocal pcall = pcall\nlocal error = error\n\nlocal _M = {\n    version = 0.3,\n\n    TYPE_CONSUMER = 1,\n    TYPE_METADATA = 2,\n}\n\n\nlocal function create_validator(schema)\n    -- local code = jsonschema.generate_validator_code(schema, opts)\n    -- local file2=io.output(\"/tmp/2.txt\")\n    -- file2:write(code)\n    -- file2:close()\n    local ok, res = pcall(jsonschema.generate_validator, schema)\n    if ok then\n        return res\n    end\n\n    return nil, res -- error message\nend\n\nlocal function get_validator(schema)\n    local validator, err = cached_validator(schema, nil,\n                                create_validator, schema)\n\n    if not validator then\n        return nil, err\n    end\n\n    return validator, nil\nend\n\nfunction _M.check(schema, json)\n    local validator, err = get_validator(schema)\n\n    if not validator then\n        return false, err\n    end\n\n    return validator(json)\nend\n\n_M.valid = get_validator\n\nsetmetatable(_M, {\n    __index = schema_def,\n    __newindex = function() error(\"no modification allowed\") end,\n})\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/string.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Wrapped string module.\n--\n-- @module core.string\n\nlocal error = error\nlocal type = type\nlocal str_byte = string.byte\nlocal str_find = string.find\nlocal ffi         = require(\"ffi\")\nlocal C           = ffi.C\nlocal ffi_cast    = ffi.cast\nlocal ngx         = ngx\nlocal ngx_decode_args  = ngx.decode_args\nlocal ngx_encode_args  = ngx.encode_args\n\n\nffi.cdef[[\n    int memcmp(const void *s1, const void *s2, size_t n);\n]]\n\n\nlocal _M = {\n    version = 0.1,\n}\n\n\nsetmetatable(_M, {__index = string})\n\n\n-- find a needle from a haystack in the plain text way\n-- note: Make sure that the haystack is 'string' type, otherwise an exception will be thrown.\nfunction _M.find(haystack, needle, from)\n    return str_find(haystack, needle, from or 1, true)\nend\n\n---\n--  Tests whether the string s begins with prefix.\n--\n-- @function core.string.has_prefix\n-- @tparam string s The string being tested.\n-- @tparam string prefix Specify the prefix.\n-- @treturn boolean Test result, true means the string s begins with prefix.\n-- @usage\n-- local res = core.string.has_prefix(\"/apisix/admin/routes\", \"/apisix/\") -- true\nfunction _M.has_prefix(s, prefix)\n    if type(s) ~= \"string\" or type(prefix) ~= \"string\" then\n        error(\"unexpected type: s:\" .. type(s) .. \", prefix:\" .. type(prefix))\n    end\n    if #s < #prefix then\n        return false\n    end\n    local rc = C.memcmp(s, prefix, #prefix)\n    return rc == 0\nend\n\n\nfunction _M.has_suffix(s, suffix)\n    if type(s) ~= \"string\" or type(suffix) ~= \"string\" then\n        error(\"unexpected type: s:\" .. type(s) .. \", suffix:\" .. type(suffix))\n    end\n    if #s < #suffix then\n        return false\n    end\n    local rc = C.memcmp(ffi_cast(\"char *\", s) + #s - #suffix, suffix, #suffix)\n    return rc == 0\nend\n\n\nfunction _M.rfind_char(s, ch, idx)\n    local b = str_byte(ch)\n    for i = idx or #s, 1, -1 do\n        if str_byte(s, i, i) == b then\n            return i\n        end\n    end\n    return nil\nend\n\n\n-- reduce network consumption by compressing string indentation\n-- this method should be used with caution\n-- it will remove the spaces at the beginning of each line\n-- and remove the spaces after `,` character\nfunction _M.compress_script(s)\n    s = ngx.re.gsub(s, [[^\\s+]], \"\", \"mjo\")\n    s = ngx.re.gsub(s, [[,\\s+]], \",\", \"mjo\")\n    return s\nend\n\n\n---\n-- Decodes a URI encoded query-string into a Lua table.\n-- All request arguments received will be decoded by default.\n--\n-- @function core.string.decode_args\n-- @tparam string args A URI encoded query-string.\n-- @treturn table the value of decoded query-string.\n-- @usage\n-- local args, err = core.string.decode_args(\"a=1&b=2\") -- {a=1, b=2}\nfunction _M.decode_args(args)\n    -- use 0 to avoid truncated result and keep the behavior as the\n    -- same as other platforms\n    return ngx_decode_args(args, 0)\nend\n\n\n---\n-- Encode the Lua table to a query args string according to the URI encoded rules.\n--\n-- @function core.string.encode_args\n-- @tparam table args The query args Lua table.\n-- @treturn string the value of query args string.\n-- @usage\n-- local str = core.string.encode_args({a=1, b=2}) -- \"a=1&b=2\"\nfunction _M.encode_args(args)\n    return ngx_encode_args(args)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/table.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Wrapped table module.\n--\n-- @module core.table\n\nlocal newproxy     = newproxy\nlocal getmetatable = getmetatable\nlocal setmetatable = setmetatable\nlocal select       = select\nlocal tostring     = tostring\nlocal new_tab      = require(\"table.new\")\nlocal nkeys        = require(\"table.nkeys\")\nlocal ipairs       = ipairs\nlocal pairs        = pairs\nlocal type         = type\nlocal ngx_re       = require(\"ngx.re\")\nlocal isarray      = require(\"table.isarray\")\n\n\nlocal _M = {\n    version = 0.2,\n    new     = new_tab,\n    clear   = require(\"table.clear\"),\n    nkeys   = nkeys,\n    insert  = table.insert,\n    concat  = table.concat,\n    sort    = table.sort,\n    clone   = require(\"table.clone\"),\n    isarray = require(\"table.isarray\"),\n    isempty = require(\"table.isempty\"),\n}\n\n\nsetmetatable(_M, {__index = table})\n\n\nfunction _M.insert_tail(tab, ...)\n    local idx = #tab\n    for i = 1, select('#', ...) do\n        idx = idx + 1\n        tab[idx] = select(i, ...)\n    end\n\n    return idx\nend\n\n\nfunction _M.set(tab, ...)\n    for i = 1, select('#', ...) do\n        tab[i] = select(i, ...)\n    end\nend\n\n\nfunction _M.try_read_attr(tab, ...)\n    local count = select('#', ...)\n\n    for i = 1, count do\n        local attr = select(i, ...)\n        if type(tab) ~= \"table\" then\n            return nil\n        end\n\n        tab = tab[attr]\n    end\n\n    return tab\nend\n\n---\n--  Test if an element exists in an array.\n--\n-- @function core.table.array_find\n-- @tparam table array The tested array.\n-- @tparam string val The tested value.\n-- @treturn number The index of tested value.\n-- @usage\n-- local arr = {\"a\", \"b\", \"c\"}\n-- local idx = core.table.array_find(arr, \"b\") -- idx = 2\nlocal function array_find(array, val)\n    if type(array) ~= \"table\" then\n        return nil\n    end\n\n    for i, v in ipairs(array) do\n        if v == val then\n            return i\n        end\n    end\n\n    return nil\nend\n_M.array_find = array_find\n\n\n-- only work under lua51 or luajit\nfunction _M.setmt__gc(t, mt)\n    local prox = newproxy(true)\n    getmetatable(prox).__gc = function() mt.__gc(t) end\n    t[prox] = true\n    return setmetatable(t, mt)\nend\n\n\nlocal deepcopy\ndo\n    local function _deepcopy(orig, copied, parent, opts)\n        -- If the array-like table contains nil in the middle,\n        -- the len might be smaller than the expected.\n        -- But it doesn't affect the correctness.\n        local len = #orig\n        local copy = new_tab(len, nkeys(orig) - len)\n        -- prevent infinite loop when a field refers its parent\n        copied[orig] = copy\n        for orig_key, orig_value in pairs(orig) do\n            local path = parent .. \".\" .. tostring(orig_key)\n            if opts and array_find(opts.shallows, path) then\n                copy[orig_key] = orig_value\n            else\n                if type(orig_value) == \"table\" then\n                    if copied[orig_value] then\n                        copy[orig_key] = copied[orig_value]\n                    else\n                        copy[orig_key] = _deepcopy(orig_value, copied, path, opts)\n                    end\n                else\n                    copy[orig_key] = orig_value\n                end\n            end\n        end\n\n        local mt = getmetatable(orig)\n        if mt ~= nil then\n            setmetatable(copy, mt)\n        end\n\n        return copy\n    end\n\n\n    local copied_recorder = {}\n\n    function deepcopy(orig, opts)\n        local orig_type = type(orig)\n        if orig_type ~= 'table' then\n            return orig\n        end\n\n        local res = _deepcopy(orig, copied_recorder, \"self\", opts)\n        _M.clear(copied_recorder)\n        return res\n    end\nend\n_M.deepcopy = deepcopy\n\n\nlocal ngx_null = ngx.null\nlocal function merge(origin, extend)\n    for k,v in pairs(extend) do\n        if type(v) == \"table\" then\n            if type(origin[k] or false) == \"table\" then\n                if isarray(origin[k]) or isarray(v) then\n                    origin[k] = v\n                else\n                    merge(origin[k] or {}, extend[k] or {})\n                end\n            else\n                origin[k] = v\n            end\n        elseif v == ngx_null then\n            origin[k] = nil\n        else\n            origin[k] = v\n        end\n    end\n\n    return origin\nend\n_M.merge = merge\n\n\nlocal function patch(node_value, sub_path, conf)\n    local sub_value = node_value\n    local sub_paths = ngx_re.split(sub_path, \"/\")\n    for i = 1, #sub_paths - 1 do\n        local sub_name = sub_paths[i]\n        if sub_value[sub_name] == nil then\n            sub_value[sub_name] = {}\n        end\n\n        sub_value = sub_value[sub_name]\n\n        if type(sub_value) ~= \"table\" then\n            return 400, \"invalid sub-path: /\"\n                      .. _M.concat(sub_paths, 1, i)\n        end\n    end\n\n    if type(sub_value) ~= \"table\" then\n        return 400, \"invalid sub-path: /\" .. sub_path\n    end\n\n    local sub_name = sub_paths[#sub_paths]\n    if sub_name and sub_name ~= \"\" then\n        sub_value[sub_name] = conf\n    else\n        node_value = conf\n    end\n\n    return nil, nil, node_value\nend\n_M.patch = patch\n\n\n-- Compare two tables as if they are sets (only compare the key part)\nfunction _M.set_eq(a, b)\n    if nkeys(a) ~= nkeys(b) then\n        return false\n    end\n\n    for k in pairs(a) do\n        if b[k] == nil then\n            return false\n        end\n    end\n\n    return true\nend\n\n\n-- Compare two elements, including their descendants\nlocal function deep_eq(a, b)\n    local type_a = type(a)\n    local type_b = type(b)\n\n    if type_a ~= 'table' or type_b ~= 'table' then\n        return a == b\n    end\n\n    local n_a = nkeys(a)\n    local n_b = nkeys(b)\n    if n_a ~= n_b then\n        return false\n    end\n\n    for k, v_a in pairs(a) do\n        local v_b = b[k]\n        local eq = deep_eq(v_a, v_b)\n        if not eq then\n            return false\n        end\n    end\n\n    return true\nend\n_M.deep_eq = deep_eq\n\n\n-- pick takes the given attributes out of object\nfunction _M.pick(obj, attrs)\n    local data = {}\n    for k, v in pairs(obj) do\n        if attrs[k] ~= nil then\n            data[k] = v\n        end\n    end\n\n    return data\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/timer.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Wrapped timer module, can cancel the running timers.\n--\n-- @module core.timer\n\nlocal log = require(\"apisix.core.log\")\nlocal sleep = require(\"apisix.core.utils\").sleep\nlocal timer_every = ngx.timer.every\nlocal timer_at = ngx.timer.at\nlocal update_time = ngx.update_time\nlocal now = ngx.now\nlocal pcall = pcall\n\n\nlocal _M = {\n    version = 0.1,\n}\n\n\nlocal function _internal(timer)\n    timer.start_time = now()\n\n    repeat\n        local ok, err = pcall(timer.callback_fun)\n        if not ok then\n            log.error(\"failed to run the timer: \", timer.name, \" err: \", err)\n\n            if timer.sleep_fail > 0 then\n                sleep(timer.sleep_fail)\n            end\n\n        elseif timer.sleep_succ > 0 then\n            sleep(timer.sleep_succ)\n        end\n\n        update_time()\n    until timer.each_ttl <= 0 or now() >= timer.start_time + timer.each_ttl\nend\n\nlocal function run_timer(premature, self)\n    if self.running or premature then\n        return\n    end\n\n    self.running = true\n\n    local ok, err = pcall(_internal, self)\n    if not ok then\n        log.error(\"failed to run timer[\", self.name, \"] err: \", err)\n    end\n\n    self.running = false\nend\n\n\nfunction _M.new(name, callback_fun, opts)\n    if not name then\n        return nil, \"missing argument: name\"\n    end\n\n    if not callback_fun then\n        return nil, \"missing argument: callback_fun\"\n    end\n\n    opts = opts or {}\n    local timer = {\n        name       = name,\n        each_ttl   = opts.each_ttl or 1,\n        sleep_succ = opts.sleep_succ or 1,\n        sleep_fail = opts.sleep_fail or 5,\n        start_time = 0,\n\n        callback_fun = callback_fun,\n        running = false,\n    }\n\n    local hdl, err = timer_every(opts.check_interval or 1,\n                                 run_timer, timer)\n    if not hdl then\n        return nil, err\n    end\n\n    hdl, err = timer_at(0, run_timer, timer)\n    if not hdl then\n        return nil, err\n    end\n\n    return timer\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/utils.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Collection of util functions.\n--\n-- @module core.utils\n\nlocal config_local   = require(\"apisix.core.config_local\")\nlocal core_str       = require(\"apisix.core.string\")\nlocal rfind_char     = core_str.rfind_char\nlocal table          = require(\"apisix.core.table\")\nlocal log            = require(\"apisix.core.log\")\nlocal string         = require(\"apisix.core.string\")\nlocal dns_client     = require(\"apisix.core.dns.client\")\nlocal ngx_re         = require(\"ngx.re\")\nlocal ipmatcher      = require(\"resty.ipmatcher\")\nlocal ffi            = require(\"ffi\")\nlocal base           = require(\"resty.core.base\")\nlocal open           = io.open\nlocal sub_str        = string.sub\nlocal str_byte       = string.byte\nlocal str_gsub       = string.gsub\nlocal tonumber       = tonumber\nlocal tostring       = tostring\nlocal re_gsub        = ngx.re.gsub\nlocal re_match       = ngx.re.match\nlocal re_gmatch      = ngx.re.gmatch\nlocal type           = type\nlocal C              = ffi.C\nlocal ffi_string     = ffi.string\nlocal ffi_new        = ffi.new\nlocal get_string_buf = base.get_string_buf\nlocal exiting        = ngx.worker.exiting\nlocal ngx_sleep      = ngx.sleep\nlocal ipairs         = ipairs\n\nlocal hostname\nlocal dns_resolvers\nlocal current_inited_resolvers\nlocal current_dns_client\nlocal max_sleep_interval = 1\n\nffi.cdef[[\n    int ngx_escape_uri(char *dst, const char *src,\n        size_t size, int type);\n    int gethostname(char *name, size_t len);\n    char *strerror(int errnum);\n]]\n\n\nlocal _M = {\n    version = 0.2,\n    parse_ipv4 = ipmatcher.parse_ipv4,\n    parse_ipv6 = ipmatcher.parse_ipv6,\n}\n\n\nfunction _M.get_seed_from_urandom()\n    local frandom, err = open(\"/dev/urandom\", \"rb\")\n    if not frandom then\n        return nil, 'failed to open /dev/urandom: ' .. err\n    end\n\n    local str = frandom:read(8)\n    frandom:close()\n    if not str then\n        return nil, 'failed to read data from /dev/urandom'\n    end\n\n    local seed = 0\n    for i = 1, 8 do\n        seed = 256 * seed + str:byte(i)\n    end\n\n    return seed\nend\n\n\nfunction _M.split_uri(uri)\n    return ngx_re.split(uri, \"/\")\nend\n\n\nlocal function dns_parse(domain, selector)\n    if dns_resolvers ~= current_inited_resolvers then\n        local local_conf = config_local.local_conf()\n        local valid = table.try_read_attr(local_conf, \"apisix\", \"dns_resolver_valid\")\n        local enable_resolv_search_opt = table.try_read_attr(local_conf, \"apisix\",\n                                                             \"enable_resolv_search_opt\")\n        local opts = {\n            nameservers = table.clone(dns_resolvers),\n            order = {\"last\", \"A\", \"AAAA\", \"CNAME\"}, -- avoid querying SRV\n        }\n\n        opts.validTtl = valid\n\n        if not enable_resolv_search_opt then\n            opts.search = {}\n        end\n\n        local client, err = dns_client.new(opts)\n        if not client then\n            return nil, \"failed to init the dns client: \" .. err\n        end\n\n        current_dns_client = client\n        current_inited_resolvers = dns_resolvers\n    end\n\n    return current_dns_client:resolve(domain, selector)\nend\n_M.dns_parse = dns_parse\n\n\nlocal function set_resolver(resolvers)\n    dns_resolvers = resolvers\nend\n_M.set_resolver = set_resolver\n\n\nfunction _M.get_resolver(resolvers)\n    return dns_resolvers\nend\n\n\nlocal function _parse_ipv4_or_host(addr)\n    local pos = rfind_char(addr, \":\", #addr - 1)\n    if not pos then\n        return addr, nil\n    end\n\n    local host = sub_str(addr, 1, pos - 1)\n    local port = sub_str(addr, pos + 1)\n    return host, tonumber(port)\nend\n\n\nlocal function _parse_ipv6_without_port(addr)\n    return addr\nend\n\n\n-- parse_addr parses 'addr' into the host and the port parts. If the 'addr'\n-- doesn't have a port, nil is used to return.\n-- For IPv6 literal host with brackets, like [::1], the square brackets will be kept.\n-- For malformed 'addr', the returned value can be anything. This method doesn't validate\n-- if the input is valid.\nfunction _M.parse_addr(addr)\n    if str_byte(addr, 1) == str_byte(\"[\") then\n        -- IPv6 format, with brackets, maybe with port\n        local right_bracket = str_byte(\"]\")\n        local len = #addr\n        if str_byte(addr, len) == right_bracket then\n            -- addr in [ip:v6] format\n            return addr, nil\n        else\n            local pos = rfind_char(addr, \":\", #addr - 1)\n            if not pos or str_byte(addr, pos - 1) ~= right_bracket then\n                -- malformed addr\n                return addr, nil\n            end\n\n            -- addr in [ip:v6]:port format\n            local host = sub_str(addr, 1, pos - 1)\n            local port = sub_str(addr, pos + 1)\n            return host, tonumber(port)\n        end\n\n    else\n        -- When we reach here, the input can be:\n        -- 1. IPv4\n        -- 2. IPv4, with port\n        -- 3. IPv6, like \"2001:db8::68\" or \"::ffff:192.0.2.1\"\n        -- 4. Malformed input\n        -- 5. Host, like \"test.com\" or \"localhost\"\n        -- 6. Host with port\n        local colon = str_byte(\":\")\n        local colon_counter = 0\n        local dot = str_byte(\".\")\n        for i = 1, #addr do\n            local ch = str_byte(addr, i, i)\n            if ch == dot then\n                return _parse_ipv4_or_host(addr)\n            elseif ch == colon then\n                colon_counter = colon_counter + 1\n                if colon_counter == 2 then\n                    return _parse_ipv6_without_port(addr)\n                end\n            end\n        end\n\n        return _parse_ipv4_or_host(addr)\n    end\nend\n\n\nfunction _M.uri_safe_encode(uri)\n    local count_escaped = C.ngx_escape_uri(nil, uri, #uri, 0)\n    local len = #uri + 2 * count_escaped\n    local buf = get_string_buf(len)\n    C.ngx_escape_uri(buf, uri, #uri, 0)\n\n    return ffi_string(buf, len)\nend\n\n\nfunction _M.validate_header_field(field)\n    for i = 1, #field do\n        local b = str_byte(field, i, i)\n        -- '!' - '~', excluding ':'\n        if not (32 < b and b < 127) or b == 58 then\n            return false\n        end\n    end\n    return true\nend\n\n\nfunction _M.validate_header_value(value)\n    if type(value) ~= \"string\" then\n        return true\n    end\n\n    for i = 1, #value do\n        local b = str_byte(value, i, i)\n        -- control characters\n        if b < 32 or b >= 127 then\n            return false\n        end\n    end\n    return true\nend\n\n\n---\n-- Returns the standard host name of the local host.\n-- only use this method in init/init_worker phase.\n--\n-- @function core.utils.gethostname\n-- @treturn string The host name of the local host.\n-- @usage\n-- local hostname = core.utils.gethostname() -- \"localhost\"\nfunction _M.gethostname()\n    if hostname then\n        return hostname\n    end\n\n    local size = 256\n    local buf = ffi_new(\"unsigned char[?]\", size)\n\n    local res = C.gethostname(buf, size)\n    if res == 0 then\n        local data = ffi_string(buf, size)\n        hostname = str_gsub(data, \"%z+$\", \"\")\n\n    else\n        hostname = \"unknown\"\n        log.error(\"failed to call gethostname(): \", ffi_string(C.strerror(ffi.errno())))\n    end\n\n    return hostname\nend\n\n\nlocal function sleep(sec)\n    if sec <= max_sleep_interval then\n        return ngx_sleep(sec)\n    end\n    ngx_sleep(max_sleep_interval)\n    if exiting() then\n        return\n    end\n    sec = sec - max_sleep_interval\n    return sleep(sec)\nend\n\n\n_M.sleep = sleep\n\n\nlocal resolve_var\ndo\n    local _ctx\n    local n_resolved\n    local pat = [[(?<!\\\\)\\$(\\{\\s*([^}]+?)\\s*\\}|([\\w\\.]+))]]\n    local _escaper\n\n    local function resolve(m)\n        local variable = m[2] or m[3]\n        -- handle the default value with ?? operator\n        local segs, err = ngx_re.split(variable, [[\\s*\\?\\?\\s*]])\n        if not segs then\n            log.error(\"failed to split variable \", variable, \": \", err)\n            return \"\"\n        end\n        local v = _ctx[segs[1]]\n\n        if v == nil then\n            if #segs == 1 then\n                return \"\"\n            end\n            v = segs[2]\n        end\n        n_resolved = n_resolved + 1\n        if _escaper then\n            return _escaper(tostring(v))\n        end\n        return tostring(v)\n    end\n\n    function resolve_var(tpl, ctx, escaper)\n        n_resolved = 0\n        if not tpl then\n            return tpl, nil, n_resolved\n        end\n\n        local from = core_str.find(tpl, \"$\")\n        if not from then\n            return tpl, nil, n_resolved\n        end\n\n        -- avoid creating temporary function\n        _ctx = ctx\n        _escaper = escaper\n        local res, _, err = re_gsub(tpl, pat, resolve, \"jo\")\n        _ctx = nil\n        _escaper = nil\n        if not res then\n            return nil, err\n        end\n\n        return res, nil, n_resolved\n    end\nend\n-- Resolve ngx.var in the given string\n_M.resolve_var = resolve_var\n\n\nlocal resolve_var_with_captures\ndo\n    local _captures\n    -- escape is not supported very well, like there is a redundant '\\' after escape \"$1\"\n    local pat = [[ (?<! \\\\) \\$ \\{? (\\d+) \\}? ]]\n\n    local function resolve(m)\n        local v = _captures[tonumber(m[1])]\n        if not v then\n            v = \"\"\n        end\n        return v\n    end\n\n    -- captures is the match result of regex uri in proxy-rewrite plugin\n    function resolve_var_with_captures(tpl, captures)\n        if not tpl then\n            return tpl, nil\n        end\n\n        local from = core_str.find(tpl, \"$\")\n        if not from then\n            return tpl, nil\n        end\n\n        captures = captures or {}\n\n        _captures = captures\n        local res, _, err = re_gsub(tpl, pat, resolve, \"jox\")\n        _captures = nil\n        if not res then\n            return nil, err\n        end\n\n        return res, nil\n    end\nend\n-- Resolve {$1, $2, ...} in the given string\n_M.resolve_var_with_captures = resolve_var_with_captures\n\n\n-- if `str` is a string containing period `some_plugin.some_field.nested_field`\n-- return the table that contains `nested_field` in its root level\n-- else return the original table `conf`\nlocal function get_root_conf(str, conf, field)\n    -- if the string contains periods, get the splits in `it` iterator\n    local it, _ = re_gmatch(str, [[([^\\.]+)]])\n    if not it then\n        return conf, field\n    end\n\n    -- add the splits into a table\n    local matches = {}\n    while true do\n        local m, _ = it()\n        if not m then\n            break\n        end\n        table.insert(matches, m[0])\n    end\n\n    -- get to the table that holds the last field\n    local num_of_matches = #matches\n    for i = 1, num_of_matches - 1 , 1 do\n        conf = conf[matches[i]]\n    end\n\n    -- return the table and the last field\n    return conf, matches[num_of_matches]\nend\n\n\nlocal function find_and_log(field, plugin_name, value)\n    local match, err = re_match(value, \"^https\")\n    if not match and not err then\n        log.warn(\"Using \", plugin_name, \" \" , field, \" with no TLS is a security risk\")\n    end\nend\n\n\nfunction _M.check_https(fields, conf, plugin_name)\n    for _, field in ipairs(fields) do\n\n        local new_conf, new_field = get_root_conf(field, conf)\n        if not new_conf then\n            return\n        end\n\n        local value = new_conf[new_field]\n        if not value then\n            return\n        end\n\n        if type(value) == \"table\" then\n            for _, v in ipairs(value) do\n                find_and_log(field, plugin_name, v)\n            end\n        else\n            find_and_log(field, plugin_name, value)\n        end\n    end\nend\n\n\nfunction _M.check_tls_bool(fields, conf, plugin_name)\n    for i, field in ipairs(fields) do\n\n        local new_conf, new_field = get_root_conf(field, conf)\n        if not new_conf then\n            return\n        end\n\n        local value = new_conf[new_field]\n\n        if value ~= true and value ~= nil then\n            log.warn(\"Keeping \", field, \" disabled in \",\n                     plugin_name, \" configuration is a security risk\")\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/core/version.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Return APISIX current version.\n--\n-- @module core.version\n\nreturn {\n    VERSION = \"3.15.0\"\n}\n"
  },
  {
    "path": "apisix/core.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal log = require(\"apisix.core.log\")\nlocal utils = require(\"apisix.core.utils\")\nlocal local_conf, err = require(\"apisix.core.config_local\").local_conf()\nif not local_conf then\n    error(\"failed to parse yaml config: \" .. err)\nend\n\nlocal config_provider = local_conf.deployment and local_conf.deployment.config_provider\n                      or \"etcd\"\nlog.info(\"use config_provider: \", config_provider)\n\nlocal config\n-- Currently, we handle JSON parsing in config_yaml, so special processing is needed here.\nif config_provider == \"json\" then\n    config = require(\"apisix.core.config_yaml\")\n    config.file_type = \"json\"\nelse\n    config = require(\"apisix.core.config_\" .. config_provider)\nend\n\nconfig.type = config_provider\n\n\nreturn {\n    version     = require(\"apisix.core.version\"),\n    log         = log,\n    config      = config,\n    config_util = require(\"apisix.core.config_util\"),\n    sleep       = utils.sleep,\n    json        = require(\"apisix.core.json\"),\n    table       = require(\"apisix.core.table\"),\n    request     = require(\"apisix.core.request\"),\n    response    = require(\"apisix.core.response\"),\n    lrucache    = require(\"apisix.core.lrucache\"),\n    schema      = require(\"apisix.core.schema\"),\n    string      = require(\"apisix.core.string\"),\n    ctx         = require(\"apisix.core.ctx\"),\n    timer       = require(\"apisix.core.timer\"),\n    id          = require(\"apisix.core.id\"),\n    ip          = require(\"apisix.core.ip\"),\n    io          = require(\"apisix.core.io\"),\n    utils       = utils,\n    dns_client  = require(\"apisix.core.dns.client\"),\n    etcd        = require(\"apisix.core.etcd\"),\n    tablepool   = require(\"tablepool\"),\n    resolver    = require(\"apisix.core.resolver\"),\n    os          = require(\"apisix.core.os\"),\n    pubsub      = require(\"apisix.core.pubsub\"),\n    math        = require(\"apisix.core.math\"),\n    event       = require(\"apisix.core.event\"),\n    env         = require(\"apisix.core.env\"),\n}\n"
  },
  {
    "path": "apisix/debug.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require      = require\nlocal yaml         = require(\"lyaml\")\nlocal log          = require(\"apisix.core.log\")\nlocal profile      = require(\"apisix.core.profile\")\nlocal lfs          = require(\"lfs\")\nlocal inspect      = require(\"inspect\")\nlocal jsonschema   = require(\"jsonschema\")\nlocal io           = io\nlocal ngx          = ngx\nlocal re_find      = ngx.re.find\nlocal get_headers  = ngx.req.get_headers\nlocal type         = type\nlocal pairs        = pairs\nlocal setmetatable = setmetatable\nlocal pcall        = pcall\nlocal ipairs       = ipairs\nlocal unpack       = unpack\nlocal debug_yaml_path = profile:yaml_path(\"debug\")\nlocal debug_yaml\nlocal debug_yaml_ctime\n\n\nlocal _M = {version = 0.1}\n\n\nlocal config_schema = {\n    type = \"object\",\n    properties = {\n        basic = {\n            properties = {\n                enable = {\n                    type = \"boolean\",\n                },\n            }\n        },\n        http_filter = {\n            properties = {\n                enable = {\n                    type = \"boolean\",\n                },\n                enable_header_name = {\n                    type = \"string\",\n                },\n            }\n        },\n        hook_conf = {\n            properties = {\n                enable = {\n                    type = \"boolean\",\n                },\n                name = {\n                    type = \"string\",\n                },\n                log_level = {\n                    enum = {\"debug\", \"info\", \"notice\", \"warn\", \"error\",\n                            \"crit\", \"alert\",\"emerg\"},\n                },\n                is_print_input_args = {\n                    type = \"boolean\",\n                },\n                is_print_return_value = {\n                    type = \"boolean\",\n                },\n            }\n        },\n    },\n    required = {\"basic\", \"http_filter\", \"hook_conf\"},\n}\n\n\nlocal function read_debug_yaml()\n    local attributes, err = lfs.attributes(debug_yaml_path)\n    if not attributes then\n        log.notice(\"failed to fetch \", debug_yaml_path, \" attributes: \", err)\n        return\n    end\n\n    -- log.info(\"change: \", json.encode(attributes))\n    local last_change_time = attributes.change\n    if debug_yaml_ctime == last_change_time then\n        return\n    end\n\n    local f, err = io.open(debug_yaml_path, \"r\")\n    if not f then\n        log.error(\"failed to open file \", debug_yaml_path, \" : \", err)\n        return\n    end\n\n    local found_end_flag\n    for i = 1, 10 do\n        f:seek('end', -i)\n\n        local end_flag = f:read(\"*a\")\n        -- log.info(i, \" flag: \", end_flag)\n        if re_find(end_flag, [[#END\\s*]], \"jo\") then\n            found_end_flag = true\n            break\n        end\n    end\n\n    if not found_end_flag then\n        f:seek(\"set\")\n        local size = f:seek(\"end\")\n        f:close()\n\n        if size > 8 then\n            log.warn(\"missing valid end flag in file \", debug_yaml_path)\n        end\n        return\n    end\n\n    f:seek('set')\n    local yaml_config = f:read(\"*a\")\n    f:close()\n\n    local debug_yaml_new = yaml.load(yaml_config)\n    if not debug_yaml_new then\n        log.error(\"failed to parse the content of file \" .. debug_yaml_path)\n        return\n    end\n\n    debug_yaml_new.hooks = debug_yaml_new.hooks or {}\n    debug_yaml = debug_yaml_new\n    debug_yaml_ctime = last_change_time\n\n    -- validate the debug yaml config\n    local validator = jsonschema.generate_validator(config_schema)\n    local ok, err = validator(debug_yaml)\n    if not ok then\n        log.error(\"failed to validate debug config \" .. err)\n        return\n    end\n\n    return true\nend\n\n\nlocal sync_debug_hooks\ndo\n    local pre_mtime\n    local enabled_hooks = {}\n\nlocal function apply_new_fun(module, fun_name, file_path, hook_conf)\n    local log_level = hook_conf.log_level or \"warn\"\n\n    if not module or type(module[fun_name]) ~= \"function\" then\n        log.error(\"failed to find function [\", fun_name,\n                  \"] in module:\", file_path)\n        return\n    end\n\n    local fun = module[fun_name]\n    local fun_org\n    if enabled_hooks[fun] then\n        fun_org = enabled_hooks[fun].org\n        enabled_hooks[fun] = nil\n    else\n        fun_org = fun\n    end\n\n    local t = {fun_org = fun_org}\n    local mt = {}\n\n    function mt.__call(self, ...)\n        local arg = {...}\n        local http_filter = debug_yaml.http_filter\n        local api_ctx = ngx.ctx.api_ctx\n        local enable_by_hook = not (http_filter and http_filter.enable)\n        local enable_by_header_filter = (http_filter and http_filter.enable)\n                and (api_ctx and api_ctx.enable_dynamic_debug)\n        if hook_conf.is_print_input_args then\n            if enable_by_hook or enable_by_header_filter then\n                log[log_level](\"call require(\\\"\", file_path, \"\\\").\", fun_name,\n                               \"() args:\", inspect(arg))\n            end\n        end\n\n        local ret = {self.fun_org(...)}\n        if hook_conf.is_print_return_value then\n            if enable_by_hook or enable_by_header_filter then\n                log[log_level](\"call require(\\\"\", file_path, \"\\\").\", fun_name,\n                               \"() return:\", inspect(ret))\n            end\n        end\n        return unpack(ret)\n    end\n\n    setmetatable(t, mt)\n    enabled_hooks[t] = {\n        org = fun_org, new = t, mod = module,\n        fun_name = fun_name\n    }\n    module[fun_name] = t\nend\n\n\nfunction sync_debug_hooks()\n    if not debug_yaml_ctime or debug_yaml_ctime == pre_mtime then\n        return\n    end\n\n    for _, hook in pairs(enabled_hooks) do\n        local m = hook.mod\n        local name = hook.fun_name\n        m[name] = hook.org\n    end\n\n    enabled_hooks = {}\n\n    local hook_conf = debug_yaml.hook_conf\n    if not hook_conf.enable then\n        pre_mtime = debug_yaml_ctime\n        return\n    end\n\n    local hook_name = hook_conf.name or \"\"\n    local hooks = debug_yaml[hook_name]\n    if not hooks then\n        pre_mtime = debug_yaml_ctime\n        return\n    end\n\n    for file_path, fun_names in pairs(hooks) do\n        local ok, module = pcall(require, file_path)\n        if not ok then\n            log.error(\"failed to load module [\", file_path, \"]: \", module)\n\n        else\n            for _, fun_name in ipairs(fun_names) do\n                apply_new_fun(module, fun_name, file_path, hook_conf)\n            end\n        end\n    end\n\n    pre_mtime = debug_yaml_ctime\nend\n\nend --do\n\n\nlocal function sync_debug_status(premature)\n    if premature then\n        return\n    end\n\n    if not read_debug_yaml() then\n        return\n    end\n\n    sync_debug_hooks()\nend\n\n\nlocal function check()\n    if not debug_yaml or not debug_yaml.http_filter then\n        return false\n    end\n\n    local http_filter = debug_yaml.http_filter\n    if not http_filter or not http_filter.enable_header_name or not http_filter.enable then\n        return false\n    end\n\n    return true\nend\n\nfunction _M.dynamic_debug(api_ctx)\n    if not check() then\n        return\n    end\n\n    if get_headers()[debug_yaml.http_filter.enable_header_name] then\n        api_ctx.enable_dynamic_debug = true\n    end\nend\n\n\nfunction _M.enable_debug()\n    if not debug_yaml or not debug_yaml.basic then\n        return false\n    end\n\n    return debug_yaml.basic.enable\nend\n\n\nfunction _M.init_worker()\n    local process = require(\"ngx.process\")\n    if process.type() ~= \"worker\" then\n        return\n    end\n\n    sync_debug_status()\n    ngx.timer.every(1, sync_debug_status)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/consul/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require            = require\nlocal local_conf         = require(\"apisix.core.config_local\").local_conf()\nlocal core               = require(\"apisix.core\")\nlocal core_sleep         = require(\"apisix.core.utils\").sleep\nlocal resty_consul       = require('resty.consul')\nlocal http               = require('resty.http')\nlocal util               = require(\"apisix.cli.util\")\nlocal ipairs             = ipairs\nlocal error              = error\nlocal ngx                = ngx\nlocal unpack             = unpack\nlocal tonumber           = tonumber\nlocal pairs              = pairs\nlocal ngx_timer_at       = ngx.timer.at\nlocal ngx_timer_every    = ngx.timer.every\nlocal log                = core.log\nlocal json_delay_encode  = core.json.delay_encode\nlocal process            = require(\"ngx.process\")\nlocal ngx_worker_id      = ngx.worker.id\nlocal exiting            = ngx.worker.exiting\nlocal thread_spawn       = ngx.thread.spawn\nlocal thread_wait        = ngx.thread.wait\nlocal thread_kill        = ngx.thread.kill\nlocal math_random        = math.random\nlocal pcall              = pcall\nlocal null               = ngx.null\nlocal type               = type\nlocal next               = next\n\nlocal consul_dict = ngx.shared.consul\nif not consul_dict then\n    error(\"lua_shared_dict \\\"consul\\\" not configured\")\nend\n\nlocal default_service\nlocal default_weight\nlocal sort_type\nlocal skip_service_map = core.table.new(0, 1)\nlocal dump_params\n\nlocal consul_services\n-- Per-worker LRU cache: avoids shared dict access on every request.\n-- neg_ttl caches unknown services. invalid_stale ensures expired\n-- entries are refreshed from the shared dict instead of re-cached.\nlocal nodes_cache = core.lrucache.new({\n    ttl = 1,\n    count = 1024,\n    invalid_stale = true,\n    neg_ttl = 1,\n    neg_count = 64,\n})\n\nlocal default_skip_services = {\"consul\"}\nlocal default_random_range = 5\nlocal default_catalog_error_index = -1\nlocal default_health_error_index = -2\nlocal watch_type_catalog = 1\nlocal watch_type_health = 2\nlocal max_retry_time = 256\n\nlocal _M = {\n    version = 0.3,\n}\n\n\nlocal function fetch_node_from_shdict(service_name)\n    local value = consul_dict:get(service_name)\n    if not value then\n        return nil, \"consul service not found: \" .. service_name\n    end\n\n    local nodes, err = core.json.decode(value)\n    if not nodes then\n        return nil, \"failed to decode nodes for service: \"\n                    .. service_name .. \", error: \" .. (err or \"\")\n    end\n\n    return nodes\nend\n\n\nfunction _M.all_nodes()\n    local keys = consul_dict:get_keys(0)\n    local services = core.table.new(0, #keys)\n    for i, key in ipairs(keys) do\n        local value = consul_dict:get(key)\n        if value then\n            local nodes, err = core.json.decode(value)\n            if nodes then\n                services[key] = nodes\n            else\n                log.error(\"failed to decode nodes for service: \", key, \", error: \", err)\n            end\n        end\n\n        if i % 100 == 0 then\n            ngx.sleep(0)\n        end\n    end\n    return services\nend\n\n\nfunction _M.nodes(service_name)\n    local nodes, err = nodes_cache(service_name, nil,\n                                   fetch_node_from_shdict, service_name)\n    if not nodes then\n        log.error(\"fetch nodes failed by \", service_name, \", error: \", err)\n        return default_service and {default_service}\n    end\n\n    log.info(\"process id: \", ngx_worker_id(), \", [\", service_name, \"] = \",\n        json_delay_encode(nodes, true))\n\n    return nodes\nend\n\n\nlocal function update_all_services(consul_server_url, up_services)\n    -- write new/updated values first so readers never see a missing service\n    local i = 0\n    for k, v in pairs(up_services) do\n        local content, err = core.json.encode(v)\n        if content then\n            local ok, set_err, forcible = consul_dict:set(k, content)\n            if not ok then\n                log.error(\"failed to set nodes for service: \", k, \", error: \", set_err,\n                          \", please consider increasing lua_shared_dict consul size\")\n            elseif forcible then\n                log.warn(\"consul shared dict is full, forcibly evicting items while \",\n                         \"setting nodes for service: \", k,\n                         \", please consider increasing lua_shared_dict consul size\")\n            end\n        else\n            log.error(\"failed to encode nodes for service: \", k, \", error: \", err)\n        end\n        i = i + 1\n        if i % 100 == 0 then\n            ngx.sleep(0)\n        end\n    end\n\n    -- then delete keys that are no longer present\n    local old_services = consul_services[consul_server_url] or {}\n    for k, _ in pairs(old_services) do\n        if not up_services[k] then\n            consul_dict:delete(k)\n        end\n    end\n\n    consul_services[consul_server_url] = up_services\n\n    log.info(\"update all services to shared dict\")\nend\n\n\nlocal function read_dump_services()\n    local data, err = util.read_file(dump_params.path)\n    if not data then\n        log.error(\"read dump file get error: \", err)\n        return\n    end\n\n    log.info(\"read dump file: \", data)\n    data = util.trim(data)\n    if #data == 0 then\n        log.error(\"dump file is empty\")\n        return\n    end\n\n    local entity, err = core.json.decode(data)\n    if not entity then\n        log.error(\"decoded dump data got error: \", err, \", file content: \", data)\n        return\n    end\n\n    if not entity.services or not entity.last_update then\n        log.warn(\"decoded dump data miss fields, file content: \", data)\n        return\n    end\n\n    local now_time = ngx.time()\n    log.info(\"dump file last_update: \", entity.last_update, \", dump_params.expire: \",\n        dump_params.expire, \", now_time: \", now_time)\n    if dump_params.expire ~= 0 and (entity.last_update + dump_params.expire) < now_time then\n        log.warn(\"dump file: \", dump_params.path, \" had expired, ignored it\")\n        return\n    end\n\n    for k, v in pairs(entity.services) do\n        local content, json_err = core.json.encode(v)\n        if content then\n            consul_dict:set(k, content)\n        else\n            log.error(\"failed to encode dump service: \", k, \", error: \", json_err)\n        end\n    end\n    log.info(\"load dump file into shared dict success\")\nend\n\n\nlocal function write_dump_services()\n    -- build services from the privileged agent's in-memory tracking table\n    -- to avoid a full shared dict scan + JSON decode via _M.all_nodes()\n    local services = core.table.new(0, 8)\n    for _, svcs in pairs(consul_services) do\n        for k, v in pairs(svcs) do\n            services[k] = v\n        end\n    end\n\n    local entity = {\n        services = services,\n        last_update = ngx.time(),\n        expire = dump_params.expire, -- later need handle it\n    }\n    local data = core.json.encode(entity)\n    local succ, err = util.write_file(dump_params.path, data)\n    if not succ then\n        log.error(\"write dump into file got error: \", err)\n    end\nend\n\n\nlocal function show_dump_file()\n    if not dump_params then\n        return 503, \"dump params is nil\"\n    end\n\n    local data, err = util.read_file(dump_params.path)\n    if not data then\n        return 503, err\n    end\n\n    return 200, data\nend\n\n\nlocal function get_retry_delay(retry_delay)\n    if not retry_delay or retry_delay >= max_retry_time then\n        retry_delay = 1\n    else\n        retry_delay = retry_delay * 4\n    end\n\n    return retry_delay\nend\n\n\nlocal function get_opts(consul_server, is_catalog)\n    local opts = {\n        host = consul_server.host,\n        port = consul_server.port,\n        connect_timeout = consul_server.connect_timeout,\n        read_timeout = consul_server.read_timeout,\n        default_args = {\n            token = consul_server.token,\n        }\n    }\n    if not consul_server.keepalive then\n        return opts\n    end\n\n    opts.default_args.wait = consul_server.wait_timeout --blocked wait!=0; unblocked by wait=0\n\n    if is_catalog then\n        opts.default_args.index = consul_server.catalog_index\n    else\n        opts.default_args.index = consul_server.health_index\n    end\n\n    return opts\nend\n\n\nlocal function watch_catalog(consul_server)\n    local client = resty_consul:new(get_opts(consul_server, true))\n\n    ::RETRY::\n    local watch_result, watch_err = client:get(consul_server.consul_watch_catalog_url)\n    local watch_error_info = (watch_err ~= nil and watch_err)\n                             or ((watch_result ~= nil and watch_result.status ~= 200)\n                             and watch_result.status)\n    if watch_error_info then\n        log.error(\"connect consul: \", consul_server.consul_server_url,\n            \" by sub url: \", consul_server.consul_watch_catalog_url,\n            \", got watch result: \", json_delay_encode(watch_result),\n            \", with error: \", watch_error_info)\n\n        return watch_type_catalog, default_catalog_error_index\n    end\n\n    if consul_server.catalog_index > 0\n            and consul_server.catalog_index == tonumber(watch_result.headers['X-Consul-Index']) then\n        local random_delay = math_random(default_random_range)\n        log.info(\"watch catalog has no change, re-watch consul after \", random_delay, \" seconds\")\n        core_sleep(random_delay)\n        goto RETRY\n    end\n\n    return watch_type_catalog, watch_result.headers['X-Consul-Index']\nend\n\n\nlocal function watch_health(consul_server)\n    local client = resty_consul:new(get_opts(consul_server, false))\n\n    ::RETRY::\n    local watch_result, watch_err = client:get(consul_server.consul_watch_health_url)\n    local watch_error_info = (watch_err ~= nil and watch_err)\n            or ((watch_result ~= nil and watch_result.status ~= 200)\n            and watch_result.status)\n    if watch_error_info then\n        log.error(\"connect consul: \", consul_server.consul_server_url,\n            \" by sub url: \", consul_server.consul_watch_health_url,\n            \", got watch result: \", json_delay_encode(watch_result),\n            \", with error: \", watch_error_info)\n\n        return watch_type_health, default_health_error_index\n    end\n\n    if consul_server.health_index > 0\n            and consul_server.health_index == tonumber(watch_result.headers['X-Consul-Index']) then\n        local random_delay = math_random(default_random_range)\n        log.info(\"watch health has no change, re-watch consul after \", random_delay, \" seconds\")\n        core_sleep(random_delay)\n        goto RETRY\n    end\n\n    return watch_type_health, watch_result.headers['X-Consul-Index']\nend\n\n\nlocal function check_keepalive(consul_server, retry_delay)\n    if consul_server.keepalive and not exiting() then\n        local ok, err = ngx_timer_at(0, _M.connect, consul_server, retry_delay)\n        if not ok then\n            log.error(\"create ngx_timer_at got error: \", err)\n            return\n        end\n    end\nend\n\n\nlocal function update_index(consul_server, catalog_index, health_index)\n    local c_index = 0\n    local h_index = 0\n    if catalog_index ~= nil then\n        c_index = tonumber(catalog_index)\n    end\n\n    if health_index ~= nil then\n        h_index = tonumber(health_index)\n    end\n\n    if c_index > 0 then\n        consul_server.catalog_index = c_index\n    end\n\n    if h_index > 0 then\n        consul_server.health_index = h_index\n    end\nend\n\n\nlocal function is_not_empty(value)\n    if value == nil or value == null\n            or (type(value) == \"table\" and not next(value))\n            or (type(value) == \"string\" and value == \"\")\n    then\n        return false\n    end\n\n    return true\nend\n\n\nlocal function watch_result_is_valid(watch_type, index, catalog_index, health_index)\n    if index <= 0 then\n        return false\n    end\n\n    if watch_type == watch_type_catalog then\n        if index == catalog_index then\n            return false\n        end\n    else\n        if index == health_index then\n            return false\n        end\n    end\n\n    return true\nend\n\n\nlocal function combine_sort_nodes_cmp(left, right)\n    if left.host ~= right.host then\n        return left.host < right.host\n    end\n\n    return left.port < right.port\nend\n\n\nlocal function port_sort_nodes_cmp(left, right)\n    return left.port < right.port\nend\n\n\nlocal function host_sort_nodes_cmp(left, right)\n    return left.host < right.host\nend\n\n\nfunction _M.connect(premature, consul_server, retry_delay)\n    if premature then\n        return\n    end\n\n    local catalog_thread, spawn_catalog_err = thread_spawn(watch_catalog, consul_server)\n    if not catalog_thread then\n        local random_delay = math_random(default_random_range)\n        log.error(\"failed to spawn thread watch catalog: \", spawn_catalog_err,\n            \", retry connecting consul after \", random_delay, \" seconds\")\n        core_sleep(random_delay)\n\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n\n    local health_thread, err = thread_spawn(watch_health, consul_server)\n    if not health_thread then\n        thread_kill(catalog_thread)\n        local random_delay = math_random(default_random_range)\n        log.error(\"failed to spawn thread watch health: \", err, \", retry connecting consul after \",\n            random_delay, \" seconds\")\n        core_sleep(random_delay)\n\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n\n    local thread_wait_ok, watch_type, index = thread_wait(catalog_thread, health_thread)\n    thread_kill(catalog_thread)\n    thread_kill(health_thread)\n    if not thread_wait_ok then\n        local random_delay = math_random(default_random_range)\n        log.error(\"failed to wait thread: \", watch_type, \", retry connecting consul after \",\n                random_delay, \" seconds\")\n        core_sleep(random_delay)\n\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n\n    -- double check index has changed\n    if not watch_result_is_valid(tonumber(watch_type),\n            tonumber(index), consul_server.catalog_index, consul_server.health_index) then\n        retry_delay = get_retry_delay(retry_delay)\n        log.warn(\"get all svcs got err, retry connecting consul after \", retry_delay, \" seconds\")\n        core_sleep(retry_delay)\n\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n\n    local consul_client = resty_consul:new({\n        host = consul_server.host,\n        port = consul_server.port,\n        connect_timeout = consul_server.connect_timeout,\n        read_timeout = consul_server.read_timeout,\n        default_args = {\n            token = consul_server.token\n        }\n    })\n    local catalog_success, catalog_res, catalog_err = pcall(function()\n        return consul_client:get(consul_server.consul_watch_catalog_url)\n    end)\n    if not catalog_success then\n        log.error(\"connect consul: \", consul_server.consul_server_url,\n            \" by sub url: \", consul_server.consul_watch_catalog_url,\n            \", got catalog result: \", json_delay_encode(catalog_res))\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n    local catalog_error_info = (catalog_err ~= nil and catalog_err)\n            or ((catalog_res ~= nil and catalog_res.status ~= 200)\n            and catalog_res.status)\n    if catalog_error_info then\n        log.error(\"connect consul: \", consul_server.consul_server_url,\n            \" by sub url: \", consul_server.consul_watch_catalog_url,\n            \", got catalog result: \", json_delay_encode(catalog_res),\n            \", with error: \", catalog_error_info)\n\n        retry_delay = get_retry_delay(retry_delay)\n        log.warn(\"get all svcs got err, retry connecting consul after \", retry_delay, \" seconds\")\n        core_sleep(retry_delay)\n\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n\n    -- get health index\n    local success, health_res, health_err = pcall(function()\n        return consul_client:get(consul_server.consul_watch_health_url)\n    end)\n    if not success then\n        log.error(\"connect consul: \", consul_server.consul_server_url,\n            \" by sub url: \", consul_server.consul_watch_health_url,\n            \", got health result: \", json_delay_encode(health_res))\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n    local health_error_info = (health_err ~= nil and health_err)\n            or ((health_res ~= nil and health_res.status ~= 200)\n            and health_res.status)\n    if health_error_info then\n        log.error(\"connect consul: \", consul_server.consul_server_url,\n            \" by sub url: \", consul_server.consul_watch_health_url,\n            \", got health result: \", json_delay_encode(health_res),\n            \", with error: \", health_error_info)\n\n        retry_delay = get_retry_delay(retry_delay)\n        log.warn(\"get all svcs got err, retry connecting consul after \", retry_delay, \" seconds\")\n        core_sleep(retry_delay)\n\n        check_keepalive(consul_server, retry_delay)\n        return\n    end\n\n    log.info(\"connect consul: \", consul_server.consul_server_url,\n        \", catalog_result status: \", catalog_res.status,\n        \", catalog_result.headers.index: \", catalog_res.headers['X-Consul-Index'],\n        \", consul_server.index: \", consul_server.index,\n        \", consul_server: \", json_delay_encode(consul_server))\n\n    -- if the current index is different from the last index, then update the service\n    if (consul_server.catalog_index ~= tonumber(catalog_res.headers['X-Consul-Index']))\n            or (consul_server.health_index ~= tonumber(health_res.headers['X-Consul-Index'])) then\n        local up_services = core.table.new(0, #catalog_res.body)\n        for service_name, _ in pairs(catalog_res.body) do\n            -- check if the service_name is 'skip service'\n            if skip_service_map[service_name] then\n                goto CONTINUE\n            end\n\n            -- get node from service\n            local svc_url = consul_server.consul_sub_url .. \"/\" .. service_name\n            local svc_success, result, get_err = pcall(function()\n                return consul_client:get(svc_url, {passing = true})\n            end)\n            local error_info = (get_err ~= nil and get_err) or\n                    ((result ~= nil and result.status ~= 200) and result.status)\n            if not svc_success or error_info then\n                log.error(\"connect consul: \", consul_server.consul_server_url,\n                    \", by service url: \", svc_url, \", with error: \", error_info)\n                goto CONTINUE\n            end\n\n            -- decode body, decode json, update service, error handling\n            -- check result body is not nil and not empty\n            if is_not_empty(result.body) then\n                -- add services to table\n                local nodes = up_services[service_name]\n                local nodes_uniq = {}\n                for _, node in ipairs(result.body) do\n                    if not node.Service then\n                        goto CONTINUE\n                    end\n\n                    local svc_address, svc_port = node.Service.Address, node.Service.Port\n                    -- Handle nil or 0 port case - default to 80 for HTTP services\n                    if not svc_port or svc_port == 0 then\n                        svc_port = 80\n                    end\n                    -- if nodes is nil, new nodes table and set to up_services\n                    if not nodes then\n                        nodes = core.table.new(1, 0)\n                        up_services[service_name] = nodes\n                    end\n                    -- not store duplicate service IDs.\n                    local service_id = svc_address .. \":\" .. svc_port\n                    if not nodes_uniq[service_id] then\n                        -- add node to nodes table\n                        core.table.insert(nodes, {\n                            host = svc_address,\n                            port = tonumber(svc_port),\n                            weight = default_weight,\n                        })\n                        nodes_uniq[service_id] = true\n                    end\n                end\n                if nodes then\n                    if sort_type == \"port_sort\" then\n                        core.table.sort(nodes, port_sort_nodes_cmp)\n\n                    elseif sort_type == \"host_sort\" then\n                        core.table.sort(nodes, host_sort_nodes_cmp)\n\n                    elseif sort_type == \"combine_sort\" then\n                        core.table.sort(nodes, combine_sort_nodes_cmp)\n\n                    end\n                end\n                up_services[service_name] = nodes\n            end\n            :: CONTINUE ::\n        end\n\n        update_all_services(consul_server.consul_server_url, up_services)\n\n        if dump_params then\n            ngx_timer_at(0, write_dump_services)\n        end\n\n        update_index(consul_server,\n                catalog_res.headers['X-Consul-Index'],\n                health_res.headers['X-Consul-Index'])\n    end\n\n    check_keepalive(consul_server, retry_delay)\nend\n\n\nlocal function format_consul_params(consul_conf)\n    local consul_server_list = core.table.new(0, #consul_conf.servers)\n\n    for _, v in pairs(consul_conf.servers) do\n        local scheme, host, port, path = unpack(http.parse_uri(nil, v))\n        if scheme ~= \"http\" then\n            return nil, \"only support consul http schema address, eg: http://address:port\"\n        elseif path ~= \"/\" or core.string.has_suffix(v, '/') then\n            return nil, \"invalid consul server address, the valid format: http://address:port\"\n        end\n        core.table.insert(consul_server_list, {\n            host = host,\n            port = port,\n            token = consul_conf.token,\n            connect_timeout = consul_conf.timeout.connect,\n            read_timeout = consul_conf.timeout.read,\n            wait_timeout = consul_conf.timeout.wait,\n            consul_watch_catalog_url = \"/catalog/services\",\n            consul_sub_url = \"/health/service\",\n            consul_watch_health_url = \"/health/state/any\",\n            consul_server_url = v .. \"/v1\",\n            weight = consul_conf.weight,\n            keepalive = consul_conf.keepalive,\n            health_index = 0,\n            catalog_index = 0,\n            fetch_interval = consul_conf.fetch_interval -- fetch interval to next connect consul\n        })\n    end\n    return consul_server_list, nil\nend\n\n\nfunction _M.init_worker()\n    local consul_conf = local_conf.discovery.consul\n    dump_params = consul_conf.dump\n\n    default_weight = consul_conf.weight\n    sort_type = consul_conf.sort_type\n    -- set default service, used when the server node cannot be found\n    if consul_conf.default_service then\n        default_service = consul_conf.default_service\n        default_service.weight = default_weight\n    end\n\n    if process.type() ~= \"privileged agent\" then\n        return\n    end\n\n    -- flush stale data that may persist across reloads,\n    -- since consul_services is re-initialized empty\n    consul_dict:flush_all()\n\n    if consul_conf.dump then\n        if consul_conf.dump.load_on_init then\n            read_dump_services()\n        end\n    end\n\n    log.notice(\"consul_conf: \", json_delay_encode(consul_conf, true))\n\n    if consul_conf.skip_services then\n        skip_service_map = core.table.new(0, #consul_conf.skip_services)\n        for _, v in ipairs(consul_conf.skip_services) do\n            skip_service_map[v] = true\n        end\n    end\n    -- set up default skip service\n    for _, v in ipairs(default_skip_services) do\n        skip_service_map[v] = true\n    end\n\n    local consul_servers_list, err = format_consul_params(consul_conf)\n    if err then\n        error(\"format consul config got error: \" .. err)\n    end\n    log.info(\"consul_server_list: \", json_delay_encode(consul_servers_list, true))\n\n    consul_services = core.table.new(0, 1)\n    -- success or failure\n    for _, server in ipairs(consul_servers_list) do\n        local ok, err = ngx_timer_at(0, _M.connect, server)\n        if not ok then\n            error(\"create consul got error: \" .. err)\n        end\n\n        if server.keepalive == false then\n            ngx_timer_every(server.fetch_interval, _M.connect, server)\n        end\n    end\nend\n\n\nfunction _M.dump_data()\n    return {config = local_conf.discovery.consul, services = _M.all_nodes()}\nend\n\n\nfunction _M.control_api()\n    return {\n        {\n            methods = {\"GET\"},\n            uris = {\"/show_dump_file\"},\n            handler = show_dump_file,\n        }\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/consul/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn {\n    type = \"object\",\n    properties = {\n        servers = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n            }\n        },\n        shared_size = {\n            type = \"string\",\n            pattern = [[^[1-9][0-9]*m$]],\n            default = \"1m\",\n        },\n        token = {type = \"string\", default = \"\"},\n        fetch_interval = {type = \"integer\", minimum = 1, default = 3},\n        keepalive = {\n            type = \"boolean\",\n            default = true\n        },\n        weight = {type = \"integer\", minimum = 1, default = 1},\n        timeout = {\n            type = \"object\",\n            properties = {\n                connect = {type = \"integer\", minimum = 1, default = 2000},\n                read = {type = \"integer\", minimum = 1, default = 2000},\n                wait = {type = \"integer\", minimum = 1, default = 60}\n            },\n            default = {\n                connect = 2000,\n                read = 2000,\n                wait = 60,\n            }\n        },\n        sort_type = {\n            type = \"string\",\n            enum = {\"origin\", \"host_sort\", \"port_sort\", \"combine_sort\"},\n            default = \"origin\",\n        },\n        skip_services = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n            }\n        },\n        dump = {\n            type = \"object\",\n            properties = {\n                path = {type = \"string\", minLength = 1},\n                load_on_init = {type = \"boolean\", default = true},\n                expire = {type = \"integer\", default = 0},\n            },\n            required = {\"path\"},\n        },\n        default_service = {\n            type = \"object\",\n            properties = {\n                host = {type = \"string\"},\n                port = {type = \"integer\"},\n                metadata = {\n                    type = \"object\",\n                    properties = {\n                        fail_timeout = {type = \"integer\", default = 1},\n                        weight = {type = \"integer\", default = 1},\n                        max_fails = {type = \"integer\", default = 1}\n                    },\n                    default = {\n                        fail_timeout = 1,\n                        weight = 1,\n                        max_fails = 1\n                    }\n                }\n            }\n        }\n    },\n\n    required = {\"servers\"}\n}\n\n"
  },
  {
    "path": "apisix/discovery/consul_kv/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require            = require\nlocal local_conf         = require(\"apisix.core.config_local\").local_conf()\nlocal core               = require(\"apisix.core\")\nlocal core_sleep         = require(\"apisix.core.utils\").sleep\nlocal resty_consul       = require('resty.consul')\nlocal cjson              = require('cjson')\nlocal http               = require('resty.http')\nlocal util               = require(\"apisix.cli.util\")\nlocal ipairs             = ipairs\nlocal error              = error\nlocal ngx                = ngx\nlocal unpack             = unpack\nlocal ngx_re_match       = ngx.re.match\nlocal tonumber           = tonumber\nlocal pairs              = pairs\nlocal ipairs             = ipairs\nlocal ngx_timer_at       = ngx.timer.at\nlocal ngx_timer_every    = ngx.timer.every\nlocal log                = core.log\nlocal ngx_decode_base64  = ngx.decode_base64\nlocal json_delay_encode  = core.json.delay_encode\nlocal cjson_null         = cjson.null\n\nlocal applications = core.table.new(0, 5)\nlocal default_service\nlocal default_weight\nlocal default_prefix_rule\nlocal skip_keys_map = core.table.new(0, 1)\nlocal dump_params\n\nlocal events\nlocal events_list\nlocal consul_apps\n\nlocal _M = {\n    version = 0.3,\n}\n\n\nlocal function discovery_consul_callback(data, event, source, pid)\n    applications = data\n    log.notice(\"update local variable application, event is: \", event,\n        \"source: \", source, \"server pid:\", pid,\n        \", application: \", core.json.encode(applications, true))\nend\n\n\nfunction _M.all_nodes()\n    return applications\nend\n\n\nfunction _M.nodes(service_name)\n    if not applications then\n        log.error(\"application is nil, failed to fetch nodes for : \", service_name)\n        return\n    end\n\n    local resp_list = applications[service_name]\n\n    if not resp_list then\n        log.error(\"fetch nodes failed by \", service_name, \", return default service\")\n        return default_service and {default_service}\n    end\n\n    log.info(\"process id: \", ngx.worker.id(), \", applications[\", service_name, \"] = \",\n        json_delay_encode(resp_list, true))\n\n    return resp_list\nend\n\n\nlocal function parse_instance(node, server_name_prefix)\n    local key = node.Key\n\n    if key == cjson_null or not key or #key == 0 then\n        log.error(\"consul_key_empty, server_name_prefix: \", server_name_prefix,\n            \", node: \", json_delay_encode(node, true))\n        return false\n    end\n\n    local result = ngx_re_match(key, default_prefix_rule, \"jo\")\n    if not result then\n        log.error(\"server name parse error, server_name_prefix: \", server_name_prefix,\n            \", node: \", json_delay_encode(node, true))\n        return false\n    end\n\n    local sn, host, port = result[1], result[2], result[3]\n\n    -- if exist, skip special kesy\n    if sn and skip_keys_map[sn] then\n        return false\n    end\n\n    -- base64 value   = \"IHsid2VpZ2h0IjogMTIwLCAibWF4X2ZhaWxzIjogMiwgImZhaWxfdGltZW91dCI6IDJ9\"\n    -- ori    value   = \"{\"weight\": 120, \"max_fails\": 2, \"fail_timeout\": 2}\"\n    local metadataBase64 = node.Value\n    if metadataBase64 == cjson_null or not metadataBase64 or #metadataBase64 == 0 then\n        log.error(\"error: consul_value_empty, server_name_prefix: \", server_name_prefix,\n            \", node: \", json_delay_encode(node, true))\n        return false\n    end\n\n    local metadata, err = core.json.decode(ngx_decode_base64(metadataBase64))\n    if err then\n        log.error(\"invalid upstream value, server_name_prefix: \", server_name_prefix,\n            \",err: \", err, \", node: \", json_delay_encode(node, true))\n        return false\n    elseif metadata.check_status == false or metadata.check_status == \"false\" then\n        log.error(\"server node unhealthy, server_name_prefix: \", server_name_prefix,\n            \", node: \", json_delay_encode(node, true))\n        return false\n    end\n\n    return true, host, tonumber(port), metadata, sn\nend\n\n\nlocal function update_application(server_name_prefix, data)\n    local sn\n    local up_apps = core.table.new(0, #data)\n    local weight = default_weight\n\n    for _, node in ipairs(data) do\n        local succ, ip, port, metadata, server_name = parse_instance(node, server_name_prefix)\n        if succ then\n            sn = server_name_prefix .. server_name\n            local nodes = up_apps[sn]\n            if not nodes then\n                nodes = core.table.new(1, 0)\n                up_apps[sn] = nodes\n            end\n            core.table.insert(nodes, {\n                host = ip,\n                port = port,\n                weight = metadata and metadata.weight or weight,\n            })\n        end\n    end\n\n    -- clean old unused data\n    local old_apps = consul_apps[server_name_prefix] or {}\n    for k, _ in pairs(old_apps) do\n        applications[k] = nil\n    end\n    core.table.clear(old_apps)\n\n    for k, v in pairs(up_apps) do\n        applications[k] = v\n    end\n    consul_apps[server_name_prefix] = up_apps\n\n    log.info(\"update applications: \", core.json.encode(applications))\nend\n\n\nlocal function read_dump_srvs()\n    local data, err = util.read_file(dump_params.path)\n    if not data then\n        log.notice(\"read dump file get error: \", err)\n        return\n    end\n\n    log.info(\"read dump file: \", data)\n    data = util.trim(data)\n    if #data == 0 then\n        log.error(\"dump file is empty\")\n        return\n    end\n\n    local entity, err  = core.json.decode(data)\n    if not entity then\n        log.error(\"decoded dump data got error: \", err, \", file content: \", data)\n        return\n    end\n\n    if not entity.services or not entity.last_update then\n        log.warn(\"decoded dump data miss fields, file content: \", data)\n        return\n    end\n\n    local now_time = ngx.time()\n    log.info(\"dump file last_update: \", entity.last_update, \", dump_params.expire: \",\n        dump_params.expire, \", now_time: \", now_time)\n    if dump_params.expire ~= 0  and (entity.last_update + dump_params.expire) < now_time then\n       log.warn(\"dump file: \", dump_params.path, \" had expired, ignored it\")\n       return\n    end\n\n    applications = entity.services\n    log.info(\"load dump file into memory success\")\nend\n\n\nlocal function write_dump_srvs()\n    local entity = {\n        services = applications,\n        last_update = ngx.time(),\n        expire = dump_params.expire, -- later need handle it\n    }\n    local data = core.json.encode(entity)\n    local succ, err =  util.write_file(dump_params.path, data)\n    if not succ then\n        log.error(\"write dump into file got error: \", err)\n    end\nend\n\n\nlocal function show_dump_file()\n    if not dump_params then\n        return 503, \"dump params is nil\"\n    end\n\n    local data, err = util.read_file(dump_params.path)\n    if not data then\n        return 503, err\n    end\n\n    return 200, data\nend\n\n\nfunction _M.connect(premature, consul_server, retry_delay)\n    if premature then\n        return\n    end\n\n    local consul_client = resty_consul:new({\n        host = consul_server.host,\n        port = consul_server.port,\n        connect_timeout = consul_server.connect_timeout,\n        read_timeout = consul_server.read_timeout,\n        default_args = consul_server.default_args,\n    })\n\n    log.info(\"consul_server: \", json_delay_encode(consul_server, true))\n    local result, err = consul_client:get(consul_server.consul_key)\n    local error_info = (err ~= nil and err)\n            or ((result ~= nil and result.status ~= 200)\n            and result.status)\n    if error_info then\n        log.error(\"connect consul: \", consul_server.server_name_key,\n            \" by key: \", consul_server.consul_key,\n            \", got result: \", json_delay_encode(result, true),\n            \", with error: \", error_info)\n\n        if not retry_delay then\n            retry_delay = 1\n        else\n            retry_delay = retry_delay * 4\n        end\n\n        log.warn(\"retry connecting consul after \", retry_delay, \" seconds\")\n        core_sleep(retry_delay)\n\n        goto ERR\n    end\n\n    log.info(\"connect consul: \", consul_server.server_name_key,\n        \", result status: \", result.status,\n        \", result.headers.index: \", result.headers['X-Consul-Index'],\n        \", result body: \", json_delay_encode(result.body))\n\n    -- if current index different last index then update application\n    if consul_server.index ~= result.headers['X-Consul-Index'] then\n        consul_server.index = result.headers['X-Consul-Index']\n        -- only long connect type use index\n        if consul_server.keepalive then\n            consul_server.default_args.index = result.headers['X-Consul-Index']\n        end\n\n        -- decode body, decode json, update application, error handling\n        if result.body and #result.body ~= 0 then\n            log.notice(\"server_name: \", consul_server.server_name_key,\n                \", header: \", core.json.encode(result.headers, true),\n                \", body: \", core.json.encode(result.body, true))\n\n            update_application(consul_server.server_name_key, result.body)\n            --update events\n            local ok, err = events:post(events_list._source, events_list.updating, applications)\n            if not ok then\n                log.error(\"post_event failure with \", events_list._source,\n                    \", update application error: \", err)\n            end\n\n            if dump_params then\n                ngx_timer_at(0, write_dump_srvs)\n            end\n        end\n    end\n\n    :: ERR ::\n    local keepalive = consul_server.keepalive\n    if keepalive then\n        local ok, err = ngx_timer_at(0, _M.connect, consul_server, retry_delay)\n        if not ok then\n            log.error(\"create ngx_timer_at got error: \", err)\n            return\n        end\n    end\nend\n\n\nlocal function format_consul_params(consul_conf)\n    local consul_server_list = core.table.new(0, #consul_conf.servers)\n    local args = {\n        token = consul_conf.token,\n        recurse = true\n    }\n\n    if consul_conf.keepalive then\n        args.wait = consul_conf.timeout.wait --blocked wait!=0; unblocked by wait=0\n        args.index = 0\n    end\n\n    for _, v in pairs(consul_conf.servers) do\n        local scheme, host, port, path = unpack(http.parse_uri(nil, v))\n        if scheme ~= \"http\" then\n            return nil, \"only support consul http schema address, eg: http://address:port\"\n        elseif path ~= \"/\" or core.string.has_suffix(v, '/') then\n            return nil, \"invalid consul server address, the valid format: http://address:port\"\n        end\n\n        core.table.insert(consul_server_list, {\n            host = host,\n            port = port,\n            connect_timeout = consul_conf.timeout.connect,\n            read_timeout = consul_conf.timeout.read,\n            consul_key = \"/kv/\" .. consul_conf.prefix,\n            server_name_key = v .. \"/v1/kv/\",\n            weight = consul_conf.weight,\n            keepalive = consul_conf.keepalive,\n            default_args = args,\n            index = 0,\n            fetch_interval = consul_conf.fetch_interval -- fetch interval to next connect consul\n        })\n    end\n\n    return consul_server_list\nend\n\n\nfunction _M.init_worker()\n    local consul_conf = local_conf.discovery.consul_kv\n\n    if consul_conf.dump then\n      local dump = consul_conf.dump\n      dump_params = dump\n\n      if dump.load_on_init then\n          read_dump_srvs()\n      end\n    end\n\n    events = require(\"apisix.events\")\n    events_list = events:event_list(\n        \"discovery_consul_update_application\",\n        \"updating\"\n    )\n\n    if 0 ~= ngx.worker.id() then\n        events:register(discovery_consul_callback, events_list._source, events_list.updating)\n        return\n    end\n\n    log.notice(\"consul_conf: \", core.json.encode(consul_conf))\n    default_weight = consul_conf.weight\n    -- set default service, used when the server node cannot be found\n    if consul_conf.default_service then\n        default_service = consul_conf.default_service\n        default_service.weight = default_weight\n    end\n    default_prefix_rule = \"(\" .. consul_conf.prefix .. \"/.*/)([a-zA-Z0-9.]+):([0-9]+)\"\n    log.info(\"default params, default_weight: \", default_weight,\n            \", default_prefix_rule: \", default_prefix_rule)\n    if consul_conf.skip_keys then\n        skip_keys_map = core.table.new(0, #consul_conf.skip_keys)\n        for _, v in ipairs(consul_conf.skip_keys) do\n            skip_keys_map[v] = true\n        end\n    end\n\n    local consul_servers_list, err = format_consul_params(consul_conf)\n    if err then\n        error(err)\n        return\n    end\n    log.info(\"consul_server_list: \", core.json.encode(consul_servers_list))\n\n    consul_apps = core.table.new(0, 1)\n    -- success or failure\n    for _, server in ipairs(consul_servers_list) do\n        local ok, err = ngx_timer_at(0, _M.connect, server)\n        if not ok then\n            error(\"create consul_kv got error: \" .. err)\n            return\n        end\n\n        if server.keepalive == false then\n            ngx_timer_every(server.fetch_interval, _M.connect, server)\n        end\n    end\nend\n\n\nfunction _M.dump_data()\n    return {config = local_conf.discovery.consul_kv, services = applications}\nend\n\n\nfunction _M.control_api()\n    return {\n        {\n            methods = {\"GET\"},\n            uris = {\"/show_dump_file\"},\n            handler = show_dump_file,\n        }\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/consul_kv/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn {\n    type = \"object\",\n    properties = {\n        servers = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n            }\n        },\n        token = {type = \"string\", default = \"\"},\n        fetch_interval = {type = \"integer\", minimum = 1, default = 3},\n        keepalive = {\n            type = \"boolean\",\n            default = true\n        },\n        prefix = {type = \"string\", default = \"upstreams\"},\n        weight = {type = \"integer\", minimum = 1, default = 1},\n        timeout = {\n            type = \"object\",\n            properties = {\n                connect = {type = \"integer\", minimum = 1, default = 2000},\n                read = {type = \"integer\", minimum = 1, default = 2000},\n                wait = {type = \"integer\", minimum = 1, default = 60}\n            },\n            default = {\n                connect = 2000,\n                read = 2000,\n                wait = 60,\n            }\n        },\n        skip_keys = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n            }\n        },\n        dump = {\n            type = \"object\",\n            properties = {\n                path = {type = \"string\", minLength = 1},\n                load_on_init = {type = \"boolean\", default = true},\n                expire = {type = \"integer\", default = 0},\n            },\n            required = {\"path\"},\n        },\n        default_service = {\n            type = \"object\",\n            properties = {\n                host = {type = \"string\"},\n                port = {type = \"integer\"},\n                metadata = {\n                    type = \"object\",\n                    properties = {\n                        fail_timeout = {type = \"integer\", default = 1},\n                        weight = {type = \"integer\", default = 1},\n                        max_fails = {type = \"integer\", default = 1}\n                    },\n                    default = {\n                        fail_timeout = 1,\n                        weight = 1,\n                        max_fails = 1\n                    }\n                }\n            }\n        }\n    },\n\n    required = {\"servers\"}\n}\n\n"
  },
  {
    "path": "apisix/discovery/dns/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core          = require(\"apisix.core\")\nlocal config_local  = require(\"apisix.core.config_local\")\nlocal is_http       = ngx.config.subsystem == \"http\"\nlocal ipairs        = ipairs\nlocal error         = error\n\n\nlocal dns_client\nlocal _M = {}\n\n\nfunction _M.nodes(service_name)\n    local host, port = core.utils.parse_addr(service_name)\n    core.log.info(\"discovery dns with host \", host, \", port \", port)\n\n    local records, err = dns_client:resolve(host, core.dns_client.RETURN_ALL)\n    if not records then\n        return nil, err\n    end\n\n    local nodes = core.table.new(#records, 0)\n    local index = 1\n    for _, r in ipairs(records) do\n        if r.address then\n            local node_port = port\n            if not node_port and r.port ~= 0 then\n                -- if the port is zero, fallback to use the default\n                node_port = r.port\n            end\n\n            -- ignore zero port when subsystem is stream\n            if node_port or is_http then\n                nodes[index] = {host = r.address, weight = r.weight or 1, port = node_port}\n                if r.priority then\n                    -- for SRV record, nodes with lower priority are chosen first\n                    nodes[index].priority = -r.priority\n                end\n                index = index + 1\n            end\n        end\n    end\n\n    return nodes\nend\n\n\nfunction _M.init_worker()\n    local local_conf = config_local.local_conf()\n    local servers = local_conf.discovery.dns.servers\n    local resolv_conf = local_conf.discovery.dns.resolv_conf\n    local default_order = {\"last\", \"SRV\", \"A\", \"AAAA\", \"CNAME\"}\n    local order = core.table.try_read_attr(local_conf, \"discovery\", \"dns\", \"order\")\n    order = order or default_order\n\n    local opts = {\n        hosts = {},\n        resolvConf = resolv_conf,\n        nameservers = servers,\n        order = order,\n    }\n\n    local client, err = core.dns_client.new(opts)\n    if not client then\n        error(\"failed to init the dns client: \", err)\n        return\n    end\n\n    dns_client = client\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/dns/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn {\n    type = \"object\",\n    properties = {\n        servers = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n            },\n        },\n        resolv_conf = {\n            type = \"string\",\n        },\n        order = {\n            type = \"array\",\n            minItems = 1,\n            maxItems = 5,\n            uniqueItems = true,\n            items = {\n                enum = {\"last\", \"SRV\", \"A\", \"AAAA\", \"CNAME\"}\n            },\n        },\n    },\n    oneOf = {\n        {\n            required = {\"servers\"},\n        },\n        {\n            required = {\"resolv_conf\"},\n        }\n    }\n}\n"
  },
  {
    "path": "apisix/discovery/eureka/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal local_conf         = require(\"apisix.core.config_local\").local_conf()\nlocal http               = require(\"resty.http\")\nlocal core               = require(\"apisix.core\")\nlocal ipmatcher          = require(\"resty.ipmatcher\")\nlocal ipairs             = ipairs\nlocal tostring           = tostring\nlocal type               = type\nlocal math_random        = math.random\nlocal ngx                = ngx\nlocal ngx_timer_at       = ngx.timer.at\nlocal ngx_timer_every    = ngx.timer.every\nlocal string_sub         = string.sub\nlocal str_find           = core.string.find\nlocal log                = core.log\n\nlocal default_weight\nlocal applications\n\n\nlocal _M = {\n    version = 0.1,\n}\n\n\n-- build all eureka endpoints from config\nlocal function build_endpoints()\n    local host_list = local_conf.discovery and\n        local_conf.discovery.eureka and local_conf.discovery.eureka.host\n    if not host_list or #host_list == 0 then\n        log.error(\"do not set eureka.host\")\n        return nil\n    end\n\n    local built_endpoints = core.table.new(#host_list, 0)\n    for _, h in ipairs(host_list) do\n        local url = h\n        local basic_auth\n        local auth_idx = str_find(url, \"@\")\n        if auth_idx then\n            local protocol_idx = str_find(url, \"://\")\n            local protocol = string_sub(url, 1, protocol_idx + 2)\n            local user_and_password = string_sub(url, protocol_idx + 3, auth_idx - 1)\n            local other = string_sub(url, auth_idx + 1)\n            url = protocol .. other\n            basic_auth = \"Basic \" .. ngx.encode_base64(user_and_password)\n        end\n        if local_conf.discovery.eureka.prefix then\n            url = url .. local_conf.discovery.eureka.prefix\n        end\n        if string_sub(url, #url) ~= \"/\" then\n            url = url .. \"/\"\n        end\n        core.table.insert(built_endpoints, { url = url, auth = basic_auth })\n    end\n    return built_endpoints\nend\n\n\nlocal function request(request_uri, basic_auth, method, path, query, body)\n    log.info(\"eureka uri:\", request_uri, \".\")\n    local url = request_uri .. path\n    local headers = core.table.new(0, 5)\n    headers['Connection'] = 'Keep-Alive'\n    headers['Accept'] = 'application/json'\n\n    if basic_auth then\n        headers['Authorization'] = basic_auth\n    end\n\n    if body and 'table' == type(body) then\n        local err\n        body, err = core.json.encode(body)\n        if not body then\n            return nil, 'invalid body : ' .. err\n        end\n        -- log.warn(method, url, body)\n        headers['Content-Type'] = 'application/json'\n    end\n\n    local httpc = http.new()\n    local timeout = local_conf.discovery.eureka.timeout\n    local connect_timeout = timeout and timeout.connect or 2000\n    local send_timeout = timeout and timeout.send or 2000\n    local read_timeout = timeout and timeout.read or 5000\n    log.info(\"connect_timeout:\", connect_timeout, \", send_timeout:\", send_timeout,\n            \", read_timeout:\", read_timeout, \".\")\n    httpc:set_timeouts(connect_timeout, send_timeout, read_timeout)\n    return httpc:request_uri(url, {\n        version = 1.1,\n        method = method,\n        headers = headers,\n        query = query,\n        body = body,\n        ssl_verify = false,\n    })\nend\n\n\nlocal function parse_instance(instance)\n    local status = instance.status\n    local overridden_status = instance.overriddenstatus or instance.overriddenStatus\n    if overridden_status and overridden_status ~= \"UNKNOWN\" then\n        status = overridden_status\n    end\n\n    if status ~= \"UP\" then\n        return\n    end\n    local port\n    if tostring(instance.port[\"@enabled\"]) == \"true\" and instance.port[\"$\"] then\n        port = instance.port[\"$\"]\n        -- secure = false\n    end\n    if tostring(instance.securePort[\"@enabled\"]) == \"true\" and instance.securePort[\"$\"] then\n        port = instance.securePort[\"$\"]\n        -- secure = true\n    end\n    local ip = instance.ipAddr\n    if not ipmatcher.parse_ipv4(ip) and\n            not ipmatcher.parse_ipv6(ip) then\n        log.info(instance.app, \" service \", instance.hostName, \" node IP \", ip,\n                \" is not an IP, trying to resolve it.\")\n        local ip_info, err = core.resolver.parse_domain(ip)\n        if ip_info then\n            return ip_info, port, instance.metadata, ip\n        else\n            log.error(\"failed to resolve \", ip, \": \", err)\n            return\n        end\n    end\n    return ip, port, instance.metadata\nend\n\n\nlocal function fetch_full_registry(premature)\n    if premature then\n        return\n    end\n\n    local endpoints = build_endpoints()\n    if not endpoints or #endpoints == 0 then\n        return\n    end\n\n    -- try endpoints from random position, failover on error\n    local selected_endpoint\n    local selected_body\n    local num_endpoints = #endpoints\n    local start = math_random(num_endpoints)\n    for i = 0, num_endpoints - 1 do\n        local endpoint = endpoints[(start + i - 1) % num_endpoints + 1]\n        local r, e = request(endpoint.url, endpoint.auth, \"GET\", \"apps\")\n        if r and r.body and r.status == 200 then\n            selected_endpoint = endpoint\n            selected_body = r.body\n            break\n        end\n        log.error(\"failed to fetch registry from \", endpoint.url, \": \",\n                 e or (r and (\"status=\" .. tostring(r.status)) or \"unknown\"))\n    end\n\n    if not selected_endpoint then\n        log.error(\"failed to fetch registry from all eureka hosts, \",\n                  \"no healthy endpoint found, tried \", num_endpoints, \" host(s)\")\n        return\n    end\n\n    local data, err = core.json.decode(selected_body)\n    if not data then\n        log.error(\"invalid response body: \", selected_body, \" err: \", err)\n        return\n    end\n    local apps = data.applications.application\n    local up_apps = core.table.new(0, #apps)\n    for _, app in ipairs(apps) do\n        for _, instance in ipairs(app.instance) do\n            local ip, port, metadata, domain = parse_instance(instance)\n            if ip and port then\n                local nodes = up_apps[app.name]\n                if not nodes then\n                    nodes = core.table.new(#app.instance, 0)\n                    up_apps[app.name] = nodes\n                end\n                core.table.insert(nodes, {\n                    host = ip,\n                    port = port,\n                    weight = metadata and metadata.weight or default_weight,\n                    metadata = metadata,\n                    domain = domain,\n                })\n                if metadata then\n                    -- remove useless data\n                    metadata.weight = nil\n                end\n            end\n        end\n    end\n    applications = up_apps\n    log.info(\"successfully updated service registry, services count=\",\n             core.table.nkeys(up_apps), \"; source=\", selected_endpoint.url)\nend\n\n\nfunction _M.nodes(service_name)\n    if not applications then\n        log.error(\"failed to fetch nodes for : \", service_name)\n        return\n    end\n\n    return applications[service_name]\nend\n\n\nfunction _M.init_worker()\n    default_weight = local_conf.discovery.eureka.weight or 100\n    log.info(\"default_weight:\", default_weight, \".\")\n    local fetch_interval = local_conf.discovery.eureka.fetch_interval or 30\n    log.info(\"fetch_interval:\", fetch_interval, \".\")\n    ngx_timer_at(0, fetch_full_registry)\n    ngx_timer_every(fetch_interval, fetch_full_registry)\nend\n\n\nfunction _M.dump_data()\n    return {config = local_conf.discovery.eureka, services = applications or {}}\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/eureka/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn {\n    type = \"object\",\n    properties = {\n        host = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n            },\n        },\n        fetch_interval = {type = \"integer\", minimum = 1, default = 30},\n        prefix = {type = \"string\"},\n        weight = {type = \"integer\", minimum = 0},\n        timeout = {\n            type = \"object\",\n            properties = {\n                connect = {type = \"integer\", minimum = 1, default = 2000},\n                send = {type = \"integer\", minimum = 1, default = 2000},\n                read = {type = \"integer\", minimum = 1, default = 5000},\n            }\n        },\n    },\n    required = {\"host\"}\n}\n"
  },
  {
    "path": "apisix/discovery/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal log          = require(\"apisix.core.log\")\nlocal local_conf   = require(\"apisix.core.config_local\").local_conf()\nlocal pairs        = pairs\n\nlocal discovery_type = local_conf.discovery\nlocal discovery = {}\n\nif discovery_type then\n    for discovery_name, _ in pairs(discovery_type) do\n        log.info(\"use discovery: \", discovery_name)\n        discovery[discovery_name] = require(\"apisix.discovery.\" .. discovery_name)\n    end\nend\n\nfunction discovery.init_worker()\n    if discovery_type then\n        for discovery_name, _ in pairs(discovery_type) do\n            discovery[discovery_name].init_worker()\n        end\n    end\nend\n\nreturn {\n    version = 0.1,\n    discovery = discovery\n}\n"
  },
  {
    "path": "apisix/discovery/kubernetes/informer_factory.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = ngx\nlocal ipairs = ipairs\nlocal string = string\nlocal math = math\nlocal type = type\nlocal core = require(\"apisix.core\")\nlocal http = require(\"resty.http\")\n\nlocal function list_query(informer)\n    local arguments = {\n        limit = informer.limit,\n    }\n\n    if informer.continue and informer.continue ~= \"\" then\n        arguments.continue = informer.continue\n    end\n\n    if informer.label_selector and informer.label_selector ~= \"\" then\n        arguments.labelSelector = informer.label_selector\n    end\n\n    if informer.field_selector and informer.field_selector ~= \"\" then\n        arguments.fieldSelector = informer.field_selector\n    end\n\n    return ngx.encode_args(arguments)\nend\n\n\nlocal function list(httpc, apiserver, informer)\n    local response, err = httpc:request({\n        path = informer.path,\n        query = list_query(informer),\n        headers = {\n            [\"Host\"] = apiserver.host .. \":\" .. apiserver.port,\n            [\"Authorization\"] = \"Bearer \" .. apiserver.token,\n            [\"Accept\"] = \"application/json\",\n            [\"Connection\"] = \"keep-alive\"\n        }\n    })\n\n    core.log.info(\"--raw=\", informer.path, \"?\", list_query(informer))\n\n    if not response then\n        return false, \"RequestError\", err or \"\"\n    end\n\n    if response.status ~= 200 then\n        return false, response.reason, response:read_body() or \"\"\n    end\n\n    local body, err = response:read_body()\n    if err then\n        return false, \"ReadBodyError\", err\n    end\n\n    local data = core.json.decode(body)\n    if not data or data.kind ~= informer.list_kind then\n        return false, \"UnexpectedBody\", body\n    end\n\n    informer.version = data.metadata.resourceVersion\n\n    if informer.on_added then\n        for _, item in ipairs(data.items or {}) do\n            informer:on_added(item, \"list\")\n        end\n    end\n\n    informer.continue = data.metadata.continue\n    if informer.continue and informer.continue ~= \"\" then\n        list(httpc, apiserver, informer)\n    end\n\n    return true\nend\n\n\nlocal function watch_query(informer)\n    local arguments = {\n        watch = \"true\",\n        allowWatchBookmarks = \"true\",\n        timeoutSeconds = informer.overtime,\n    }\n\n    if informer.version and informer.version ~= \"\" then\n        arguments.resourceVersion = informer.version\n    end\n\n    if informer.label_selector and informer.label_selector ~= \"\" then\n        arguments.labelSelector = informer.label_selector\n    end\n\n    if informer.field_selector and informer.field_selector ~= \"\" then\n        arguments.fieldSelector = informer.field_selector\n    end\n\n    return ngx.encode_args(arguments)\nend\n\n\nlocal function split_event (body, callback, ...)\n    local gmatch_iterator, err = ngx.re.gmatch(body, \"{\\\"type\\\":.*}\\n\", \"jao\")\n    if not gmatch_iterator then\n        return false, nil, \"GmatchError\", err\n    end\n\n    local captures\n    local captured_size = 0\n    local ok, reason\n    while true do\n        captures, err = gmatch_iterator()\n\n        if err then\n            return false, nil, \"GmatchError\", err\n        end\n\n        if not captures then\n            break\n        end\n\n        captured_size = captured_size + #captures[0]\n\n        ok, reason, err = callback(captures[0], ...)\n        if not ok then\n            return false, nil, reason, err\n        end\n    end\n\n    local remainder_body\n    if captured_size == #body then\n        remainder_body = \"\"\n    elseif captured_size == 0 then\n        remainder_body = body\n    elseif captured_size < #body then\n        remainder_body = string.sub(body, captured_size + 1)\n    end\n\n    return true, remainder_body\nend\n\n\nlocal function dispatch_event(event_string, informer)\n    local event = core.json.decode(event_string)\n\n    if not event or not event.type or not event.object then\n        return false, \"UnexpectedBody\", event_string\n    end\n\n    local tp = event.type\n\n    if tp == \"ERROR\" then\n        if event.object.code == 410 then\n            return false, \"ResourceGone\", nil\n        end\n        return false, \"UnexpectedBody\", event_string\n    end\n\n    local object = event.object\n    informer.version = object.metadata.resourceVersion\n\n    if tp == \"ADDED\" then\n        if informer.on_added then\n            informer:on_added(object, \"watch\")\n        end\n    elseif tp == \"DELETED\" then\n        if informer.on_deleted then\n            informer:on_deleted(object)\n        end\n    elseif tp == \"MODIFIED\" then\n        if informer.on_modified then\n            informer:on_modified(object)\n        end\n        -- elseif type == \"BOOKMARK\" then\n        --    do nothing\n    end\n\n    return true\nend\n\n\nlocal function watch(httpc, apiserver, informer)\n    local watch_times = 8\n    for _ = 1, watch_times do\n        local watch_seconds = 1800 + math.random(9, 999)\n        informer.overtime = watch_seconds\n        local http_seconds = watch_seconds + 120\n        httpc:set_timeouts(2000, 3000, http_seconds * 1000)\n\n        local response, err = httpc:request({\n            path = informer.path,\n            query = watch_query(informer),\n            headers = {\n                [\"Host\"] = apiserver.host .. \":\" .. apiserver.port,\n                [\"Authorization\"] = \"Bearer \" .. apiserver.token,\n                [\"Accept\"] = \"application/json\",\n                [\"Connection\"] = \"keep-alive\"\n            }\n        })\n\n        core.log.info(\"--raw=\", informer.path, \"?\", watch_query(informer))\n\n        if err then\n            return false, \"RequestError\", err\n        end\n\n        if response.status ~= 200 then\n            return false, response.reason, response:read_body() or \"\"\n        end\n\n        local ok\n        local remainder_body\n        local body\n        local reason\n\n        while true do\n            body, err = response.body_reader()\n            if err then\n                return false, \"ReadBodyError\", err\n            end\n\n            if not body then\n                break\n            end\n\n            if remainder_body and #remainder_body > 0 then\n                body = remainder_body .. body\n            end\n\n            ok, remainder_body, reason, err = split_event(body, dispatch_event, informer)\n            if not ok then\n                if reason == \"ResourceGone\" then\n                    return true\n                end\n                return false, reason, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nlocal function list_watch(informer, apiserver)\n    local ok\n    local reason, message\n    local httpc = http.new()\n\n    informer.continue = \"\"\n    informer.version = \"\"\n\n    informer.fetch_state = \"connecting\"\n    core.log.info(\"begin to connect \", apiserver.host, \":\", apiserver.port)\n\n    ok, message = httpc:connect({\n        scheme = apiserver.schema,\n        host = apiserver.host,\n        port = apiserver.port,\n        ssl_verify = false\n    })\n\n    if not ok then\n        informer.fetch_state = \"connect failed\"\n        core.log.error(\"connect apiserver failed, apiserver.host: \", apiserver.host,\n                \", apiserver.port: \", apiserver.port, \", message : \", message)\n        return false\n    end\n\n    core.log.info(\"begin to list \", informer.kind)\n    informer.fetch_state = \"listing\"\n    if informer.pre_list then\n        informer:pre_list()\n    end\n\n    ok, reason, message = list(httpc, apiserver, informer)\n    if not ok then\n        informer.fetch_state = \"list failed\"\n        core.log.error(\"list failed, kind: \", informer.kind,\n                \", reason: \", reason, \", message : \", message)\n        return false\n    end\n\n    informer.fetch_state = \"list finished\"\n    if informer.post_list then\n        informer:post_list()\n    end\n\n    core.log.info(\"begin to watch \", informer.kind)\n    informer.fetch_state = \"watching\"\n    ok, reason, message = watch(httpc, apiserver, informer)\n    if not ok then\n        informer.fetch_state = \"watch failed\"\n        core.log.error(\"watch failed, kind: \", informer.kind,\n                \", reason: \", reason, \", message : \", message)\n        return false\n    end\n\n    informer.fetch_state = \"watch finished\"\n\n    return true\nend\n\nlocal _M = {\n}\n\nfunction _M.new(group, version, kind, plural, namespace)\n    local tp\n    tp = type(group)\n    if tp ~= \"nil\" and tp ~= \"string\" then\n        return nil, \"group should set to string or nil type but \" .. tp\n    end\n\n    tp = type(namespace)\n    if tp ~= \"nil\" and tp ~= \"string\" then\n        return nil, \"namespace should set to string or nil type but \" .. tp\n    end\n\n    tp = type(version)\n    if tp ~= \"string\" or version == \"\" then\n        return nil, \"version should set to non-empty string\"\n    end\n\n    tp = type(kind)\n    if tp ~= \"string\" or kind == \"\" then\n        return nil, \"kind should set to non-empty string\"\n    end\n\n    tp = type(plural)\n    if tp ~= \"string\" or plural == \"\" then\n        return nil, \"plural should set to non-empty string\"\n    end\n\n    local path = \"\"\n    if group == nil or group == \"\" then\n        path = path .. \"/api/\" .. version\n    else\n        path = path .. \"/apis/\" .. group .. \"/\" .. version\n    end\n\n    if namespace and namespace ~= \"\" then\n        path = path .. \"/namespaces/\" .. namespace\n    end\n    path = path .. \"/\" .. plural\n\n    return {\n        kind = kind,\n        list_kind = kind .. \"List\",\n        plural = plural,\n        path = path,\n        limit = 120,\n        label_selector = \"\",\n        field_selector = \"\",\n        overtime = \"1800\",\n        version = \"\",\n        continue = \"\",\n        list_watch = list_watch\n    }\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/kubernetes/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = ngx\nlocal type = type\nlocal unpack = unpack\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal string = string\nlocal tonumber = tonumber\nlocal tostring = tostring\nlocal os = os\nlocal error = error\nlocal pcall = pcall\nlocal setmetatable = setmetatable\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal process = require(\"ngx.process\")\nlocal core = require(\"apisix.core\")\nlocal util = require(\"apisix.cli.util\")\nlocal local_conf = require(\"apisix.core.config_local\").local_conf()\nlocal informer_factory = require(\"apisix.discovery.kubernetes.informer_factory\")\n\n\nlocal ctx\n\nlocal endpoint_lrucache = core.lrucache.new({\n    ttl = 300,\n    count = 1024\n})\n\nlocal endpoint_buffer = {}\nlocal kubernetes_service_name_label = \"kubernetes.io/service-name\"\n\nlocal function sort_nodes_cmp(left, right)\n    if left.host ~= right.host then\n        return left.host < right.host\n    end\n\n    return left.port < right.port\nend\n\nlocal function update_endpoint_slices_cache(handle, endpoint_key, slice, slice_name)\n    if not handle.endpoint_slices_cache[endpoint_key] then\n        handle.endpoint_slices_cache[endpoint_key] = {}\n    end\n    local endpoint_slices = handle.endpoint_slices_cache[endpoint_key]\n    endpoint_slices[slice_name] = slice\nend\n\nlocal function get_endpoints_from_cache(handle, endpoint_key)\n    local endpoint_slices = handle.endpoint_slices_cache[endpoint_key] or {}\n    local endpoints = {}\n    for _, endpoint_slice in pairs(endpoint_slices) do\n        for port, targets in pairs(endpoint_slice) do\n            if not endpoints[port] then\n                endpoints[port] = core.table.new(0, #targets)\n            end\n            core.table.insert_tail(endpoints[port], unpack(targets))\n        end\n    end\n\n    return endpoints\nend\n\nlocal function update_endpoint_dict(handle, endpoints, endpoint_key)\n    local endpoint_content = core.json.encode(endpoints, true)\n    local endpoint_version = ngx.crc32_long(endpoint_content)\n    local _, err\n    _, err = handle.endpoint_dict:safe_set(endpoint_key .. \"#version\", endpoint_version)\n    if err then\n        return false, \"set endpoint version into discovery DICT failed, \" .. err\n    end\n    _, err = handle.endpoint_dict:safe_set(endpoint_key, endpoint_content)\n    if err then\n        handle.endpoint_dict:delete(endpoint_key .. \"#version\")\n        return false, \"set endpoint into discovery DICT failed, \" .. err\n    end\n\n    return true\nend\n\nlocal function validate_endpoint_slice(endpoint_slice)\n    if not endpoint_slice.metadata then\n        return false, \"endpoint_slice has no metadata, endpointSlice: \"\n                .. core.json.encode(endpoint_slice)\n    end\n    if not endpoint_slice.metadata.name then\n        return false, \"endpoint_slice has no metadata.name, endpointSlice: \"\n                .. core.json.encode(endpoint_slice)\n    end\n    if not endpoint_slice.metadata.namespace then\n        return false, \"endpoint_slice has no metadata.namespace, endpointSlice: \"\n                .. core.json.encode(endpoint_slice)\n    end\n    if not endpoint_slice.metadata.labels\n            or not endpoint_slice.metadata.labels[kubernetes_service_name_label] then\n        return false, \"endpoint_slice has no service-name, endpointSlice: \"\n                .. core.json.encode(endpoint_slice)\n    end\n\n    return true\nend\n\nlocal function on_endpoint_slices_modified(handle, endpoint_slice, operate)\n    local ok, err = validate_endpoint_slice(endpoint_slice)\n    if not ok then\n        core.log.error(\"endpoint_slice validation fail: \", err)\n        return\n    end\n    if handle.namespace_selector and\n            not handle:namespace_selector(endpoint_slice.metadata.namespace) then\n        return\n    end\n\n    core.log.debug(\"get endpoint_slice: \", core.json.delay_encode(endpoint_slice))\n    --record nodes to every port in service\n    local port_to_nodes = {}\n\n    local slice_endpoints = endpoint_slice.endpoints\n    if not slice_endpoints or slice_endpoints == ngx.null then\n        slice_endpoints = {}\n    end\n\n    for _, endpoint in ipairs(slice_endpoints) do\n        if endpoint.addresses\n                and endpoint.conditions\n                and endpoint.conditions.ready then\n            local addresses = endpoint.addresses\n            for _, port in ipairs(endpoint_slice.ports or {}) do\n                local port_name\n                if port.name then\n                    port_name = port.name\n                elseif port.targetPort then\n                    port_name = tostring(port.targetPort)\n                else\n                    port_name = tostring(port.port)\n                end\n\n                local nodes = port_to_nodes[port_name]\n                if nodes == nil then\n                    nodes = core.table.new(0, #slice_endpoints * #addresses)\n                    port_to_nodes[port_name] = nodes\n                end\n\n                for _, ip in ipairs(addresses) do\n                    core.table.insert(nodes, {\n                        host = ip,\n                        port = port.port,\n                        weight = handle.default_weight\n                    })\n                end\n            end\n        end\n    end\n\n    local endpoint_key = endpoint_slice.metadata.namespace\n            .. \"/\" .. endpoint_slice.metadata.labels[kubernetes_service_name_label]\n    update_endpoint_slices_cache(handle, endpoint_key, port_to_nodes, endpoint_slice.metadata.name)\n\n    local cached_endpoints = get_endpoints_from_cache(handle, endpoint_key)\n    for _, nodes in pairs(cached_endpoints) do\n        core.table.sort(nodes, sort_nodes_cmp)\n    end\n\n    local ok, err = update_endpoint_dict(handle, cached_endpoints, endpoint_key)\n    if not ok then\n        core.log.error(\"failed to update endpoint dict for endpoint: \", endpoint_key,\n                \", err: \", err)\n        return\n    end\n    if operate == \"list\" then\n        handle.current_keys_hash[endpoint_key] = true\n        handle.current_keys_hash[endpoint_key .. \"#version\"] = true\n    end\nend\n\nlocal function on_endpoint_slices_deleted(handle, endpoint_slice)\n    local ok, err = validate_endpoint_slice(endpoint_slice)\n    if not ok then\n        core.log.error(\"endpoint_slice validation fail: \", err)\n        return\n    end\n\n    if handle.namespace_selector and\n            not handle:namespace_selector(endpoint_slice.metadata.namespace) then\n        return\n    end\n\n    core.log.debug(\"delete endpoint_slice: \", core.json.delay_encode(endpoint_slice))\n\n    local endpoint_key = endpoint_slice.metadata.namespace\n            .. \"/\" .. endpoint_slice.metadata.labels[kubernetes_service_name_label]\n    update_endpoint_slices_cache(handle, endpoint_key, nil, endpoint_slice.metadata.name)\n\n    local cached_endpoints = get_endpoints_from_cache(handle, endpoint_key)\n    for _, nodes in pairs(cached_endpoints) do\n        core.table.sort(nodes, sort_nodes_cmp)\n    end\n\n    ok, err = update_endpoint_dict(handle, cached_endpoints, endpoint_key)\n    if not ok then\n        core.log.error(\"failed to update endpoint dict for endpoint: \", endpoint_key,\n                \", err: \", err)\n    end\nend\n\nlocal function on_endpoint_modified(handle, endpoint, operate)\n    if handle.namespace_selector and\n            not handle:namespace_selector(endpoint.metadata.namespace) then\n        return\n    end\n\n    core.log.debug(core.json.delay_encode(endpoint))\n    core.table.clear(endpoint_buffer)\n\n    local subsets = endpoint.subsets\n    for _, subset in ipairs(subsets or {}) do\n        if subset.addresses then\n            local addresses = subset.addresses\n            for _, port in ipairs(subset.ports or {}) do\n                local port_name\n                if port.name then\n                    port_name = port.name\n                elseif port.targetPort then\n                    port_name = tostring(port.targetPort)\n                else\n                    port_name = tostring(port.port)\n                end\n\n                local nodes = endpoint_buffer[port_name]\n                if nodes == nil then\n                    nodes = core.table.new(0, #subsets * #addresses)\n                    endpoint_buffer[port_name] = nodes\n                end\n\n                for _, address in ipairs(subset.addresses) do\n                    core.table.insert(nodes, {\n                        host = address.ip,\n                        port = port.port,\n                        weight = handle.default_weight\n                    })\n                end\n            end\n        end\n    end\n\n\n    for _, nodes in pairs(endpoint_buffer) do\n        core.table.sort(nodes, sort_nodes_cmp)\n    end\n\n    local endpoint_key = endpoint.metadata.namespace .. \"/\" .. endpoint.metadata.name\n    local ok, err = update_endpoint_dict(handle, endpoint_buffer, endpoint_key)\n    if not ok then\n        core.log.error(\"failed to update endpoint dict for endpoint: \", endpoint_key,\n                \", err: \", err)\n        return\n    end\n    if operate == \"list\" then\n        handle.current_keys_hash[endpoint_key] = true\n        handle.current_keys_hash[endpoint_key .. \"#version\"] = true\n    end\nend\n\n\nlocal function on_endpoint_deleted(handle, endpoint)\n    if handle.namespace_selector and\n            not handle:namespace_selector(endpoint.metadata.namespace) then\n        return\n    end\n\n    core.log.debug(core.json.delay_encode(endpoint))\n    local endpoint_key = endpoint.metadata.namespace .. \"/\" .. endpoint.metadata.name\n    handle.endpoint_dict:delete(endpoint_key .. \"#version\")\n    handle.endpoint_dict:delete(endpoint_key)\nend\n\n\nlocal function pre_list(handle)\n    handle.current_keys_hash = {}\n    handle.existing_keys = handle.endpoint_dict:get_keys(0)\n    if handle.endpoint_slices_cache then\n        handle.endpoint_slices_cache = {}\n    end\nend\n\n\nlocal function post_list(handle)\n    if handle.existing_keys and handle.current_keys_hash then\n        for _, key in ipairs(handle.existing_keys) do\n            if not handle.current_keys_hash[key] then\n                core.log.info(\"kubernetes discovery module found dirty data in shared dict, key: \",\n                              key)\n                handle.endpoint_dict:delete(key)\n            end\n        end\n        handle.existing_keys = nil\n        handle.current_keys_hash = nil\n    end\n    local _, err = handle.endpoint_dict:safe_set(\"discovery_ready\", true)\n    if err then\n        core.log.error(\"set discovery_ready flag into discovery DICT failed, \", err)\n    end\nend\n\n\nlocal function setup_label_selector(conf, informer)\n    informer.label_selector = conf.label_selector\nend\n\n\nlocal function setup_namespace_selector(conf, informer)\n    local ns = conf.namespace_selector\n    if ns == nil then\n        informer.namespace_selector = nil\n        return\n    end\n\n    if ns.equal then\n        informer.field_selector = \"metadata.namespace=\" .. ns.equal\n        informer.namespace_selector = nil\n        return\n    end\n\n    if ns.not_equal then\n        informer.field_selector = \"metadata.namespace!=\" .. ns.not_equal\n        informer.namespace_selector = nil\n        return\n    end\n\n    if ns.match then\n        informer.namespace_selector = function(self, namespace)\n            local match = conf.namespace_selector.match\n            local m, err\n            for _, v in ipairs(match) do\n                m, err = ngx.re.match(namespace, v, \"jo\")\n                if m and m[0] == namespace then\n                    return true\n                end\n                if err then\n                    core.log.error(\"ngx.re.match failed: \", err)\n                end\n            end\n            return false\n        end\n        return\n    end\n\n    if ns.not_match then\n        informer.namespace_selector = function(self, namespace)\n            local not_match = conf.namespace_selector.not_match\n            local m, err\n            for _, v in ipairs(not_match) do\n                m, err = ngx.re.match(namespace, v, \"jo\")\n                if m and m[0] == namespace then\n                    return false\n                end\n                if err then\n                    return false\n                end\n            end\n            return true\n        end\n        return\n    end\n\n    return\nend\n\n\nlocal function read_env(key)\n    if #key > 3 then\n        local first, second = string.byte(key, 1, 2)\n        if first == string.byte('$') and second == string.byte('{') then\n            local last = string.byte(key, #key)\n            if last == string.byte('}') then\n                local env = string.sub(key, 3, #key - 1)\n                local value = os.getenv(env)\n                if not value then\n                    return nil, \"not found environment variable \" .. env\n                end\n                return value\n            end\n        end\n    end\n    return key\nend\n\nlocal function read_token(token_file)\n    local token, err = util.read_file(token_file)\n    if err then\n        return nil, err\n    end\n\n    -- remove possible extra whitespace\n    return util.trim(token)\nend\n\nlocal function get_apiserver(conf)\n    local apiserver = {\n        schema = \"\",\n        host = \"\",\n        port = \"\",\n    }\n\n    apiserver.schema = conf.service.schema\n    if apiserver.schema ~= \"http\" and apiserver.schema ~= \"https\" then\n        return nil, \"service.schema should set to one of [http,https] but \" .. apiserver.schema\n    end\n\n    local err\n    apiserver.host, err = read_env(conf.service.host)\n    if err then\n        return nil, err\n    end\n\n    if apiserver.host == \"\" then\n        return nil, \"service.host should set to non-empty string\"\n    end\n\n    local port\n    port, err = read_env(conf.service.port)\n    if err then\n        return nil, err\n    end\n\n    apiserver.port = tonumber(port)\n    if not apiserver.port or apiserver.port <= 0 or apiserver.port > 65535 then\n        return nil, \"invalid port value: \" .. apiserver.port\n    end\n\n    if conf.client.token then\n        local token, err = read_env(conf.client.token)\n        if err then\n            return nil, err\n        end\n        apiserver.token = util.trim(token)\n    elseif conf.client.token_file and conf.client.token_file ~= \"\" then\n        setmetatable(apiserver, {\n            __index = function(_, key)\n                if key ~= \"token\" then\n                    return\n                end\n\n                local token_file, err = read_env(conf.client.token_file)\n                if err then\n                    core.log.error(\"failed to read token file path: \", err)\n                    return\n                end\n\n                local token, err = read_token(token_file)\n                if err then\n                    core.log.error(\"failed to read token from file: \", err)\n                    return\n                end\n                core.log.debug(\"re-read the token value\")\n                return token\n            end\n        })\n    else\n        return nil, \"one of [client.token,client.token_file] should be set but none\"\n    end\n\n    if apiserver.schema == \"https\" and apiserver.token == \"\" then\n        return nil, \"apiserver.token should set to non-empty string when service.schema is https\"\n    end\n\n    return apiserver\nend\n\nlocal function create_endpoint_lrucache(endpoint_dict, endpoint_key, endpoint_port)\n    local endpoint_content = endpoint_dict:get(endpoint_key)\n    if not endpoint_content then\n        core.log.error(\"get empty endpoint content from discovery DIC, this should not happen \",\n                endpoint_key)\n        return nil\n    end\n\n    local endpoint = core.json.decode(endpoint_content)\n    if not endpoint then\n        core.log.error(\"decode endpoint content failed, this should not happen, content: \",\n                endpoint_content)\n        return nil\n    end\n\n    return endpoint[endpoint_port]\nend\n\n\nlocal _M = {\n    version = \"0.0.1\"\n}\n\n\nlocal function start_fetch(handle)\n    local timer_runner\n    timer_runner = function(premature)\n        if premature then\n            return\n        end\n\n        local ok, status = pcall(handle.list_watch, handle, handle.apiserver)\n\n        local retry_interval = 0\n        if not ok then\n            core.log.error(\"list_watch failed, kind: \", handle.kind,\n                    \", reason: \", \"RuntimeException\", \", message : \", status)\n            retry_interval = 40\n        elseif not status then\n            retry_interval = 40\n        end\n\n        ngx.timer.at(retry_interval, timer_runner)\n    end\n    ngx.timer.at(0, timer_runner)\nend\n\n\nlocal function get_endpoint_dict_name(id)\n    local shm = \"kubernetes\"\n\n    if id and type(id) == \"string\" and #id > 0 then\n        shm = shm .. \"-\" .. id\n    end\n\n    if not is_http then\n        shm = shm .. \"-stream\"\n    end\n    return shm\nend\n\n\nlocal function get_endpoint_dict(id)\n    local dict_name = get_endpoint_dict_name(id)\n    return ngx.shared[dict_name]\nend\n\n\nlocal function single_mode_init(conf)\n    local endpoint_dict = get_endpoint_dict()\n\n    if not endpoint_dict then\n        error(\"failed to get lua_shared_dict: ngx.shared.kubernetes, \" ..\n                \"please check your APISIX version\")\n    end\n\n    if process.type() ~= \"privileged agent\" then\n        ctx = endpoint_dict\n        return\n    end\n\n    local apiserver, err = get_apiserver(conf)\n    if err then\n        error(err)\n        return\n    end\n\n    local default_weight = conf.default_weight\n    local endpoints_informer, err\n    if conf.watch_endpoint_slices then\n        endpoints_informer, err = informer_factory.new(\"discovery.k8s.io\", \"v1\",\n                                                       \"EndpointSlice\", \"endpointslices\", \"\")\n    else\n        endpoints_informer, err = informer_factory.new(\"\", \"v1\", \"Endpoints\", \"endpoints\", \"\")\n    end\n    if err then\n        error(err)\n        return\n    end\n\n    setup_namespace_selector(conf, endpoints_informer)\n    setup_label_selector(conf, endpoints_informer)\n\n    if conf.watch_endpoint_slices then\n        endpoints_informer.on_added = on_endpoint_slices_modified\n        endpoints_informer.on_modified = on_endpoint_slices_modified\n        endpoints_informer.on_deleted = on_endpoint_slices_deleted\n        endpoints_informer.endpoint_slices_cache = {}\n    else\n        endpoints_informer.on_added = on_endpoint_modified\n        endpoints_informer.on_modified = on_endpoint_modified\n        endpoints_informer.on_deleted = on_endpoint_deleted\n    end\n\n    endpoints_informer.pre_list = pre_list\n    endpoints_informer.post_list = post_list\n\n    ctx = setmetatable({\n        endpoint_dict = endpoint_dict,\n        apiserver = apiserver,\n        default_weight = default_weight\n    }, { __index = endpoints_informer })\n\n    start_fetch(ctx)\nend\n\n\nlocal function single_mode_nodes(service_name)\n    local pattern = \"^(.*):(.*)$\" -- namespace/name:port_name\n    local match = ngx.re.match(service_name, pattern, \"jo\")\n    if not match then\n        core.log.error(\"get unexpected upstream service_name:　\", service_name)\n        return nil\n    end\n\n    local endpoint_dict = ctx\n    local endpoint_key = match[1]\n    local endpoint_port = match[2]\n    local endpoint_version = endpoint_dict:get(endpoint_key .. \"#version\")\n    if not endpoint_version then\n        core.log.info(\"get empty endpoint version from discovery DICT \", endpoint_key)\n        return nil\n    end\n\n    return endpoint_lrucache(service_name, endpoint_version,\n            create_endpoint_lrucache, endpoint_dict, endpoint_key, endpoint_port)\nend\n\n\nlocal function multiple_mode_worker_init(confs)\n    for _, conf in ipairs(confs) do\n\n        local id = conf.id\n        if ctx[id] then\n            error(\"duplicate id value\")\n        end\n\n        local endpoint_dict = get_endpoint_dict(id)\n        if not endpoint_dict then\n            error(string.format(\"failed to get lua_shared_dict: ngx.shared.kubernetes-%s, \", id) ..\n                    \"please check your APISIX version\")\n        end\n\n        ctx[id] = endpoint_dict\n    end\nend\n\n\nlocal function multiple_mode_init(confs)\n    ctx = core.table.new(#confs, 0)\n\n    if process.type() ~= \"privileged agent\" then\n        multiple_mode_worker_init(confs)\n        return\n    end\n\n    for _, conf in ipairs(confs) do\n        local id = conf.id\n\n        if ctx[id] then\n            error(\"duplicate id value\")\n        end\n\n        local endpoint_dict = get_endpoint_dict(id)\n        if not endpoint_dict then\n            error(string.format(\"failed to get lua_shared_dict: ngx.shared.kubernetes-%s, \", id) ..\n                    \"please check your APISIX version\")\n        end\n\n        local apiserver, err = get_apiserver(conf)\n        if err then\n            error(err)\n            return\n        end\n\n        local default_weight = conf.default_weight\n\n        local endpoints_informer, err\n        if conf.watch_endpoint_slices then\n            endpoints_informer, err = informer_factory.new(\"discovery.k8s.io\", \"v1\",\n                                                           \"EndpointSlice\", \"endpointslices\", \"\")\n        else\n            endpoints_informer, err = informer_factory.new(\"\", \"v1\", \"Endpoints\", \"endpoints\", \"\")\n        end\n        if err then\n            error(err)\n            return\n        end\n\n        setup_namespace_selector(conf, endpoints_informer)\n        setup_label_selector(conf, endpoints_informer)\n\n        if conf.watch_endpoint_slices then\n            endpoints_informer.on_added = on_endpoint_slices_modified\n            endpoints_informer.on_modified = on_endpoint_slices_modified\n            endpoints_informer.on_deleted = on_endpoint_slices_deleted\n            endpoints_informer.endpoint_slices_cache = {}\n        else\n            endpoints_informer.on_added = on_endpoint_modified\n            endpoints_informer.on_modified = on_endpoint_modified\n            endpoints_informer.on_deleted = on_endpoint_deleted\n        end\n\n        endpoints_informer.pre_list = pre_list\n        endpoints_informer.post_list = post_list\n\n        ctx[id] = setmetatable({\n            endpoint_dict = endpoint_dict,\n            apiserver = apiserver,\n            default_weight = default_weight\n        }, { __index = endpoints_informer })\n    end\n\n    for _, item in pairs(ctx) do\n        start_fetch(item)\n    end\nend\n\n\nlocal function multiple_mode_nodes(service_name)\n    local pattern = \"^(.*)/(.*/.*):(.*)$\" -- id/namespace/name:port_name\n    local match = ngx.re.match(service_name, pattern, \"jo\")\n    if not match then\n        core.log.error(\"get unexpected upstream service_name:　\", service_name)\n        return nil\n    end\n\n    local id = match[1]\n    local endpoint_dict = ctx[id]\n    if not endpoint_dict then\n        core.log.error(\"id not exist\")\n        return nil\n    end\n\n    local endpoint_key = match[2]\n    local endpoint_port = match[3]\n    local endpoint_version = endpoint_dict:get(endpoint_key .. \"#version\")\n    if not endpoint_version then\n        core.log.info(\"get empty endpoint version from discovery DICT \", endpoint_key)\n        return nil\n    end\n\n    return endpoint_lrucache(service_name, endpoint_version,\n            create_endpoint_lrucache, endpoint_dict, endpoint_key, endpoint_port)\nend\n\n\nfunction _M.init_worker()\n    local discovery_conf = local_conf.discovery.kubernetes\n    core.log.info(\"kubernetes discovery conf: \", core.json.delay_encode(discovery_conf))\n    if #discovery_conf == 0 then\n        _M.nodes = single_mode_nodes\n        single_mode_init(discovery_conf)\n    else\n        _M.nodes = multiple_mode_nodes\n        multiple_mode_init(discovery_conf)\n    end\nend\n\n\nlocal function dump_endpoints_from_dict(endpoint_dict)\n    local keys, err = endpoint_dict:get_keys(0)\n    if err then\n        core.log.error(\"get keys from discovery dict failed: \", err)\n        return\n    end\n\n    if not keys or #keys == 0 then\n        return\n    end\n\n    local endpoints = {}\n    for i = 1, #keys do\n        local key = keys[i]\n        -- skip key with suffix #version\n        if key:sub(-#\"#version\") ~= \"#version\" then\n            local value = endpoint_dict:get(key)\n            core.table.insert(endpoints, {\n                name = key,\n                value = value\n            })\n        end\n    end\n\n    return endpoints\nend\n\n\nfunction _M.dump_data()\n    local discovery_conf = local_conf.discovery.kubernetes\n    local eps = {}\n\n    if #discovery_conf == 0 then\n        -- Single mode: discovery_conf is a single configuration object\n        local endpoint_dict = get_endpoint_dict()\n        local endpoints = dump_endpoints_from_dict(endpoint_dict)\n        if endpoints then\n            core.table.insert(eps, {\n                endpoints = endpoints\n            })\n        end\n    else\n        -- Multiple mode: discovery_conf is an array of configuration objects\n        for _, conf in ipairs(discovery_conf) do\n            local endpoint_dict = get_endpoint_dict(conf.id)\n            local endpoints = dump_endpoints_from_dict(endpoint_dict)\n            if endpoints then\n                core.table.insert(eps, {\n                    id = conf.id,\n                    endpoints = endpoints\n                })\n            end\n        end\n    end\n\n    return {config = discovery_conf, endpoints = eps}\nend\n\n\nlocal function check_ready(id)\n    local endpoint_dict = get_endpoint_dict(id)\n    if not endpoint_dict then\n        core.log.error(\"failed to get lua_shared_dict:\", get_endpoint_dict_name(id),\n                       \", please check your APISIX version\")\n        return false, \"failed to get lua_shared_dict: \" .. get_endpoint_dict_name(id)\n            .. \", please check your APISIX version\"\n    end\n    -- check flag\n    local ready = endpoint_dict:get(\"discovery_ready\")\n    if not ready then\n        core.log.warn(\"kubernetes discovery not ready\")\n        return false, \"kubernetes discovery not ready\"\n    end\n    return true\nend\n\n\nlocal function single_mode_check_discovery_ready()\n    local _, err = check_ready()\n    if err then\n        return false, err\n    end\n    return true\nend\n\n\nlocal function multiple_mode_check_discovery_ready(confs)\n    for _, conf in ipairs(confs) do\n        local _, err = check_ready(conf.id)\n        if err then\n            return false, err\n        end\n    end\n    return true\nend\n\n\nfunction _M.check_discovery_ready()\n    local discovery_conf = local_conf.discovery and local_conf.discovery.kubernetes\n    if not discovery_conf then\n        return true\n    end\n    if #discovery_conf == 0 then\n        return single_mode_check_discovery_ready()\n    else\n        return multiple_mode_check_discovery_ready(discovery_conf)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/kubernetes/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal host_patterns = {\n    { pattern = [[^\\${[_A-Za-z]([_A-Za-z0-9]*[_A-Za-z])*}$]] },\n    { pattern = [[^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$]] },\n}\n\nlocal port_patterns = {\n    { pattern = [[^\\${[_A-Za-z]([_A-Za-z0-9]*[_A-Za-z])*}$]] },\n    { pattern = [[^(([1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5]))$]] },\n}\n\nlocal schema_schema = {\n    type = \"string\",\n    enum = { \"http\", \"https\" },\n    default = \"https\",\n}\n\nlocal token_patterns = {\n    { pattern = [[\\${[_A-Za-z]([_A-Za-z0-9]*[_A-Za-z])*}$]] },\n    { pattern = [[^[A-Za-z0-9+\\/._=-]{0,4096}$]] },\n}\n\nlocal token_schema = {\n    type = \"string\",\n    oneOf = token_patterns,\n}\n\nlocal token_file_schema = {\n    type = \"string\",\n    pattern = [[^[^\\:*?\"<>|]*$]],\n    minLength = 1,\n    maxLength = 500,\n}\n\nlocal namespace_pattern = [[^[a-z0-9]([-a-z0-9_.]*[a-z0-9])?$]]\n\nlocal namespace_regex_pattern = [[^[\\x21-\\x7e]*$]]\n\nlocal namespace_selector_schema = {\n    type = \"object\",\n    properties = {\n        equal = {\n            type = \"string\",\n            pattern = namespace_pattern,\n        },\n        not_equal = {\n            type = \"string\",\n            pattern = namespace_pattern,\n        },\n        match = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                pattern = namespace_regex_pattern\n            },\n            minItems = 1\n        },\n        not_match = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                pattern = namespace_regex_pattern\n            },\n            minItems = 1\n        },\n    },\n    oneOf = {\n        { required = {} },\n        { required = { \"equal\" } },\n        { required = { \"not_equal\" } },\n        { required = { \"match\" } },\n        { required = { \"not_match\" } }\n    },\n}\n\nlocal label_selector_schema = {\n    type = \"string\",\n}\n\nlocal default_weight_schema = {\n    type = \"integer\",\n    default = 50,\n    minimum = 0,\n}\n\nlocal shared_size_schema = {\n    type = \"string\",\n    pattern = [[^[1-9][0-9]*m$]],\n    default = \"1m\",\n}\n\nlocal watch_endpoint_slices_schema = {\n    type = \"boolean\",\n    default = false,\n}\n\nreturn {\n    anyOf = {\n        {\n            type = \"object\",\n            properties = {\n                service = {\n                    type = \"object\",\n                    properties = {\n                        schema = schema_schema,\n                        host = {\n                            type = \"string\",\n                            oneOf = host_patterns,\n                            default = \"${KUBERNETES_SERVICE_HOST}\",\n                        },\n                        port = {\n                            type = \"string\",\n                            oneOf = port_patterns,\n                            default = \"${KUBERNETES_SERVICE_PORT}\",\n                        },\n                    },\n                    default = {\n                        schema = \"https\",\n                        host = \"${KUBERNETES_SERVICE_HOST}\",\n                        port = \"${KUBERNETES_SERVICE_PORT}\",\n                    }\n                },\n                client = {\n                    type = \"object\",\n                    properties = {\n                        token = token_schema,\n                        token_file = token_file_schema,\n                    },\n                    default = {\n                        token_file = \"/var/run/secrets/kubernetes.io/serviceaccount/token\"\n                    },\n                    [\"if\"] = {\n                        [\"not\"] = {\n                            anyOf = {\n                                { required = { \"token\" } },\n                                { required = { \"token_file\" } },\n                            }\n                        }\n                    },\n                    [\"then\"] = {\n                        properties = {\n                            token_file = {\n                                default = \"/var/run/secrets/kubernetes.io/serviceaccount/token\"\n                            }\n                        }\n                    }\n                },\n                namespace_selector = namespace_selector_schema,\n                label_selector = label_selector_schema,\n                default_weight = default_weight_schema,\n                shared_size = shared_size_schema,\n                watch_endpoint_slices = watch_endpoint_slices_schema,\n            },\n        },\n        {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"object\",\n                properties = {\n                    id = {\n                        type = \"string\",\n                        pattern = [[^[a-z0-9]{1,64}$]]\n                    },\n                    service = {\n                        type = \"object\",\n                        properties = {\n                            schema = schema_schema,\n                            host = {\n                                type = \"string\",\n                                oneOf = host_patterns,\n                            },\n                            port = {\n                                type = \"string\",\n                                oneOf = port_patterns,\n                            },\n                        },\n                        required = { \"host\", \"port\" }\n                    },\n                    client = {\n                        type = \"object\",\n                        properties = {\n                            token = token_schema,\n                            token_file = token_file_schema,\n                        },\n                        oneOf = {\n                            { required = { \"token\" } },\n                            { required = { \"token_file\" } },\n                        },\n                    },\n                    namespace_selector = namespace_selector_schema,\n                    label_selector = label_selector_schema,\n                    default_weight = default_weight_schema,\n                    shared_size = shared_size_schema,\n                    watch_endpoint_slices = watch_endpoint_slices_schema,\n                },\n                required = { \"id\", \"service\", \"client\" }\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "apisix/discovery/nacos/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal require            = require\nlocal local_conf         = require('apisix.core.config_local').local_conf()\nlocal http               = require('resty.http')\nlocal core               = require('apisix.core')\nlocal ipairs             = ipairs\nlocal pairs              = pairs\nlocal type               = type\nlocal math_random        = math.random\nlocal ngx                = ngx\nlocal ngx_re             = require('ngx.re')\nlocal ngx_timer_at       = ngx.timer.at\nlocal ngx_timer_every    = ngx.timer.every\nlocal string             = string\nlocal string_sub         = string.sub\nlocal str_byte           = string.byte\nlocal str_find           = core.string.find\nlocal log                = core.log\n\nlocal default_weight\nlocal nacos_dict = ngx.shared.nacos --key: namespace_id.group_name.service_name\nif not nacos_dict then\n    error(\"lua_shared_dict \\\"nacos\\\" not configured\")\nend\n\nlocal auth_path = 'auth/login'\nlocal instance_list_path = 'ns/instance/list?healthyOnly=true&serviceName='\nlocal default_namespace_id = \"public\"\nlocal default_group_name = \"DEFAULT_GROUP\"\nlocal access_key\nlocal secret_key\n\n\nlocal _M = {}\n\nlocal function get_key(namespace_id, group_name, service_name)\n    return namespace_id .. '.' .. group_name .. '.' .. service_name\nend\n\nlocal function request(request_uri, path, body, method, basic_auth)\n    local url = request_uri .. path\n    log.info('request url:', url)\n    local headers = {}\n    headers['Accept'] = 'application/json'\n\n    if basic_auth then\n        headers['Authorization'] = basic_auth\n    end\n\n    if body and 'table' == type(body) then\n        local err\n        body, err = core.json.encode(body)\n        if not body then\n            return nil, 'invalid body : ' .. err\n        end\n        headers['Content-Type'] = 'application/json'\n    end\n\n    local httpc = http.new()\n    local timeout = local_conf.discovery.nacos.timeout\n    local connect_timeout = timeout.connect\n    local send_timeout = timeout.send\n    local read_timeout = timeout.read\n    log.info('connect_timeout:', connect_timeout, ', send_timeout:', send_timeout,\n             ', read_timeout:', read_timeout)\n    httpc:set_timeouts(connect_timeout, send_timeout, read_timeout)\n    local res, err = httpc:request_uri(url, {\n        method = method,\n        headers = headers,\n        body = body,\n        ssl_verify = true,\n    })\n    if not res then\n        return nil, err\n    end\n\n    if not res.body or res.status ~= 200 then\n        return nil, 'status = ' .. res.status\n    end\n\n    local json_str = res.body\n    local data, err = core.json.decode(json_str)\n    if not data then\n        return nil, err\n    end\n    return data\nend\n\n\nlocal function get_url(request_uri, path)\n    return request(request_uri, path, nil, 'GET', nil)\nend\n\n\nlocal function post_url(request_uri, path, body)\n    return request(request_uri, path, body, 'POST', nil)\nend\n\n\nlocal function get_token_param(base_uri, username, password)\n    if not username or not password then\n        return ''\n    end\n\n    local args = { username = username, password = password}\n    local data, err = post_url(base_uri, auth_path .. '?' .. ngx.encode_args(args), nil)\n    if err then\n        log.error('nacos login fail:', username, ' ', password, ' desc:', err)\n        return nil, err\n    end\n    return '&accessToken=' .. data.accessToken\nend\n\n\nlocal function get_namespace_param(namespace_id)\n    local param = ''\n    if namespace_id then\n        local args = {namespaceId = namespace_id}\n        param = '&' .. ngx.encode_args(args)\n    end\n    return param\nend\n\n\nlocal function get_group_name_param(group_name)\n    local param = ''\n    if group_name then\n        local args = {groupName = group_name}\n        param = '&' .. ngx.encode_args(args)\n    end\n    return param\nend\n\n\nlocal function get_signed_param(group_name, service_name)\n    local param = ''\n    if access_key ~= '' and secret_key ~= '' then\n        local str_to_sign = ngx.now() * 1000 .. '@@' .. group_name .. '@@' .. service_name\n        local args = {\n            ak = access_key,\n            data = str_to_sign,\n            signature = ngx.encode_base64(ngx.hmac_sha1(secret_key, str_to_sign))\n        }\n        param = '&' .. ngx.encode_args(args)\n    end\n    return param\nend\n\n\nlocal function build_base_uri(url)\n    local auth_idx = core.string.rfind_char(url, '@')\n    local username, password\n    if auth_idx then\n        local protocol_idx = str_find(url, '://')\n        local protocol = string_sub(url, 1, protocol_idx + 2)\n        local user_and_password = string_sub(url, protocol_idx + 3, auth_idx - 1)\n        local arr = ngx_re.split(user_and_password, ':')\n        if #arr == 2 then\n            username = arr[1]\n            password = arr[2]\n        end\n        local other = string_sub(url, auth_idx + 1)\n        url = protocol .. other\n    end\n\n    if local_conf.discovery.nacos.prefix then\n        url = url .. local_conf.discovery.nacos.prefix\n    end\n\n    if str_byte(url, #url) ~= str_byte('/') then\n        url = url .. '/'\n    end\n\n    return url, username, password\nend\n\n\nlocal function get_base_uri_by_index(index)\n    local host = local_conf.discovery.nacos.host\n\n    local url = host[index]\n    if not url then\n        return nil\n    end\n\n    return build_base_uri(url)\nend\n\n\nlocal function de_duplication(services, namespace_id, group_name, service_name, scheme)\n    for _, service in ipairs(services) do\n        if service.namespace_id == namespace_id and service.group_name == group_name\n                and service.service_name == service_name and service.scheme == scheme then\n            return true\n        end\n    end\n    return false\nend\n\n\nlocal function iter_and_add_service(services, values)\n    if not values then\n        return\n    end\n\n    for _, value in core.config_util.iterate_values(values) do\n        local conf = value.value\n        if not conf then\n            goto CONTINUE\n        end\n\n        local up\n        if conf.upstream then\n            up = conf.upstream\n        else\n            up = conf\n        end\n\n        local namespace_id = (up.discovery_args and up.discovery_args.namespace_id)\n                             or default_namespace_id\n\n        local group_name = (up.discovery_args and up.discovery_args.group_name)\n                           or default_group_name\n\n        local dup = de_duplication(services, namespace_id, group_name,\n                up.service_name, up.scheme)\n        if dup then\n            goto CONTINUE\n        end\n\n        if up.discovery_type == 'nacos' then\n            core.table.insert(services, {\n                service_name = up.service_name,\n                namespace_id = namespace_id,\n                group_name = group_name,\n                scheme = up.scheme,\n            })\n        end\n        ::CONTINUE::\n    end\nend\n\n\nlocal function get_nacos_services()\n    local services = {}\n\n    -- here we use lazy load to work around circle dependency\n    local get_upstreams = require('apisix.upstream').upstreams\n    local get_routes = require('apisix.router').http_routes\n    local get_stream_routes = require('apisix.router').stream_routes\n    local get_services = require('apisix.http.service').services\n    local values = get_upstreams()\n    iter_and_add_service(services, values)\n    values = get_routes()\n    iter_and_add_service(services, values)\n    values = get_services()\n    iter_and_add_service(services, values)\n    values = get_stream_routes()\n    iter_and_add_service(services, values)\n    return services\nend\n\nlocal function is_grpc(scheme)\n    if scheme == 'grpc' or scheme == 'grpcs' then\n        return true\n    end\n\n    return false\nend\n\nlocal curr_service_in_use = {}\n\n\nlocal function fetch_from_host(base_uri, username, password, services)\n    local token_param, err = get_token_param(base_uri, username, password)\n    if err then\n        return false, err\n    end\n\n    local service_names = {}\n    local nodes_cache = {}\n    local had_success = false\n\n    for _, service_info in ipairs(services) do\n        local namespace_id = service_info.namespace_id\n        local group_name = service_info.group_name\n        local scheme = service_info.scheme or ''\n        local namespace_param = get_namespace_param(namespace_id)\n        local group_name_param = get_group_name_param(group_name)\n        local signature_param = get_signed_param(group_name, service_info.service_name)\n        local query_path = instance_list_path .. service_info.service_name\n                           .. token_param .. namespace_param .. group_name_param\n                           .. signature_param\n        local data, req_err = get_url(base_uri, query_path)\n        if req_err then\n            log.error('failed to fetch instances for service [', service_info.service_name,\n                      '] from ', base_uri, ', error: ', req_err)\n        else\n            had_success = true\n\n            local key = get_key(namespace_id, group_name, service_info.service_name)\n            service_names[key] = true\n\n            local hosts = data.hosts\n            if type(hosts) ~= 'table' then\n                hosts = {}\n            end\n\n            local nodes = {}\n            for _, host in ipairs(hosts) do\n                local node = {\n                    host = host.ip,\n                    port = host.port,\n                    weight = host.weight or default_weight,\n                }\n                -- docs: https://github.com/yidongnan/grpc-spring-boot-starter/pull/496\n                if is_grpc(scheme) and host.metadata and host.metadata.gRPC_port then\n                    node.port = host.metadata.gRPC_port\n                end\n\n                core.table.insert(nodes, node)\n            end\n\n            if #nodes > 0 then\n                nodes_cache[key] = nodes\n            end\n        end\n    end\n\n    if not had_success then\n        return false, 'all nacos services fetch failed'\n    end\n\n    for key, nodes in pairs(nodes_cache) do\n        local content = core.json.encode(nodes)\n        nacos_dict:set(key, content)\n    end\n\n    for key, _ in pairs(curr_service_in_use) do\n        if not service_names[key] then\n            nacos_dict:delete(key)\n        end\n    end\n\n    curr_service_in_use = service_names\n    return true\nend\n\n\nlocal function fetch_full_registry(premature)\n    if premature then\n        return\n    end\n\n    local infos = get_nacos_services()\n    if #infos == 0 then\n        return\n    end\n\n    local host_list = local_conf.discovery.nacos.host\n    local host_count = #host_list\n    local start = math_random(host_count)\n\n    for i = 0, host_count - 1 do\n        local idx = (start + i - 1) % host_count + 1\n        local base_uri, username, password = get_base_uri_by_index(idx)\n\n        if not base_uri then\n            log.warn('nacos host at index ', idx, ' is invalid, skip')\n        else\n            local ok, err = fetch_from_host(base_uri, username, password, infos)\n            if ok then\n                return\n            end\n            log.error('fetch_from_host: ', base_uri, ' err:', err)\n        end\n    end\n\n    log.error('failed to fetch nacos registry from all hosts')\nend\n\n\nfunction _M.nodes(service_name, discovery_args)\n    local namespace_id = discovery_args and\n            discovery_args.namespace_id or default_namespace_id\n    local group_name = discovery_args\n            and discovery_args.group_name or default_group_name\n    local key = get_key(namespace_id, group_name, service_name)\n    local value = nacos_dict:get(key)\n    if not value then\n        core.log.error(\"nacos service not found: \", service_name)\n        return nil\n    end\n    local nodes = core.json.decode(value)\n    return nodes\nend\n\n\nfunction _M.init_worker()\n    default_weight = local_conf.discovery.nacos.weight\n    log.info('default_weight:', default_weight)\n    local fetch_interval = local_conf.discovery.nacos.fetch_interval\n    log.info('fetch_interval:', fetch_interval)\n    access_key = local_conf.discovery.nacos.access_key\n    secret_key = local_conf.discovery.nacos.secret_key\n    ngx_timer_at(0, fetch_full_registry)\n    ngx_timer_every(fetch_interval, fetch_full_registry)\nend\n\n\nfunction _M.dump_data()\n    local keys = nacos_dict:get_keys(0)\n    local applications = {}\n    for _, key in ipairs(keys) do\n        local value = nacos_dict:get(key)\n        if value then\n            local nodes = core.json.decode(value)\n            if nodes then\n                applications[key] = {\n                    nodes = nodes,\n                }\n            end\n        end\n    end\n    return {services = applications or {}}\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/nacos/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal host_pattern = [[^http(s)?:\\/\\/([a-zA-Z0-9-_.]+:.+\\@)?[a-zA-Z0-9-_.:]+$]]\nlocal prefix_pattern = [[^[\\/a-zA-Z0-9-_.]+$]]\n\n\nreturn {\n    type = 'object',\n    properties = {\n        host = {\n            type = 'array',\n            minItems = 1,\n            items = {\n                type = 'string',\n                pattern = host_pattern,\n                minLength = 2,\n                maxLength = 100,\n            },\n        },\n        fetch_interval = {type = 'integer', minimum = 1, default = 30},\n        prefix = {\n            type = 'string',\n            pattern = prefix_pattern,\n            maxLength = 100,\n            default = '/nacos/v1/'\n        },\n        weight = {type = 'integer', minimum = 1, default = 100},\n        timeout = {\n            type = 'object',\n            properties = {\n                connect = {type = 'integer', minimum = 1, default = 2000},\n                send = {type = 'integer', minimum = 1, default = 2000},\n                read = {type = 'integer', minimum = 1, default = 5000},\n            },\n            default = {\n                connect = 2000,\n                send = 2000,\n                read = 5000,\n            }\n        },\n        access_key = {type = 'string', default = ''},\n        secret_key = {type = 'string', default = ''},\n    },\n    required = {'host'}\n}\n"
  },
  {
    "path": "apisix/discovery/tars/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx = ngx\nlocal format = string.format\nlocal ipairs = ipairs\nlocal error = error\nlocal tonumber = tonumber\nlocal local_conf = require(\"apisix.core.config_local\").local_conf()\nlocal core = require(\"apisix.core\")\nlocal mysql = require(\"resty.mysql\")\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal process = require(\"ngx.process\")\n\nlocal endpoint_dict\n\nlocal full_query_sql = [[ select servant, group_concat(endpoint order by endpoint) as endpoints\nfrom t_server_conf left join t_adapter_conf tac using (application, server_name, node_name)\nwhere setting_state = 'active' and present_state = 'active'\ngroup by servant ]]\n\nlocal incremental_query_sql = [[\nselect servant, (setting_state = 'active' and present_state = 'active') activated,\ngroup_concat(endpoint order by endpoint) endpoints\nfrom t_server_conf left join t_adapter_conf tac using (application, server_name, node_name)\nwhere (application, server_name) in\n(\nselect application, server_name from t_server_conf\nwhere registry_timestamp > now() - interval %d second\nunion\nselect application, server_name from t_adapter_conf\nwhere registry_timestamp > now() - interval %d second\n)\ngroup by servant, activated order by activated desc ]]\n\nlocal _M = {\n    version = 0.1,\n}\n\nlocal default_weight\n\nlocal last_fetch_full_time = 0\nlocal last_db_error\n\nlocal endpoint_lrucache = core.lrucache.new({\n    ttl = 300,\n    count = 1024\n})\n\nlocal activated_buffer = core.table.new(10, 0)\nlocal nodes_buffer = core.table.new(0, 5)\n\n\n--[[\nendpoints format as follows:\n  tcp -h 172.16.1.1 -p 11 -t 6000 -e 0,tcp -e 0 -p 12 -h 172.16.1.1,tcp -p 13 -h 172.16.1.1\nwe extract host and port value via endpoints_pattern\n--]]\nlocal endpoints_pattern = core.table.concat(\n        { [[tcp(\\s*-[te]\\s*(\\S+)){0,2}\\s*-([hpHP])\\s*(\\S+)(\\s*-[teTE]\\s*(\\S+))]],\n          [[{0,2}\\s*-([hpHP])\\s*(\\S+)(\\s*-[teTE]\\s*(\\S+)){0,2}\\s*(,|$)]] }\n)\n\n\nlocal function update_endpoint(servant, nodes)\n    local endpoint_content = core.json.encode(nodes, true)\n    local endpoint_version = ngx.crc32_long(endpoint_content)\n    core.log.debug(\"set servant \", servant, endpoint_content)\n    local _, err\n    _, err = endpoint_dict:safe_set(servant .. \"#version\", endpoint_version)\n    if err then\n        core.log.error(\"set endpoint version into nginx shared dict failed, \", err)\n        return\n    end\n    _, err = endpoint_dict:safe_set(servant, endpoint_content)\n    if err then\n        core.log.error(\"set endpoint into nginx shared dict failed, \", err)\n        endpoint_dict:delete(servant .. \"#version\")\n    end\nend\n\n\nlocal function delete_endpoint(servant)\n    core.log.info(\"delete servant \", servant)\n    endpoint_dict:delete(servant .. \"#version\")\n    endpoint_dict:delete(servant)\nend\n\n\nlocal function add_endpoint_to_lrucache(servant)\n    local endpoint_content, err = endpoint_dict:get_stale(servant)\n    if not endpoint_content then\n        core.log.error(\"get empty endpoint content, servant: \", servant, \", err: \", err)\n        return nil\n    end\n\n    local endpoint, err = core.json.decode(endpoint_content)\n    if not endpoint then\n        core.log.error(\"decode json failed, content: \", endpoint_content, \", err: \", err)\n        return nil\n    end\n\n    return endpoint\nend\n\n\nlocal function get_endpoint(servant)\n\n    --[[\n    fetch_full function will:\n         1: call endpoint_dict:flush_all()\n         2: setup servant:nodes pairs into endpoint_dict\n         3: call endpoint_dict:flush_expired()\n\n    get_endpoint may be called during the 2 step of the fetch_full function,\n    so we must use endpoint_dict:get_stale() to get value instead endpoint_dict:get()\n    --]]\n\n    local endpoint_version, err = endpoint_dict:get_stale(servant .. \"#version\")\n    if not endpoint_version  then\n        if err then\n            core.log.error(\"get empty endpoint version, servant: \", servant, \", err: \", err)\n        end\n        return nil\n    end\n    return endpoint_lrucache(servant, endpoint_version, add_endpoint_to_lrucache, servant)\nend\n\n\nlocal function extract_endpoint(query_result)\n    for _, p in ipairs(query_result) do\n        repeat\n            local servant = p.servant\n\n            if servant == ngx.null then\n                break\n            end\n\n            if p.activated == 1 then\n                activated_buffer[servant] = ngx.null\n            elseif p.activated == 0 then\n                if activated_buffer[servant] == nil then\n                    delete_endpoint(servant)\n                end\n                break\n            end\n\n            core.table.clear(nodes_buffer)\n            local iterator = ngx.re.gmatch(p.endpoints, endpoints_pattern, \"jao\")\n            while true do\n                local captures, err = iterator()\n                if err then\n                    core.log.error(\"gmatch failed, error: \", err, \" , endpoints: \", p.endpoints)\n                    break\n                end\n\n                if not captures then\n                    break\n                end\n\n                local host, port\n                if captures[3] == \"h\" or captures[3] == \"H\" then\n                    host = captures[4]\n                    port = tonumber(captures[8])\n                else\n                    host = captures[8]\n                    port = tonumber(captures[4])\n                end\n\n                core.table.insert(nodes_buffer, {\n                    host = host,\n                    port = port,\n                    weight = default_weight,\n                })\n            end\n            update_endpoint(servant, nodes_buffer)\n        until true\n    end\nend\n\n\nlocal function fetch_full(db_cli)\n    local res, err, errcode, sqlstate = db_cli:query(full_query_sql)\n    --[[\n    res format is as follows:\n    {\n        {\n            servant = \"A.AServer.FirstObj\",\n            endpoints = \"tcp -h 172.16.1.1 -p 10001 -e 0 -t 3000,tcp -p 10002 -h 172.16.1.2 -t 3000\"\n        },\n        {\n            servant = \"A.AServer.SecondObj\",\n            endpoints = \"tcp -t 3000 -p 10002 -h 172.16.1.2\"\n        },\n    }\n\n    if current endpoint_dict is as follows:\n      key1:nodes1, key2:nodes2, key3:nodes3\n\n    then fetch_full get follow results:\n      key1:nodes1, key4:nodes4, key5:nodes5\n\n    at this time, we need\n      1: setup key4:nodes4, key5:nodes5\n      2: delete key2:nodes2, key3:nodes3\n\n    to achieve goals, we should:\n      1: before setup results, execute endpoint_dict:flush_all()\n      2:  after setup results, execute endpoint_dict:flush_expired()\n    --]]\n    if not res then\n        core.log.error(\"query failed, error: \", err, \", \", errcode, \" \", sqlstate)\n        return err\n    end\n\n    endpoint_dict:flush_all()\n    extract_endpoint(res)\n\n    while err == \"again\" do\n        res, err, errcode, sqlstate = db_cli:read_result()\n        if not res then\n            if err then\n                core.log.error(\"read result failed, error: \", err, \", \", errcode, \" \", sqlstate)\n            end\n            return err\n        end\n        extract_endpoint(res)\n    end\n    endpoint_dict:flush_expired()\n\n    return nil\nend\n\n\nlocal function fetch_incremental(db_cli)\n    local res, err, errcode, sqlstate = db_cli:query(incremental_query_sql)\n    --[[\n    res is as follows:\n    {\n        {\n            activated=1,\n            servant = \"A.AServer.FirstObj\",\n            endpoints = \"tcp -h 172.16.1.1 -p 10001 -e 0 -t 3000,tcp -p 10002 -h 172.16.1.2 -t 3000\"\n        },\n        {\n            activated=0,\n            servant = \"A.AServer.FirstObj\",\n            endpoints = \"tcp -t 3000 -p 10001 -h 172.16.1.3\"\n        },\n        {\n            activated=0,\n            servant = \"B.BServer.FirstObj\",\n            endpoints = \"tcp -t 3000 -p 10002 -h 172.16.1.2\"\n        },\n    }\n\n    for each item:\n      if activated==1, setup\n      if activated==0, if there is a other item had same servant and activate==1, ignore\n      if activated==0, and there is no other item had same servant, delete\n    --]]\n    if not res then\n        core.log.error(\"query failed, error: \", err, \", \", errcode, \" \", sqlstate)\n        return err\n    end\n\n    core.table.clear(activated_buffer)\n    extract_endpoint(res)\n\n    while err == \"again\" do\n        res, err, errcode, sqlstate = db_cli:read_result()\n        if not res then\n            if err then\n                core.log.error(\"read result failed, error: \", err, \", \", errcode, \" \", sqlstate)\n            end\n            return err\n        end\n        extract_endpoint(res)\n    end\n\n    return nil\nend\n\n\nlocal function fetch_endpoint(premature, conf)\n    if premature then\n        return\n    end\n\n    local db_cli, err = mysql:new()\n    if not db_cli then\n        core.log.error(\"failed to instantiate mysql: \", err)\n        return\n    end\n    db_cli:set_timeout(3000)\n\n    local ok, err, errcode, sqlstate = db_cli:connect(conf.db_conf)\n    if not ok then\n        core.log.error(\"failed to connect mysql: \", err, \", \", errcode, \", \", sqlstate)\n        return\n    end\n\n    local now = ngx.time()\n\n    if last_db_error or last_fetch_full_time + conf.full_fetch_interval <= now then\n        last_fetch_full_time = now\n        last_db_error = fetch_full(db_cli)\n    else\n        last_db_error = fetch_incremental(db_cli)\n    end\n\n    if not last_db_error then\n        db_cli:set_keepalive(120 * 1000, 1)\n    end\nend\n\n\nfunction _M.nodes(servant)\n    return get_endpoint(servant)\nend\n\nlocal function get_endpoint_dict()\n    local shm = \"tars\"\n\n    if not is_http then\n        shm = shm .. \"-stream\"\n    end\n\n    return ngx.shared[shm]\nend\n\nfunction _M.init_worker()\n    endpoint_dict = get_endpoint_dict()\n    if not endpoint_dict then\n        error(\"failed to get lua_shared_dict: tars, please check your APISIX version\")\n    end\n\n    if process.type() ~= \"privileged agent\" then\n        return\n    end\n\n    local conf = local_conf.discovery.tars\n    default_weight = conf.default_weight\n\n    core.log.info(\"conf \", core.json.delay_encode(conf))\n    local backtrack_time = conf.incremental_fetch_interval + 5\n    incremental_query_sql = format(incremental_query_sql, backtrack_time, backtrack_time)\n\n    ngx.timer.at(0, fetch_endpoint, conf)\n    ngx.timer.every(conf.incremental_fetch_interval, fetch_endpoint, conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/discovery/tars/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal host_pattern = [[^([a-zA-Z0-9-_.]+:.+\\@)?[a-zA-Z0-9-_.:]+$]]\n\nreturn {\n    type = 'object',\n    properties = {\n        db_conf = {\n            type = 'object',\n            properties = {\n                host = { type = 'string', minLength = 1, maxLength = 500, pattern = host_pattern },\n                port = { type = 'integer', minimum = 1, maximum = 65535, default = 3306 },\n                database = { type = 'string', minLength = 1, maxLength = 64 },\n                user = { type = 'string', minLength = 1, maxLength = 64 },\n                password = { type = 'string', minLength = 1, maxLength = 64 },\n            },\n            required = { 'host', 'database', 'user', 'password' }\n        },\n        full_fetch_interval = {\n            type = 'integer', minimum = 90, maximum = 3600, default = 300,\n        },\n        incremental_fetch_interval = {\n            type = 'integer', minimum = 5, maximum = 60, default = 15,\n        },\n        default_weight = {\n            type = 'integer', minimum = 0, maximum = 100, default = 100,\n        },\n    },\n    required = { 'db_conf' }\n}\n"
  },
  {
    "path": "apisix/events.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal require      = require\nlocal error        = error\nlocal assert       = assert\nlocal tostring     = tostring\nlocal pairs        = pairs\nlocal setmetatable = setmetatable\nlocal ngx          = ngx\nlocal core         = require(\"apisix.core\")\n\nlocal _M = {\n}\n\n-- use lua-resty-events\nlocal function init_resty_events()\n    local listening = \"unix:\" .. ngx.config.prefix() .. \"logs/\"\n    if ngx.config.subsystem == \"http\" then\n        listening = listening .. \"worker_events.sock\"\n    else\n        listening = listening .. \"stream_worker_events.sock\"\n    end\n    core.log.info(\"subsystem: \" .. ngx.config.subsystem .. \" listening sock: \" .. listening)\n\n    local opts = {\n        unique_timeout = 5,     -- life time of unique event data in lrucache\n        broker_id = 0,          -- broker server runs in nginx worker #0\n        listening = listening,  -- unix socket for broker listening\n    }\n\n    local we = require(\"resty.events.compat\")\n    assert(we.configure(opts))\n    assert(we.configured())\n\n    return we\nend\n\n\nfunction _M.init_worker()\n    if _M.inited then\n        -- prevent duplicate initializations in the same worker to\n        -- avoid potentially unexpected behavior\n        return\n    end\n\n    _M.inited = true\n    _M.worker_events = init_resty_events()\nend\n\n\nfunction _M.register(self, ...)\n    return self.worker_events.register(...)\nend\n\n\nfunction _M.event_list(self, source, ...)\n    -- a patch for the lua-resty-events to support event_list\n    -- this snippet is copied from the lua-resty-worker-events lib\n    local events = { _source = source }\n    for _, event in pairs({...}) do\n        events[event] = event\n    end\n    return setmetatable(events, {\n        __index = function(_, key)\n        error(\"event '\"..tostring(key)..\"' is an unknown event\", 2)\n        end\n    })\nend\n\n\nfunction _M.post(self, ...)\n    return self.worker_events.post(...)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/global_rules.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core           = require(\"apisix.core\")\nlocal plugin_checker = require(\"apisix.plugin\").plugin_checker\nlocal plugin         = require(\"apisix.plugin\")\n\nlocal error = error\n\n\nlocal _M = {}\n\nlocal global_rules\n\n\nlocal function filter(global_rule)\n    if not global_rule.value or not global_rule.value.plugins then\n        return\n    end\n    plugin.set_plugins_meta_parent(global_rule.value.plugins, global_rule)\nend\n\n\nfunction _M.init_worker()\n    local err\n    global_rules, err = core.config.new(\"/global_rules\", {\n        automatic = true,\n        item_schema = core.schema.global_rule,\n        checker = plugin_checker,\n        filter = filter\n    })\n    if not global_rules then\n        error(\"failed to create etcd instance for fetching /global_rules : \"\n            .. err)\n    end\nend\n\n\nfunction _M.global_rules()\n    if not global_rules then\n        return nil, nil\n    end\n    return global_rules.values, global_rules.conf_version\nend\n\n\nfunction _M.get_pre_index()\n    if not global_rules then\n        return nil\n    end\n    return global_rules.prev_index\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/healthcheck_manager.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal ipairs   = ipairs\nlocal pcall   = pcall\nlocal exiting      = ngx.worker.exiting\nlocal pairs    = pairs\nlocal tostring = tostring\nlocal core = require(\"apisix.core\")\nlocal config_local   = require(\"apisix.core.config_local\")\nlocal resource = require(\"apisix.resource\")\nlocal upstream_utils = require(\"apisix.utils.upstream\")\nlocal healthcheck\nlocal tab_clone = core.table.clone\nlocal timer_every = ngx.timer.every\nlocal jp = require(\"jsonpath\")\nlocal config_util = require(\"apisix.core.config_util\")\n\nlocal _M = {}\nlocal working_pool = {}     -- resource_path -> {version = ver, checker = checker}\nlocal waiting_pool = {}      -- resource_path -> resource_ver\n\nlocal DELAYED_CLEAR_TIMEOUT = 10\nlocal healthcheck_shdict_name = \"upstream-healthcheck\"\n\n\nlocal function get_healthchecker_name(value)\n    return \"upstream#\" .. (value.resource_key or value.upstream.resource_key)\nend\n_M.get_healthchecker_name = get_healthchecker_name\n\n\nlocal function create_checker(up_conf)\n    if not up_conf.checks then\n        return nil\n    end\n    local local_conf = config_local.local_conf()\n    if local_conf and local_conf.apisix and local_conf.apisix.disable_upstream_healthcheck then\n        core.log.info(\"healthchecker won't be created: disabled upstream healthcheck\")\n        return nil\n    end\n    core.log.info(\"creating healthchecker for upstream: \", up_conf.resource_key)\n    if not healthcheck then\n        healthcheck = require(\"resty.healthcheck\")\n    end\n\n    local checker, err = healthcheck.new({\n        name = get_healthchecker_name(up_conf),\n        shm_name = healthcheck_shdict_name,\n        checks = up_conf.checks,\n        events_module = \"resty.events\",\n    })\n\n    if not checker then\n        core.log.error(\"failed to create healthcheck: \", err)\n        return nil\n    end\n\n    -- Add target nodes\n    local host = up_conf.checks and up_conf.checks.active and up_conf.checks.active.host\n    local port = up_conf.checks and up_conf.checks.active and up_conf.checks.active.port\n    local up_hdr = up_conf.pass_host == \"rewrite\" and up_conf.upstream_host\n    local use_node_hdr = up_conf.pass_host == \"node\" or nil\n\n    for _, node in ipairs(up_conf.nodes) do\n        local host_hdr = up_hdr or (use_node_hdr and node.domain)\n        local ok, err = checker:add_target(node.host, port or node.port, host,\n                                        true, host_hdr)\n        if not ok then\n            core.log.error(\"failed to add healthcheck target: \", node.host, \":\",\n                          port or node.port, \" err: \", err)\n        end\n    end\n\n    return checker\nend\n\n\nfunction _M.fetch_checker(resource_path, resource_ver)\n    local working_item = working_pool[resource_path]\n    if working_item and working_item.version == resource_ver then\n        return working_item.checker\n    end\n\n    if waiting_pool[resource_path] == resource_ver then\n        return nil\n    end\n\n    -- Add to waiting pool with version\n    core.log.info(\"adding \", resource_path, \" to waiting pool with version: \", resource_ver)\n    waiting_pool[resource_path] = resource_ver\n    return nil\nend\n\n\nfunction _M.fetch_node_status(checker, ip, port, hostname)\n    -- check if the checker is valid\n    if not checker or checker.dead then\n        return true\n    end\n\n    return checker:get_target_status(ip, port, hostname)\nend\n\n\nlocal function add_working_pool(resource_path, resource_ver, checker)\n    working_pool[resource_path] = {\n        version = resource_ver,\n        checker = checker\n    }\nend\n\nlocal function find_in_working_pool(resource_path, resource_ver)\n    local checker = working_pool[resource_path]\n    if not checker then\n        return nil  -- not found\n    end\n\n    if checker.version ~= resource_ver then\n        core.log.info(\"version mismatch for resource: \", resource_path,\n                    \" current version: \", checker.version, \" requested version: \", resource_ver)\n        return nil  -- version not match\n    end\n    return checker\nend\n\n\nlocal function get_plugin_name(path)\n    -- Extract JSON path (after '#') or use full path\n    local json_path = path:match(\"#(.+)$\") or path\n    -- Match plugin name in the JSON path segment\n    return json_path:match(\"^plugins%['([^']+)'%]\")\n        or json_path:match('^plugins%[\"([^\"]+)\"%]')\n        or json_path:match(\"^plugins%.([^%.]+)\")\nend\n\nlocal function timer_create_checker()\n    if core.table.nkeys(waiting_pool) == 0 then\n        return\n    end\n\n    local waiting_snapshot = tab_clone(waiting_pool)\n    for resource_path, resource_ver in pairs(waiting_snapshot) do\n        do\n            if find_in_working_pool(resource_path, resource_ver) then\n                core.log.info(\"resource: \", resource_path,\n                             \" already in working pool with version: \",\n                               resource_ver)\n                goto continue\n            end\n            local res_conf = resource.fetch_latest_conf(resource_path)\n            if not res_conf then\n                goto continue\n            end\n            local upstream\n            local plugin_name = get_plugin_name(resource_path)\n            if plugin_name and plugin_name ~= \"\" then\n                local _, sub_path = config_util.parse_path(resource_path)\n                local json_path = \"$.\" .. sub_path\n                --- the users of the API pass the jsonpath(in resourcepath) to\n                --- upstream_constructor_config which is passed to the\n                --- callback construct_upstream to create an upstream dynamically\n                local upstream_constructor_config = jp.value(res_conf.value, json_path)\n                local plugin = require(\"apisix.plugins.\" .. plugin_name)\n                upstream = plugin.construct_upstream(upstream_constructor_config)\n                upstream.resource_key = resource_path\n            else\n                upstream = res_conf.value.upstream or res_conf.value\n            end\n            local new_version = upstream_utils.version(res_conf.modifiedIndex,\n                                                             upstream._nodes_ver)\n            core.log.info(\"checking waiting pool for resource: \", resource_path,\n                    \" current version: \", new_version, \" requested version: \", resource_ver)\n            if resource_ver ~= new_version then\n                goto continue\n            end\n\n            -- if a checker exists then delete it before creating a new one\n            local existing_checker = working_pool[resource_path]\n            if existing_checker then\n                existing_checker.checker:delayed_clear(DELAYED_CLEAR_TIMEOUT)\n                existing_checker.checker:stop()\n                core.log.info(\"releasing existing checker: \", tostring(existing_checker.checker),\n                              \" for resource: \", resource_path, \" and version: \",\n                              existing_checker.version)\n            end\n            local checker = create_checker(upstream)\n            if not checker then\n                goto continue\n            end\n            core.log.info(\"create new checker: \", tostring(checker), \" for resource: \",\n                        resource_path, \" and version: \", resource_ver)\n            add_working_pool(resource_path, resource_ver, checker)\n        end\n\n        ::continue::\n        waiting_pool[resource_path] = nil\n    end\nend\n\n\nlocal function timer_working_pool_check()\n    if core.table.nkeys(working_pool) == 0 then\n        return\n    end\n\n    local working_snapshot = tab_clone(working_pool)\n    for resource_path, item in pairs(working_snapshot) do\n        --- remove from working pool if resource doesn't exist\n        local res_conf = resource.fetch_latest_conf(resource_path)\n        local need_destroy = true\n        if res_conf and res_conf.value then\n            local upstream\n            local plugin_name = get_plugin_name(resource_path)\n            if plugin_name and plugin_name ~= \"\" then\n                local _, sub_path = config_util.parse_path(resource_path)\n                local json_path = \"$.\" .. sub_path\n                --- the users of the API pass the jsonpath(in resourcepath) to\n                --- upstream_constructor_config which is passed to the\n                --- callback construct_upstream to create an upstream dynamically\n                local upstream_constructor_config = jp.value(res_conf.value, json_path)\n                local plugin = require(\"apisix.plugins.\" .. plugin_name)\n                upstream = plugin.construct_upstream(upstream_constructor_config)\n                upstream.resource_key = resource_path\n            else\n                upstream = res_conf.value.upstream or res_conf.value\n            end\n            local current_ver = upstream_utils.version(res_conf.modifiedIndex,\n                                                    upstream._nodes_ver)\n            core.log.info(\"checking working pool for resource: \", resource_path,\n                        \" current version: \", current_ver, \" item version: \", item.version)\n            if item.version == current_ver then\n                need_destroy = false\n            end\n        end\n\n        if need_destroy then\n            working_pool[resource_path] = nil\n            item.checker.dead = true\n            item.checker:delayed_clear(DELAYED_CLEAR_TIMEOUT)\n            item.checker:stop()\n            core.log.info(\"try to release checker: \", tostring(item.checker), \" for resource: \",\n                        resource_path, \" and version : \", item.version)\n        end\n    end\nend\n\nfunction _M.init_worker()\n    local timer_create_checker_running = false\n    local timer_working_pool_check_running = false\n    timer_every(1, function ()\n        if not exiting() then\n            if timer_create_checker_running then\n                core.log.warn(\"timer_create_checker is already running, skipping this iteration\")\n                return\n            end\n            timer_create_checker_running = true\n            local ok, err = pcall(timer_create_checker)\n            if not ok then\n                core.log.error(\"failed to run timer_create_checker: \", err)\n            end\n            timer_create_checker_running = false\n        end\n    end)\n    timer_every(1, function ()\n        if not exiting() then\n            if timer_working_pool_check_running then\n                core.log.warn(\"timer_working_pool_check is already running skipping iteration\")\n                return\n            end\n            timer_working_pool_check_running = true\n            local ok, err = pcall(timer_working_pool_check)\n            if not ok then\n                core.log.error(\"failed to run timer_working_pool_check: \", err)\n            end\n            timer_working_pool_check_running = false\n        end\n    end)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/http/route.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal radixtree = require(\"resty.radixtree\")\nlocal router = require(\"apisix.utils.router\")\nlocal service_fetch = require(\"apisix.http.service\").get\nlocal core = require(\"apisix.core\")\nlocal expr = require(\"resty.expr.v1\")\nlocal plugin_checker = require(\"apisix.plugin\").plugin_checker\nlocal event = require(\"apisix.core.event\")\nlocal ipairs = ipairs\nlocal type = type\nlocal error = error\nlocal loadstring = loadstring\n\n\nlocal _M = {}\n\n\nfunction _M.create_radixtree_uri_router(routes, uri_routes, with_parameter)\n    routes = routes or {}\n\n    core.table.clear(uri_routes)\n\n    for _, route in ipairs(routes) do\n        if type(route) == \"table\" then\n            local status = core.table.try_read_attr(route, \"value\", \"status\")\n            -- check the status\n            if status and status == 0 then\n                goto CONTINUE\n            end\n\n            local filter_fun, err\n            if route.value.filter_func then\n                filter_fun, err = loadstring(\n                                        \"return \" .. route.value.filter_func,\n                                        \"router#\" .. route.value.id)\n                if not filter_fun then\n                    core.log.error(\"failed to load filter function: \", err,\n                                   \" route id: \", route.value.id)\n                    goto CONTINUE\n                end\n\n                filter_fun = filter_fun()\n            end\n\n            local hosts = route.value.hosts or route.value.host\n            if not hosts and route.value.service_id then\n                local service = service_fetch(route.value.service_id)\n                if not service then\n                    core.log.error(\"failed to fetch service configuration by \",\n                                   \"id: \", route.value.service_id)\n                    -- we keep the behavior that missing service won't affect the route matching\n                else\n                    hosts = service.value.hosts\n                end\n            end\n\n            core.log.info(\"insert uri route: \",\n                          core.json.delay_encode(route.value, true))\n            core.table.insert(uri_routes, {\n                paths = route.value.uris or route.value.uri,\n                methods = route.value.methods,\n                priority = route.value.priority,\n                hosts = hosts,\n                remote_addrs = route.value.remote_addrs\n                               or route.value.remote_addr,\n                vars = route.value.vars,\n                filter_fun = filter_fun,\n                handler = function (api_ctx, match_opts)\n                    api_ctx.matched_params = nil\n                    api_ctx.matched_route = route\n                    api_ctx.curr_req_matched = match_opts.matched\n                end\n            })\n\n            ::CONTINUE::\n        end\n    end\n\n    event.push(event.CONST.BUILD_ROUTER, routes)\n    core.log.info(\"route items: \", core.json.delay_encode(uri_routes, true))\n\n    if with_parameter then\n        return radixtree.new(uri_routes)\n    else\n        return router.new(uri_routes)\n    end\nend\n\n\nfunction _M.match_uri(uri_router, api_ctx)\n    local match_opts = core.tablepool.fetch(\"route_match_opts\", 0, 4)\n    match_opts.method = api_ctx.var.request_method\n    match_opts.host = api_ctx.var.host\n    match_opts.remote_addr = api_ctx.var.remote_addr\n    match_opts.vars = api_ctx.var\n    match_opts.matched = core.tablepool.fetch(\"matched_route_record\", 0, 4)\n\n    local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx, match_opts)\n    core.tablepool.release(\"route_match_opts\", match_opts)\n    return ok\nend\n\n\n-- additional check for synced route configuration, run after schema check\nlocal function check_route(route)\n    local ok, err = plugin_checker(route)\n    if not ok then\n        return nil, err\n    end\n\n    if route.vars then\n        ok, err = expr.new(route.vars)\n        if not ok then\n            return nil, \"failed to validate the 'vars' expression: \" .. err\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.init_worker(filter)\n    local user_routes, err = core.config.new(\"/routes\", {\n            automatic = true,\n            item_schema = core.schema.route,\n            checker = check_route,\n            filter = filter,\n        })\n    if not user_routes then\n        error(\"failed to create etcd instance for fetching /routes : \" .. err)\n    end\n\n    return user_routes\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/http/router/radixtree_host_uri.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal router = require(\"apisix.utils.router\")\nlocal core = require(\"apisix.core\")\nlocal event = require(\"apisix.core.event\")\nlocal get_services = require(\"apisix.http.service\").services\nlocal service_fetch = require(\"apisix.http.service\").get\nlocal ipairs = ipairs\nlocal type = type\nlocal tab_insert = table.insert\nlocal loadstring = loadstring\nlocal pairs = pairs\nlocal cached_router_version\nlocal cached_service_version\nlocal host_router\nlocal only_uri_router\n\n\nlocal _M = {version = 0.1}\n\n\nlocal function push_host_router(route, host_routes, only_uri_routes)\n    if type(route) ~= \"table\" then\n        return\n    end\n\n    local filter_fun, err\n    if route.value.filter_func then\n        filter_fun, err = loadstring(\n                                \"return \" .. route.value.filter_func,\n                                \"router#\" .. route.value.id)\n        if not filter_fun then\n            core.log.error(\"failed to load filter function: \", err,\n                            \" route id: \", route.value.id)\n            return\n        end\n\n        filter_fun = filter_fun()\n    end\n\n    local hosts = route.value.hosts\n    if not hosts then\n        if route.value.host then\n            hosts = {route.value.host}\n        elseif route.value.service_id then\n            local service = service_fetch(route.value.service_id)\n            if not service then\n                core.log.error(\"failed to fetch service configuration by \",\n                                \"id: \", route.value.service_id)\n                -- we keep the behavior that missing service won't affect the route matching\n            else\n                hosts = service.value.hosts\n            end\n        end\n    end\n\n    local radixtree_route = {\n        paths = route.value.uris or route.value.uri,\n        methods = route.value.methods,\n        priority = route.value.priority,\n        remote_addrs = route.value.remote_addrs\n                       or route.value.remote_addr,\n        vars = route.value.vars,\n        filter_fun = filter_fun,\n        handler = function (api_ctx, match_opts)\n            api_ctx.matched_params = nil\n            api_ctx.matched_route = route\n            api_ctx.curr_req_matched = match_opts.matched\n            api_ctx.real_curr_req_matched_path = match_opts.matched._path\n        end\n    }\n\n    if hosts == nil then\n        core.table.insert(only_uri_routes, radixtree_route)\n        return\n    end\n\n    for i, host in ipairs(hosts) do\n        local host_rev = host:reverse()\n        if not host_routes[host_rev] then\n            host_routes[host_rev] = {radixtree_route}\n        else\n            tab_insert(host_routes[host_rev], radixtree_route)\n        end\n    end\nend\n\n\nlocal function create_radixtree_router(routes)\n    local host_routes = {}\n    local only_uri_routes = {}\n    host_router = nil\n    routes = routes or {}\n\n    for _, route in ipairs(routes) do\n        local status = core.table.try_read_attr(route, \"value\", \"status\")\n        -- check the status\n        if not status or status == 1 then\n            push_host_router(route, host_routes, only_uri_routes)\n        end\n    end\n\n    -- create router: host_router\n    local host_router_routes = {}\n    for host_rev, routes in pairs(host_routes) do\n        local sub_router = router.new(routes)\n\n        core.table.insert(host_router_routes, {\n            paths = host_rev,\n            filter_fun = function(vars, opts, ...)\n                return sub_router:dispatch(vars.uri, opts, ...)\n            end,\n            handler = function (api_ctx, match_opts)\n                api_ctx.real_curr_req_matched_host = match_opts.matched._path\n            end\n        })\n    end\n\n    event.push(event.CONST.BUILD_ROUTER, routes)\n\n    if #host_router_routes > 0 then\n        host_router = router.new(host_router_routes)\n    end\n\n    -- create router: only_uri_router\n    only_uri_router = router.new(only_uri_routes)\n    return true\nend\n\nfunction _M.match(api_ctx)\n    local user_routes = _M.user_routes\n    local _, service_version = get_services()\n    if not cached_router_version or cached_router_version ~= user_routes.conf_version\n        or not cached_service_version or cached_service_version ~= service_version\n    then\n        create_radixtree_router(user_routes.values)\n        cached_router_version = user_routes.conf_version\n        cached_service_version = service_version\n    end\n\n    return _M.matching(api_ctx)\nend\n\n\nfunction _M.matching(api_ctx)\n    core.log.info(\"route match mode: radixtree_host_uri\")\n\n    local match_opts = core.tablepool.fetch(\"route_match_opts\", 0, 16)\n    match_opts.method = api_ctx.var.request_method\n    match_opts.remote_addr = api_ctx.var.remote_addr\n    match_opts.vars = api_ctx.var\n    match_opts.host = api_ctx.var.host\n    match_opts.matched = core.tablepool.fetch(\"matched_route_record\", 0, 4)\n\n    if host_router then\n        local host_uri = api_ctx.var.host\n        local ok = host_router:dispatch(host_uri:reverse(), match_opts, api_ctx, match_opts)\n        if ok then\n            if api_ctx.real_curr_req_matched_path then\n                api_ctx.curr_req_matched._path = api_ctx.real_curr_req_matched_path\n                api_ctx.real_curr_req_matched_path = nil\n            end\n            if api_ctx.real_curr_req_matched_host then\n                api_ctx.curr_req_matched._host = api_ctx.real_curr_req_matched_host:reverse()\n                api_ctx.real_curr_req_matched_host = nil\n            end\n            core.tablepool.release(\"route_match_opts\", match_opts)\n            return true\n        end\n    end\n\n    local ok = only_uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx, match_opts)\n    core.tablepool.release(\"route_match_opts\", match_opts)\n    return ok\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/http/router/radixtree_uri.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal base_router = require(\"apisix.http.route\")\nlocal get_services = require(\"apisix.http.service\").services\nlocal cached_router_version\nlocal cached_service_version\n\n\nlocal _M = {version = 0.2}\n\n\n    local uri_routes = {}\n    local uri_router\nfunction _M.match(api_ctx)\n    local user_routes = _M.user_routes\n    local _, service_version = get_services()\n    if not cached_router_version or cached_router_version ~= user_routes.conf_version\n        or not cached_service_version or cached_service_version ~= service_version\n    then\n        uri_router = base_router.create_radixtree_uri_router(user_routes.values,\n                                                             uri_routes, false)\n        cached_router_version = user_routes.conf_version\n        cached_service_version = service_version\n    end\n\n    if not uri_router then\n        core.log.error(\"failed to fetch valid `uri` router: \")\n        return true\n    end\n\n    return _M.matching(api_ctx)\nend\n\n\nfunction _M.matching(api_ctx)\n    core.log.info(\"route match mode: radixtree_uri\")\n    return base_router.match_uri(uri_router, api_ctx)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/http/router/radixtree_uri_with_parameter.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal base_router = require(\"apisix.http.route\")\nlocal get_services = require(\"apisix.http.service\").services\nlocal cached_router_version\nlocal cached_service_version\n\n\nlocal _M = {}\n\n\n    local uri_routes = {}\n    local uri_router\nfunction _M.match(api_ctx)\n    local user_routes = _M.user_routes\n    local _, service_version = get_services()\n    if not cached_router_version or cached_router_version ~= user_routes.conf_version\n        or not cached_service_version or cached_service_version ~= service_version\n    then\n        uri_router = base_router.create_radixtree_uri_router(user_routes.values,\n                                                             uri_routes, true)\n        cached_router_version = user_routes.conf_version\n        cached_service_version = service_version\n    end\n\n    if not uri_router then\n        core.log.error(\"failed to fetch valid `uri_with_parameter` router: \")\n        return true\n    end\n\n    return _M.matching(api_ctx)\nend\n\n\nfunction _M.matching(api_ctx)\n    core.log.info(\"route match mode: radixtree_uri_with_parameter\")\n    return base_router.match_uri(uri_router, api_ctx)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/http/service.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core   = require(\"apisix.core\")\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal plugin_checker = require(\"apisix.plugin\").plugin_checker\nlocal plugin = require(\"apisix.plugin\")\nlocal services\nlocal error = error\n\n\nlocal _M = {\n    version = 0.2,\n}\n\n\nfunction _M.get(service_id)\n    return services:get(service_id)\nend\n\n\nfunction _M.services()\n    if not services then\n        return nil, nil\n    end\n\n    return services.values, services.conf_version\nend\n\n\nlocal function filter(service)\n    service.has_domain = false\n    if not service.value then\n        return\n    end\n\n\n    plugin.set_plugins_meta_parent(service.value.plugins, service)\n\n    apisix_upstream.filter_upstream(service.value.upstream, service)\n\n    core.log.info(\"filter service: \", core.json.delay_encode(service, true))\nend\n\n\nfunction _M.init_worker()\n    local err\n    services, err = core.config.new(\"/services\", {\n        automatic = true,\n        item_schema = core.schema.service,\n        checker = plugin_checker,\n        filter = filter,\n    })\n    if not services then\n        error(\"failed to create etcd instance for fetching /services: \" .. err)\n        return\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/include/apisix/model/pubsub.proto",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nsyntax = \"proto3\";\n\noption java_package = \"org.apache.apisix.api.pubsub\";\noption java_outer_classname = \"PubSubProto\";\noption java_multiple_files = true;\noption go_package = \"github.com/apache/apisix/api/pubsub;pubsub\";\n\n/**\n * Ping command, used to keep the websocket connection alive\n *\n * The state field is used to pass some non-specific information,\n * which will be returned in the pong response as is.\n */\nmessage CmdPing {\n    bytes state = 1;\n}\n\n/**\n * An empty command, a placeholder for testing purposes only\n */\nmessage CmdEmpty {}\n\n/**\n * Get the offset of the specified topic partition from Apache Kafka.\n */\nmessage CmdKafkaListOffset {\n    string topic = 1;\n    int32 partition = 2;\n    int64 timestamp = 3;\n}\n\n/**\n * Fetch messages of the specified topic partition from Apache Kafka.\n */\nmessage CmdKafkaFetch {\n    string topic = 1;\n    int32 partition = 2;\n    int64 offset = 3;\n}\n\n/**\n * Client request definition for pubsub scenarios\n *\n * The sequence field is used to associate requests and responses.\n * Apache APISIX will set a consistent sequence for the associated\n * requests and responses, and the client can explicitly know the\n * response corresponding to any of the requests.\n *\n * The req field is the command data sent by the client, and its\n * type will be chosen from any of the lists in the definition.\n *\n * Field numbers 1 to 30 in the definition are used to define basic\n * information and future extensions, and numbers after 30 are used\n * to define commands.\n */\nmessage PubSubReq {\n    int64 sequence = 1;\n    oneof req {\n        CmdEmpty           cmd_empty             = 31;\n        CmdPing            cmd_ping              = 32;\n        CmdKafkaFetch      cmd_kafka_fetch       = 33;\n        CmdKafkaListOffset cmd_kafka_list_offset = 34;\n    };\n}\n\n/**\n * The response body of the service when an error occurs,\n * containing the error code and the error message.\n */\nmessage ErrorResp {\n    int32 code = 1;\n    string message = 2;\n}\n\n/**\n * Pong response, the state field will pass through the\n * value in the Ping command field.\n */\nmessage PongResp {\n    bytes state = 1;\n}\n\n/**\n * The definition of a message in Kafka with the current message\n * offset, production timestamp, Key, and message content.\n */\nmessage KafkaMessage {\n    int64 offset = 1;\n    int64 timestamp = 2;\n    bytes key = 3;\n    bytes value = 4;\n}\n\n/**\n * The response of Fetch messages from Apache Kafka.\n */\nmessage KafkaFetchResp {\n    repeated KafkaMessage messages = 1;\n}\n\n/**\n * The response of list offset from Apache Kafka.\n */\nmessage KafkaListOffsetResp {\n    int64 offset = 1;\n}\n\n/**\n * Server response definition for pubsub scenarios\n *\n * The sequence field will be the same as the value in the\n * request, which is used to associate the associated request\n * and response.\n *\n * The resp field is the response data sent by the server, and\n * its type will be chosen from any of the lists in the definition.\n */\nmessage PubSubResp {\n    int64 sequence = 1;\n    oneof resp {\n        ErrorResp           error_resp             = 31;\n        PongResp            pong_resp              = 32;\n        KafkaFetchResp      kafka_fetch_resp       = 33;\n        KafkaListOffsetResp kafka_list_offset_resp = 34;\n    };\n}\n"
  },
  {
    "path": "apisix/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require         = require\n-- set the JIT options before any code, to prevent error \"changing jit stack size is not\n-- allowed when some regexs have already been compiled and cached\"\nif require(\"ffi\").os == \"Linux\" then\n    require(\"ngx.re\").opt(\"jit_stack_size\", 200 * 1024)\nend\n\nrequire(\"jit.opt\").start(\"minstitch=2\", \"maxtrace=4000\",\n                         \"maxrecord=8000\", \"sizemcode=64\",\n                         \"maxmcode=4000\", \"maxirconst=1000\")\n\nrequire(\"apisix.patch\").patch()\nlocal core            = require(\"apisix.core\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal plugin_config   = require(\"apisix.plugin_config\")\nlocal consumer_group  = require(\"apisix.consumer_group\")\nlocal script          = require(\"apisix.script\")\nlocal service_fetch   = require(\"apisix.http.service\").get\nlocal admin_init      = require(\"apisix.admin.init\")\nlocal get_var         = require(\"resty.ngxvar\").fetch\nlocal router          = require(\"apisix.router\")\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal apisix_secret   = require(\"apisix.secret\")\nlocal set_upstream    = apisix_upstream.set_by_route\nlocal apisix_ssl      = require(\"apisix.ssl\")\nlocal apisix_global_rules    = require(\"apisix.global_rules\")\nlocal upstream_util   = require(\"apisix.utils.upstream\")\nlocal xrpc            = require(\"apisix.stream.xrpc\")\nlocal ctxdump         = require(\"resty.ctxdump\")\nlocal debug           = require(\"apisix.debug\")\nlocal pubsub_kafka    = require(\"apisix.pubsub.kafka\")\nlocal resource        = require(\"apisix.resource\")\nlocal trusted_addresses_util = require(\"apisix.utils.trusted-addresses\")\nlocal tracer          = require(\"apisix.tracer\")\n\nlocal discovery = require(\"apisix.discovery.init\").discovery\nlocal ngx             = ngx\nlocal get_method      = ngx.req.get_method\nlocal ngx_exit        = ngx.exit\nlocal math            = math\nlocal ipairs          = ipairs\nlocal ngx_now         = ngx.now\nlocal ngx_var         = ngx.var\nlocal re_split        = require(\"ngx.re\").split\nlocal str_byte        = string.byte\nlocal str_sub         = string.sub\nlocal tonumber        = tonumber\nlocal type            = type\nlocal pairs           = pairs\nlocal tostring        = tostring\nlocal pcall           = pcall\nlocal ngx_re_match    = ngx.re.match\nlocal control_api_router\n\nlocal is_http = false\nif ngx.config.subsystem == \"http\" then\n    is_http = true\n    control_api_router = require(\"apisix.control.router\")\nend\n\nlocal ok, apisix_base_flags = pcall(require, \"resty.apisix.patch\")\nif not ok then\n    apisix_base_flags = {}\nend\n\nlocal load_balancer\nlocal local_conf\nlocal ver_header = \"APISIX/\" .. core.version.VERSION\n\nlocal has_mod, apisix_ngx_client = pcall(require, \"resty.apisix.client\")\n\nlocal _M = {version = 0.4}\n\n\nfunction _M.http_init(args)\n    core.resolver.init_resolver(args)\n    core.id.init()\n    core.env.init()\n\n    local process = require(\"ngx.process\")\n    local ok, err = process.enable_privileged_agent()\n    if not ok then\n        core.log.error(\"failed to enable privileged_agent: \", err)\n    end\n\n    if core.config.init then\n        local ok, err = core.config.init()\n        if not ok then\n            core.log.error(\"failed to load the configuration: \", err)\n        end\n    end\n\n    xrpc.init()\nend\n\n\nfunction _M.http_init_worker()\n    local seed, err = core.utils.get_seed_from_urandom()\n    if not seed then\n        core.log.warn('failed to get seed from urandom: ', err)\n        seed = ngx_now() * 1000 + ngx.worker.pid()\n    end\n    math.randomseed(seed)\n    -- for testing only\n    core.log.info(\"random test in [1, 10000]: \", math.random(1, 10000))\n\n    require(\"apisix.events\").init_worker()\n\n    core.lrucache.init_worker()\n\n    if discovery and discovery.init_worker then\n        discovery.init_worker()\n    end\n    require(\"apisix.balancer\").init_worker()\n    load_balancer = require(\"apisix.balancer\")\n    require(\"apisix.admin.init\").init_worker()\n\n    require(\"apisix.timers\").init_worker()\n\n    require(\"apisix.debug\").init_worker()\n\n    if core.config.init_worker then\n        local ok, err = core.config.init_worker()\n        if not ok then\n            core.log.error(\"failed to init worker process of \", core.config.type,\n                           \" config center, err: \", err)\n        end\n    end\n\n    plugin.init_worker()\n    router.http_init_worker()\n    require(\"apisix.http.service\").init_worker()\n    plugin_config.init_worker()\n    require(\"apisix.consumer\").init_worker()\n    consumer_group.init_worker()\n    apisix_secret.init_worker()\n\n    apisix_global_rules.init_worker()\n\n    apisix_upstream.init_worker()\n    require(\"apisix.plugins.ext-plugin.init\").init_worker()\n\n    control_api_router.init_worker()\n    local_conf = core.config.local_conf()\n\n    if local_conf.apisix and local_conf.apisix.enable_server_tokens == false then\n        ver_header = \"APISIX\"\n    end\n\n    -- To ensure that all workers related to Prometheus metrics are initialized,\n    -- we need to put the initialization of the Prometheus plugin here.\n    plugin.init_prometheus()\n\n    trusted_addresses_util.init_worker()\nend\n\n\nfunction _M.http_exit_worker()\n    -- TODO: we can support stream plugin later - currently there is not `destroy` method\n    -- in stream plugins\n    plugin.exit_worker()\n    require(\"apisix.plugins.ext-plugin.init\").exit_worker()\nend\n\n\nfunction _M.ssl_phase()\n    local ok, err = router.router_ssl.set(ngx.ctx.matched_ssl)\n    if not ok then\n        if err then\n            core.log.error(\"failed to fetch ssl config: \", err)\n        end\n        ngx_exit(-1)\n    end\nend\n\n\nfunction _M.ssl_client_hello_phase()\n    local sni, err = apisix_ssl.server_name(true)\n    if not sni or type(sni) ~= \"string\" then\n        local advise = \"please check if the client requests via IP or uses an outdated \" ..\n        \"protocol. If you need to report an issue, \" ..\n        \"provide a packet capture file of the TLS handshake.\"\n        core.log.error(\"failed to find SNI: \" .. (err or advise))\n        ngx_exit(-1)\n    end\n    local tls_ext_status_req = apisix_ssl.get_status_request_ext()\n\n    local ngx_ctx = ngx.ctx\n    local api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n    ngx_ctx.api_ctx = api_ctx\n    api_ctx.ngx_ctx = ngx_ctx\n\n    local span = tracer.start(ngx_ctx, \"ssl_client_hello_phase\", tracer.kind.server)\n\n    local ok, err = router.router_ssl.match_and_set(api_ctx, true, sni)\n\n    ngx_ctx.matched_ssl = api_ctx.matched_ssl\n    core.tablepool.release(\"api_ctx\", api_ctx)\n    ngx_ctx.api_ctx = nil\n    ngx_ctx.tls_ext_status_req = tls_ext_status_req\n\n    if not ok then\n        if err then\n            core.log.error(\"failed to fetch ssl config: \", err)\n        end\n        core.log.error(\"failed to match any SSL certificate by SNI: \", sni)\n        span:set_status(tracer.status.ERROR, \"no matched SSL\")\n        span:finish(ngx_ctx)\n        ngx_exit(-1)\n    end\n\n    ok, err = apisix_ssl.set_protocols_by_clienthello(ngx_ctx.matched_ssl.value.ssl_protocols)\n    if not ok then\n        core.log.error(\"failed to set ssl protocols: \", err)\n        span:set_status(tracer.status.ERROR, \"failed set protocols\")\n        span:finish(ngx_ctx)\n        ngx_exit(-1)\n    end\n\n    -- in stream subsystem, ngx.ssl.server_name() return hostname of ssl session in preread phase,\n    -- so that we can't get real SNI without recording it in ngx.ctx during client_hello phase\n    ngx.ctx.client_hello_sni = sni\n    span:finish(ngx_ctx)\nend\n\n\nlocal function stash_ngx_ctx()\n    local ref = ctxdump.stash_ngx_ctx()\n    core.log.info(\"stash ngx ctx: \", ref)\n    ngx_var.ctx_ref = ref\nend\n\n\nlocal function fetch_ctx()\n    local ref = ngx_var.ctx_ref\n    core.log.info(\"fetch ngx ctx: \", ref)\n    local ctx = ctxdump.apply_ngx_ctx(ref)\n    ngx_var.ctx_ref = ''\n    return ctx\nend\n\nlocal function parse_domain_in_route(route)\n    local nodes = route.value.upstream.dns_nodes\n    local new_nodes, err = upstream_util.parse_domain_for_nodes(nodes)\n    if not new_nodes then\n        return nil, err\n    end\n\n    local up_conf = route.value.upstream\n    local ok = upstream_util.compare_upstream_node(up_conf, new_nodes)\n    if ok then\n        return route\n    end\n\n    local nodes_ver = resource.get_nodes_ver(route.value.upstream.resource_key)\n    if not nodes_ver then\n        nodes_ver = 0\n    end\n    nodes_ver = nodes_ver + 1\n    route.value._nodes_ver = nodes_ver\n    route.value.upstream.nodes = new_nodes\n    resource.set_nodes_ver_and_nodes(route.value.upstream.resource_key,\n                                                    nodes_ver, new_nodes)\n    -- remove plugin before logging to avoid logging sensitive info\n    local route_log = core.table.deepcopy(route)\n    route_log.value.plugins = nil\n    route_log.value.auth_conf = nil\n    core.log.info(\"parse route which contain domain: \",\n                core.json.delay_encode(route_log, true))\n    return route\nend\n\n\nlocal function set_upstream_host(api_ctx, picked_server)\n    local up_conf = api_ctx.upstream_conf\n    if up_conf.pass_host then\n        api_ctx.pass_host = up_conf.pass_host\n        api_ctx.upstream_host = up_conf.upstream_host\n    end\n\n    local pass_host = api_ctx.pass_host or \"pass\"\n    if pass_host == \"pass\" then\n        return\n    end\n\n    if pass_host == \"rewrite\" then\n        api_ctx.var.upstream_host = api_ctx.upstream_host\n        return\n    end\n\n    api_ctx.var.upstream_host = picked_server.upstream_host\nend\n\n\nlocal function set_upstream_headers(api_ctx, picked_server)\n    set_upstream_host(api_ctx, picked_server)\nend\n\n\n-- verify the TLS session resumption by checking if the SNI in the client hello\n-- matches the hostname of the SSL session, this is to prevent the mTLS bypass security issue.\nlocal function verify_tls_session_resumption()\n    local session_hostname, err = apisix_ssl.session_hostname()\n    if err then\n        core.log.error(\"failed to get session hostname: \", err)\n        return false\n    end\n    if session_hostname and session_hostname ~= ngx.ctx.client_hello_sni then\n        core.log.error(\"sni in client hello mismatch hostname of ssl session, \",\n                         \"sni: \", ngx.ctx.client_hello_sni, \", hostname: \", session_hostname)\n        return false\n    end\n\n    return true\nend\n\n\nlocal function verify_tls_client(ctx)\n    local matched = router.router_ssl.match_and_set(ctx, true)\n    if not matched then\n        return true\n    end\n\n    local matched_ssl = ctx.matched_ssl\n    if matched_ssl.value.client and apisix_ssl.support_client_verification() then\n        local res = ngx_var.ssl_client_verify\n        if res ~= \"SUCCESS\" then\n            if res == \"NONE\" then\n                core.log.error(\"client certificate was not present\")\n            else\n                core.log.error(\"client certificate verification is not passed: \", res)\n            end\n\n            return false\n        end\n\n        if not verify_tls_session_resumption() then\n            return false\n        end\n    end\n\n    return true\nend\n\n\nlocal function uri_matches_skip_mtls_route_patterns(ssl, uri)\n    for _, pat in ipairs(ssl.value.client.skip_mtls_uri_regex) do\n        if ngx_re_match(uri, pat,  \"jo\") then\n            return true\n        end\n    end\nend\n\n\nlocal function verify_https_client(ctx)\n    local scheme = ctx.var.scheme\n    if scheme ~= \"https\" then\n        return true\n    end\n\n    local matched_ssl = ngx.ctx.matched_ssl\n    if matched_ssl.value.client\n        and matched_ssl.value.client.skip_mtls_uri_regex\n        and apisix_ssl.support_client_verification()\n        and (not uri_matches_skip_mtls_route_patterns(matched_ssl, ngx.var.uri)) then\n        local res = ctx.var.ssl_client_verify\n        if res ~= \"SUCCESS\" then\n            if res == \"NONE\" then\n                core.log.error(\"client certificate was not present\")\n            else\n                core.log.error(\"client certificate verification is not passed: \", res)\n            end\n\n            return false\n        end\n    end\n\n    local host = ctx.var.host\n    local matched = router.router_ssl.match_and_set(ctx, true, host)\n    if not matched then\n        return true\n    end\n\n    local matched_ssl = ctx.matched_ssl\n    if matched_ssl.value.client and apisix_ssl.support_client_verification() then\n        local verified = apisix_base_flags.client_cert_verified_in_handshake\n        if not verified then\n            -- vanilla OpenResty requires to check the verification result\n            local res = ctx.var.ssl_client_verify\n            if res ~= \"SUCCESS\" then\n                if res == \"NONE\" then\n                    core.log.error(\"client certificate was not present\")\n                else\n                    core.log.error(\"client certificate verification is not passed: \", res)\n                end\n\n                return false\n            end\n        end\n\n        local sni = apisix_ssl.server_name()\n        if sni ~= host then\n            -- There is a case that the user configures a SSL object with `*.domain`,\n            -- and the client accesses with SNI `a.domain` but uses Host `b.domain`.\n            -- This case is complex and we choose to restrict the access until there\n            -- is a stronge demand in real world.\n            core.log.error(\"client certificate verified with SNI \", sni,\n                           \", but the host is \", host)\n            return false\n        end\n\n        if not verify_tls_session_resumption() then\n            return false\n        end\n    end\n\n    return true\nend\n\n\nlocal function normalize_uri_like_servlet(uri)\n    local found = core.string.find(uri, ';')\n    if not found then\n        return uri\n    end\n\n    local segs, err = re_split(uri, \"/\", \"jo\")\n    if not segs then\n        return nil, err\n    end\n\n    local len = #segs\n    for i = 1, len do\n        local seg = segs[i]\n        local pos = core.string.find(seg, ';')\n        if pos then\n            seg = seg:sub(1, pos - 1)\n            -- reject bad uri which bypasses with ';'\n            if seg == \".\" or seg == \"..\" then\n                return nil, \"dot segment with parameter\"\n            end\n            if seg == \"\" and i < len then\n                return nil, \"empty segment with parameters\"\n            end\n\n            segs[i] = seg\n\n            seg = seg:lower()\n            if seg == \"%2e\" or seg == \"%2e%2e\" then\n                return nil, \"encoded dot segment\"\n            end\n        end\n    end\n\n    return core.table.concat(segs, '/')\nend\n\n\nlocal function common_phase(phase_name)\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        return\n    end\n\n    local global_rules, conf_version = apisix_global_rules.global_rules()\n    plugin.run_global_rules(api_ctx, global_rules, conf_version, phase_name)\n\n    if api_ctx.script_obj then\n        script.run(phase_name, api_ctx)\n        return api_ctx, true\n    end\n\n    return plugin.run_plugin(phase_name, nil, api_ctx)\nend\n\n\nfunction _M.handle_upstream(api_ctx, route, enable_websocket)\n    -- some plugins(ai-proxy...) request upstream by http client directly\n    if api_ctx.bypass_nginx_upstream then\n        common_phase(\"before_proxy\")\n        return\n    end\n\n    local up_id = route.value.upstream_id\n\n    -- used for the traffic-split plugin\n    if api_ctx.upstream_id then\n        up_id = api_ctx.upstream_id\n    end\n\n    if up_id then\n        local upstream = apisix_upstream.get_by_id(up_id)\n        if not upstream then\n            if is_http then\n                return core.response.exit(502)\n            end\n\n            return ngx_exit(1)\n        end\n\n        api_ctx.matched_upstream = upstream\n\n    else\n        if route.has_domain then\n            local err\n            route, err = parse_domain_in_route(route)\n            if err then\n                core.log.error(\"failed to get resolved route: \", err)\n                return core.response.exit(500)\n            end\n\n            api_ctx.conf_version = route.modifiedIndex\n            api_ctx.matched_route = route\n        end\n\n        local route_val = route.value\n\n        api_ctx.matched_upstream = route_val.upstream\n    end\n\n    if api_ctx.matched_upstream and api_ctx.matched_upstream.tls and\n        api_ctx.matched_upstream.tls.client_cert_id then\n\n        local cert_id = api_ctx.matched_upstream.tls.client_cert_id\n        local upstream_ssl = router.router_ssl.get_by_id(cert_id)\n        if not upstream_ssl or upstream_ssl.type ~= \"client\" then\n            local err  = upstream_ssl and\n                \"ssl type should be 'client'\" or\n                \"ssl id [\" .. cert_id .. \"] not exits\"\n            core.log.error(\"failed to get ssl cert: \", err)\n\n            if is_http then\n                return core.response.exit(502)\n            end\n\n            return ngx_exit(1)\n        end\n\n        core.log.info(\"matched ssl: \",\n                  core.json.delay_encode(upstream_ssl, true))\n        api_ctx.upstream_ssl = upstream_ssl\n    end\n\n    if enable_websocket then\n        api_ctx.var.upstream_upgrade    = api_ctx.var.http_upgrade\n        api_ctx.var.upstream_connection = api_ctx.var.http_connection\n        core.log.info(\"enabled websocket for route: \", route.value.id)\n    end\n\n    -- load balancer is not required by kafka upstream, so the upstream\n    -- node selection process is intercepted and left to kafka to\n    -- handle on its own\n    if api_ctx.matched_upstream and api_ctx.matched_upstream.scheme == \"kafka\" then\n        return pubsub_kafka.access(api_ctx)\n    end\n\n    local code, err = set_upstream(route, api_ctx)\n    if code then\n        core.log.error(\"failed to set upstream: \", err)\n        core.response.exit(code)\n    end\n\n    local server, err = load_balancer.pick_server(route, api_ctx)\n    if not server then\n        core.log.error(\"failed to pick server: \", err)\n        return core.response.exit(502)\n    end\n\n    api_ctx.picked_server = server\n\n    set_upstream_headers(api_ctx, server)\n\n    -- run the before_proxy method in access phase first to avoid always reinit request\n    common_phase(\"before_proxy\")\n\n    local up_scheme = api_ctx.upstream_scheme\n    if up_scheme == \"grpcs\" or up_scheme == \"grpc\" then\n        stash_ngx_ctx()\n        return ngx.exec(\"@grpc_pass\")\n    end\n\n    if api_ctx.dubbo_proxy_enabled then\n        stash_ngx_ctx()\n        return ngx.exec(\"@dubbo_pass\")\n    end\nend\n\n\nlocal function handle_x_forwarded_headers(api_ctx)\n    local addr_is_trusted = trusted_addresses_util.is_trusted(api_ctx.var.realip_remote_addr)\n\n    -- Only untrusted values need to be overwritten or cleared.\n    if not addr_is_trusted then\n        -- store the original x-forwarded-* headers\n        -- to allow future use by other plugins or processes\n        api_ctx.var.original_x_forwarded_proto = api_ctx.var.http_x_forwarded_proto\n        api_ctx.var.original_x_forwarded_host = api_ctx.var.http_x_forwarded_host\n        api_ctx.var.original_x_forwarded_port = api_ctx.var.http_x_forwarded_port\n        api_ctx.var.original_x_forwarded_for = api_ctx.var.http_x_forwarded_for\n\n        -- trusted ones\n        -- ref: ngx_tpl.lua#L831-L840\n        --\n        -- these values are observed directly by APISIX and cannot be forged,\n        -- making them highly credible.\n        local proto = api_ctx.var.scheme\n        local host = api_ctx.var.host\n        local port = api_ctx.var.server_port\n\n        -- override the x-forwarded-* headers to the trusted ones.\n        -- make sure that the correct values ​​are obtained\n        -- in the subsequent stages using `core.request.header`.\n        core.request.set_header(api_ctx, \"X-Forwarded-Proto\", proto)\n        core.request.set_header(api_ctx, \"X-Forwarded-Host\", host)\n        core.request.set_header(api_ctx, \"X-Forwarded-Port\", port)\n        -- later processed in ngx_tpl by `$proxy_add_x_forwarded_for`.\n        core.request.set_header(api_ctx, \"X-Forwarded-For\", nil)\n\n        -- update the cached value in http_x_forwarded_* to the trusted ones.\n        -- make sure that the correct values ​​are obtained\n        -- in the subsequent stages using `var.http_x_forwarded_*`.\n        api_ctx.var.http_x_forwarded_proto = proto\n        api_ctx.var.http_x_forwarded_host = host\n        api_ctx.var.http_x_forwarded_port = port\n        api_ctx.var.http_x_forwarded_for = nil\n    end\nend\n\n\n-- in ngx_tpl.lua#L831-L840,\n-- there is such code: `proxy_set_header X-Forwarded-XXX $var_x_forwarded_xxx;`\n-- that is, set the `X-Forwarded-XXX` header through `var_x_forwarded_xxx`.\n--\n-- therefore, it is necessary to set the trusted `http_x_forwarded_xxx` to `var_x_forwarded_xxx`.\n-- So that the `X-Forwarded-XXX` header is updated to a trusted value.\n--\n-- currently, only following headers are updated through these variables:\n-- - X-Forwarded-Proto\n-- - X-Forwarded-Port\n-- - X-Forwarded-Host\n--\n-- the `X-Forwarded-For` header is not updated through these variables.\n-- because it is set by the `proxy_add_x_forwarded_for` directive.\nlocal function set_upstream_x_forwarded_headers(api_ctx)\n    local proto = api_ctx.var.http_x_forwarded_proto\n    if proto then\n        api_ctx.var.var_x_forwarded_proto = proto\n    end\n\n    local port = api_ctx.var.http_x_forwarded_port\n    if port then\n        api_ctx.var.var_x_forwarded_port = port\n    end\n\n    local host = api_ctx.var.http_x_forwarded_host\n    if host then\n        api_ctx.var.var_x_forwarded_host = host\n    end\nend\n\n\nfunction _M.http_access_phase()\n    -- from HTTP/3 to HTTP/1.1 we need to convert :authority pesudo-header\n    -- to Host header, so we set upstream_host variable here.\n    if ngx.req.http_version() == 3 then\n        ngx.var.upstream_host = ngx.var.host .. \":\" .. ngx.var.server_port\n    end\n    local ngx_ctx = ngx.ctx\n\n    -- always fetch table from the table pool, we don't need a reused api_ctx\n    local api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n    ngx_ctx.api_ctx = api_ctx\n    api_ctx.ngx_ctx = ngx_ctx\n\n    core.ctx.set_vars_meta(api_ctx)\n\n    local span = tracer.start(ngx_ctx, \"apisix.phase.access\", tracer.kind.server)\n\n    if not verify_https_client(api_ctx) then\n        return core.response.exit(400)\n    end\n\n    debug.dynamic_debug(api_ctx)\n\n    local uri = api_ctx.var.uri\n    if local_conf.apisix then\n        if local_conf.apisix.delete_uri_tail_slash then\n            if str_byte(uri, #uri) == str_byte(\"/\") then\n                api_ctx.var.uri = str_sub(api_ctx.var.uri, 1, #uri - 1)\n                core.log.info(\"remove the end of uri '/', current uri: \", api_ctx.var.uri)\n            end\n        end\n\n        if local_conf.apisix.normalize_uri_like_servlet then\n            local new_uri, err = normalize_uri_like_servlet(uri)\n            if not new_uri then\n                core.log.error(\"failed to normalize: \", err)\n                return core.response.exit(400)\n            end\n\n            api_ctx.var.uri = new_uri\n            -- forward the original uri so the servlet upstream\n            -- can consume the param after ';'\n            api_ctx.var.upstream_uri = uri\n        end\n    end\n\n    -- To prevent being hacked by untrusted request_uri, here we\n    -- record the normalized but not rewritten uri as request_uri,\n    -- the original request_uri can be accessed via var.real_request_uri\n    api_ctx.var.real_request_uri = api_ctx.var.request_uri\n    api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or \"\")\n\n    handle_x_forwarded_headers(api_ctx)\n\n    local match_span = tracer.start(ngx_ctx, \"http_router_match\", tracer.kind.internal)\n    router.router_http.match(api_ctx)\n\n    local route = api_ctx.matched_route\n    if not route then\n        match_span:set_status(tracer.status.ERROR, \"no matched route\")\n        match_span:finish(ngx.ctx)\n        -- run global rule when there is no matching route\n        local global_rules, conf_version = apisix_global_rules.global_rules()\n        plugin.run_global_rules(api_ctx, global_rules, conf_version, nil)\n\n        core.log.info(\"not find any matched route\")\n        return core.response.exit(404,\n                    {error_msg = \"404 Route Not Found\"})\n    end\n    match_span:finish(ngx_ctx)\n\n    core.log.info(\"matched route: \",\n                  core.json.delay_encode(api_ctx.matched_route, true))\n\n    local enable_websocket = route.value.enable_websocket\n\n    if route.value.plugin_config_id then\n        local conf = plugin_config.get(route.value.plugin_config_id)\n        if not conf then\n            core.log.error(\"failed to fetch plugin config by \",\n                            \"id: \", route.value.plugin_config_id)\n            return core.response.exit(503)\n        end\n\n        route = plugin_config.merge(route, conf)\n    end\n\n    if route.value.service_id then\n        local service = service_fetch(route.value.service_id)\n        if not service then\n            core.log.error(\"failed to fetch service configuration by \",\n                           \"id: \", route.value.service_id)\n            return core.response.exit(404)\n        end\n\n        route = plugin.merge_service_route(service, route)\n        api_ctx.matched_route = route\n        api_ctx.conf_type = \"route&service\"\n        api_ctx.conf_version = route.modifiedIndex .. \"&\" .. service.modifiedIndex\n        api_ctx.conf_id = route.value.id .. \"&\" .. service.value.id\n        api_ctx.service_id = service.value.id\n        api_ctx.service_name = service.value.name\n\n        if enable_websocket == nil then\n            enable_websocket = service.value.enable_websocket\n        end\n\n    else\n        api_ctx.conf_type = \"route\"\n        api_ctx.conf_version = route.modifiedIndex\n        api_ctx.conf_id = route.value.id\n    end\n    api_ctx.route_id = route.value.id\n    api_ctx.route_name = route.value.name\n\n    -- run global rule\n    local global_rules, conf_version = apisix_global_rules.global_rules()\n    plugin.run_global_rules(api_ctx, global_rules, conf_version, nil)\n\n    if route.value.script then\n        script.load(route, api_ctx)\n        script.run(\"access\", api_ctx)\n\n    else\n        local plugins = plugin.filter(api_ctx, route)\n        api_ctx.plugins = plugins\n        plugin.run_plugin(\"rewrite\", plugins, api_ctx)\n        if api_ctx.consumer then\n            local changed\n            local group_conf\n\n            if api_ctx.consumer.group_id then\n                group_conf = consumer_group.get(api_ctx.consumer.group_id)\n                if not group_conf then\n                    core.log.error(\"failed to fetch consumer group config by \",\n                        \"id: \", api_ctx.consumer.group_id)\n                    return core.response.exit(503)\n                end\n            end\n\n            route, changed = plugin.merge_consumer_route(\n                route,\n                api_ctx.consumer,\n                group_conf,\n                api_ctx\n            )\n\n            core.log.info(\"find consumer \", api_ctx.consumer.username,\n                          \", config changed: \", changed)\n\n            if changed then\n                api_ctx.matched_route = route\n                core.table.clear(api_ctx.plugins)\n                local phase = \"rewrite_in_consumer\"\n                api_ctx.plugins = plugin.filter(api_ctx, route, api_ctx.plugins, nil, phase)\n                -- rerun rewrite phase for newly added plugins in consumer\n                plugin.run_plugin(phase, api_ctx.plugins, api_ctx)\n            end\n        end\n        plugin.run_plugin(\"access\", plugins, api_ctx)\n    end\n    span:finish(ngx_ctx)\n\n    _M.handle_upstream(api_ctx, route, enable_websocket)\n\n    set_upstream_x_forwarded_headers(api_ctx)\nend\n\n\nfunction _M.dubbo_access_phase()\n    ngx.ctx = fetch_ctx()\nend\n\n\nfunction _M.grpc_access_phase()\n    ngx.ctx = fetch_ctx()\n\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        return\n    end\n\n    local code, err = apisix_upstream.set_grpcs_upstream_param(api_ctx)\n    if code then\n        core.log.error(\"failed to set grpcs upstream param: \", err)\n        core.response.exit(code)\n    end\n\n    if api_ctx.enable_mirror == true and has_mod then\n        apisix_ngx_client.enable_mirror()\n    end\nend\n\n\nlocal function set_resp_upstream_status(up_status)\n    local_conf = core.config.local_conf()\n\n    if local_conf.apisix and local_conf.apisix.show_upstream_status_in_response_header then\n        core.response.set_header(\"X-APISIX-Upstream-Status\", up_status)\n    elseif #up_status == 3 then\n        if tonumber(up_status) >= 500 and tonumber(up_status) <= 599 then\n            core.response.set_header(\"X-APISIX-Upstream-Status\", up_status)\n        end\n    elseif #up_status > 3 then\n        -- the up_status can be \"502, 502\" or \"502, 502 : \"\n        local last_status\n        if str_byte(up_status, -1) == str_byte(\" \") then\n            last_status = str_sub(up_status, -6, -3)\n        else\n            last_status = str_sub(up_status, -3)\n        end\n\n        if tonumber(last_status) >= 500 and tonumber(last_status) <= 599 then\n            core.response.set_header(\"X-APISIX-Upstream-Status\", up_status)\n        end\n    end\nend\n\n\nfunction _M.http_header_filter_phase()\n    local ngx_ctx = ngx.ctx\n    local span = tracer.start(ngx_ctx, \"apisix.phase.header_filter\", tracer.kind.server)\n    core.response.set_header(\"Server\", ver_header)\n\n    local up_status = get_var(\"upstream_status\")\n    if up_status then\n        set_resp_upstream_status(up_status)\n    end\n\n    common_phase(\"header_filter\")\n\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        return\n    end\n\n    local debug_headers = api_ctx.debug_headers\n    if debug_headers then\n        local deduplicate = core.table.new(core.table.nkeys(debug_headers), 0)\n        for k, v in pairs(debug_headers) do\n            core.table.insert(deduplicate, k)\n        end\n        core.response.set_header(\"Apisix-Plugins\", core.table.concat(deduplicate, \", \"))\n    end\n    span:finish(ngx_ctx)\n\n    tracer.start(ngx_ctx, \"apisix.phase.body_filter\", tracer.kind.server)\nend\n\n\nfunction _M.http_body_filter_phase()\n    common_phase(\"body_filter\")\n    common_phase(\"delayed_body_filter\")\nend\n\n\nlocal function healthcheck_passive(api_ctx)\n    local checker = api_ctx.up_checker\n    if not checker then\n        return\n    end\n\n    local up_conf = api_ctx.upstream_conf\n    local passive = up_conf.checks and up_conf.checks.passive\n    if not passive then\n        return\n    end\n\n    core.log.info(\"enabled healthcheck passive\")\n    local host = up_conf.checks and up_conf.checks.active\n                 and up_conf.checks.active.host\n    local port = up_conf.checks and up_conf.checks.active\n                 and up_conf.checks.active.port or api_ctx.balancer_port\n\n    local resp_status = ngx.status\n\n    if not is_http then\n        -- 200 is the only success status code for TCP\n        if resp_status ~= 200 then\n            checker:report_tcp_failure(api_ctx.balancer_ip, port, host, nil, \"passive\")\n        end\n        return\n    end\n\n    local http_statuses = passive and passive.healthy and\n                          passive.healthy.http_statuses\n    core.log.info(\"passive.healthy.http_statuses: \",\n                  core.json.delay_encode(http_statuses))\n    if http_statuses then\n        for i, status in ipairs(http_statuses) do\n            if resp_status == status then\n                checker:report_http_status(api_ctx.balancer_ip,\n                                           port,\n                                           host,\n                                           resp_status)\n            end\n        end\n    end\n\n    http_statuses = passive and passive.unhealthy and\n                    passive.unhealthy.http_statuses\n    core.log.info(\"passive.unhealthy.http_statuses: \",\n                  core.json.delay_encode(http_statuses))\n    if not http_statuses then\n        return\n    end\n\n    for i, status in ipairs(http_statuses) do\n        if resp_status == status then\n            checker:report_http_status(api_ctx.balancer_ip,\n                                       port,\n                                       host,\n                                       resp_status)\n        end\n    end\nend\n\n\nfunction _M.status()\n    core.response.exit(200, core.json.encode({ status = \"ok\" }))\nend\n\n\nlocal function discovery_ready_check()\n    local discovery_type = local_conf.discovery\n    if not discovery_type then\n        return true\n    end\n    for discovery_name, _ in pairs(discovery_type) do\n        local dis_module = discovery[discovery_name]\n        if dis_module.check_discovery_ready then\n            local ready, message = dis_module.check_discovery_ready()\n            if not ready then\n                return false, message\n            end\n        end\n    end\n    return true\nend\n\nlocal function config_ready_check()\n    local role = core.table.try_read_attr(local_conf, \"deployment\", \"role\")\n    local provider = core.table.try_read_attr(local_conf, \"deployment\",\n                                              \"role_\" .. role, \"config_provider\")\n    if provider ~= \"yaml\" and provider ~= \"etcd\" then\n        return false, \"unknown config provider: \" .. tostring(provider)\n    end\n\n    local status_shdict = ngx.shared[\"status-report\"]\n    if not status_shdict then\n        core.log.error(\"failed to get ngx.shared dict status-report\")\n        return false, \"failed to get ngx.shared dict status-report\"\n    end\n    local ids = status_shdict:get_keys()\n\n    local worker_count = ngx.worker.count()\n    if #ids ~= worker_count then\n        local error = \"worker count: \" .. worker_count .. \" but status report count: \" .. #ids\n        core.log.error(error)\n        return false, error\n    end\n    for _, id in ipairs(ids) do\n        local ready = status_shdict:get(id)\n        if not ready then\n            local error = \"worker id: \" .. id .. \" has not received configuration\"\n            core.log.error(error)\n            return false, error\n        end\n    end\n\n    return true\nend\n\nfunction _M.status_ready()\n    local ready, message = config_ready_check()\n    if not ready then\n        core.response.exit(503, core.json.encode({\n            status = \"error\",\n            error = message\n        }))\n        return\n    end\n\n    ready, message = discovery_ready_check()\n    if not ready then\n        core.response.exit(503, core.json.encode({\n            status = \"error\",\n            error = message\n        }))\n        return\n    end\n\n    core.response.exit(200, core.json.encode({ status = \"ok\" }))\n    return\nend\n\n\nfunction _M.http_log_phase()\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        return\n    end\n    tracer.finish_all(api_ctx.ngx_ctx)\n\n    if not api_ctx.var.apisix_upstream_response_time or\n    api_ctx.var.apisix_upstream_response_time == \"\" then\n        api_ctx.var.apisix_upstream_response_time = ngx.var.upstream_response_time\n    end\n    local api_ctx = common_phase(\"log\")\n    if not api_ctx then\n        return\n    end\n\n    healthcheck_passive(api_ctx)\n\n    if api_ctx.server_picker and api_ctx.server_picker.after_balance then\n        api_ctx.server_picker.after_balance(api_ctx, false)\n    end\n\n    core.ctx.release_vars(api_ctx)\n    if api_ctx.plugins then\n        core.tablepool.release(\"plugins\", api_ctx.plugins)\n    end\n\n    if api_ctx.curr_req_matched then\n        core.tablepool.release(\"matched_route_record\", api_ctx.curr_req_matched)\n    end\n\n    tracer.release(api_ctx.ngx_ctx)\n    api_ctx.ngx_ctx = nil\n\n    core.tablepool.release(\"api_ctx\", api_ctx)\nend\n\n\nfunction _M.http_balancer_phase()\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        core.log.error(\"invalid api_ctx\")\n        return core.response.exit(500)\n    end\n\n    load_balancer.run(api_ctx.matched_route, api_ctx, common_phase)\nend\n\n\nlocal function cors_admin()\n    local_conf = core.config.local_conf()\n    if not core.table.try_read_attr(local_conf, \"deployment\", \"admin\", \"enable_admin_cors\") then\n        return\n    end\n\n    local method = get_method()\n    if method == \"OPTIONS\" then\n        core.response.set_header(\"Access-Control-Allow-Origin\", \"*\",\n            \"Access-Control-Allow-Methods\",\n            \"POST, GET, PUT, OPTIONS, DELETE, PATCH\",\n            \"Access-Control-Max-Age\", \"3600\",\n            \"Access-Control-Allow-Headers\", \"*\",\n            \"Access-Control-Allow-Credentials\", \"true\",\n            \"Content-Length\", \"0\",\n            \"Content-Type\", \"text/plain\")\n        ngx_exit(200)\n    end\n\n    core.response.set_header(\"Access-Control-Allow-Origin\", \"*\",\n                            \"Access-Control-Allow-Credentials\", \"true\",\n                            \"Access-Control-Expose-Headers\", \"*\",\n                            \"Access-Control-Max-Age\", \"3600\")\nend\n\nlocal function add_content_type()\n    core.response.set_header(\"Content-Type\", \"application/json\")\nend\n\ndo\n    local router\n\nfunction _M.http_admin()\n    if not router then\n        router = admin_init.get()\n    end\n\n    core.response.set_header(\"Server\", ver_header)\n    -- add cors rsp header\n    cors_admin()\n\n    -- add content type to rsp header\n    add_content_type()\n\n    -- core.log.info(\"uri: \", get_var(\"uri\"), \" method: \", get_method())\n    local ok = router:dispatch(get_var(\"uri\"), {method = get_method()})\n    if not ok then\n        ngx_exit(404)\n    end\nend\n\nend -- do\n\n\nfunction _M.http_control()\n    local ok = control_api_router.match(get_var(\"uri\"))\n    if not ok then\n        ngx_exit(404)\n    end\nend\n\n\nfunction _M.stream_init(args)\n    core.log.info(\"enter stream_init\")\n\n    core.resolver.init_resolver(args)\n\n    if core.config.init then\n        local ok, err = core.config.init()\n        if not ok then\n            core.log.error(\"failed to load the configuration: \", err)\n        end\n    end\n\n    xrpc.init()\nend\n\n\nfunction _M.stream_init_worker()\n    core.log.info(\"enter stream_init_worker\")\n    local seed, err = core.utils.get_seed_from_urandom()\n    if not seed then\n        core.log.warn('failed to get seed from urandom: ', err)\n        seed = ngx_now() * 1000 + ngx.worker.pid()\n    end\n    math.randomseed(seed)\n    -- for testing only\n    core.log.info(\"random stream test in [1, 10000]: \", math.random(1, 10000))\n\n    core.lrucache.init_worker()\n\n    if core.config.init_worker then\n        local ok, err = core.config.init_worker()\n        if not ok then\n            core.log.error(\"failed to init worker process of \", core.config.type,\n                           \" config center, err: \", err)\n        end\n    end\n\n    plugin.init_worker()\n    xrpc.init_worker()\n    router.stream_init_worker()\n    require(\"apisix.http.service\").init_worker()\n    apisix_upstream.init_worker()\n\n    require(\"apisix.events\").init_worker()\n\n    -- for admin api of standalone mode, we need to startup background timer and patch schema etc.\n    require(\"apisix.admin.init\").init_worker()\n\n    if discovery and discovery.init_worker then\n        discovery.init_worker()\n    end\n\n    load_balancer = require(\"apisix.balancer\")\n\n    local_conf = core.config.local_conf()\nend\n\n\nfunction _M.stream_preread_phase()\n    local ngx_ctx = ngx.ctx\n    local api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n    ngx_ctx.api_ctx = api_ctx\n\n    if not verify_tls_client(api_ctx) then\n        return ngx_exit(1)\n    end\n\n    core.ctx.set_vars_meta(api_ctx)\n\n    local ok, err = router.router_stream.match(api_ctx)\n    if not ok then\n        core.log.error(err)\n        return ngx_exit(1)\n    end\n\n    core.log.info(\"matched route: \",\n                  core.json.delay_encode(api_ctx.matched_route, true))\n\n    local matched_route = api_ctx.matched_route\n    if not matched_route then\n        return ngx_exit(1)\n    end\n\n\n    local up_id = matched_route.value.upstream_id\n    if up_id then\n        local upstream = apisix_upstream.get_by_id(up_id)\n        if not upstream then\n            if is_http then\n                return core.response.exit(502)\n            end\n\n            return ngx_exit(1)\n        end\n\n        api_ctx.matched_upstream = upstream\n\n    elseif matched_route.value.service_id then\n        local service = service_fetch(matched_route.value.service_id)\n        if not service then\n            core.log.error(\"failed to fetch service configuration by \",\n                    \"id: \", matched_route.value.service_id)\n            return core.response.exit(404)\n        end\n\n        matched_route = plugin.merge_service_stream_route(service, matched_route)\n        api_ctx.matched_route = matched_route\n        api_ctx.conf_type = \"stream_route&service\"\n        api_ctx.conf_version = matched_route.modifiedIndex .. \"&\" .. service.modifiedIndex\n        api_ctx.conf_id = matched_route.value.id .. \"&\" .. service.value.id\n        api_ctx.service_id = service.value.id\n        api_ctx.service_name = service.value.name\n        api_ctx.matched_upstream = matched_route.value.upstream\n        if matched_route.value.upstream_id and not matched_route.value.upstream then\n            local upstream = apisix_upstream.get_by_id(matched_route.value.upstream_id)\n            if not upstream then\n                if is_http then\n                    return core.response.exit(502)\n                end\n\n                return ngx_exit(1)\n            end\n\n            api_ctx.matched_upstream = upstream\n        end\n    else\n        if matched_route.has_domain then\n            local err\n            matched_route, err = parse_domain_in_route(matched_route)\n            if err then\n                core.log.error(\"failed to get resolved route: \", err)\n                return ngx_exit(1)\n            end\n\n            api_ctx.matched_route = matched_route\n        end\n\n        local route_val = matched_route.value\n        api_ctx.matched_upstream = route_val.upstream\n    end\n\n    local plugins = core.tablepool.fetch(\"plugins\", 32, 0)\n    api_ctx.plugins = plugin.stream_filter(matched_route, plugins)\n    -- core.log.info(\"valid plugins: \", core.json.delay_encode(plugins, true))\n\n    api_ctx.conf_type = \"stream/route\"\n    api_ctx.conf_version = matched_route.modifiedIndex\n    api_ctx.conf_id = matched_route.value.id\n\n    plugin.run_plugin(\"preread\", plugins, api_ctx)\n\n    if matched_route.value.protocol then\n        xrpc.run_protocol(matched_route.value.protocol, api_ctx)\n        return\n    end\n\n    local code, err = set_upstream(matched_route, api_ctx)\n    if code then\n        core.log.error(\"failed to set upstream: \", err)\n        return ngx_exit(1)\n    end\n\n    local server, err = load_balancer.pick_server(matched_route, api_ctx)\n    if not server then\n        core.log.error(\"failed to pick server: \", err)\n        return ngx_exit(1)\n    end\n\n    api_ctx.picked_server = server\n\n    -- run the before_proxy method in preread phase first to avoid always reinit request\n    common_phase(\"before_proxy\")\nend\n\n\nfunction _M.stream_balancer_phase()\n    core.log.info(\"enter stream_balancer_phase\")\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        core.log.error(\"invalid api_ctx\")\n        return ngx_exit(1)\n    end\n\n    load_balancer.run(api_ctx.matched_route, api_ctx, common_phase)\nend\n\n\nfunction _M.stream_log_phase()\n    core.log.info(\"enter stream_log_phase\")\n\n    local api_ctx = plugin.run_plugin(\"log\")\n    if not api_ctx then\n        return\n    end\n\n    healthcheck_passive(api_ctx)\n\n    core.ctx.release_vars(api_ctx)\n    if api_ctx.plugins then\n        core.tablepool.release(\"plugins\", api_ctx.plugins)\n    end\n\n    core.tablepool.release(\"api_ctx\", api_ctx)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/inspect/dbg.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal string_format = string.format\nlocal debug = debug\nlocal ipairs = ipairs\nlocal pcall = pcall\nlocal table_insert = table.insert\nlocal jit = jit\n\nlocal _M = {}\n\nlocal hooks = {}\n\nfunction _M.getname(n)\n    if n.what == \"C\" then\n        return n.name\n    end\n    local lc = string_format(\"%s:%d\", n.short_src, n.currentline)\n    if n.what ~= \"main\" and n.namewhat ~= \"\" then\n        return string_format(\"%s (%s)\", lc, n.name)\n    else\n        return lc\n    end\nend\n\nlocal function hook(_, arg)\n    local level = 2\n    local finfo = debug.getinfo(level, \"nSlf\")\n    local key = finfo.source .. \"#\" .. arg\n\n    local hooks2 = {}\n    local removed_hooks = {}\n    for _, hook in ipairs(hooks) do\n        if key:sub(-#hook.key) == hook.key then\n            local filter_func = hook.filter_func\n            local info = {finfo = finfo, uv = {}, vals = {}}\n\n            -- upvalues\n            local i = 1\n            while true do\n                local name, value = debug.getupvalue(finfo.func, i)\n                if name == nil then break end\n                if name:sub(1, 1) ~= \"(\" then\n                    info.uv[name] = value\n                end\n                i = i + 1\n            end\n\n            -- local values\n            local i = 1\n            while true do\n                local name, value = debug.getlocal(level, i)\n                if not name then break end\n                if name:sub(1, 1) ~= \"(\" then\n                    info.vals[name] = value\n                end\n                i = i + 1\n            end\n\n            local r1, r2_or_err = pcall(filter_func, info)\n            if not r1 then\n                core.log.error(\"inspect: pcall filter_func:\", r2_or_err)\n                table_insert(removed_hooks, hook)\n            elseif r2_or_err == false then\n                -- if filter_func returns false, keep the hook\n                table_insert(hooks2, hook)\n            else\n                table_insert(removed_hooks, hook)\n            end\n        else\n            -- key not match, keep the hook\n            table_insert(hooks2, hook)\n        end\n    end\n\n    for _, hook in ipairs(removed_hooks) do\n        core.log.warn(\"inspect: remove hook: \", hook.key)\n    end\n\n    -- disable debug mode if all hooks done\n    if #hooks2 ~= #hooks then\n        hooks = hooks2\n        if #hooks == 0 then\n            core.log.warn(\"inspect: all hooks removed\")\n            debug.sethook()\n            if jit then\n                jit.on()\n            end\n        end\n    end\nend\n\nfunction _M.set_hook(file, line, func, filter_func)\n    if file == nil then\n        file = \"=stdin\"\n    end\n\n    local key = file .. \"#\" .. line\n    table_insert(hooks, {key = key, filter_func = filter_func})\n\n    if jit then\n        jit.flush(func)\n        jit.off()\n    end\n\n    debug.sethook(hook, \"l\")\nend\n\nfunction _M.unset_hook(file, line)\n    if file == nil then\n        file = \"=stdin\"\n    end\n\n    local hooks2 = {}\n\n    local key = file .. \"#\" .. line\n    for i, hook in ipairs(hooks) do\n        if hook.key ~= key then\n            table_insert(hooks2, hook)\n        end\n    end\n\n    if #hooks2 ~= #hooks then\n        hooks = hooks2\n        if #hooks == 0 then\n            debug.sethook()\n            if jit then\n                jit.on()\n            end\n        end\n    end\nend\n\nfunction _M.unset_all()\n    if #hooks > 0 then\n        hooks = {}\n        debug.sethook()\n        if jit then\n            jit.on()\n        end\n    end\nend\n\nfunction _M.hooks()\n    return hooks\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/inspect/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal dbg = require(\"apisix.inspect.dbg\")\nlocal lfs = require(\"lfs\")\nlocal pl_path = require(\"pl.path\")\nlocal io = io\nlocal table_insert = table.insert\nlocal pcall = pcall\nlocal ipairs = ipairs\nlocal os = os\nlocal ngx = ngx\nlocal loadstring = loadstring\nlocal format = string.format\n\nlocal _M = {}\n\nlocal last_modified = 0\n\nlocal stop = false\n\nlocal running = false\n\nlocal last_report_time = 0\n\nlocal REPORT_INTERVAL = 30 -- secs\n\nlocal function run_lua_file(file)\n    local f, err = io.open(file, \"rb\")\n    if not f then\n        return false, err\n    end\n    local code, err = f:read(\"*all\")\n    f:close()\n    if code == nil then\n        return false, format(\"cannot read hooks file: %s\", err)\n    end\n    local func, err = loadstring(code)\n    if not func then\n        return false, err\n    end\n    func()\n    return true\nend\n\nlocal function setup_hooks(file)\n    if pl_path.exists(file) then\n        dbg.unset_all()\n        local _, err = pcall(run_lua_file, file)\n        local hooks = {}\n        for _, hook in ipairs(dbg.hooks()) do\n            table_insert(hooks, hook.key)\n        end\n        core.log.warn(\"set hooks: err: \", err, \", hooks: \", core.json.delay_encode(hooks))\n    end\nend\n\nlocal function reload_hooks(premature, delay, file)\n    if premature or stop then\n        stop = false\n        running = false\n        return\n    end\n\n    local time, err = lfs.attributes(file, 'modification')\n    if err then\n        if last_modified ~= 0 then\n            core.log.info(err, \", disable all hooks\")\n            dbg.unset_all()\n            last_modified = 0\n        end\n    elseif time ~= last_modified then\n        setup_hooks(file)\n        last_modified = time\n    else\n        local ts = os.time()\n        if ts - last_report_time >= REPORT_INTERVAL then\n            local hooks = {}\n            for _, hook in ipairs(dbg.hooks()) do\n                table_insert(hooks, hook.key)\n            end\n            core.log.info(\"alive hooks: \", core.json.encode(hooks))\n            last_report_time = ts\n        end\n    end\n\n    local ok, err = ngx.timer.at(delay, reload_hooks, delay, file)\n    if not ok then\n        core.log.error(\"failed to create the timer: \", err)\n        running = false\n    end\nend\n\nfunction _M.init(delay, file)\n    if not running then\n        file = file or \"/usr/local/apisix/plugin_inspect_hooks.lua\"\n        delay = delay or 3\n\n        setup_hooks(file)\n\n        local ok, err = ngx.timer.at(delay, reload_hooks, delay, file)\n        if not ok then\n            core.log.error(\"failed to create the timer: \", err)\n            return\n        end\n        running = true\n    end\nend\n\nfunction _M.destroy()\n    stop = true\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/patch.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nrequire(\"resty.dns.resolver\") -- preload dns resolver to prevent recursive patch\nlocal ipmatcher = require(\"resty.ipmatcher\")\nlocal socket = require(\"socket\")\nlocal unix_socket = require(\"socket.unix\")\nlocal ssl = require(\"ssl\")\nlocal ngx = ngx\nlocal get_phase = ngx.get_phase\nlocal ngx_socket = ngx.socket\nlocal original_tcp = ngx.socket.tcp\nlocal original_udp = ngx.socket.udp\nlocal concat_tab = table.concat\nlocal debug = debug\nlocal new_tab = require(\"table.new\")\nlocal log = ngx.log\nlocal WARN = ngx.WARN\nlocal ipairs = ipairs\nlocal select = select\nlocal setmetatable = setmetatable\nlocal string = string\nlocal table = table\nlocal type = type\nlocal tonumber = tonumber\n\n\nlocal config_local\nlocal _M = {}\n\n\nlocal function get_local_conf()\n    if not config_local then\n        config_local = require(\"apisix.core.config_local\")\n    end\n\n    return config_local.local_conf()\nend\n\n\nlocal patch_tcp_socket\ndo\n    local old_tcp_sock_connect\n\n    local function new_tcp_sock_connect(sock, host, port, opts)\n        local core_str = require(\"apisix.core.string\")\n        local resolver = require(\"apisix.core.resolver\")\n\n        if host then\n            if core_str.has_prefix(host, \"unix:\") then\n                if not opts then\n                    -- workaround for https://github.com/openresty/lua-nginx-module/issues/860\n                    return old_tcp_sock_connect(sock, host)\n                end\n\n            elseif not ipmatcher.parse_ipv4(host) and not ipmatcher.parse_ipv6(host) then\n                local err\n                host, err = resolver.parse_domain(host)\n                if not host then\n                    return nil, \"failed to parse domain: \" .. err\n                end\n            end\n        end\n\n        return old_tcp_sock_connect(sock, host, port, opts)\n    end\n\n\n    function patch_tcp_socket(sock)\n        if not old_tcp_sock_connect then\n            old_tcp_sock_connect = sock.connect\n        end\n\n        sock.connect = new_tcp_sock_connect\n        return sock\n    end\nend\n\n\ndo -- `math.randomseed` patch\n    -- `math.random` generates PRND(pseudo-random numbers) from the seed set by `math.randomseed`\n    -- Many module libraries use `ngx.time` and `ngx.worker.pid` to generate seeds which may\n    -- loss randomness in container env (where pids are identical, e.g. root pid is 1)\n    -- Kubernetes may launch multi instance with deployment RS at the same time, `ngx.time` may\n    -- get same return in the pods.\n    -- Therefore, this global patch enforce entire framework to use\n    -- the best-practice PRND generates.\n\n    local resty_random = require(\"resty.random\")\n    local math_randomseed = math.randomseed\n    local seeded = {}\n\n    -- make linter happy\n    -- luacheck: ignore\n    math.randomseed = function()\n        local worker_pid = ngx.worker.pid()\n\n        -- check seed mark\n        if seeded[worker_pid] then\n            log(ngx.DEBUG, debug.traceback(\"Random seed has been inited\", 2))\n            return\n        end\n\n        -- generate randomseed\n        -- chose 6 from APISIX's SIX, 256 ^ 6 should do the trick\n        -- it shouldn't be large than 16 to prevent overflow.\n        local random_bytes = resty_random.bytes(6)\n        local t = {}\n\n        for i = 1, #random_bytes do\n            t[i] = string.byte(random_bytes, i)\n        end\n\n        local s = table.concat(t)\n\n        math_randomseed(tonumber(s))\n        seeded[worker_pid] = true\n    end\nend -- do\n\n\nlocal patch_udp_socket\ndo\n    local old_udp_sock_setpeername\n\n    local function new_udp_sock_setpeername(sock, host, port)\n        local core_str = require(\"apisix.core.string\")\n        local resolver = require(\"apisix.core.resolver\")\n\n        if host then\n            if core_str.has_prefix(host, \"unix:\") then\n                return old_udp_sock_setpeername(sock, host)\n            end\n\n            if not ipmatcher.parse_ipv4(host) and not ipmatcher.parse_ipv6(host) then\n                local err\n                host, err = resolver.parse_domain(host)\n                if not host then\n                    return nil, \"failed to parse domain: \" .. err\n                end\n            end\n        end\n\n        return old_udp_sock_setpeername(sock, host, port)\n    end\n\n\n    function patch_udp_socket(sock)\n        if not old_udp_sock_setpeername then\n            old_udp_sock_setpeername = sock.setpeername\n        end\n\n        sock.setpeername = new_udp_sock_setpeername\n        return sock\n    end\nend\n\n\nlocal function flatten(args)\n    local buf = new_tab(#args, 0)\n    for i, v in ipairs(args) do\n        local ty = type(v)\n        if ty == \"table\" then\n            buf[i] = flatten(v)\n        elseif ty == \"boolean\" then\n            buf[i] = v and \"true\" or \"false\"\n        elseif ty == \"nil\" then\n            buf[i] = \"nil\"\n        else\n            buf[i] = v\n        end\n    end\n    return concat_tab(buf)\nend\n\n\nlocal luasocket_wrapper = {\n    connect = function (self, host, port)\n        if not port then\n            -- unix socket\n            self.sock = unix_socket()\n            if self.timeout then\n                self.sock:settimeout(self.timeout)\n            end\n\n            local path = host:sub(#(\"unix:\") + 1)\n            return self.sock:connect(path)\n        end\n\n        if host:byte(1) == string.byte('[') then\n            -- ipv6, form as '[::1]', remove '[' and ']'\n            host = host:sub(2, -2)\n            self.sock = self.tcp6\n        else\n            self.sock = self.tcp4\n        end\n\n        return self.sock:connect(host, port)\n    end,\n\n    send = function(self, ...)\n        if select('#', ...) == 1 and type(select(1, ...)) == \"string\" then\n            -- fast path\n            return self.sock:send(...)\n        end\n\n        -- luasocket's send only accepts a single string\n        return self.sock:send(flatten({...}))\n    end,\n\n    getreusedtimes = function ()\n        return 0\n    end,\n    setkeepalive = function (self)\n        self.sock:close()\n        return 1\n    end,\n\n    settimeout = function (self, time)\n        if time then\n            time = time / 1000\n        end\n\n        self.timeout = time\n\n        return self.sock:settimeout(time)\n    end,\n    settimeouts = function (self, connect_time, read_time, write_time)\n        connect_time = connect_time or 0\n        read_time = read_time or 0\n        write_time = write_time or 0\n\n        -- set the max one as the timeout\n        local time = connect_time\n        if time < read_time then\n            time = read_time\n        end\n        if time < write_time then\n            time = write_time\n        end\n\n        if time > 0 then\n            time = time / 1000\n        else\n            time = nil\n        end\n\n        self.timeout = time\n\n        return self.sock:settimeout(time)\n    end,\n\n    tlshandshake = function (self, options)\n        local reused_session = options.reused_session\n        local server_name = options.server_name\n        local verify = options.verify\n        local send_status_req = options.ocsp_status_req\n\n        if reused_session then\n            log(WARN, \"reused_session is not supported yet\")\n        end\n\n        if send_status_req then\n            log(WARN, \"send_status_req is not supported yet\")\n        end\n\n        local params = {\n            mode = \"client\",\n            protocol = \"any\",\n            verify = verify and \"peer\" or \"none\",\n            certificate = options.client_cert_path,\n            key = options.client_priv_key_path,\n            options = {\n                \"all\",\n                \"no_sslv2\",\n                \"no_sslv3\",\n                \"no_tlsv1\"\n            }\n        }\n\n        local local_conf, err = get_local_conf()\n        if not local_conf then\n            return nil, err\n        end\n\n        local apisix_ssl = local_conf.apisix.ssl\n        if apisix_ssl and apisix_ssl.ssl_trusted_certificate then\n            params.cafile = apisix_ssl.ssl_trusted_certificate\n        end\n\n        local sec_sock, err = ssl.wrap(self.sock, params)\n        if not sec_sock then\n            return false, err\n        end\n\n        if server_name then\n            sec_sock:sni(server_name)\n        end\n\n        local success\n        success, err = sec_sock:dohandshake()\n        if not success then\n            return false, err\n        end\n\n        self.sock = sec_sock\n        return true\n    end,\n\n    sslhandshake = function (self, reused_session, server_name, verify, send_status_req)\n        return self:tlshandshake({\n            reused_session = reused_session,\n            server_name = server_name,\n            verify = verify,\n            ocsp_status_req = send_status_req,\n        })\n    end\n}\n\n\nlocal mt = {\n    __index = function(self, key)\n        local sock = self.sock\n        local fn = luasocket_wrapper[key]\n        if fn then\n            self[key] = fn\n            return fn\n        end\n\n        local origin = sock[key]\n        if type(origin) ~= \"function\" then\n            return origin\n        end\n\n        fn = function(_, ...)\n            return origin(sock, ...)\n        end\n\n        self[key] = fn\n        return fn\n    end\n}\n\nlocal function luasocket_tcp()\n    local sock = socket.tcp()\n    local tcp4 = socket.tcp4()\n    local tcp6 = socket.tcp6()\n    return setmetatable({sock = sock, tcp4 = tcp4, tcp6 = tcp6}, mt)\nend\n\n\nfunction _M.patch()\n    -- make linter happy\n    -- luacheck: ignore\n    ngx_socket.tcp = function ()\n        local phase = get_phase()\n        if phase ~= \"init\" and phase ~= \"init_worker\" then\n            return patch_tcp_socket(original_tcp())\n        end\n\n        return luasocket_tcp()\n    end\n\n    ngx_socket.udp = function ()\n        return patch_udp_socket(original_udp())\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require       = require\nlocal core          = require(\"apisix.core\")\nlocal config_util   = require(\"apisix.core.config_util\")\nlocal enable_debug  = require(\"apisix.debug\").enable_debug\nlocal wasm          = require(\"apisix.wasm\")\nlocal expr          = require(\"resty.expr.v1\")\nlocal apisix_ssl    = require(\"apisix.ssl\")\nlocal re_split      = require(\"ngx.re\").split\nlocal ngx           = ngx\nlocal ngx_ok        = ngx.OK\nlocal ngx_print     = ngx.print\nlocal ngx_flush     = ngx.flush\nlocal crc32         = ngx.crc32_short\nlocal ngx_exit      = ngx.exit\nlocal pkg_loaded    = package.loaded\nlocal sort_tab      = table.sort\nlocal pcall         = pcall\nlocal ipairs        = ipairs\nlocal pairs         = pairs\nlocal type          = type\nlocal local_plugins = core.table.new(32, 0)\nlocal tostring      = tostring\nlocal error         = error\nlocal getmetatable  = getmetatable\nlocal setmetatable  = setmetatable\nlocal tracer    = require(\"apisix.tracer\")\n-- make linter happy to avoid error: getting the Lua global \"load\"\n-- luacheck: globals load, ignore lua_load\nlocal lua_load          = load\nlocal is_http       = ngx.config.subsystem == \"http\"\nlocal local_plugins_hash    = core.table.new(0, 32)\nlocal stream_local_plugins  = core.table.new(32, 0)\nlocal stream_local_plugins_hash = core.table.new(0, 32)\n\n\nlocal merged_route = core.lrucache.new({\n    ttl = 300, count = 512\n})\nlocal merged_stream_route = core.lrucache.new({\n    ttl = 300, count = 512\n})\nlocal expr_lrucache = core.lrucache.new({\n    ttl = 300, count = 512\n})\nlocal meta_pre_func_load_lrucache = core.lrucache.new({\n    ttl = 300, count = 512\n})\nlocal merge_global_rule_lrucache = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\nlocal local_conf\nlocal check_plugin_metadata\n\nlocal _M = {\n    version         = 0.3,\n\n    load_times      = 0,\n    plugins         = local_plugins,\n    plugins_hash    = local_plugins_hash,\n\n    stream_load_times= 0,\n    stream_plugins  = stream_local_plugins,\n    stream_plugins_hash = stream_local_plugins_hash,\n}\n\n\nlocal function plugin_attr(name)\n    -- TODO: get attr from synchronized data\n    local local_conf = core.config.local_conf()\n    return core.table.try_read_attr(local_conf, \"plugin_attr\", name)\nend\n_M.plugin_attr = plugin_attr\n\n\nlocal function sort_plugin(l, r)\n    return l.priority > r.priority\nend\n\nlocal function custom_sort_plugin(l, r)\n    return l._meta.priority > r._meta.priority\nend\n\nlocal function check_disable(plugin_conf)\n    if not plugin_conf then\n        return nil\n    end\n\n    if not plugin_conf._meta then\n       return nil\n    end\n\n    if type(plugin_conf._meta) ~= \"table\" then\n        return nil\n    end\n\n    return plugin_conf._meta.disable\nend\n\nlocal PLUGIN_TYPE_HTTP = 1\nlocal PLUGIN_TYPE_STREAM = 2\nlocal PLUGIN_TYPE_HTTP_WASM = 3\nlocal function unload_plugin(name, plugin_type)\n    if plugin_type == PLUGIN_TYPE_HTTP_WASM then\n        return\n    end\n\n    -- Don't unload stream plugins in the HTTP subsystem.\n    if plugin_type == PLUGIN_TYPE_STREAM and is_http then\n        return\n    end\n\n    local pkg_name = \"apisix.plugins.\" .. name\n    if plugin_type == PLUGIN_TYPE_STREAM then\n        pkg_name = \"apisix.stream.plugins.\" .. name\n    end\n\n    local old_plugin = pkg_loaded[pkg_name]\n    if old_plugin and type(old_plugin.destroy) == \"function\" then\n        old_plugin.destroy()\n    end\n\n    pkg_loaded[pkg_name] = nil\nend\n\n\nlocal function load_plugin(name, plugins_list, plugin_type)\n    local ok, plugin\n    if plugin_type == PLUGIN_TYPE_HTTP_WASM  then\n        -- for wasm plugin, we pass the whole attrs instead of name\n        ok, plugin = wasm.require(name)\n        name = name.name\n    else\n        local pkg_name = \"apisix.plugins.\" .. name\n        if plugin_type == PLUGIN_TYPE_STREAM then\n            pkg_name = \"apisix.stream.plugins.\" .. name\n        end\n\n        ok, plugin = pcall(require, pkg_name)\n    end\n\n    if not ok then\n        core.log.error(\"failed to load plugin [\", name, \"] err: \", plugin)\n        return\n    end\n\n    if not plugin.priority then\n        core.log.error(\"invalid plugin [\", name,\n                        \"], missing field: priority\")\n        return\n    end\n\n    if not plugin.version then\n        core.log.error(\"invalid plugin [\", name, \"] missing field: version\")\n        return\n    end\n\n    if type(plugin.schema) ~= \"table\" then\n        core.log.error(\"invalid plugin [\", name, \"] schema field\")\n        return\n    end\n\n    if not plugin.schema.properties then\n        plugin.schema.properties = {}\n    end\n\n    local properties = plugin.schema.properties\n    local plugin_injected_schema = core.schema.plugin_injected_schema\n\n    if plugin.schema['$comment'] ~= plugin_injected_schema['$comment'] then\n        if properties._meta then\n            core.log.error(\"invalid plugin [\", name,\n                           \"]: found forbidden '_meta' field in the schema\")\n            return\n        end\n\n        properties._meta = plugin_injected_schema._meta\n        -- new injected fields should be added under `_meta`\n        -- 1. so we won't break user's code when adding any new injected fields\n        -- 2. the semantics is clear, especially in the doc and in the caller side\n\n        plugin.schema['$comment'] = plugin_injected_schema['$comment']\n    end\n\n    plugin.name = name\n    plugin.attr = plugin_attr(name)\n    core.table.insert(plugins_list, plugin)\n\n    -- Don't initialize stream plugins in the HTTP subsystem.\n    -- The modules are loaded for schema validation (admin API),\n    -- but init/workflow_handler functions must only run in the stream subsystem.\n    if plugin_type == PLUGIN_TYPE_STREAM and is_http then\n        return\n    end\n\n    if plugin.init then\n        plugin.init()\n    end\n\n    if plugin.workflow_handler then\n        plugin.workflow_handler()\n    end\n\n    return\nend\n\n\nlocal function load(plugin_names, wasm_plugin_names)\n    local processed = {}\n    for _, name in ipairs(plugin_names) do\n        if processed[name] == nil then\n            processed[name] = true\n        end\n    end\n    for _, attrs in ipairs(wasm_plugin_names) do\n        if processed[attrs.name] == nil then\n            processed[attrs.name] = attrs\n        end\n    end\n\n    core.log.warn(\"new plugins: \", core.json.delay_encode(processed))\n\n    for name, plugin in pairs(local_plugins_hash) do\n        local ty = PLUGIN_TYPE_HTTP\n        if plugin.type == \"wasm\" then\n            ty = PLUGIN_TYPE_HTTP_WASM\n        end\n        unload_plugin(name, ty)\n    end\n\n    core.table.clear(local_plugins)\n    core.table.clear(local_plugins_hash)\n\n    for name, value in pairs(processed) do\n        local ty = PLUGIN_TYPE_HTTP\n        if type(value) == \"table\" then\n            ty = PLUGIN_TYPE_HTTP_WASM\n            name = value\n        end\n        load_plugin(name, local_plugins, ty)\n    end\n\n    -- sort by plugin's priority\n    if #local_plugins > 1 then\n        sort_tab(local_plugins, sort_plugin)\n    end\n\n    for i, plugin in ipairs(local_plugins) do\n        local_plugins_hash[plugin.name] = plugin\n        if enable_debug() then\n            core.log.warn(\"loaded plugin and sort by priority:\",\n                          \" \", plugin.priority,\n                          \" name: \", plugin.name)\n        end\n    end\n\n    _M.load_times = _M.load_times + 1\n    core.log.info(\"load plugin times: \", _M.load_times)\n    return true\nend\n\n\nlocal function load_stream(plugin_names)\n    local processed = {}\n    for _, name in ipairs(plugin_names) do\n        if processed[name] == nil then\n            processed[name] = true\n        end\n    end\n\n    core.log.warn(\"new plugins: \", core.json.delay_encode(processed))\n\n    for name in pairs(stream_local_plugins_hash) do\n        unload_plugin(name, PLUGIN_TYPE_STREAM)\n    end\n\n    core.table.clear(stream_local_plugins)\n    core.table.clear(stream_local_plugins_hash)\n\n    for name in pairs(processed) do\n        load_plugin(name, stream_local_plugins, PLUGIN_TYPE_STREAM)\n    end\n\n    -- sort by plugin's priority\n    if #stream_local_plugins > 1 then\n        sort_tab(stream_local_plugins, sort_plugin)\n    end\n\n    for i, plugin in ipairs(stream_local_plugins) do\n        stream_local_plugins_hash[plugin.name] = plugin\n        if enable_debug() then\n            core.log.warn(\"loaded stream plugin and sort by priority:\",\n                          \" \", plugin.priority,\n                          \" name: \", plugin.name)\n        end\n    end\n\n    _M.stream_load_times = _M.stream_load_times + 1\n    core.log.info(\"stream plugins: \",\n                  core.json.delay_encode(stream_local_plugins, true))\n    core.log.info(\"load stream plugin times: \", _M.stream_load_times)\n    return true\nend\n\n\nlocal function get_plugin_names(config)\n    local http_plugin_names\n    local stream_plugin_names\n\n    if not config then\n        -- called during starting or hot reload in admin\n        local err\n        local_conf, err = core.config.local_conf(true)\n        if not local_conf then\n            -- the error is unrecoverable, so we need to raise it\n            error(\"failed to load the configuration file: \" .. err)\n        end\n\n        http_plugin_names = local_conf.plugins\n        stream_plugin_names = local_conf.stream_plugins\n    else\n        -- called during synchronizing plugin data\n        http_plugin_names = {}\n        stream_plugin_names = {}\n        local plugins_conf = config.value\n        -- plugins_conf can be nil when another instance writes into etcd key \"/apisix/plugins/\"\n        if not plugins_conf then\n            return true\n        end\n\n        for _, conf in ipairs(plugins_conf) do\n            if conf.stream then\n                core.table.insert(stream_plugin_names, conf.name)\n            else\n                core.table.insert(http_plugin_names, conf.name)\n            end\n        end\n    end\n\n    return false, http_plugin_names, stream_plugin_names\nend\n\n\nfunction _M.load(config)\n    local ignored, http_plugin_names, stream_plugin_names = get_plugin_names(config)\n    if ignored then\n        return local_plugins\n    end\n\n    if ngx.config.subsystem == \"http\" then\n        if not http_plugin_names then\n            core.log.error(\"failed to read plugin list from local file\")\n        else\n            local wasm_plugin_names = {}\n            if local_conf.wasm then\n                wasm_plugin_names = local_conf.wasm.plugins\n            end\n\n            local ok, err = load(http_plugin_names, wasm_plugin_names)\n            if not ok then\n                core.log.error(\"failed to load plugins: \", err)\n            end\n        end\n    end\n\n    if not stream_plugin_names then\n        core.log.warn(\"failed to read stream plugin list from local file\")\n    else\n        local ok, err = load_stream(stream_plugin_names)\n        if not ok then\n            core.log.error(\"failed to load stream plugins: \", err)\n        end\n    end\n\n    -- for test\n    return local_plugins\nend\n\n\nfunction _M.exit_worker()\n    for name, plugin in pairs(local_plugins_hash) do\n        local ty = PLUGIN_TYPE_HTTP\n        if plugin.type == \"wasm\" then\n            ty = PLUGIN_TYPE_HTTP_WASM\n        end\n        unload_plugin(name, ty)\n    end\n\n    -- we need to load stream plugin so that we can check their schemas in\n    -- Admin API. Maybe we can avoid calling `load` in this case? So that\n    -- we don't need to call `destroy` too\n    for name in pairs(stream_local_plugins_hash) do\n        unload_plugin(name, PLUGIN_TYPE_STREAM)\n    end\nend\n\n\nlocal function trace_plugins_info_for_debug(ctx, plugins)\n    if not enable_debug() then\n        return\n    end\n\n    if not plugins then\n        if is_http and not ngx.headers_sent then\n            core.response.add_header(\"Apisix-Plugins\", \"no plugin\")\n        else\n            core.log.warn(\"Apisix-Plugins: no plugin\")\n        end\n\n        return\n    end\n\n    local t = {}\n    for i = 1, #plugins, 2 do\n        core.table.insert(t, plugins[i].name)\n    end\n    if is_http and not ngx.headers_sent then\n        if ctx then\n            local debug_headers = ctx.debug_headers\n            if not debug_headers then\n                debug_headers = core.table.new(0, 5)\n            end\n            for i, v in ipairs(t) do\n                debug_headers[v] = true\n            end\n            ctx.debug_headers = debug_headers\n        end\n    else\n        core.log.warn(\"Apisix-Plugins: \", core.table.concat(t, \", \"))\n    end\nend\n\n\nlocal function meta_filter(ctx, plugin_name, plugin_conf)\n    local filter = plugin_conf._meta and plugin_conf._meta.filter\n    if not filter then\n        return true\n    end\n\n    local match_cache_key =\n        ctx.conf_type .. \"#\" .. ctx.conf_id .. \"#\"\n            .. ctx.conf_version .. \"#\" .. plugin_name .. \"#meta_filter_matched\"\n    if ctx[match_cache_key] ~= nil then\n        return ctx[match_cache_key]\n    end\n\n    local ex, ok, err\n    if ctx then\n        ex, err = expr_lrucache(plugin_name .. ctx.conf_type .. ctx.conf_id,\n                                 ctx.conf_version, expr.new, filter)\n    else\n        ex, err = expr.new(filter)\n    end\n    if not ex then\n        core.log.warn(\"failed to get the 'vars' expression: \", err ,\n                         \" plugin_name: \", plugin_name)\n        return true\n    end\n    ok, err = ex:eval(ctx.var)\n    if err then\n        core.log.warn(\"failed to run the 'vars' expression: \", err,\n                         \" plugin_name: \", plugin_name)\n        return true\n    end\n\n    ctx[match_cache_key] = ok\n    return ok\nend\n\n\nfunction _M.filter(ctx, conf, plugins, route_conf, phase)\n    local user_plugin_conf = conf.value.plugins\n    if user_plugin_conf == nil or\n       core.table.nkeys(user_plugin_conf) == 0 then\n        trace_plugins_info_for_debug(nil, nil)\n        -- when 'plugins' is given, always return 'plugins' itself instead\n        -- of another one\n        return plugins or core.tablepool.fetch(\"plugins\", 0, 0)\n    end\n\n    local custom_sort = false\n    local route_plugin_conf = route_conf and route_conf.value.plugins\n    plugins = plugins or core.tablepool.fetch(\"plugins\", 32, 0)\n    for _, plugin_obj in ipairs(local_plugins) do\n        local name = plugin_obj.name\n        local plugin_conf = user_plugin_conf[name]\n\n        if type(plugin_conf) ~= \"table\" then\n            goto continue\n        end\n\n        if check_disable(plugin_conf) then\n            goto continue\n        end\n\n        if plugin_obj.run_policy == \"prefer_route\" and route_plugin_conf ~= nil then\n            local plugin_conf_in_route = route_plugin_conf[name]\n            local disable_in_route = check_disable(plugin_conf_in_route)\n            if plugin_conf_in_route and not disable_in_route then\n                goto continue\n            end\n        end\n\n        -- in the rewrite phase, the plugin executes in the following order:\n        -- 1. execute the rewrite phase of the plugins on route(including the auth plugins)\n        -- 2. merge plugins from consumer and route\n        -- 3. execute the rewrite phase of the plugins on consumer(phase: rewrite_in_consumer)\n        -- in this case, we need to skip the plugins that was already executed(step 1)\n        if phase == \"rewrite_in_consumer\"\n                and (not plugin_conf._from_consumer or plugin_obj.type == \"auth\") then\n            plugin_conf._skip_rewrite_in_consumer = true\n        end\n\n        if plugin_conf._meta and plugin_conf._meta.priority then\n            custom_sort = true\n        end\n\n        core.table.insert(plugins, plugin_obj)\n        core.table.insert(plugins, plugin_conf)\n\n        ::continue::\n    end\n\n    trace_plugins_info_for_debug(ctx, plugins)\n\n    if custom_sort then\n        local tmp_plugin_objs = core.tablepool.fetch(\"tmp_plugin_objs\", 0, #plugins / 2)\n        local tmp_plugin_confs = core.tablepool.fetch(\"tmp_plugin_confs\", #plugins / 2, 0)\n\n        for i = 1, #plugins, 2 do\n            local plugin_obj = plugins[i]\n            local plugin_conf = plugins[i + 1]\n\n            tmp_plugin_objs[plugin_conf] = plugin_obj\n            core.table.insert(tmp_plugin_confs, plugin_conf)\n\n            if not plugin_conf._meta then\n                plugin_conf._meta = core.table.new(0, 1)\n                plugin_conf._meta.priority = plugin_obj.priority\n            else\n                if not plugin_conf._meta.priority then\n                    plugin_conf._meta.priority = plugin_obj.priority\n                end\n            end\n        end\n\n        sort_tab(tmp_plugin_confs, custom_sort_plugin)\n\n        local index\n        for i = 1, #tmp_plugin_confs do\n            index = i * 2 - 1\n            local plugin_conf = tmp_plugin_confs[i]\n            local plugin_obj = tmp_plugin_objs[plugin_conf]\n            plugins[index] = plugin_obj\n            plugins[index + 1] = plugin_conf\n        end\n\n        core.tablepool.release(\"tmp_plugin_objs\", tmp_plugin_objs)\n        core.tablepool.release(\"tmp_plugin_confs\", tmp_plugin_confs)\n    end\n\n    return plugins\nend\n\n\nfunction _M.stream_filter(user_route, plugins)\n    plugins = plugins or core.table.new(#stream_local_plugins * 2, 0)\n    local user_plugin_conf = user_route.value.plugins\n    if user_plugin_conf == nil then\n        trace_plugins_info_for_debug(nil, nil)\n        return plugins\n    end\n\n    for _, plugin_obj in ipairs(stream_local_plugins) do\n        local name = plugin_obj.name\n        local plugin_conf = user_plugin_conf[name]\n\n        local disable = check_disable(plugin_conf)\n        if type(plugin_conf) == \"table\" and not disable then\n            core.table.insert(plugins, plugin_obj)\n            core.table.insert(plugins, plugin_conf)\n        end\n    end\n\n    trace_plugins_info_for_debug(nil, plugins)\n\n    return plugins\nend\n\n\nlocal function merge_service_route(service_conf, route_conf)\n    local new_conf = core.table.deepcopy(service_conf)\n    new_conf.value.service_id = new_conf.value.id\n    new_conf.value.id = route_conf.value.id\n    new_conf.modifiedIndex = route_conf.modifiedIndex\n\n    if route_conf.value.plugins then\n        for name, conf in pairs(route_conf.value.plugins) do\n            if not new_conf.value.plugins then\n                new_conf.value.plugins = {}\n            end\n\n            new_conf.value.plugins[name] = conf\n        end\n    end\n\n    local route_upstream = route_conf.value.upstream\n    if route_upstream then\n        new_conf.value.upstream = route_upstream\n        new_conf.value.upstream_id = nil\n        new_conf.has_domain = route_conf.has_domain\n    end\n\n    if route_conf.value.upstream_id then\n        new_conf.value.upstream_id = route_conf.value.upstream_id\n        new_conf.has_domain = route_conf.has_domain\n    end\n\n    if route_conf.value.script then\n        new_conf.value.script = route_conf.value.script\n    end\n\n    if route_conf.value.timeout then\n        new_conf.value.timeout = route_conf.value.timeout\n    end\n\n    if route_conf.value.name then\n        new_conf.value.name = route_conf.value.name\n    else\n        new_conf.value.name = nil\n    end\n\n    if route_conf.value.hosts then\n        new_conf.value.hosts = route_conf.value.hosts\n    end\n    if not new_conf.value.hosts and route_conf.value.host then\n        new_conf.value.host = route_conf.value.host\n    end\n\n    if route_conf.value.labels then\n        new_conf.value.labels = route_conf.value.labels\n    end\n\n    -- core.log.info(\"merged conf : \", core.json.delay_encode(new_conf))\n    return new_conf\nend\n\n\nfunction _M.merge_service_route(service_conf, route_conf)\n    core.log.info(\"service conf: \", core.json.delay_encode(service_conf, true))\n    core.log.info(\"  route conf: \", core.json.delay_encode(route_conf, true))\n\n    local route_service_key = route_conf.value.id .. \"#\"\n        .. route_conf.modifiedIndex .. \"#\" .. service_conf.modifiedIndex\n    return merged_route(route_service_key, service_conf,\n                        merge_service_route,\n                        service_conf, route_conf)\nend\n\n\nlocal function merge_service_stream_route(service_conf, route_conf)\n    -- because many fields in Service are not supported by stream route,\n    -- so we copy the stream route as base object\n    local new_conf = core.table.deepcopy(route_conf)\n    if service_conf.value.plugins then\n        for name, conf in pairs(service_conf.value.plugins) do\n            if not new_conf.value.plugins then\n                new_conf.value.plugins = {}\n            end\n\n            if not new_conf.value.plugins[name] then\n                new_conf.value.plugins[name] = conf\n            end\n        end\n    end\n\n    new_conf.value.service_id = nil\n\n    if not new_conf.value.upstream and service_conf.value.upstream then\n        new_conf.value.upstream = service_conf.value.upstream\n    end\n\n    if not new_conf.value.upstream_id and service_conf.value.upstream_id then\n        new_conf.value.upstream_id = service_conf.value.upstream_id\n    end\n\n    return new_conf\nend\n\n\nfunction _M.merge_service_stream_route(service_conf, route_conf)\n    core.log.info(\"service conf: \", core.json.delay_encode(service_conf, true))\n    core.log.info(\"  stream route conf: \", core.json.delay_encode(route_conf, true))\n\n    local version = route_conf.modifiedIndex .. \"#\" .. service_conf.modifiedIndex\n    local route_service_key = route_conf.value.id .. \"#\"\n            .. version\n    return merged_stream_route(route_service_key, version,\n            merge_service_stream_route,\n            service_conf, route_conf)\nend\n\n\nlocal function merge_consumer_route(route_conf, consumer_conf, consumer_group_conf)\n    local has_consumer_plugins = consumer_conf.plugins and\n                                    core.table.nkeys(consumer_conf.plugins) > 0\n    local has_group_plugins = consumer_group_conf and\n                                consumer_group_conf.value and\n                                consumer_group_conf.value.plugins and\n                                core.table.nkeys(consumer_group_conf.value.plugins) > 0\n\n    if not has_consumer_plugins and not has_group_plugins then\n        core.log.info(\"consumer and consumer group have no plugins\")\n        return route_conf\n    end\n\n    local new_route_conf = core.table.deepcopy(route_conf)\n\n    if has_group_plugins then\n        for name, conf in pairs(consumer_group_conf.value.plugins) do\n            if not new_route_conf.value.plugins then\n                new_route_conf.value.plugins = {}\n            end\n\n            if new_route_conf.value.plugins[name] == nil then\n                conf._from_consumer = true\n            end\n            new_route_conf.value.plugins[name] = conf\n        end\n    end\n\n\n    if has_consumer_plugins then\n        for name, conf in pairs(consumer_conf.plugins) do\n            if not new_route_conf.value.plugins then\n                new_route_conf.value.plugins = {}\n            end\n\n            if new_route_conf.value.plugins[name] == nil then\n                conf._from_consumer = true\n            end\n            new_route_conf.value.plugins[name] = conf\n        end\n    end\n\n    return new_route_conf\nend\n\n\nfunction _M.merge_consumer_route(route_conf, consumer_conf, consumer_group_conf, api_ctx)\n    core.log.info(\"route conf: \", core.json.delay_encode(route_conf))\n    core.log.info(\"consumer group conf: \", core.json.delay_encode(consumer_group_conf))\n\n    local flag = route_conf.value.id .. \"#\" .. route_conf.modifiedIndex\n                 .. \"#\" .. consumer_conf.id .. \"#\" .. consumer_conf.modifiedIndex\n\n    if consumer_group_conf then\n        flag = flag .. \"#\" .. consumer_group_conf.value.id\n            .. \"#\" .. consumer_group_conf.modifiedIndex\n    end\n\n    local new_conf = merged_route(flag, api_ctx.conf_version,\n                        merge_consumer_route, route_conf, consumer_conf, consumer_group_conf)\n\n    api_ctx.conf_type = api_ctx.conf_type .. \"&consumer\"\n    api_ctx.conf_version = api_ctx.conf_version .. \"&\" ..\n                           api_ctx.consumer_ver\n    api_ctx.conf_id = api_ctx.conf_id .. \"&\" .. api_ctx.consumer_name\n\n    if consumer_group_conf then\n        api_ctx.conf_type = api_ctx.conf_type .. \"&consumer_group\"\n        api_ctx.conf_version = api_ctx.conf_version .. \"&\" .. consumer_group_conf.modifiedIndex\n        api_ctx.conf_id = api_ctx.conf_id .. \"&\" .. consumer_group_conf.value.id\n    end\n\n    return new_conf, new_conf ~= route_conf\nend\n\n\nlocal init_plugins_syncer\ndo\n    local plugins_conf\n\n    function init_plugins_syncer()\n        local err\n        plugins_conf, err = core.config.new(\"/plugins\", {\n            automatic = true,\n            item_schema = core.schema.plugins,\n            single_item = true,\n            filter = function(item)\n                -- we need to pass 'item' instead of plugins_conf because\n                -- the latter one is nil at the first run\n                _M.load(item)\n            end,\n        })\n        if not plugins_conf then\n            error(\"failed to create etcd instance for fetching /plugins : \" .. err)\n        end\n    end\nend\n\n\nfunction _M.init_prometheus()\n    local _, http_plugin_names, stream_plugin_names = get_plugin_names()\n    local enabled_in_http = core.table.array_find(http_plugin_names, \"prometheus\")\n    local enabled_in_stream = core.table.array_find(stream_plugin_names, \"prometheus\")\n\n    -- For stream-only mode, there are separate calls in ngx_tpl.lua.\n    -- And for other modes, whether in stream or http plugins,\n    -- the prometheus exporter needs to be initialized.\n    if is_http and (enabled_in_http or enabled_in_stream) then\n        require(\"apisix.plugins.prometheus.exporter\").init_exporter_timer()\n    end\nend\n\n\nfunction _M.init_worker()\n    -- someone's plugin needs to be initialized after prometheus\n    -- see https://github.com/apache/apisix/issues/3286\n    _M.load()\n\n    if local_conf and not local_conf.apisix.enable_admin then\n        init_plugins_syncer()\n    end\n\n    local plugin_metadatas, err = core.config.new(\"/plugin_metadata\",\n        {\n            automatic = true,\n            checker = check_plugin_metadata\n        }\n    )\n    if not plugin_metadatas then\n        error(\"failed to create etcd instance for fetching /plugin_metadatas : \"\n              .. err)\n    end\n\n    _M.plugin_metadatas = plugin_metadatas\nend\n\n\nfunction _M.plugin_metadata(name)\n    return _M.plugin_metadatas:get(name)\nend\n\n\nfunction _M.get(name)\n    return local_plugins_hash and local_plugins_hash[name]\nend\n\n\nfunction _M.get_stream(name)\n    return stream_local_plugins_hash and stream_local_plugins_hash[name]\nend\n\n\nfunction _M.get_all(attrs)\n    local http_plugins = {}\n    local stream_plugins = {}\n\n    if local_plugins_hash then\n        for name, plugin_obj in pairs(local_plugins_hash) do\n            http_plugins[name] = core.table.pick(plugin_obj, attrs)\n        end\n    end\n\n    if stream_local_plugins_hash then\n        for name, plugin_obj in pairs(stream_local_plugins_hash) do\n            stream_plugins[name] = core.table.pick(plugin_obj, attrs)\n        end\n    end\n\n    return http_plugins, stream_plugins\nend\n\n\n-- conf_version returns a version which only depends on the value of conf,\n-- instead of where this plugin conf belongs to\nfunction _M.conf_version(conf)\n    if not conf._version then\n        local data = core.json.stably_encode(conf)\n        conf._version = tostring(crc32(data))\n        core.log.info(\"init plugin-level conf version: \", conf._version, \", from \", data)\n    end\n\n    return conf._version\nend\n\n\nlocal function check_single_plugin_schema(name, plugin_conf, schema_type, skip_disabled_plugin)\n    if type(plugin_conf) ~= \"table\" then\n        return false, \"invalid plugin conf \" ..\n            core.json.encode(plugin_conf, true) ..\n            \" for plugin [\" .. name .. \"]\"\n    end\n\n    local plugin_obj = local_plugins_hash[name]\n    if not plugin_obj then\n        if skip_disabled_plugin then\n            core.log.warn(\"skipping check schema for disabled or unknown plugin [\",\n                                    name, \"]. Enable the plugin or modify configuration\")\n            return true\n        else\n            return false, \"unknown plugin [\" .. name .. \"]\"\n        end\n    end\n\n    if plugin_obj.check_schema then\n        local ok, err = plugin_obj.check_schema(plugin_conf, schema_type)\n        if not ok then\n            return false, \"failed to check the configuration of plugin \"\n                .. name .. \" err: \" .. err\n        end\n\n        if plugin_conf._meta then\n            if plugin_conf._meta.filter then\n                ok, err = expr.new(plugin_conf._meta.filter)\n                if not ok then\n                    return nil, \"failed to validate the 'vars' expression: \" .. err\n                end\n            end\n\n            if plugin_conf._meta.pre_function then\n                local pre_function, err = meta_pre_func_load_lrucache(plugin_conf._meta.pre_function\n                                          , \"\",\n                                          lua_load,\n                                          plugin_conf._meta.pre_function, \"meta pre_function\")\n                if not pre_function then\n                    return nil, \"failed to load _meta.pre_function in plugin \" .. name .. \": \"\n                                 .. err\n                end\n            end\n        end\n    end\n\n    return true\nend\n\n\nlocal enable_data_encryption\nlocal function enable_gde()\n    if enable_data_encryption == nil then\n        enable_data_encryption =\n            core.table.try_read_attr(local_conf, \"apisix\", \"data_encryption\",\n                    \"enable_encrypt_fields\") and (core.config.type == \"etcd\")\n        _M.enable_data_encryption = enable_data_encryption\n    end\n\n    return enable_data_encryption\nend\n_M.enable_gde = enable_gde\n\n\nlocal function get_plugin_schema_for_gde(name, schema_type)\n    local plugin_schema = local_plugins_hash and local_plugins_hash[name]\n    if not plugin_schema then\n        return nil\n    end\n\n    local schema\n    if schema_type == core.schema.TYPE_CONSUMER then\n        -- when we use a non-auth plugin in the consumer,\n        -- where the consumer_schema field does not exist,\n        -- we need to fallback to it's schema for encryption and decryption.\n        schema = plugin_schema.consumer_schema or plugin_schema.schema\n    elseif schema_type == core.schema.TYPE_METADATA then\n        schema = plugin_schema.metadata_schema\n    else\n        schema = plugin_schema.schema\n    end\n\n    return schema\nend\n\n\nlocal function decrypt_conf(name, conf, schema_type)\n    if not enable_gde() then\n        return\n    end\n    local schema = get_plugin_schema_for_gde(name, schema_type)\n    if not schema then\n        core.log.warn(\"failed to get schema for plugin: \", name)\n        return\n    end\n\n    if schema.encrypt_fields and not core.table.isempty(schema.encrypt_fields) then\n        for _, key in ipairs(schema.encrypt_fields) do\n            if conf[key] then\n                local decrypted, err = apisix_ssl.aes_decrypt_pkey(conf[key], \"data_encrypt\")\n                if not decrypted then\n                    core.log.warn(\"failed to decrypt the conf of plugin [\", name,\n                                  \"] key [\", key, \"], err: \", err)\n                else\n                    conf[key] = decrypted\n                end\n            elseif core.string.find(key, \".\") then\n                -- decrypt fields has indents\n                local res, err = re_split(key, \"\\\\.\", \"jo\")\n                if not res then\n                    core.log.warn(\"failed to split key [\", key, \"], err: \", err)\n                    return\n                end\n\n                -- we only support two levels\n                if conf[res[1]] and conf[res[1]][res[2]] then\n                    local decrypted, err = apisix_ssl.aes_decrypt_pkey(\n                                           conf[res[1]][res[2]], \"data_encrypt\")\n                    if not decrypted then\n                        core.log.warn(\"failed to decrypt the conf of plugin [\", name,\n                                      \"] key [\", key, \"], err: \", err)\n                    else\n                        conf[res[1]][res[2]] = decrypted\n                    end\n                end\n            end\n        end\n    end\nend\n_M.decrypt_conf = decrypt_conf\n\n\nlocal function encrypt_conf(name, conf, schema_type)\n    if not enable_gde() then\n        return\n    end\n    local schema = get_plugin_schema_for_gde(name, schema_type)\n    if not schema then\n        core.log.warn(\"failed to get schema for plugin: \", name)\n        return\n    end\n\n    if schema.encrypt_fields and not core.table.isempty(schema.encrypt_fields) then\n        for _, key in ipairs(schema.encrypt_fields) do\n            if conf[key] then\n                local encrypted, err = apisix_ssl.aes_encrypt_pkey(conf[key], \"data_encrypt\")\n                if not encrypted then\n                    core.log.warn(\"failed to encrypt the conf of plugin [\", name,\n                                  \"] key [\", key, \"], err: \", err)\n                else\n                    conf[key] = encrypted\n                end\n            elseif core.string.find(key, \".\") then\n                -- encrypt fields has indents\n                local res, err = re_split(key, \"\\\\.\", \"jo\")\n                if not res then\n                    core.log.warn(\"failed to split key [\", key, \"], err: \", err)\n                    return\n                end\n\n                -- we only support two levels\n                if conf[res[1]] and conf[res[1]][res[2]] then\n                    local encrypted, err = apisix_ssl.aes_encrypt_pkey(\n                                           conf[res[1]][res[2]], \"data_encrypt\")\n                    if not encrypted then\n                        core.log.warn(\"failed to encrypt the conf of plugin [\", name,\n                                      \"] key [\", key, \"], err: \", err)\n                    else\n                        conf[res[1]][res[2]] = encrypted\n                    end\n                end\n            end\n        end\n    end\nend\n_M.encrypt_conf = encrypt_conf\n\n\ncheck_plugin_metadata = function(item)\n    local ok, err = check_single_plugin_schema(item.id, item,\n                                               core.schema.TYPE_METADATA, true)\n    if ok and enable_gde() then\n        decrypt_conf(item.id, item, core.schema.TYPE_METADATA)\n    end\n\n    return ok, err\nend\n\n\nlocal function check_schema(plugins_conf, schema_type, skip_disabled_plugin)\n    for name, plugin_conf in pairs(plugins_conf) do\n        local ok, err = check_single_plugin_schema(name, plugin_conf,\n            schema_type, skip_disabled_plugin)\n        if not ok then\n            return false, err\n        end\n    end\n\n    return true\nend\n_M.check_schema = check_schema\n\n\nlocal function stream_check_schema(plugins_conf, schema_type, skip_disabled_plugin)\n    for name, plugin_conf in pairs(plugins_conf) do\n        core.log.info(\"check stream plugin schema, name: \", name,\n                      \": \", core.json.delay_encode(plugin_conf, true))\n        if type(plugin_conf) ~= \"table\" then\n            return false, \"invalid plugin conf \" ..\n                core.json.encode(plugin_conf, true) ..\n                \" for plugin [\" .. name .. \"]\"\n        end\n\n        local plugin_obj = stream_local_plugins_hash[name]\n        if not plugin_obj then\n            if skip_disabled_plugin then\n                goto CONTINUE\n            else\n                return false, \"unknown plugin [\" .. name .. \"]\"\n            end\n        end\n\n        if plugin_obj.check_schema then\n            local ok, err = plugin_obj.check_schema(plugin_conf, schema_type)\n            if not ok then\n                return false, \"failed to check the configuration of \"\n                              .. \"stream plugin [\" .. name .. \"]: \" .. err\n            end\n        end\n\n        ::CONTINUE::\n    end\n\n    return true\nend\n_M.stream_check_schema = stream_check_schema\n\n\nfunction _M.plugin_checker(item, schema_type)\n    if item.plugins then\n        local skip_disabled_plugins = not (core.config.type == \"yaml\" or core.config.type == \"json\")\n        local ok, err = check_schema(item.plugins, schema_type, skip_disabled_plugins)\n\n        if ok and enable_gde() then\n            -- decrypt conf\n            for name, conf in pairs(item.plugins) do\n                decrypt_conf(name, conf, schema_type)\n            end\n        end\n        return ok, err\n    end\n\n    return true\nend\n\n\nfunction _M.stream_plugin_checker(item, in_cp)\n    if item.plugins then\n        local skip_disabled_plugins = not in_cp\n        if core.config.type == \"yaml\" or core.config.type == \"json\" then\n            skip_disabled_plugins = false\n        end\n        return stream_check_schema(item.plugins, nil, skip_disabled_plugins)\n    end\n\n    return true\nend\n\nlocal function run_meta_pre_function(conf, api_ctx, name)\n    if conf._meta and conf._meta.pre_function then\n        local _, pre_function = pcall(meta_pre_func_load_lrucache(conf._meta.pre_function, \"\",\n                                lua_load,\n                                conf._meta.pre_function, \"meta pre_function\"))\n        local ok, err = pcall(pre_function, conf, api_ctx)\n        if not ok then\n            core.log.error(\"pre_function execution for plugin \", name, \" failed: \", err)\n        end\n    end\nend\n\nfunction _M.run_plugin(phase, plugins, api_ctx)\n    local plugin_run = false\n    api_ctx = api_ctx or ngx.ctx.api_ctx\n    if not api_ctx then\n        return\n    end\n\n    plugins = plugins or api_ctx.plugins\n    if not plugins or #plugins == 0 then\n        return api_ctx\n    end\n\n    if phase ~= \"log\"\n        and phase ~= \"header_filter\"\n        and phase ~= \"body_filter\"\n        and phase ~= \"delayed_body_filter\"\n    then\n        for i = 1, #plugins, 2 do\n\n            if phase == \"rewrite_in_consumer\" and plugins[i + 1]._skip_rewrite_in_consumer then\n                goto CONTINUE\n            end\n\n            local phase_func = phase == \"rewrite_in_consumer\" and plugins[i][\"rewrite\"]\n                               or plugins[i][phase]\n            if phase_func then\n                local conf = plugins[i + 1]\n                if not meta_filter(api_ctx, plugins[i][\"name\"], conf)then\n                    goto CONTINUE\n                end\n\n                run_meta_pre_function(conf, api_ctx, plugins[i][\"name\"])\n                plugin_run = true\n                api_ctx._plugin_name = plugins[i][\"name\"]\n                local code, body = phase_func(conf, api_ctx)\n                api_ctx._plugin_name = nil\n                if code or body then\n                    if is_http then\n                        if code >= 400 then\n                            core.log.warn(plugins[i].name, \" exits with http status code \", code)\n\n                            if conf._meta and conf._meta.error_response then\n                                -- Whether or not the original error message is output,\n                                -- always return the configured message\n                                -- so the caller can't guess the real error\n                                body = conf._meta.error_response\n                            end\n                        end\n\n                        core.response.exit(code, body)\n                    else\n                        if code >= 400 then\n                            core.log.warn(plugins[i].name, \" exits with status code \", code)\n                        end\n\n                        ngx_exit(1)\n                    end\n                end\n            end\n\n            ::CONTINUE::\n        end\n        return api_ctx, plugin_run\n    end\n\n    for i = 1, #plugins, 2 do\n        local phase_func = plugins[i][phase]\n        local conf = plugins[i + 1]\n        if phase_func and meta_filter(api_ctx, plugins[i][\"name\"], conf) then\n            plugin_run = true\n            run_meta_pre_function(conf, api_ctx, plugins[i][\"name\"])\n            api_ctx._plugin_name = plugins[i][\"name\"]\n            local span = tracer.start(api_ctx.ngx_ctx, \"apisix.phase.\" .. phase\n                                        .. \".plugins.\" .. api_ctx._plugin_name)\n            phase_func(conf, api_ctx)\n            span:finish(api_ctx.ngx_ctx)\n            api_ctx._plugin_name = nil\n        end\n    end\n\n    return api_ctx, plugin_run\nend\n\nfunction _M.set_plugins_meta_parent(plugins, parent)\n    if not plugins then\n        return\n    end\n    for _, plugin_conf in pairs(plugins) do\n        if not plugin_conf._meta then\n            plugin_conf._meta = {}\n        end\n        if not plugin_conf._meta.parent then\n            local parent_info = {\n                resource_key = parent.key,\n                resource_version = tostring(parent.modifiedIndex)\n            }\n            local mt_table = getmetatable(plugin_conf._meta)\n            if mt_table then\n                mt_table.parent = parent_info\n            else\n                plugin_conf._meta = setmetatable(plugin_conf._meta,\n                                                    { __index = {parent = parent_info} })\n            end\n        end\n    end\nend\n\n\nlocal function merge_global_rules(global_rules, conf_version)\n    -- First pass: identify duplicate plugins across all global rules\n    local plugins_hash = {}\n    local seen_plugin = {}\n    local values = global_rules\n    for _, global_rule in config_util.iterate_values(values) do\n        if global_rule.value and global_rule.value.plugins then\n            for plugin_name, plugin_conf in pairs(global_rule.value.plugins) do\n                if seen_plugin[plugin_name] then\n                    core.log.error(\"Found \", plugin_name,\n                                  \" configured across different global rules.\",\n                                  \" Removing it from execution list\")\n                    plugins_hash[plugin_name] = nil\n                else\n                    plugins_hash[plugin_name] = plugin_conf\n                    seen_plugin[plugin_name] = true\n                end\n            end\n        end\n    end\n\n    local dummy_global_rule = {\n        key = \"/apisix/global_rules/dummy\",\n        value = {\n            updated_time = ngx.time(),\n            plugins = plugins_hash,\n            created_time = ngx.time(),\n            id = 1,\n        },\n        createdIndex = conf_version,\n        modifiedIndex = conf_version,\n        clean_handlers = {},\n    }\n\n    return dummy_global_rule\nend\n\n\nfunction _M.run_global_rules(api_ctx, global_rules, conf_version, phase_name)\n    if global_rules and #global_rules > 0 then\n        local span = tracer.start(api_ctx.ngx_ctx, \"run_global_rules\", tracer.kind.internal)\n        local orig_conf_type = api_ctx.conf_type\n        local orig_conf_version = api_ctx.conf_version\n        local orig_conf_id = api_ctx.conf_id\n\n        if phase_name == nil then\n            api_ctx.global_rules = global_rules\n        end\n\n        local dummy_global_rule = merge_global_rule_lrucache(conf_version,\n                                                             global_rules,\n                                                             merge_global_rules,\n                                                             global_rules,\n                                                             conf_version)\n\n        local plugins = core.tablepool.fetch(\"plugins\", 32, 0)\n        local route = api_ctx.matched_route\n        api_ctx.conf_type = \"global_rule\"\n        api_ctx.conf_version = dummy_global_rule.modifiedIndex\n        api_ctx.conf_id = dummy_global_rule.value.id\n\n        core.table.clear(plugins)\n        plugins = _M.filter(api_ctx, dummy_global_rule, plugins, route)\n\n        if phase_name == nil then\n            _M.run_plugin(\"rewrite\", plugins, api_ctx)\n            _M.run_plugin(\"access\", plugins, api_ctx)\n        else\n            _M.run_plugin(phase_name, plugins, api_ctx)\n        end\n        core.tablepool.release(\"plugins\", plugins)\n\n        api_ctx.conf_type = orig_conf_type\n        api_ctx.conf_version = orig_conf_version\n        api_ctx.conf_id = orig_conf_id\n        span:finish(api_ctx.ngx_ctx)\n    end\nend\n\nfunction _M.lua_response_filter(api_ctx, headers, body)\n    local plugins = api_ctx.plugins\n    if not plugins or #plugins == 0 then\n        -- if there is no any plugin, just print the original body to downstream\n        ngx_print(body)\n        ngx_flush()\n        return\n    end\n    for i = 1, #plugins, 2 do\n        local phase_func = plugins[i][\"lua_body_filter\"]\n        if phase_func then\n            local conf = plugins[i + 1]\n            if not meta_filter(api_ctx, plugins[i][\"name\"], conf)then\n                goto CONTINUE\n            end\n\n            run_meta_pre_function(conf, api_ctx, plugins[i][\"name\"])\n            local code, new_body = phase_func(conf, api_ctx, headers, body)\n            if code then\n                if code ~= ngx_ok then\n                    ngx.status = code\n                end\n\n                ngx_print(new_body)\n                ngx_exit(ngx_ok)\n            end\n            if new_body then\n                body = new_body\n            end\n        end\n\n        ::CONTINUE::\n    end\n    ngx_print(body)\n    ngx_flush()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugin_config.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core           = require(\"apisix.core\")\nlocal plugin_checker = require(\"apisix.plugin\").plugin_checker\nlocal plugin         = require(\"apisix.plugin\")\n\nlocal pairs = pairs\nlocal error = error\n\n\nlocal plugin_configs\n\n\nlocal _M = {\n}\n\n\nlocal function filter(global_rule)\n    if not global_rule.value or not global_rule.value.plugins then\n        return\n    end\n    plugin.set_plugins_meta_parent(global_rule.value.plugins, global_rule)\nend\n\n\nfunction _M.init_worker()\n    local err\n    plugin_configs, err = core.config.new(\"/plugin_configs\", {\n        automatic = true,\n        item_schema = core.schema.plugin_config,\n        checker = plugin_checker,\n        filter = filter,\n    })\n    if not plugin_configs then\n        error(\"failed to sync /plugin_configs: \" .. err)\n    end\nend\n\n\nfunction _M.plugin_configs()\n    if not plugin_configs then\n        return nil, nil\n    end\n    return plugin_configs.values, plugin_configs.conf_version\nend\n\n\nfunction _M.get(id)\n    return plugin_configs:get(id)\nend\n\n\nfunction _M.merge(route_conf, plugin_config)\n    if route_conf.prev_plugin_config_ver == plugin_config.modifiedIndex then\n        return route_conf\n    end\n\n    if not route_conf.value.plugins then\n        route_conf.value.plugins = {}\n    end\n\n    if route_conf.orig_plugins then\n        -- recover\n        route_conf.value.plugins = route_conf.orig_plugins\n    else\n        -- backup in the first time\n        route_conf.orig_plugins = route_conf.value.plugins\n    end\n\n    route_conf.value.plugins = core.table.clone(route_conf.value.plugins)\n\n    for name, value in pairs(plugin_config.value.plugins) do\n        if not route_conf.value.plugins[name] then\n            route_conf.value.plugins[name] = value\n        end\n    end\n\n    route_conf.modifiedIndex = route_conf.orig_modifiedIndex .. \"#\" .. plugin_config.modifiedIndex\n    route_conf.prev_plugin_config_ver = plugin_config.modifiedIndex\n\n    return route_conf\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-aliyun-content-moderation.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx       = ngx\nlocal ngx_ok    = ngx.OK\nlocal os        = os\nlocal pairs     = pairs\nlocal ipairs    = ipairs\nlocal table     = table\nlocal string    = string\nlocal url       = require(\"socket.url\")\nlocal utf8      = require(\"lua-utf8\")\nlocal core      = require(\"apisix.core\")\nlocal http      = require(\"resty.http\")\nlocal uuid      = require(\"resty.jit-uuid\")\nlocal ai_schema = require(\"apisix.plugins.ai-drivers.schema\")\n\nlocal sse       = require(\"apisix.plugins.ai-drivers.sse\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        stream_check_mode = {\n            type = \"string\",\n            enum = {\"realtime\", \"final_packet\"},\n            default = \"final_packet\",\n            description = [[\n            realtime: batched checks during streaming | final_packet: append risk_level at end\n            ]]\n        },\n        stream_check_cache_size = {\n            type = \"integer\",\n            minimum = 1,\n            default = 128,\n            description = \"max characters per moderation batch in realtime mode\"\n        },\n        stream_check_interval = {\n            type = \"number\",\n            minimum = 0.1,\n            default = 3,\n            description = \"seconds between batch checks in realtime mode\"\n        },\n        endpoint = {type = \"string\", minLength = 1},\n        region_id = {type =\"string\", minLength = 1},\n        access_key_id = {type = \"string\", minLength = 1},\n        access_key_secret = {type =\"string\", minLength = 1},\n        check_request = {type = \"boolean\", default = true},\n        check_response = {type = \"boolean\", default = false},\n        request_check_service = {type = \"string\", minLength = 1, default = \"llm_query_moderation\"},\n        request_check_length_limit = {type = \"number\", default = 2000},\n        response_check_service = {type = \"string\", minLength = 1,\n                                  default = \"llm_response_moderation\"},\n        response_check_length_limit = {type = \"number\", default = 5000},\n        risk_level_bar = {type = \"string\",\n                          enum = {\"none\", \"low\", \"medium\", \"high\", \"max\"},\n                          default = \"high\"},\n        deny_code = {type = \"number\", default = 200},\n        deny_message = {type = \"string\"},\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            default = 10000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 30},\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {type = \"integer\", minimum = 1000, default = 60000},\n        ssl_verify = {type = \"boolean\", default = true },\n    },\n    encrypt_fields = {\"access_key_secret\"},\n    required = { \"endpoint\", \"region_id\", \"access_key_id\", \"access_key_secret\" },\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 1029,\n    name     = \"ai-aliyun-content-moderation\",\n    schema   = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function risk_level_to_int(risk_level)\n    local risk_levels = {\n        [\"max\"] = 4,\n        [\"high\"] = 3,\n        [\"medium\"] = 2,\n        [\"low\"] = 1,\n        [\"none\"] = 0\n    }\n    return risk_levels[risk_level] or -1\nend\n\n\n-- openresty ngx.escape_uri don't escape some sub-delimis in rfc 3986 but aliyun do it,\n-- in order to we can calculate same signature with aliyun, we need escape those chars manually\nlocal sub_delims_rfc3986 = {\n    [\"!\"] = \"%%21\",\n    [\"'\"] = \"%%27\",\n    [\"%(\"] = \"%%28\",\n    [\"%)\"] = \"%%29\",\n    [\"*\"] = \"%%2A\",\n}\nlocal function url_encoding(raw_str)\n    local encoded_str = ngx.escape_uri(raw_str)\n    for k, v in pairs(sub_delims_rfc3986) do\n        encoded_str = string.gsub(encoded_str, k, v)\n    end\n    return encoded_str\nend\n\n\nlocal function calculate_sign(params, secret)\n    local params_arr = {}\n    for k, v in pairs(params) do\n        table.insert(params_arr, ngx.escape_uri(k) .. \"=\" .. url_encoding(v))\n    end\n    table.sort(params_arr)\n    local canonical_str = table.concat(params_arr, \"&\")\n    local str_to_sign = \"POST&%2F&\" .. ngx.escape_uri(canonical_str)\n    core.log.debug(\"string to calculate signature: \", str_to_sign)\n    return ngx.encode_base64(ngx.hmac_sha1(secret, str_to_sign))\nend\n\n\nlocal function check_single_content(ctx, conf, content, service_name)\n    local timestamp = os.date(\"!%Y-%m-%dT%TZ\")\n    local random_id = uuid.generate_v4()\n    local params = {\n        [\"AccessKeyId\"] = conf.access_key_id,\n        [\"Action\"] = \"TextModerationPlus\",\n        [\"Format\"] = \"JSON\",\n        [\"RegionId\"] = conf.region_id,\n        [\"Service\"] = service_name,\n        [\"ServiceParameters\"] = core.json.encode({sessionId = ctx.session_id, content = content}),\n        [\"SignatureMethod\"] = \"HMAC-SHA1\",\n        [\"SignatureNonce\"] = random_id,\n        [\"SignatureVersion\"] = \"1.0\",\n        [\"Timestamp\"] = timestamp,\n        [\"Version\"] = \"2022-03-02\",\n    }\n    params[\"Signature\"] = calculate_sign(params, conf.access_key_secret .. \"&\")\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout)\n\n    local parsed_url = url.parse(conf.endpoint)\n    local ok, err = httpc:connect({\n        scheme = parsed_url and parsed_url.scheme or \"https\",\n        host = parsed_url and parsed_url.host,\n        port = parsed_url and parsed_url.port,\n        ssl_verify = conf.ssl_verify,\n        ssl_server_name = parsed_url and parsed_url.host,\n        pool_size = conf.keepalive and conf.keepalive_pool,\n    })\n    if not ok then\n        return nil, \"failed to connect: \" .. err\n    end\n\n    local body = ngx.encode_args(params)\n    core.log.debug(\"text moderation request body: \", body)\n    local res, err = httpc:request{\n        method = \"POST\",\n        body = body,\n        path = \"/\",\n        headers = {\n            [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n        }\n    }\n    if not res then\n        return nil, \"failed to request: \" .. err\n    end\n    local raw_res_body, err = res:read_body()\n    if not raw_res_body then\n        return nil, \"failed to read response body: \" .. err\n    end\n    if conf.keepalive then\n        local ok, err = httpc:set_keepalive(conf.keepalive_timeout, conf.keepalive_pool)\n        if not ok then\n            core.log.warn(\"failed to keepalive connection: \", err)\n        end\n    end\n    if res.status ~= 200 then\n        return nil, \"failed to request aliyun text moderation service, status: \" .. res.status\n                        .. \", x-acs-request-id: \" .. (res.headers[\"x-acs-request-id\"] or \"\")\n                        .. \", body: \" .. raw_res_body\n    end\n\n    core.log.debug(\"raw response: \", raw_res_body)\n    local response, err = core.json.decode(raw_res_body)\n    if not response then\n        return nil, \"failed to decode response, \"\n                        .. \", x-acs-request-id: \" .. (res.headers[\"x-acs-request-id\"] or \"\")\n                        .. \", err\" .. err .. \", body: \" .. raw_res_body\n    end\n\n    local risk_level = response.Data and response.Data.RiskLevel\n    if not risk_level then\n        return nil, \"failed to get risk level: \" .. raw_res_body\n    end\n    ctx.var.llm_content_risk_level = risk_level\n    if risk_level_to_int(risk_level) < risk_level_to_int(conf.risk_level_bar) then\n        return false\n    end\n    -- answer is readable message for human\n    return true, response.Data.Advice and response.Data.Advice[1]\n                        and response.Data.Advice[1].Answer\nend\n\n\n-- we need to return a provider compatible response without broken the ai client\nlocal function deny_message(provider, message, model, stream, usage)\n    local content = message or \"Your request violate our content policy.\"\n    if ai_schema.is_openai_compatible_provider(provider) then\n        if stream then\n            local data = {\n                id = uuid.generate_v4(),\n                object = \"chat.completion.chunk\",\n                model = model,\n                choices = {\n                    {\n                        index = 0,\n                        delta = {\n                            content = content,\n                        },\n                        finish_reason = \"stop\"\n                    }\n                },\n                usage = usage,\n            }\n\n            return \"data: \" .. core.json.encode(data) .. \"\\n\\n\" .. \"data: [DONE]\"\n        else\n            return core.json.encode({\n                id = uuid.generate_v4(),\n                object = \"chat.completion\",\n                model = model,\n                choices = {\n                  {\n                    index = 0,\n                    message = {\n                      role = \"assistant\",\n                      content = content\n                    },\n                    finish_reason = \"stop\"\n                  }\n                },\n                usage = usage,\n              })\n        end\n    end\n\n    core.log.error(\"unsupported provider: \", provider)\n    return content\nend\n\n\nlocal function content_moderation(ctx, conf, provider, model, content, length_limit,\n                                  stream, usage, service_name)\n    core.log.debug(\"execute content moderation, content: \", content)\n    if not ctx.session_id then\n        ctx.session_id = uuid.generate_v4()\n    end\n    if #content <= length_limit then\n        local hit, err = check_single_content(ctx, conf, content, service_name)\n        if hit then\n            return conf.deny_code, deny_message(provider, conf.deny_message or err,\n                                                    model, stream, usage)\n        end\n        if err then\n            core.log.error(\"failed to check content: \", err)\n        end\n        return\n    end\n\n    local index = 1\n    while true do\n        if index > #content then\n            return\n        end\n        local hit, err = check_single_content(ctx, conf,\n                                                utf8.sub(content, index, index + length_limit - 1),\n                                                service_name)\n        index = index + length_limit\n        if hit then\n            return conf.deny_code, deny_message(provider, conf.deny_message or err,\n                                                    model, stream, usage)\n        end\n        if err then\n            core.log.error(\"failed to check content: \", err)\n        end\n    end\nend\n\n\nlocal function request_content_moderation(ctx, conf, content, model)\n    if not content or #content == 0 then\n        return\n    end\n    local provider = ctx.picked_ai_instance.provider\n    local stream = ctx.var.request_type == \"ai_stream\"\n    return content_moderation(ctx, conf, provider, model, content, conf.request_check_length_limit,\n                                stream, {\n                                    prompt_tokens = 0,\n                                    completion_tokens = 0,\n                                    total_tokens = 0\n                                }, conf.request_check_service)\nend\n\n\nlocal function response_content_moderation(ctx, conf, content)\n    if not content or #content == 0 then\n        return\n    end\n    local provider = ctx.picked_ai_instance.provider\n    local model = ctx.var.request_llm_model or ctx.var.llm_model\n    local stream = ctx.var.request_type == \"ai_stream\"\n    local usage = ctx.var.llm_raw_usage\n    return content_moderation(ctx, conf, provider, model, content,\n                                conf.response_check_length_limit,\n                                stream, usage, conf.response_check_service)\nend\n\nfunction _M.access(conf, ctx)\n    if not ctx.picked_ai_instance then\n        return 500, \"no ai instance picked, \" ..\n                \"ai-aliyun-content-moderation plugin must be used with \" ..\n                \"ai-proxy or ai-proxy-multi plugin\"\n    end\n    local provider = ctx.picked_ai_instance.provider\n    if not conf.check_request then\n        core.log.info(\"skip request check for this request\")\n        return\n    end\n    local ct = core.request.header(ctx, \"Content-Type\")\n    if ct and not core.string.has_prefix(ct, \"application/json\") then\n        return 400, \"unsupported content-type: \" .. ct .. \", only application/json is supported\"\n    end\n    local request_tab, err = core.request.get_json_request_body_table()\n    if not request_tab then\n        return 400, err\n    end\n    local ok, err = core.schema.check(ai_schema.chat_request_schema[provider], request_tab)\n    if not ok then\n        return 400, \"request format doesn't match schema: \" .. err\n    end\n\n    core.log.info(\"current ai provider: \", provider)\n\n    if ai_schema.is_openai_compatible_provider(provider) then\n        local contents = {}\n        for _, message in ipairs(request_tab.messages) do\n            if message.content then\n                core.table.insert(contents, message.content)\n            end\n        end\n        local content_to_check = table.concat(contents, \" \")\n        local code, message = request_content_moderation(ctx, conf,\n                                                        content_to_check, request_tab.model)\n        if code then\n            if request_tab.stream then\n                core.response.set_header(\"Content-Type\", \"text/event-stream\")\n                return code, message\n            else\n                core.response.set_header(\"Content-Type\", \"application/json\")\n                return code, message\n            end\n        end\n        return\n    end\n    return 500, \"unsupported provider: \" .. provider\nend\n\n\nfunction _M.lua_body_filter(conf, ctx, headers, body)\n    if not conf.check_response then\n        core.log.info(\"skip response check for this request\")\n        return\n    end\n    local request_type = ctx.var.request_type\n\n    if request_type == \"ai_chat\" then\n        local content = ctx.var.llm_response_text\n        return response_content_moderation(ctx, conf, content)\n    end\n\n    if conf.stream_check_mode == \"final_packet\" then\n        if not ctx.var.llm_response_text then\n            return\n        end\n        response_content_moderation(ctx, conf, ctx.var.llm_response_text)\n        local events = sse.decode(body)\n        for _, event in ipairs(events) do\n            if event.type == \"message\" then\n                local data, err = core.json.decode(event.data)\n                if not data then\n                    core.log.warn(\"failed to decode SSE data: \", err)\n                    goto CONTINUE\n                end\n                data.risk_level = ctx.var.llm_content_risk_level\n                event.data = core.json.encode(data)\n            end\n            ::CONTINUE::\n        end\n\n        local raw_events = {}\n        local contains_done_event = false\n        for _, event in ipairs(events) do\n            if event.type == \"done\" then\n                contains_done_event = true\n            end\n            table.insert(raw_events, sse.encode(event))\n        end\n        if not contains_done_event then\n            table.insert(raw_events, \"data: [DONE]\")\n        end\n        return ngx_ok, table.concat(raw_events, \"\\n\")\n    end\n\n    if conf.stream_check_mode == \"realtime\" then\n        ctx.content_moderation_cache = ctx.content_moderation_cache or \"\"\n        local content = table.concat(ctx.llm_response_contents_in_chunk, \"\")\n        ctx.content_moderation_cache = ctx.content_moderation_cache .. content\n        local now_time = ngx.now()\n        ctx.last_moderate_time = ctx.last_moderate_time or now_time\n        if #ctx.content_moderation_cache < conf.stream_check_cache_size\n                and now_time - ctx.last_moderate_time < conf.stream_check_interval\n                and not ctx.var.llm_request_done then\n            return\n        end\n        ctx.last_moderate_time = now_time\n        local _, message = response_content_moderation(ctx, conf, ctx.content_moderation_cache)\n        if message then\n            return ngx_ok, message\n        end\n        ctx.content_moderation_cache = \"\" -- reset cache\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-aws-content-moderation.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nrequire(\"resty.aws.config\") -- to read env vars before initing aws module\n\nlocal core = require(\"apisix.core\")\nlocal aws = require(\"resty.aws\")\nlocal aws_instance\n\nlocal http = require(\"resty.http\")\nlocal fetch_secrets = require(\"apisix.secret\").fetch_secrets\n\nlocal pairs = pairs\nlocal unpack = unpack\nlocal type = type\nlocal ipairs = ipairs\nlocal HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR\nlocal HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST\n\nlocal moderation_categories_pattern = \"^(PROFANITY|HATE_SPEECH|INSULT|\"..\n                                      \"HARASSMENT_OR_ABUSE|SEXUAL|VIOLENCE_OR_THREAT)$\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        comprehend = {\n            type = \"object\",\n            properties = {\n                access_key_id = { type = \"string\" },\n                secret_access_key = { type = \"string\" },\n                region = { type = \"string\" },\n                endpoint = {\n                    type = \"string\",\n                    pattern = [[^https?://]]\n                },\n                ssl_verify = {\n                    type = \"boolean\",\n                    default = true\n                }\n            },\n            required = { \"access_key_id\", \"secret_access_key\", \"region\", }\n        },\n        moderation_categories = {\n            type = \"object\",\n            patternProperties = {\n                [moderation_categories_pattern] = {\n                    type = \"number\",\n                    minimum = 0,\n                    maximum = 1\n                }\n            },\n            additionalProperties = false\n        },\n        moderation_threshold = {\n            type = \"number\",\n            minimum = 0,\n            maximum = 1,\n            default = 0.5\n        }\n    },\n    required = { \"comprehend\" },\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 1050,\n    name     = \"ai-aws-content-moderation\",\n    schema   = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    conf = fetch_secrets(conf, true)\n    if not conf then\n        return HTTP_INTERNAL_SERVER_ERROR, \"failed to retrieve secrets from conf\"\n    end\n\n    local body, err = core.request.get_body()\n    if not body then\n        return HTTP_BAD_REQUEST, err\n    end\n\n    local comprehend = conf.comprehend\n\n    if not aws_instance then\n        aws_instance = aws()\n    end\n    local credentials = aws_instance:Credentials({\n        accessKeyId = comprehend.access_key_id,\n        secretAccessKey = comprehend.secret_access_key,\n        sessionToken = comprehend.session_token,\n    })\n\n    local default_endpoint = \"https://comprehend.\" .. comprehend.region .. \".amazonaws.com\"\n    local scheme, host, port = unpack(http:parse_uri(comprehend.endpoint or default_endpoint))\n    local endpoint = scheme .. \"://\" .. host\n    aws_instance.config.endpoint = endpoint\n    aws_instance.config.ssl_verify = comprehend.ssl_verify\n\n    local comprehend = aws_instance:Comprehend({\n        credentials = credentials,\n        endpoint = endpoint,\n        region = comprehend.region,\n        port = port,\n    })\n\n    local res, err = comprehend:detectToxicContent({\n        LanguageCode = \"en\",\n        TextSegments = {{\n            Text = body\n        }},\n    })\n\n    if not res then\n        core.log.error(\"failed to send request to \", endpoint, \": \", err)\n        return HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    local results = res.body and res.body.ResultList\n    if type(results) ~= \"table\" or core.table.isempty(results) then\n        return HTTP_INTERNAL_SERVER_ERROR, \"failed to get moderation results from response\"\n    end\n\n    for _, result in ipairs(results) do\n        if conf.moderation_categories then\n            for _, item in pairs(result.Labels) do\n                if not conf.moderation_categories[item.Name] then\n                    goto continue\n                end\n                if item.Score > conf.moderation_categories[item.Name] then\n                    return HTTP_BAD_REQUEST, \"request body exceeds \" .. item.Name .. \" threshold\"\n                end\n                ::continue::\n            end\n        end\n\n        if result.Toxicity > conf.moderation_threshold then\n            return HTTP_BAD_REQUEST, \"request body exceeds toxicity threshold\"\n        end\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/aimlapi.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        host = \"api.aimlapi.com\",\n        path = \"/chat/completions\",\n        port = 443\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/anthropic.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        host = \"api.anthropic.com\",\n        path = \"/v1/chat/completions\",\n        port = 443\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/azure-openai.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        path = \"/completions\",\n        port = 443,\n        remove_model = true\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/deepseek.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        host = \"api.deepseek.com\",\n        path = \"/chat/completions\",\n        port = 443\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/gemini.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        host = \"generativelanguage.googleapis.com\",\n        path = \"/v1beta/openai/chat/completions\",\n        port = 443\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/openai-base.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal _M = {}\n\nlocal mt = {\n    __index = _M\n}\n\nlocal CONTENT_TYPE_JSON = \"application/json\"\n\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal http = require(\"resty.http\")\nlocal url  = require(\"socket.url\")\nlocal sse  = require(\"apisix.plugins.ai-drivers.sse\")\nlocal google_oauth = require(\"apisix.utils.google-cloud-oauth\")\n\nlocal lrucache = require(\"resty.lrucache\")\nlocal ngx  = ngx\nlocal ngx_now = ngx.now\n\nlocal table = table\nlocal pairs = pairs\nlocal type  = type\nlocal math  = math\nlocal os    = os\nlocal ipairs = ipairs\nlocal setmetatable = setmetatable\nlocal str_lower = string.lower\n\nlocal HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR\nlocal HTTP_GATEWAY_TIMEOUT = ngx.HTTP_GATEWAY_TIMEOUT\n\n\nfunction _M.new(opt)\n    return setmetatable(opt, mt)\nend\n\n\nfunction _M.validate_request(ctx)\n        local ct = core.request.header(ctx, \"Content-Type\") or CONTENT_TYPE_JSON\n        if not core.string.has_prefix(ct, CONTENT_TYPE_JSON) then\n            return nil, \"unsupported content-type: \" .. ct .. \", only application/json is supported\"\n        end\n\n        local request_table, err = core.request.get_json_request_body_table()\n        if not request_table then\n            return nil, err\n        end\n\n        return request_table, nil\nend\n\n\nlocal function handle_error(err)\n    if core.string.find(err, \"timeout\") then\n        return HTTP_GATEWAY_TIMEOUT\n    end\n    return HTTP_INTERNAL_SERVER_ERROR\nend\n\n\nlocal function read_response(conf, ctx, res, response_filter)\n    local body_reader = res.body_reader\n    if not body_reader then\n        core.log.warn(\"AI service sent no response body\")\n        return HTTP_INTERNAL_SERVER_ERROR\n    end\n\n    local content_type = res.headers[\"Content-Type\"]\n    core.response.set_header(\"Content-Type\", content_type)\n\n    if content_type and core.string.find(content_type, \"text/event-stream\") then\n        local contents = {}\n        while true do\n            local chunk, err = body_reader() -- will read chunk by chunk\n            ctx.var.apisix_upstream_response_time = math.floor((ngx_now() -\n                                             ctx.llm_request_start_time) * 1000)\n            if err then\n                core.log.warn(\"failed to read response chunk: \", err)\n                return handle_error(err)\n            end\n            if not chunk then\n                return\n            end\n\n            if ctx.var.llm_time_to_first_token == \"0\" then\n                ctx.var.llm_time_to_first_token = math.floor(\n                                                (ngx_now() - ctx.llm_request_start_time) * 1000)\n            end\n\n            local events = sse.decode(chunk)\n            ctx.llm_response_contents_in_chunk = {}\n            for _, event in ipairs(events) do\n                if event.type == \"message\" then\n                    local data, err = core.json.decode(event.data)\n                    if not data then\n                        core.log.warn(\"failed to decode SSE data: \", err)\n                        goto CONTINUE\n                    end\n\n                    if data and type(data.choices) == \"table\" and #data.choices > 0 then\n                        for _, choice in ipairs(data.choices) do\n                            if type(choice) == \"table\"\n                                    and type(choice.delta) == \"table\"\n                                    and type(choice.delta.content) == \"string\" then\n                                core.table.insert(contents, choice.delta.content)\n                                core.table.insert(ctx.llm_response_contents_in_chunk,\n                                                        choice.delta.content)\n                            end\n                        end\n                    end\n\n\n                    -- usage field is null for non-last events, null is parsed as userdata type\n                    if data and type(data.usage) == \"table\" then\n                        core.log.info(\"got token usage from ai service: \",\n                                            core.json.delay_encode(data.usage))\n                        ctx.llm_raw_usage = data.usage\n                        ctx.ai_token_usage = {\n                            prompt_tokens = data.usage.prompt_tokens or 0,\n                            completion_tokens = data.usage.completion_tokens or 0,\n                            total_tokens = data.usage.total_tokens or 0,\n                        }\n                        ctx.var.llm_prompt_tokens = ctx.ai_token_usage.prompt_tokens\n                        ctx.var.llm_completion_tokens = ctx.ai_token_usage.completion_tokens\n                        ctx.var.llm_response_text = table.concat(contents, \"\")\n                    end\n                elseif event.type == \"done\" then\n                    ctx.var.llm_request_done = true\n                end\n\n                ::CONTINUE::\n            end\n\n            plugin.lua_response_filter(ctx, res.headers, chunk)\n        end\n    end\n\n    local headers = res.headers\n    local raw_res_body, err = res:read_body()\n    if not raw_res_body then\n        core.log.warn(\"failed to read response body: \", err)\n        return handle_error(err)\n    end\n    ngx.status = res.status\n    ctx.var.llm_time_to_first_token = math.floor((ngx_now() - ctx.llm_request_start_time) * 1000)\n    ctx.var.apisix_upstream_response_time = ctx.var.llm_time_to_first_token\n    local res_body, err = core.json.decode(raw_res_body)\n    if err then\n        core.log.warn(\"invalid response body from ai service: \", raw_res_body, \" err: \", err,\n            \", it will cause token usage not available\")\n    else\n        if response_filter then\n            local resp = {\n                headers = headers,\n                body = res_body,\n            }\n            local code, err = response_filter(conf, ctx, resp)\n            if code then\n                return code, err\n            end\n            if resp.body then\n                local body, err = core.json.encode(resp.body)\n                if not body then\n                    core.log.error(\"failed to encode response body after response filter: \", err)\n                    return 500\n                end\n                raw_res_body = body\n            end\n            headers = resp.headers\n        end\n        core.log.info(\"got token usage from ai service: \", core.json.delay_encode(res_body.usage))\n        ctx.ai_token_usage = {}\n        if type(res_body.usage) == \"table\" then\n            ctx.llm_raw_usage = res_body.usage\n            ctx.ai_token_usage.prompt_tokens = res_body.usage.prompt_tokens or 0\n            ctx.ai_token_usage.completion_tokens = res_body.usage.completion_tokens or 0\n            ctx.ai_token_usage.total_tokens = res_body.usage.total_tokens or 0\n        end\n        ctx.var.llm_prompt_tokens = ctx.ai_token_usage.prompt_tokens or 0\n        ctx.var.llm_completion_tokens = ctx.ai_token_usage.completion_tokens or 0\n\n        if type(res_body.choices) == \"table\" and #res_body.choices > 0 then\n            local contents = {}\n            for _, choice in ipairs(res_body.choices) do\n                if type(choice) == \"table\"\n                        and type(choice.message) == \"table\"\n                        and type(choice.message.content) == \"string\" then\n                    core.table.insert(contents, choice.message.content)\n                end\n            end\n            local content_to_check = table.concat(contents, \" \")\n            ctx.var.llm_response_text = content_to_check\n        end\n    end\n    plugin.lua_response_filter(ctx, headers, raw_res_body)\nend\n\n-- We want to forward all client headers to the LLM upstream by copying headers from the client\n-- but copying content-length is destructive, similarly some headers like `host`\n-- should not be forwarded either\nlocal function construct_forward_headers(ext_opts_headers, ctx)\n    local blacklist = {\n        \"host\",\n        \"content-length\"\n    }\n\n    -- make header keys lower case to overwrite downstream headers correctly,\n    -- because downstream headers are lower case\n    local opts_headers_lower = {}\n    for k, v in pairs(ext_opts_headers or {}) do\n        opts_headers_lower[str_lower(k)] = v\n    end\n    local headers = core.table.merge(core.request.headers(ctx), opts_headers_lower)\n    headers[\"Content-Type\"] = \"application/json\"\n\n    for _, h in ipairs(blacklist) do\n        headers[h] = nil\n    end\n\n    return headers\nend\n\n\nlocal gcp_access_token_cache = lrucache.new(1024 * 4)\n\nlocal function fetch_gcp_access_token(ctx, name, gcp_conf)\n    local key = core.lrucache.plugin_ctx_id(ctx, name)\n    local access_token = gcp_access_token_cache:get(key)\n    if access_token then\n        return access_token\n    end\n    -- generate access token\n    local auth_conf = {}\n    local service_account_json = gcp_conf.service_account_json or\n                                    os.getenv(\"GCP_SERVICE_ACCOUNT\")\n    if type(service_account_json) == \"string\" and service_account_json ~= \"\" then\n        local conf, err = core.json.decode(service_account_json)\n        if not conf then\n            return nil, \"invalid gcp service account json: \" .. (err or \"unknown error\")\n        end\n        auth_conf = conf\n    end\n    local oauth = google_oauth.new(auth_conf)\n    access_token = oauth:generate_access_token()\n    if not access_token then\n        return nil, \"failed to get google oauth token\"\n    end\n    local ttl = oauth.access_token_ttl or 6\n    if gcp_conf.expire_early_secs and ttl > gcp_conf.expire_early_secs then\n        ttl = ttl - gcp_conf.expire_early_secs\n    end\n    if gcp_conf.max_ttl and ttl > gcp_conf.max_ttl then\n        ttl = gcp_conf.max_ttl\n    end\n    gcp_access_token_cache:set(key, access_token, ttl)\n    core.log.debug(\"set gcp access token in cache with ttl: \", ttl, \", key: \", key)\n    return access_token\nend\n\n\nfunction _M.request(self, ctx, conf, request_table, extra_opts)\n    local httpc, err = http.new()\n    if not httpc then\n        core.log.error(\"failed to create http client to send request to LLM server: \", err)\n        return HTTP_INTERNAL_SERVER_ERROR\n    end\n    httpc:set_timeout(conf.timeout)\n\n    core.log.info(\"request extra_opts to LLM server: \", core.json.delay_encode(extra_opts, true))\n\n    local auth = extra_opts.auth or {}\n    local token\n    if auth.gcp then\n        local access_token, err = fetch_gcp_access_token(ctx, extra_opts.name,\n                                        auth.gcp)\n        if not access_token then\n            core.log.error(\"failed to get gcp access token: \", err)\n            return 500\n        end\n        token = access_token\n    end\n\n    local endpoint = extra_opts.endpoint\n    local parsed_url\n    if endpoint then\n        parsed_url = url.parse(endpoint)\n    end\n\n    local scheme = parsed_url and parsed_url.scheme or \"https\"\n    local host = parsed_url and parsed_url.host or self.host\n    local port = parsed_url and parsed_url.port\n    if not port then\n        if scheme == \"https\" then\n            port = 443\n        else\n            port = 80\n        end\n    end\n\n    local query_params = auth.query or {}\n\n    if type(parsed_url) == \"table\" and parsed_url.query and #parsed_url.query > 0 then\n        local args_tab = core.string.decode_args(parsed_url.query)\n        if type(args_tab) == \"table\" then\n            core.table.merge(query_params, args_tab)\n        end\n    end\n\n    local path = (parsed_url and parsed_url.path or self.path)\n\n    local headers = construct_forward_headers(auth.header or {}, ctx)\n    if token then\n        headers[\"Authorization\"] = \"Bearer \" .. token\n    end\n\n    local params = {\n        method = \"POST\",\n        scheme = scheme,\n        headers = headers,\n        ssl_verify = conf.ssl_verify,\n        path = path,\n        query = query_params,\n        host = host,\n        port = port,\n        ssl_server_name = parsed_url and parsed_url.host or self.host,\n    }\n\n    if extra_opts.model_options then\n        for opt, val in pairs(extra_opts.model_options) do\n            request_table[opt] = val\n        end\n    end\n    params.body = request_table\n\n    if self.remove_model then\n        request_table.model = nil\n    end\n\n    if self.request_filter then\n        local code, err = self.request_filter(extra_opts.conf, ctx, params)\n        if code then\n            return code, err\n        end\n    end\n\n    core.log.info(\"sending request to LLM server: \", core.json.delay_encode(params, true))\n\n    local ok, err = httpc:connect(params)\n    if not ok then\n        core.log.error(\"failed to connect to LLM server: \", err)\n        return handle_error(err)\n    end\n\n    local req_json, err = core.json.encode(params.body)\n    if not req_json then\n        return 500, \"failed to encode request body: \" .. (err or \"unknown error\")\n    end\n\n    params.body = req_json\n\n    local res, err = httpc:request(params)\n    if not res then\n        core.log.warn(\"failed to send request to LLM server: \", err)\n        return handle_error(err)\n    end\n\n    -- handling this error separately is needed for retries\n    if res.status == 429 or (res.status >= 500 and res.status < 600 )then\n        return res.status\n    end\n\n    local code, body = read_response(extra_opts.conf, ctx, res, self.response_filter)\n\n    if conf.keepalive then\n        local ok, err = httpc:set_keepalive(conf.keepalive_timeout, conf.keepalive_pool)\n        if not ok then\n            core.log.warn(\"failed to keepalive connection: \", err)\n        end\n    end\n\n    return code, body\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/openai-compatible.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new({})\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/openai.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        host = \"api.openai.com\",\n        path = \"/v1/chat/completions\",\n        port = 443\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/openrouter.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new(\n    {\n        host = \"openrouter.ai\",\n        path = \"/api/v1/chat/completions\",\n        port = 443\n    }\n)\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal _M = {}\n\nlocal openai_compatible_chat_schema = {\n        type = \"object\",\n        properties = {\n            messages = {\n                type = \"array\",\n                minItems = 1,\n                items = {\n                    properties = {\n                        role = {\n                            type = \"string\",\n                            enum = {\"system\", \"user\", \"assistant\"}\n                        },\n                        content = {\n                            type = \"string\",\n                            minLength = \"1\",\n                        },\n                    },\n                    additionalProperties = false,\n                    required = {\"role\", \"content\"},\n                },\n            }\n        },\n        required = {\"messages\"}\n    }\n\nlocal openai_compatible_list = {\n    \"openai\",\n    \"deepseek\",\n    \"aimlapi\",\n    \"anthropic\",\n    \"openai-compatible\",\n    \"azure-openai\",\n    \"openrouter\",\n    \"vertex-ai\",\n    \"gemini\",\n}\n\n-- Export list of all providers\n-- currently all are OpenAI-compatible\n-- If incompatible providers with OpenAI API are added,\n-- please merge these lists and still export from this variable.\n_M.providers = openai_compatible_list\n\n_M.chat_request_schema = {}\n\ndo\n    local openai_compatible_kv = {}\n    for _, provider in ipairs(openai_compatible_list) do\n        _M.chat_request_schema[provider] = openai_compatible_chat_schema\n        openai_compatible_kv[provider] = true\n    end\n\n    function _M.is_openai_compatible_provider(provider)\n        return openai_compatible_kv[provider] == true\n    end\nend\n\nreturn  _M\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/sse.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal table = require(\"apisix.core.table\")\nlocal tonumber = tonumber\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal _M = {}\n\nlocal ngx_re = require(\"ngx.re\")\n\nfunction _M.decode(chunk)\n    local events = {}\n\n    if not chunk then\n        return events\n    end\n\n    -- Split chunk into individual SSE events\n    local raw_events, err = ngx_re.split(chunk, \"\\n\\n\")\n    if not raw_events then\n        core.log.warn(\"failed to split SSE chunk: \", err)\n        return events\n    end\n    for _, raw_event in ipairs(raw_events) do\n        local event = {\n            type = \"message\",  -- default event type\n            data = {},\n            id = nil,\n            retry = nil\n        }\n        if core.string.find(raw_event, \"data: [DONE]\") then\n            event.type = \"done\"\n            event.data = \"[DONE]\\n\\n\"\n            table.insert(events, event)\n            goto CONTINUE\n        end\n        local lines, err = ngx_re.split(raw_event, \"\\n\")\n        if not lines then\n            core.log.warn(\"failed to split event lines: \", err)\n            goto CONTINUE\n        end\n\n        for _, line in ipairs(lines) do\n            local name, value = line:match(\"^([^:]+): ?(.+)$\")\n            if not name then goto NEXT_LINE end\n\n            name = name:lower()\n\n            if name == \"event\" then\n                event.type = value\n            elseif name == \"data\" then\n                table.insert(event.data, value)\n            elseif name == \"id\" then\n                event.id = value\n            elseif name == \"retry\" then\n                event.retry = tonumber(value)\n            end\n\n            ::NEXT_LINE::\n        end\n\n        -- Join data lines with newline\n        event.data = table.concat(event.data, \"\\n\")\n        table.insert(events, event)\n\n        ::CONTINUE::\n    end\n\n    return events\nend\n\nfunction _M.encode(event)\n    local parts = {}\n\n    if event.type and event.type ~= \"message\" and event.type ~= \"done\" then\n        table.insert(parts, \"event: \" .. event.type)\n    end\n\n    if event.id then\n        table.insert(parts, \"id: \" .. event.id)\n    end\n\n    if event.retry then\n        table.insert(parts, \"retry: \" .. tostring(event.retry))\n    end\n\n    if event.data then\n        if event.type == \"done\" then\n            table.insert(parts, \"data: \" .. event.data)\n        else\n            for line in event.data:gmatch(\"([^\\n]+)\") do\n                table.insert(parts, \"data: \" .. line)\n            end\n        end\n\n    end\n\n    table.insert(parts, \"\")  -- Add empty line to separate events\n    return table.concat(parts, \"\\n\")\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-drivers/vertex-ai.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal string = string\nlocal str_fmt = string.format\nlocal type = type\nlocal ipairs = ipairs\n\nlocal host_template_fmt =\n        \"%s-aiplatform.googleapis.com\"\nlocal embeddings_path_template_fmt =\n        \"/v1/projects/%s/locations/%s/publishers/google/models/%s:predict\"\nlocal chat_completions_path_template_fmt =\n        \"/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions\"\n\nlocal function get_host(region)\n    return str_fmt(host_template_fmt, region)\nend\n\n\nlocal function get_chat_completions_path(project_id, region)\n    return str_fmt(chat_completions_path_template_fmt, project_id, region)\nend\n\n\nlocal function get_embeddings_path(project_id, region, model)\n    return str_fmt(embeddings_path_template_fmt, project_id, region, model)\nend\n\n\nlocal function get_node(instance_conf)\n    local host = \"aiplatform.googleapis.com\"\n    local region = core.table.try_read_attr(instance_conf, \"provider_conf\", \"region\")\n    if region then\n        host = get_host(region)\n    end\n    return {\n        scheme = \"https\",\n        host = host,\n        port = 443,\n    }\nend\n\nlocal function openai_embeddings_to_vertex_predict(openai_req)\n    if not openai_req then\n        return nil, \"empty openai request\"\n    end\n\n    local input = openai_req.input\n    if not input then\n        return nil, \"`input` is required for embeddings\"\n    end\n\n    local input_contexts = {}\n\n    if type(input) == \"string\" then\n        input_contexts = { input }\n    elseif type(input) == \"table\" then\n        for i, v in ipairs(input) do\n            if type(v) == \"string\" then\n                core.table.insert(input_contexts, v)\n            elseif type(v) == \"table\" then\n                core.table.insert(input_contexts, core.table.concat(v, \" \"))\n            else\n                return nil, \"unsupported input type at index \" .. i\n            end\n        end\n    else\n        return nil, \"`input` must be string or array\"\n    end\n\n    local instances = {}\n    for _, text in ipairs(input_contexts) do\n        core.table.insert(instances, {\n            content = text\n        })\n    end\n\n    return {\n        instances = instances\n    }\nend\n\nlocal function vertex_predict_to_openai_embeddings(vertex_resp, openai_model)\n    if type(vertex_resp) ~= \"table\" then\n        return nil, \"empty vertex response\"\n    end\n\n    local predictions = vertex_resp.predictions\n    if type(predictions) ~= \"table\" then\n        return nil, \"vertex response missing predictions\"\n    end\n\n    local data = {}\n    local total_tokens = 0\n\n    for i, pred in ipairs(predictions) do\n        local emb = pred.embeddings or {}\n        local values = emb.values\n        if type(values) ~= \"table\" then\n            return nil, \"invalid embedding at index \" .. i\n        end\n\n        if emb.statistics and emb.statistics.token_count then\n            total_tokens = total_tokens + emb.statistics.token_count\n        end\n\n        core.table.insert(data, {\n            object = \"embedding\",\n            index = i - 1,\n            embedding = values\n        })\n    end\n\n    return {\n        object = \"list\",\n        data = data,\n        model = openai_model or \"unknown\",\n        usage = {\n            prompt_tokens = total_tokens,\n            total_tokens = total_tokens,\n        }\n    }\nend\n\n\nlocal function request_filter(conf, ctx, http_params)\n    local body = http_params.body\n    if body and body.input then\n        ctx.llm_request_type = \"embeddings\"\n        local vertex_req, err = openai_embeddings_to_vertex_predict(body)\n        if not vertex_req then\n            return nil, \"failed to convert to vertex predict request: \" .. err\n        end\n        http_params.body = vertex_req\n        core.log.debug(\"using embeddings endpoint for Vertex AI\")\n    else\n        ctx.llm_request_type = \"chat_completions\"\n    end\n    ctx.llm_request_model = body and body.model\n\n    if conf.project_id and conf.region then\n        if not http_params.path then\n            local path\n            if ctx.llm_request_type == \"embeddings\" then\n                path = get_embeddings_path(conf.project_id, conf.region, body.model)\n            else\n                path = get_chat_completions_path(conf.project_id, conf.region)\n            end\n            http_params.path = path\n        end\n        if not http_params.host then\n            http_params.host = get_host(conf.region)\n        end\n    end\nend\n\n\nlocal function response_filter(conf, ctx, resp)\n    if ctx.llm_request_type == \"embeddings\" then\n        local vertex_body = resp.body\n        local openai_resp, err = vertex_predict_to_openai_embeddings(vertex_body,\n                                                                    ctx.llm_request_model)\n        if not openai_resp then\n            return 500, \"failed to convert to openai embeddings response: \" .. err\n        end\n        resp.body = openai_resp\n    end\nend\n\n\nreturn require(\"apisix.plugins.ai-drivers.openai-base\").new({\n    get_node = get_node,\n    request_filter = request_filter,\n    response_filter = response_filter,\n})\n"
  },
  {
    "path": "apisix/plugins/ai-prompt-decorator.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core  = require(\"apisix.core\")\nlocal ngx   = ngx\nlocal pairs = pairs\nlocal EMPTY = {}\n\nlocal prompt_schema = {\n    properties = {\n        role = {\n            type = \"string\",\n            enum = { \"system\", \"user\", \"assistant\" }\n        },\n        content = {\n            type = \"string\",\n            minLength = 1,\n        }\n    },\n    required = { \"role\", \"content\" }\n}\n\nlocal prompts = {\n    type = \"array\",\n    items = prompt_schema\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        prepend = prompts,\n        append = prompts,\n    },\n    anyOf = {\n        { required = { \"prepend\" } },\n        { required = { \"append\" } },\n        { required = { \"append\", \"prepend\" } },\n    },\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 1070,\n    name     = \"ai-prompt-decorator\",\n    schema   = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function get_request_body_table()\n    local body, err = core.request.get_body()\n    if not body then\n        return nil, { message = \"could not get body: \" .. err }\n    end\n\n    local body_tab, err = core.json.decode(body)\n    if not body_tab then\n        return nil, { message = \"could not get parse JSON request body: \" .. err }\n    end\n\n    return body_tab\nend\n\n\nlocal function decorate(conf, body_tab)\n    local new_messages = {}\n\n    if conf.prepend then\n        for i = 1, #conf.prepend do\n            new_messages[i] = conf.prepend[i]\n        end\n    end\n\n    for _, message in pairs(body_tab.messages) do\n        core.table.insert_tail(new_messages, message)\n    end\n\n    for _, message in pairs(conf.append or EMPTY) do\n        core.table.insert_tail(new_messages, message)\n    end\n\n    body_tab.messages = new_messages\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local body_tab, err = get_request_body_table()\n    if not body_tab then\n        return 400, err\n    end\n\n    if not body_tab.messages then\n        return 400, \"messages missing from request body\"\n    end\n    decorate(conf, body_tab) -- will decorate body_tab in place\n\n    local new_jbody, err = core.json.encode(body_tab)\n    if not new_jbody then\n        return 500, { message = \"failed to parse modified JSON request body: \" .. err }\n    end\n\n    ngx.req.set_body_data(new_jbody)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-prompt-guard.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx = ngx\nlocal ipairs = ipairs\nlocal table = table\nlocal re_compile  = require(\"resty.core.regex\").re_match_compile\nlocal re_find = ngx.re.find\n\nlocal plugin_name = \"ai-prompt-guard\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        match_all_roles = {\n            type = \"boolean\",\n            default = false,\n        },\n        match_all_conversation_history = {\n            type = \"boolean\",\n            default = false,\n        },\n        allow_patterns = {\n            type = \"array\",\n            items = {type = \"string\"},\n            default = {},\n        },\n        deny_patterns = {\n            type = \"array\",\n            items = {type = \"string\"},\n            default = {},\n        },\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1072,\n    name = plugin_name,\n    schema = schema,\n}\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    -- Validate allow_patterns\n    for _, pattern in ipairs(conf.allow_patterns) do\n        local compiled = re_compile(pattern, \"jou\")\n        if not compiled then\n            return false, \"invalid allow_pattern: \" .. pattern\n        end\n    end\n\n    -- Validate deny_patterns\n    for _, pattern in ipairs(conf.deny_patterns) do\n        local compiled = re_compile(pattern, \"jou\")\n        if not compiled then\n            return false, \"invalid deny_pattern: \" .. pattern\n        end\n    end\n\n    return true\nend\n\nlocal function get_content_to_check(conf, messages)\n    if conf.match_all_conversation_history then\n        return messages\n    end\n    local contents = {}\n    if #messages > 0 then\n        local last_msg = messages[#messages]\n        if last_msg then\n            core.table.insert(contents, last_msg)\n        end\n    end\n    return contents\nend\n\nfunction _M.access(conf, ctx)\n    local body = core.request.get_body()\n    if not body then\n        core.log.error(\"Empty request body\")\n        return 400, {message = \"Empty request body\"}\n    end\n\n    local json_body, err = core.json.decode(body)\n    if err then\n        return 400, {message = err}\n    end\n\n    local messages = json_body.messages or {}\n    messages = get_content_to_check(conf, messages)\n    if not conf.match_all_roles then\n        -- filter to only user messages\n        local new_messages = {}\n        for _, msg in ipairs(messages) do\n            if msg.role == \"user\" then\n                core.table.insert(new_messages, msg)\n            end\n        end\n        messages = new_messages\n    end\n    if #messages == 0 then --nothing to check\n        return 200\n    end\n    -- extract only messages\n    local content = {}\n    for _, msg in ipairs(messages) do\n        if msg.content then\n            core.table.insert(content, msg.content)\n        end\n    end\n    local content_to_check = table.concat(content, \" \")\n     -- Allow patterns check\n     if #conf.allow_patterns > 0 then\n        local any_allowed = false\n        for _, pattern in ipairs(conf.allow_patterns) do\n            if re_find(content_to_check, pattern, \"jou\") then\n                any_allowed = true\n                break\n            end\n        end\n        if not any_allowed then\n            return 400, {message = \"Request doesn't match allow patterns\"}\n        end\n    end\n\n    -- Deny patterns check\n    for _, pattern in ipairs(conf.deny_patterns) do\n        if re_find(content_to_check, pattern, \"jou\") then\n            return 400, {message = \"Request contains prohibited content\"}\n        end\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-prompt-template.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core              = require(\"apisix.core\")\nlocal body_transformer  = require(\"apisix.plugins.body-transformer\")\nlocal ipairs            = ipairs\n\nlocal prompt_schema = {\n    properties = {\n        role = {\n            type = \"string\",\n            enum = { \"system\", \"user\", \"assistant\" }\n        },\n        content = {\n            type = \"string\",\n            minLength = 1,\n        }\n    },\n    required = { \"role\", \"content\" }\n}\n\nlocal prompts = {\n    type = \"array\",\n    minItems = 1,\n    items = prompt_schema\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        templates = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"object\",\n                properties = {\n                    name = {\n                        type = \"string\",\n                        minLength = 1,\n                    },\n                    template = {\n                        type = \"object\",\n                        properties = {\n                            model = {\n                                type = \"string\",\n                                minLength = 1,\n                            },\n                            messages = prompts\n                        }\n                    }\n                },\n                required = {\"name\", \"template\"}\n            }\n        },\n    },\n    required = {\"templates\"},\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 1071,\n    name     = \"ai-prompt-template\",\n    schema   = schema,\n}\n\nlocal templates_lrucache = core.lrucache.new({\n    ttl = 300, count = 256\n})\n\nlocal templates_json_lrucache = core.lrucache.new({\n    ttl = 300, count = 256\n})\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function get_request_body_table()\n    local body, err = core.request.get_body()\n    if not body then\n        return nil, { message = \"could not get body: \" .. err }\n    end\n\n    local body_tab, err = core.json.decode(body)\n    if not body_tab then\n        return nil, { message = \"could not get parse JSON request body: \", err }\n    end\n\n    return body_tab\nend\n\n\nlocal function find_template(conf, template_name)\n    for _, template in ipairs(conf.templates) do\n        if template.name == template_name then\n            return template.template\n        end\n    end\n    return nil\nend\n\nfunction _M.rewrite(conf, ctx)\n    local body_tab, err = get_request_body_table()\n    if not body_tab then\n        return 400, err\n    end\n    local template_name = body_tab.template_name\n    if not template_name then\n        return 400, { message = \"template name is missing in request.\" }\n    end\n\n    local template = templates_lrucache(template_name, conf, find_template, conf, template_name)\n    if not template then\n        return 400, { message = \"template: \" .. template_name .. \" not configured.\" }\n    end\n\n    local template_json = templates_json_lrucache(template, template, core.json.encode, template)\n    core.log.info(\"sending template to body_transformer: \", template_json)\n    return body_transformer.rewrite(\n        {\n            request = {\n                template = template_json,\n                input_format = \"json\"\n            }\n        },\n        ctx\n    )\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-proxy/base.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = ngx\nlocal core = require(\"apisix.core\")\nlocal require = require\nlocal pcall   = pcall\nlocal exporter = require(\"apisix.plugins.prometheus.exporter\")\n\nlocal _M = {}\n\nfunction _M.set_logging(ctx, summaries, payloads)\n    if summaries then\n        ctx.llm_summary = {\n            request_model = ctx.var.request_llm_model,\n            model = ctx.var.llm_model,\n            duration = ctx.var.llm_time_to_first_token,\n            prompt_tokens = ctx.var.llm_prompt_tokens,\n            completion_tokens = ctx.var.llm_completion_tokens,\n            upstream_response_time = ctx.var.apisix_upstream_response_time,\n        }\n    end\n    if payloads then\n        ctx.llm_request = {\n            messages = ctx.var.llm_request_body and ctx.var.llm_request_body.messages,\n            stream = ctx.var.request_type == \"ai_stream\"\n        }\n        ctx.llm_response_text = {\n            content = ctx.var.llm_response_text\n        }\n    end\nend\n\n\n-- when on_error function is passed, before_proxy will keep on retrying until\n-- on_error returns abort code\nfunction _M.before_proxy(conf, ctx, on_error)\n    while true do\n        local ai_instance = ctx.picked_ai_instance\n        local ai_driver = require(\"apisix.plugins.ai-drivers.\" .. ai_instance.provider)\n\n        local request_body, err = ai_driver.validate_request(ctx)\n        if not request_body then\n            return 400, err\n        end\n\n        local extra_opts = {\n            name = ai_instance.name,\n            endpoint = core.table.try_read_attr(ai_instance, \"override\", \"endpoint\"),\n            model_options = ai_instance.options,\n            conf = ai_instance.provider_conf or {},\n            auth = ai_instance.auth,\n        }\n\n        if request_body.stream then\n            request_body.stream_options = {\n                include_usage = true\n            }\n            ctx.var.request_type = \"ai_stream\"\n        else\n            ctx.var.request_type = \"ai_chat\"\n        end\n        if request_body.model then\n            ctx.var.request_llm_model = request_body.model\n        end\n        local model = ai_instance.options and ai_instance.options.model or request_body.model\n        if model then\n            ctx.var.llm_model = model\n        end\n\n        local do_request = function()\n            ctx.llm_request_start_time = ngx.now()\n            ctx.var.llm_request_body = request_body\n            return ai_driver:request(ctx, conf, request_body, extra_opts)\n        end\n\n        exporter.inc_llm_active_connections(ctx)\n        local ok, code_or_err, body = pcall(do_request)\n        exporter.dec_llm_active_connections(ctx)\n        if not ok then\n            core.log.error(\"failed to send request to AI service: \", code_or_err)\n            return 500\n        end\n        if code_or_err and on_error then\n            local abort_code = on_error(ctx, conf, code_or_err)\n            if abort_code then\n                return abort_code, body\n            end\n        else\n            return code_or_err, body\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-proxy/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal schema_def = require(\"apisix.schema_def\")\nlocal ai_drivers_schema = require(\"apisix.plugins.ai-drivers.schema\")\n\nlocal _M = {}\n\nlocal auth_item_schema = {\n    type = \"object\",\n    patternProperties = {\n        [\"^[a-zA-Z0-9._-]+$\"] = {\n            type = \"string\"\n        }\n    }\n}\n\nlocal auth_schema = {\n    type = \"object\",\n    patternProperties = {\n        header = auth_item_schema,\n        query = auth_item_schema,\n        gcp = {\n            type = \"object\",\n            description = 'Whether to use GCP service account for authentication,'\n            .. ' support set env GCP_SERVICE_ACCOUNT.',\n            properties = {\n                service_account_json = {\n                    type = \"string\",\n                    description = \"GCP service account JSON content for authentication\",\n                },\n                max_ttl = {\n                    type = \"integer\",\n                    minimum = 1,\n                    description = \"Maximum TTL (in seconds) for GCP access token caching\",\n                },\n                expire_early_secs = {\n                    type = \"integer\",\n                    minimum = 0,\n                    description = \"Expire the access token early by specified seconds to avoid \" ..\n                                                                \"edge cases\",\n                    default = 60,\n                },\n            }\n        },\n    },\n    additionalProperties = false,\n}\n\nlocal model_options_schema = {\n    description = \"Key/value settings for the model\",\n    type = \"object\",\n    properties = {\n        model = {\n            type = \"string\",\n            description = \"Model to execute.\",\n        },\n    },\n    additionalProperties = true,\n}\n\nlocal provider_vertex_ai_schema = {\n    type = \"object\",\n    properties = {\n        project_id = {\n            type = \"string\",\n            description = \"Google Cloud Project ID\",\n        },\n        region = {\n            type = \"string\",\n            description = \"Google Cloud Region\",\n        },\n    },\n    required = { \"project_id\", \"region\" },\n}\n\nlocal ai_instance_schema = {\n    type = \"array\",\n    minItems = 1,\n    items = {\n        type = \"object\",\n        properties = {\n            name = {\n                type = \"string\",\n                minLength = 1,\n                maxLength = 100,\n                description = \"Name of the AI service instance.\",\n            },\n            provider = {\n                type = \"string\",\n                description = \"Type of the AI service instance.\",\n                enum = ai_drivers_schema.providers,\n            },\n            priority = {\n                type = \"integer\",\n                description = \"Priority of the provider for load balancing\",\n                default = 0,\n            },\n            weight = {\n                type = \"integer\",\n                minimum = 0,\n            },\n            auth = auth_schema,\n            options = model_options_schema,\n            override = {\n                type = \"object\",\n                properties = {\n                    endpoint = {\n                        type = \"string\",\n                        description = \"To be specified to override the endpoint of the AI Instance\",\n                    },\n                },\n            },\n            checks = {\n                type = \"object\",\n                properties = {\n                    active = schema_def.health_checker_active,\n                },\n                required = {\"active\"}\n            }\n        },\n        required = {\"name\", \"provider\", \"auth\", \"weight\"},\n        [\"if\"] = {\n            properties = { provider = { enum = { \"vertex-ai\" } } },\n        },\n        [\"then\"] = {\n            properties = {\n                provider_conf = provider_vertex_ai_schema,\n            },\n            oneOf = {\n                { required = { \"provider_conf\" } },\n                { required = { \"override\" } },\n            },\n        },\n        [\"else\"] = {},\n    },\n}\n\nlocal logging_schema = {\n    type = \"object\",\n    properties = {\n        summaries = {\n            type = \"boolean\",\n            default = false,\n            description = \"Record user request llm model, duration, req/res token\"\n        },\n        payloads = {\n            type = \"boolean\",\n            default = false,\n            description = \"Record user request and response payload\"\n        }\n    }\n}\n\n_M.ai_proxy_schema = {\n    type = \"object\",\n    properties = {\n        provider = {\n            type = \"string\",\n            description = \"Type of the AI service instance.\",\n            enum = ai_drivers_schema.providers,\n        },\n        logging = logging_schema,\n        auth = auth_schema,\n        options = model_options_schema,\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            default = 30000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {\n            type = \"integer\",\n            minimum = 1000,\n            default = 60000,\n            description = \"keepalive timeout in milliseconds\",\n        },\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 30},\n        ssl_verify = {type = \"boolean\", default = true },\n        override = {\n            type = \"object\",\n            properties = {\n                endpoint = {\n                    type = \"string\",\n                    description = \"To be specified to override the endpoint of the AI Instance\",\n                },\n            },\n        },\n    },\n    required = {\"provider\", \"auth\"}\n}\n\n_M.ai_proxy_multi_schema = {\n    type = \"object\",\n    properties = {\n        balancer = {\n            type = \"object\",\n            properties = {\n                algorithm = {\n                    type = \"string\",\n                    enum = { \"chash\", \"roundrobin\" },\n                },\n                hash_on = {\n                    type = \"string\",\n                    default = \"vars\",\n                    enum = {\n                      \"vars\",\n                      \"header\",\n                      \"cookie\",\n                      \"consumer\",\n                      \"vars_combinations\",\n                    },\n                },\n                key = {\n                    description = \"the key of chash for dynamic load balancing\",\n                    type = \"string\",\n                },\n            },\n            default = { algorithm = \"roundrobin\" }\n        },\n        instances = ai_instance_schema,\n        logging = logging_schema,\n        fallback_strategy = {\n            anyOf = {\n              {\n                type = \"string\",\n                enum = {\"instance_health_and_rate_limiting\", \"http_429\", \"http_5xx\"}\n              },\n              {\n                type = \"array\",\n                items = {\n                  type = \"string\",\n                  enum = {\"rate_limiting\", \"http_429\", \"http_5xx\"}\n                }\n              }\n            }\n        },\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            default = 30000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {\n            type = \"integer\",\n            minimum = 1000,\n            default = 60000,\n            description = \"keepalive timeout in milliseconds\",\n        },\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 30},\n        ssl_verify = {type = \"boolean\", default = true },\n    },\n    required = {\"instances\"}\n}\n\n_M.chat_request_schema = {\n    type = \"object\",\n    properties = {\n        messages = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                properties = {\n                    role = {\n                        type = \"string\",\n                        enum = {\"system\", \"user\", \"assistant\"}\n                    },\n                    content = {\n                        type = \"string\",\n                        minLength = \"1\",\n                    },\n                },\n                additionalProperties = false,\n                required = {\"role\", \"content\"},\n            },\n        }\n    },\n    required = {\"messages\"}\n}\n\nreturn  _M\n"
  },
  {
    "path": "apisix/plugins/ai-proxy-multi.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal schema = require(\"apisix.plugins.ai-proxy.schema\")\nlocal base   = require(\"apisix.plugins.ai-proxy.base\")\nlocal plugin = require(\"apisix.plugin\")\nlocal ipmatcher  = require(\"resty.ipmatcher\")\nlocal healthcheck_manager = require(\"apisix.healthcheck_manager\")\nlocal resource = require(\"apisix.resource\")\nlocal tonumber = tonumber\nlocal pairs = pairs\n\nlocal require = require\nlocal pcall = pcall\nlocal ipairs = ipairs\nlocal type = type\n\nlocal priority_balancer = require(\"apisix.balancer.priority\")\nlocal endpoint_regex = \"^(https?)://([^:/]+):?(%d*)/?.*$\"\n\nlocal pickers = {}\nlocal lrucache_server_picker = core.lrucache.new({\n    ttl = 300, count = 256\n})\n\nlocal plugin_name = \"ai-proxy-multi\"\nlocal _M = {\n    version = 0.5,\n    priority = 1041,\n    name = plugin_name,\n    schema = schema.ai_proxy_multi_schema,\n}\n\nlocal function fallback_strategy_has(strategy, name)\n    if not strategy then\n        return false\n    end\n\n    if type(strategy) == \"string\" then\n        return strategy == name\n    end\n\n    if type(strategy) == \"table\" then\n        for _, v in ipairs(strategy) do\n            if v == name then\n                return true\n            end\n        end\n    end\n\n    return false\nend\n\n\nlocal function get_chash_key_schema(hash_on)\n    if hash_on == \"vars\" then\n        return core.schema.upstream_hash_vars_schema\n    end\n\n    if hash_on == \"header\" or hash_on == \"cookie\" then\n        return core.schema.upstream_hash_header_schema\n    end\n\n    if hash_on == \"consumer\" then\n        return nil, nil\n    end\n\n    if hash_on == \"vars_combinations\" then\n        return core.schema.upstream_hash_vars_combinations_schema\n    end\n\n    return nil, \"invalid hash_on type \" .. hash_on\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema.ai_proxy_multi_schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    for _, instance in ipairs(conf.instances) do\n        local endpoint = instance and instance.override and instance.override.endpoint\n        if endpoint then\n            local scheme, host, _ = endpoint:match(endpoint_regex)\n            if not scheme or not host  then\n                return false, \"invalid endpoint\"\n            end\n        end\n        local ai_driver, err = pcall(require, \"apisix.plugins.ai-drivers.\" .. instance.provider)\n        if not ai_driver then\n            core.log.warn(\"fail to require ai provider: \", instance.provider, \", err\", err)\n            return false, \"ai provider: \" .. instance.provider .. \" is not supported.\"\n        end\n        local sa_json = core.table.try_read_attr(instance, \"auth\", \"gcp\", \"service_account_json\")\n        if sa_json then\n            local _, err = core.json.decode(sa_json)\n            if err then\n                return false, \"invalid gcp service_account_json: \" .. err\n            end\n        end\n    end\n    local algo = core.table.try_read_attr(conf, \"balancer\", \"algorithm\")\n    local hash_on = core.table.try_read_attr(conf, \"balancer\", \"hash_on\")\n    local hash_key = core.table.try_read_attr(conf, \"balancer\", \"key\")\n\n    if type(algo) == \"string\" and algo == \"chash\" then\n        if not hash_on then\n            return false, \"must configure `hash_on` when balancer algorithm is chash\"\n        end\n\n        if hash_on ~= \"consumer\" and not hash_key then\n            return false, \"must configure `hash_key` when balancer `hash_on` is not set to cookie\"\n        end\n\n        local key_schema, err = get_chash_key_schema(hash_on)\n        if err then\n            return false, \"type is chash, err: \" .. err\n        end\n\n        if key_schema then\n            local ok, err = core.schema.check(key_schema, hash_key)\n            if not ok then\n                return false, \"invalid configuration: \" .. err\n            end\n        end\n    end\n\n    return ok\nend\n\n\nlocal function transform_instances(new_instances, instance)\n    if not new_instances._priority_index then\n        new_instances._priority_index = {}\n    end\n\n    if not new_instances[instance.priority] then\n        new_instances[instance.priority] = {}\n        core.table.insert(new_instances._priority_index, instance.priority)\n    end\n\n    new_instances[instance.priority][instance.name] = instance.weight\nend\n\nlocal function parse_domain_for_node(node)\n    local host = node.domain or node.host\n    if not ipmatcher.parse_ipv4(host)\n       and not ipmatcher.parse_ipv6(host)\n    then\n        node.domain = host\n\n        local ip, err = core.resolver.parse_domain(host)\n        if ip then\n            node.host = ip\n        end\n\n        if err then\n            core.log.error(\"dns resolver domain: \", host, \" error: \", err)\n        end\n    end\nend\n\n\nlocal function resolve_endpoint(instance_conf)\n    local scheme, host, port\n    local endpoint = core.table.try_read_attr(instance_conf, \"override\", \"endpoint\")\n    if endpoint then\n        scheme, host, port = endpoint:match(endpoint_regex)\n        if port == \"\" then\n            port = (scheme == \"https\") and \"443\" or \"80\"\n        end\n        port = tonumber(port)\n    else\n        local ai_driver = require(\"apisix.plugins.ai-drivers.\" .. instance_conf.provider)\n        if ai_driver.get_node then\n            local node = ai_driver.get_node(instance_conf)\n            host = node.host\n            port = node.port\n        else\n            host = ai_driver.host\n            port = ai_driver.port\n        end\n        scheme = \"https\"\n    end\n    local new_node = {\n        host = host,\n        port = tonumber(port),\n        scheme = scheme,\n    }\n    parse_domain_for_node(new_node)\n\n    -- Compare with existing node to see if anything changed\n    local old_node = instance_conf._dns_value\n    local nodes_changed = not old_node or\n                         old_node.host ~= new_node.host\n\n    -- Only update if something changed\n    if nodes_changed then\n        instance_conf._dns_value = new_node\n        instance_conf._nodes_ver = (instance_conf._nodes_ver or 0) + 1\n        core.log.info(\"DNS resolution changed for instance: \", instance_conf.name,\n                     \" new node: \", core.json.delay_encode(new_node))\n    end\nend\n\n\nlocal function get_checkers_status_ver(checkers)\n    local status_ver_total = 0\n    for _, checker in pairs(checkers) do\n        status_ver_total = status_ver_total + checker.status_ver\n    end\n    return status_ver_total\nend\n\n\n\nlocal function fetch_health_instances(conf, checkers)\n    local instances = conf.instances\n    local new_instances = core.table.new(0, #instances)\n    if not checkers then\n        for _, ins in ipairs(conf.instances) do\n            transform_instances(new_instances, ins)\n        end\n        return new_instances\n    end\n\n    for _, ins in ipairs(instances) do\n        local checker = checkers[ins.name]\n        if checker then\n            local host = ins.checks and ins.checks.active and ins.checks.active.host\n            local port = ins.checks and ins.checks.active and ins.checks.active.port\n\n            local node = ins._dns_value\n            local ok, err = checker:get_target_status(node.host, port or node.port, host)\n            if ok then\n                transform_instances(new_instances, ins)\n            elseif err then\n                core.log.warn(\"failed to get health check target status, addr: \",\n                    node.host, \":\", port or node.port, \", host: \", host, \", err: \", err)\n            end\n        else\n            transform_instances(new_instances, ins)\n        end\n    end\n\n    if core.table.nkeys(new_instances) == 0 then\n        core.log.warn(\"all upstream nodes is unhealthy, use default\")\n        for _, ins in ipairs(instances) do\n            transform_instances(new_instances, ins)\n        end\n    end\n\n    return new_instances\nend\n\n\nlocal function create_server_picker(conf, ups_tab, checkers)\n    local picker = pickers[conf.balancer.algorithm] -- nil check\n    if not picker then\n        pickers[conf.balancer.algorithm] = require(\"apisix.balancer.\" .. conf.balancer.algorithm)\n        picker = pickers[conf.balancer.algorithm]\n    end\n\n    local new_instances = fetch_health_instances(conf, checkers)\n    core.log.info(\"fetch health instances: \", core.json.delay_encode(new_instances))\n\n    if #new_instances._priority_index > 1 then\n        core.log.info(\"new instances: \", core.json.delay_encode(new_instances))\n        return priority_balancer.new(new_instances, ups_tab, picker)\n    end\n    core.log.info(\"upstream nodes: \",\n                core.json.delay_encode(new_instances[new_instances._priority_index[1]]))\n    return picker.new(new_instances[new_instances._priority_index[1]], ups_tab)\nend\n\n\nlocal function get_instance_conf(instances, name)\n    for _, ins in ipairs(instances) do\n        if ins.name == name then\n            return ins\n        end\n    end\nend\n\n\nfunction _M.construct_upstream(instance)\n    local upstream = {}\n    local node = instance._dns_value\n    if not node then\n        return nil, \"failed to resolve endpoint for instance: \" .. instance.name\n    end\n\n    if not node.host or not node.port then\n        return nil, \"invalid upstream node: \" .. core.json.encode(node)\n    end\n\n    local node = {\n        host = node.host,\n        port = node.port,\n        scheme = node.scheme,\n        weight = instance.weight or 1,\n        priority = instance.priority or 0,\n        name = instance.name,\n    }\n    upstream.nodes = {node}\n    upstream.checks = instance.checks\n    upstream._nodes_ver = instance._nodes_ver\n    return upstream\nend\n\n\nlocal function pick_target(ctx, conf, ups_tab)\n    local checkers\n    local res_conf = resource.fetch_latest_conf(conf._meta.parent.resource_key)\n    if not res_conf then\n        return nil, nil, \"failed to fetch the parent config\"\n    end\n    local instances = res_conf.value.plugins[plugin_name].instances\n    for i, instance in ipairs(conf.instances) do\n        if instance.checks then\n            resolve_endpoint(instance)\n            -- json path is 0 indexed so we need to decrement i\n            local resource_path = conf._meta.parent.resource_key ..\n                                  \"#plugins['ai-proxy-multi'].instances[\" .. i-1 .. \"]\"\n            local resource_version = conf._meta.parent.resource_version\n            if instance._nodes_ver then\n                resource_version = resource_version .. instance._nodes_ver\n            end\n            instances[i]._dns_value = instance._dns_value\n            instances[i]._nodes_ver = instance._nodes_ver\n            local checker = healthcheck_manager.fetch_checker(resource_path, resource_version)\n            checkers = checkers or {}\n            checkers[instance.name] = checker\n        end\n    end\n\n    local version = plugin.conf_version(conf)\n    if checkers then\n        local status_ver = get_checkers_status_ver(checkers)\n        version = version .. \"#\" .. status_ver\n    end\n\n    local server_picker = ctx.server_picker\n    if not server_picker then\n        server_picker = lrucache_server_picker(ctx.matched_route.key, version,\n                                               create_server_picker, conf, ups_tab, checkers)\n    end\n    if not server_picker then\n        return nil, nil, \"failed to fetch server picker\"\n    end\n    ctx.server_picker = server_picker\n\n    local instance_name, err = server_picker.get(ctx)\n    if err then\n        return nil, nil, err\n    end\n    ctx.balancer_server = instance_name\n    if conf.fallback_strategy == \"instance_health_and_rate_limiting\" or -- for backwards compatible\n       fallback_strategy_has(conf.fallback_strategy, \"rate_limiting\") then\n        local ai_rate_limiting = require(\"apisix.plugins.ai-rate-limiting\")\n        for _ = 1, #conf.instances do\n            if ai_rate_limiting.check_instance_status(nil, ctx, instance_name) then\n                break\n            end\n            core.log.info(\"ai instance: \", instance_name,\n                             \" is not available, try to pick another one\")\n            server_picker.after_balance(ctx, true)\n            instance_name, err = server_picker.get(ctx)\n            if err then\n                return nil, nil, err\n            end\n            ctx.balancer_server = instance_name\n        end\n    end\n\n    local instance_conf = get_instance_conf(conf.instances, instance_name)\n    return instance_name, instance_conf\nend\n\n\nlocal function pick_ai_instance(ctx, conf, ups_tab)\n    local instance_name, instance_conf, err\n    if #conf.instances == 1 then\n        instance_name = conf.instances[1].name\n        instance_conf = conf.instances[1]\n    else\n        instance_name, instance_conf, err = pick_target(ctx, conf, ups_tab)\n    end\n\n    core.log.info(\"picked instance: \", instance_name)\n    return instance_name, instance_conf, err\nend\n\n\nfunction _M.access(conf, ctx)\n    local ups_tab = {}\n    local algo = core.table.try_read_attr(conf, \"balancer\", \"algorithm\")\n    if algo == \"chash\" then\n        local hash_on = core.table.try_read_attr(conf, \"balancer\", \"hash_on\")\n        local hash_key = core.table.try_read_attr(conf, \"balancer\", \"key\")\n        ups_tab[\"key\"] = hash_key\n        ups_tab[\"hash_on\"] = hash_on\n    end\n\n    local name, ai_instance, err = pick_ai_instance(ctx, conf, ups_tab)\n    if err then\n        return 503, err\n    end\n    ctx.picked_ai_instance_name = name\n    ctx.picked_ai_instance = ai_instance\n    ctx.balancer_ip = name\n    ctx.bypass_nginx_upstream = true\nend\n\n\nlocal function retry_on_error(ctx, conf, code)\n    if not ctx.server_picker then\n        return code\n    end\n    ctx.server_picker.after_balance(ctx, true)\n    if (code == 429 and fallback_strategy_has(conf.fallback_strategy, \"http_429\")) or\n       (code >= 500 and code < 600 and\n       fallback_strategy_has(conf.fallback_strategy, \"http_5xx\")) then\n        local name, ai_instance, err = pick_ai_instance(ctx, conf)\n        if err then\n            core.log.error(\"failed to pick new AI instance: \", err)\n            return 502\n        end\n        ctx.balancer_ip = name\n        ctx.picked_ai_instance_name = name\n        ctx.picked_ai_instance = ai_instance\n        return\n    end\n    return code\nend\n\nfunction _M.before_proxy(conf, ctx)\n     return base.before_proxy(conf, ctx, function (ctx, conf, code)\n        return retry_on_error(ctx, conf, code)\n    end)\nend\n\nfunction _M.log(conf, ctx)\n    if conf.logging then\n        base.set_logging(ctx, conf.logging.summaries, conf.logging.payloads)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-proxy.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal schema = require(\"apisix.plugins.ai-proxy.schema\")\nlocal base = require(\"apisix.plugins.ai-proxy.base\")\n\nlocal require = require\nlocal pcall = pcall\n\nlocal plugin_name = \"ai-proxy\"\nlocal _M = {\n    version = 0.5,\n    priority = 1040,\n    name = plugin_name,\n    schema = schema.ai_proxy_schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema.ai_proxy_schema, conf)\n    if not ok then\n        return false, err\n    end\n    local ai_driver, err = pcall(require, \"apisix.plugins.ai-drivers.\" .. conf.provider)\n    if not ai_driver then\n        core.log.warn(\"fail to require ai provider: \", conf.provider, \", err\", err)\n        return false, \"ai provider: \" .. conf.provider .. \" is not supported.\"\n    end\n    local sa_json = core.table.try_read_attr(conf, \"auth\", \"gcp\", \"service_account_json\")\n    if sa_json then\n        local _, err = core.json.decode(sa_json)\n        if err then\n            return false, \"invalid gcp service_account_json: \" .. err\n        end\n    end\n    return ok\nend\n\n\nfunction _M.access(conf, ctx)\n    ctx.picked_ai_instance_name = \"ai-proxy-\" .. conf.provider\n    ctx.picked_ai_instance = conf\n    ctx.balancer_ip = ctx.picked_ai_instance_name\n    ctx.bypass_nginx_upstream = true\nend\n\n\n_M.before_proxy = base.before_proxy\n\nfunction _M.log(conf, ctx)\n    if conf.logging then\n        base.set_logging(ctx, conf.logging.summaries, conf.logging.payloads)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-rag/embeddings/azure_openai.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR\nlocal HTTP_OK = ngx.HTTP_OK\nlocal type = type\n\nlocal _M = {}\n\n_M.schema = {\n    type = \"object\",\n    properties = {\n        endpoint = {\n            type = \"string\",\n        },\n        api_key = {\n            type = \"string\",\n        },\n    },\n    required = { \"endpoint\", \"api_key\" }\n}\n\nfunction _M.get_embeddings(conf, body, httpc)\n    local body_tab, err = core.json.encode(body)\n    if not body_tab then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    local res, err = httpc:request_uri(conf.endpoint, {\n        method = \"POST\",\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n            [\"api-key\"] = conf.api_key,\n        },\n        body = body_tab\n    })\n\n    if not res or not res.body then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    if res.status ~= HTTP_OK then\n        return nil, res.status, res.body\n    end\n\n    local res_tab, err = core.json.decode(res.body)\n    if not res_tab then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    if type(res_tab.data) ~= \"table\" or core.table.isempty(res_tab.data) then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, res.body\n    end\n\n    local embeddings, err = core.json.encode(res_tab.data[1].embedding)\n    if not embeddings then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    return res_tab.data[1].embedding\nend\n\n\n_M.request_schema = {\n    type = \"object\",\n    properties = {\n        input = {\n            type = \"string\"\n        }\n    },\n    required = { \"input\" }\n}\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-rag/vector-search/azure_ai_search.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR\nlocal HTTP_OK = ngx.HTTP_OK\n\nlocal _M = {}\n\n_M.schema = {\n    type = \"object\",\n    properties = {\n        endpoint = {\n            type = \"string\",\n        },\n        api_key = {\n            type = \"string\",\n        },\n    },\n    required = {\"endpoint\", \"api_key\"}\n}\n\n\nfunction _M.search(conf, search_body, httpc)\n    local body = {\n        vectorQueries = {\n            {\n                kind = \"vector\",\n                vector = search_body.embeddings,\n                fields = search_body.fields\n            }\n        }\n    }\n    local final_body, err = core.json.encode(body)\n    if not final_body then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    local res, err = httpc:request_uri(conf.endpoint, {\n        method = \"POST\",\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n            [\"api-key\"] = conf.api_key,\n        },\n        body = final_body\n    })\n\n    if not res or not res.body then\n        return nil, HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    if res.status ~= HTTP_OK then\n        return nil, res.status, res.body\n    end\n\n    return res.body\nend\n\n\n_M.request_schema = {\n    type = \"object\",\n    properties = {\n        fields = {\n            type = \"string\"\n        }\n    },\n    required = { \"fields\" }\n}\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-rag.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal next    = next\nlocal require = require\nlocal ngx_req = ngx.req\n\nlocal http     = require(\"resty.http\")\nlocal core     = require(\"apisix.core\")\n\nlocal azure_openai_embeddings = require(\"apisix.plugins.ai-rag.embeddings.azure_openai\").schema\nlocal azure_ai_search_schema = require(\"apisix.plugins.ai-rag.vector-search.azure_ai_search\").schema\n\nlocal HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR\nlocal HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        type = \"object\",\n        embeddings_provider = {\n            type = \"object\",\n            properties = {\n                azure_openai = azure_openai_embeddings\n            },\n            -- ensure only one provider can be configured while implementing support for\n            -- other providers\n            required = { \"azure_openai\" },\n            maxProperties = 1,\n        },\n        vector_search_provider = {\n            type = \"object\",\n            properties = {\n                azure_ai_search = azure_ai_search_schema\n            },\n            -- ensure only one provider can be configured while implementing support for\n            -- other providers\n            required = { \"azure_ai_search\" },\n            maxProperties = 1\n        },\n    },\n    required = { \"embeddings_provider\", \"vector_search_provider\" }\n}\n\nlocal request_schema = {\n    type = \"object\",\n    properties = {\n        ai_rag = {\n            type = \"object\",\n            properties = {\n                vector_search = {},\n                embeddings = {},\n            },\n            required = { \"vector_search\", \"embeddings\" }\n        }\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1060,\n    name = \"ai-rag\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    local httpc = http.new()\n    local body_tab, err = core.request.get_json_request_body_table()\n    if not body_tab then\n        return HTTP_BAD_REQUEST, err\n    end\n    if not body_tab[\"ai_rag\"] then\n        core.log.error(\"request body must have \\\"ai-rag\\\" field\")\n        return HTTP_BAD_REQUEST\n    end\n\n    local embeddings_provider = next(conf.embeddings_provider)\n    local embeddings_provider_conf = conf.embeddings_provider[embeddings_provider]\n    local embeddings_driver = require(\"apisix.plugins.ai-rag.embeddings.\" .. embeddings_provider)\n\n    local vector_search_provider = next(conf.vector_search_provider)\n    local vector_search_provider_conf = conf.vector_search_provider[vector_search_provider]\n    local vector_search_driver = require(\"apisix.plugins.ai-rag.vector-search.\" ..\n                                        vector_search_provider)\n\n    local vs_req_schema = vector_search_driver.request_schema\n    local emb_req_schema = embeddings_driver.request_schema\n\n    request_schema.properties.ai_rag.properties.vector_search = vs_req_schema\n    request_schema.properties.ai_rag.properties.embeddings = emb_req_schema\n\n    local ok, err = core.schema.check(request_schema, body_tab)\n    if not ok then\n        core.log.error(\"request body fails schema check: \", err)\n        return HTTP_BAD_REQUEST\n    end\n\n    local embeddings, status, err = embeddings_driver.get_embeddings(embeddings_provider_conf,\n                                                        body_tab[\"ai_rag\"].embeddings, httpc)\n    if not embeddings then\n        core.log.error(\"could not get embeddings: \", err)\n        return status, err\n    end\n\n    local search_body = body_tab[\"ai_rag\"].vector_search\n    search_body.embeddings = embeddings\n    local res, status, err = vector_search_driver.search(vector_search_provider_conf,\n                                                        search_body, httpc)\n    if not res then\n        core.log.error(\"could not get vector_search result: \", err)\n        return status, err\n    end\n\n    -- remove ai_rag from request body because their purpose is served\n    -- also, these values will cause failure when proxying requests to LLM.\n    body_tab[\"ai_rag\"] = nil\n\n    if not body_tab.messages then\n        body_tab.messages = {}\n    end\n\n    local augment = {\n        role = \"user\",\n        content = res\n    }\n    core.table.insert_tail(body_tab.messages, augment)\n\n    local req_body_json, err = core.json.encode(body_tab)\n    if not req_body_json then\n        return HTTP_INTERNAL_SERVER_ERROR, err\n    end\n\n    ngx_req.set_body_data(req_body_json)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-rate-limiting.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal setmetatable = setmetatable\nlocal ipairs = ipairs\nlocal type = type\nlocal core = require(\"apisix.core\")\nlocal limit_count = require(\"apisix.plugins.limit-count.init\")\n\nlocal plugin_name = \"ai-rate-limiting\"\n\nlocal instance_limit_schema = {\n    type = \"object\",\n    properties = {\n        name = {type = \"string\"},\n        limit = {\n            oneOf = {\n                {type = \"integer\", minimum = 1},\n                {type = \"string\"},\n            },\n        },\n        time_window = {\n            oneOf = {\n                {type = \"integer\", minimum = 1},\n                {type = \"string\"},\n            },\n        }\n    },\n    required = {\"name\", \"limit\", \"time_window\"}\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        limit = {\n            oneOf = {\n                {type = \"integer\", exclusiveMinimum = 0},\n                {type = \"string\"},\n            },\n        },\n        time_window = {\n            oneOf = {\n                {type = \"integer\", exclusiveMinimum = 0},\n                {type = \"string\"},\n            },\n        },\n        show_limit_quota_header = {type = \"boolean\", default = true},\n        limit_strategy = {\n            type = \"string\",\n            enum = {\"total_tokens\", \"prompt_tokens\", \"completion_tokens\"},\n            default = \"total_tokens\",\n            description = \"The strategy to limit the tokens\"\n        },\n        instances = {\n            type = \"array\",\n            items = instance_limit_schema,\n            minItems = 1,\n        },\n        rejected_code = {\n            type = \"integer\", minimum = 200, maximum = 599, default = 503\n        },\n        rejected_msg = {\n            type = \"string\", minLength = 1\n        },\n        rules = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    count = {\n                        oneOf = {\n                            {type = \"integer\", exclusiveMinimum = 0},\n                            {type = \"string\"},\n                        },\n                    },\n                    time_window = {\n                        oneOf = {\n                            {type = \"integer\", exclusiveMinimum = 0},\n                            {type = \"string\"},\n                        },\n                    },\n                    key = {type = \"string\"},\n                    header_prefix = {\n                        type = \"string\",\n                        description = \"prefix for rate limit headers\"\n                    },\n                },\n                required = {\"count\", \"time_window\", \"key\"},\n            },\n        },\n    },\n    dependencies = {\n        limit = {\"time_window\"},\n        time_window = {\"limit\"}\n    },\n    oneOf = {\n        {\n            anyOf = {\n                {\n                    required = {\"limit\", \"time_window\"}\n                },\n                {\n                    required = {\"instances\"}\n                }\n            }\n        },\n        {\n            required = {\"rules\"},\n        }\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1030,\n    name = plugin_name,\n    schema = schema\n}\n\nlocal limit_conf_cache = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function transform_limit_conf(plugin_conf, instance_conf, instance_name)\n    local limit_conf = {\n        rejected_code = plugin_conf.rejected_code,\n        rejected_msg = plugin_conf.rejected_msg,\n        show_limit_quota_header = plugin_conf.show_limit_quota_header,\n\n        -- we may expose those fields to ai-rate-limiting later\n        policy = \"local\",\n        key_type = \"constant\",\n        allow_degradation = false,\n        sync_interval = -1,\n        limit_header = \"X-AI-RateLimit-Limit\",\n        remaining_header = \"X-AI-RateLimit-Remaining\",\n        reset_header = \"X-AI-RateLimit-Reset\",\n    }\n    if plugin_conf.rules and #plugin_conf.rules > 0 then\n        limit_conf.rules = plugin_conf.rules\n        limit_conf._meta = plugin_conf._meta\n        return limit_conf\n    end\n\n    local key = plugin_name .. \"#global\"\n    local limit = plugin_conf.limit\n    local time_window = plugin_conf.time_window\n    local name = instance_name or \"\"\n    if instance_conf then\n        name = instance_conf.name\n        key = instance_conf.name\n        limit = instance_conf.limit\n        time_window = instance_conf.time_window\n    end\n    limit_conf._vid = key\n    limit_conf.key = key\n    limit_conf._meta = plugin_conf._meta\n    limit_conf.count = limit\n    limit_conf.time_window = time_window\n    limit_conf.limit_header = \"X-AI-RateLimit-Limit-\" .. name\n    limit_conf.remaining_header = \"X-AI-RateLimit-Remaining-\" .. name\n    limit_conf.reset_header = \"X-AI-RateLimit-Reset-\" .. name\n    return limit_conf\nend\n\n\nlocal function fetch_limit_conf_kvs(conf)\n    local mt = {\n        __index = function(t, k)\n            if not conf.limit then\n                return nil\n            end\n\n            local limit_conf = transform_limit_conf(conf, nil, k)\n            t[k] = limit_conf\n            return limit_conf\n        end\n    }\n    local limit_conf_kvs = setmetatable({}, mt)\n    local conf_instances = conf.instances or {}\n    for _, limit_conf in ipairs(conf_instances) do\n        limit_conf_kvs[limit_conf.name] = transform_limit_conf(conf, limit_conf)\n    end\n    return limit_conf_kvs\nend\n\n\nfunction _M.access(conf, ctx)\n    local ai_instance_name = ctx.picked_ai_instance_name\n    if not ai_instance_name then\n        return\n    end\n\n    local limit_conf\n    if conf.rules and #conf.rules > 0 then\n        limit_conf = transform_limit_conf(conf)\n    else\n        local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs, conf)\n        limit_conf = limit_conf_kvs[ai_instance_name]\n    end\n    if not limit_conf then\n        return\n    end\n    local code, msg = limit_count.rate_limit(limit_conf, ctx, plugin_name, 1, true)\n    ctx.ai_rate_limiting = code and true or false\n    return code, msg\nend\n\n\nfunction _M.check_instance_status(conf, ctx, instance_name)\n    if conf == nil then\n        local plugins = ctx.plugins\n        for i = 1, #plugins, 2 do\n            if plugins[i][\"name\"] == plugin_name then\n                conf = plugins[i + 1]\n            end\n        end\n    end\n    if not conf then\n        return true\n    end\n\n    instance_name = instance_name or ctx.picked_ai_instance_name\n    if not instance_name then\n        return nil, \"missing instance_name\"\n    end\n\n    if type(instance_name) ~= \"string\" then\n        return nil, \"invalid instance_name\"\n    end\n\n    local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs, conf)\n    local limit_conf = limit_conf_kvs[instance_name]\n    if not limit_conf then\n        return true\n    end\n\n    local code, _ = limit_count.rate_limit(limit_conf, ctx, plugin_name, 1, true)\n    if code then\n        core.log.info(\"rate limit for instance: \", instance_name, \" code: \", code)\n        return false\n    end\n    return true\nend\n\n\nlocal function get_token_usage(conf, ctx)\n    local usage = ctx.ai_token_usage\n    if not usage then\n        return\n    end\n    return usage[conf.limit_strategy]\nend\n\n\nfunction _M.log(conf, ctx)\n    local instance_name = ctx.picked_ai_instance_name\n    if not instance_name then\n        return\n    end\n\n    if ctx.ai_rate_limiting then\n        return\n    end\n\n    local used_tokens = get_token_usage(conf, ctx)\n    if not used_tokens then\n        core.log.error(\"failed to get token usage for llm service\")\n        return\n    end\n\n    core.log.info(\"instance name: \", instance_name, \" used tokens: \", used_tokens)\n\n    local limit_conf\n    if conf.rules and #conf.rules > 0 then\n        limit_conf = transform_limit_conf(conf)\n    else\n        local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs, conf)\n        limit_conf = limit_conf_kvs[instance_name]\n    end\n    if limit_conf then\n        limit_count.rate_limit(limit_conf, ctx, plugin_name, used_tokens)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai-request-rewrite.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ai_drivers_schema = require(\"apisix.plugins.ai-drivers.schema\")\nlocal require = require\nlocal pcall = pcall\nlocal ngx = ngx\nlocal HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST\nlocal HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR\nlocal plugin_name = \"ai-request-rewrite\"\n\nlocal auth_item_schema = {\n    type = \"object\",\n    patternProperties = {\n        [\"^[a-zA-Z0-9._-]+$\"] = {\n            type = \"string\"\n        }\n    }\n}\n\nlocal auth_schema = {\n    type = \"object\",\n    properties = {\n        header = auth_item_schema,\n        query = auth_item_schema\n    },\n    additionalProperties = false\n}\n\nlocal model_options_schema = {\n    description = \"Key/value settings for the model\",\n    type = \"object\",\n    properties = {\n        model = {\n            type = \"string\",\n            description = \"Model to execute. Examples: \\\"gpt-3.5-turbo\\\" for openai, \" ..\n            \"\\\"deepseek-chat\\\" for deekseek, or \\\"qwen-turbo\\\" for openai-compatible services\"\n        }\n    },\n    additionalProperties = true\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        prompt = {\n            type = \"string\",\n            description = \"The prompt to rewrite client request.\"\n        },\n        provider = {\n            type = \"string\",\n            description = \"Name of the AI service provider.\",\n            enum = ai_drivers_schema.providers,\n        },\n        auth = auth_schema,\n        options = model_options_schema,\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 60000,\n            default = 30000,\n            description = \"Total timeout in milliseconds for requests to LLM service, \" ..\n            \"including connect, send, and read timeouts.\"\n        },\n        keepalive = {\n            type = \"boolean\",\n            default = true\n        },\n        keepalive_pool = {\n            type = \"integer\",\n            minimum = 1,\n            default = 30\n        },\n        ssl_verify = {\n            type = \"boolean\",\n            default = true\n        },\n        override = {\n            type = \"object\",\n            properties = {\n                endpoint = {\n                    type = \"string\",\n                    description = \"To be specified to override \" ..\n                    \"the endpoint of the AI service provider.\"\n                }\n            }\n        }\n    },\n    required = {\"prompt\", \"provider\", \"auth\"}\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1073,\n    name = plugin_name,\n    schema = schema\n}\n\nlocal function request_to_llm(conf, request_table, ctx)\n    local ok, ai_driver = pcall(require, \"apisix.plugins.ai-drivers.\" .. conf.provider)\n    if not ok then\n        return nil, nil, \"failed to load ai-driver: \" .. conf.provider\n    end\n\n    local extra_opts = {\n        endpoint = core.table.try_read_attr(conf, \"override\", \"endpoint\"),\n        auth = conf.auth,\n        model_options = conf.options\n    }\n    ctx.llm_request_start_time = ngx.now()\n    ctx.var.llm_request_body = request_table\n    return ai_driver:request(ctx, conf, request_table, extra_opts)\nend\n\n\nlocal function parse_llm_response(res_body)\n    local response_table, err = core.json.decode(res_body)\n\n    if err then\n        return nil, \"failed to decode llm response \" .. \", err: \" .. err\n    end\n\n    if not response_table.choices or not response_table.choices[1] then\n        return nil, \"'choices' not in llm response\"\n    end\n\n    local message = response_table.choices[1].message\n    if not message then\n        return nil, \"'message' not in llm response choices\"\n    end\n\n    local data = {\n        data = message.content\n    }\n    return core.json.encode(data)\nend\n\n\nfunction _M.check_schema(conf)\n    -- openai-compatible should be used with override.endpoint\n    if conf.provider == \"openai-compatible\" then\n        local override = conf.override\n\n        if not override or not override.endpoint then\n            return false, \"override.endpoint is required for openai-compatible provider\"\n        end\n    end\n\n    return core.schema.check(schema, conf)\nend\n\nfunction _M.lua_body_filter(conf, ctx, headers, body)\n    if ngx.status > 299 then\n        core.log.error(\"LLM service returned error status: \", ngx.status)\n        return HTTP_INTERNAL_SERVER_ERROR\n    end\n\n    -- Parse LLM response\n    local llm_response, err = parse_llm_response(body)\n    if err then\n        core.log.error(\"failed to parse LLM response: \", err)\n        return HTTP_INTERNAL_SERVER_ERROR\n    end\n\n    return ngx.OK, llm_response\nend\n\n\nfunction _M.access(conf, ctx)\n    local client_request_body, err = core.request.get_body()\n    if err then\n        core.log.warn(\"failed to get request body: \", err)\n        return HTTP_BAD_REQUEST\n    end\n\n    if not client_request_body then\n        core.log.warn(\"missing request body\")\n        return\n    end\n\n    -- Prepare request for LLM service\n    local ai_request_table = {\n        messages = {\n            {\n                role = \"system\",\n                content = conf.prompt\n            },\n            {\n                role = \"user\",\n                content = client_request_body\n            }\n        },\n        stream = false\n    }\n\n    -- Send request to LLM service\n    local code, _, err = request_to_llm(conf, ai_request_table, ctx)\n    if err then\n        core.log.error(\"failed to request LLM: \", err)\n        return HTTP_INTERNAL_SERVER_ERROR\n    end\n    if code == 429 or (code >= 500 and code < 600 ) then\n        core.log.error(\"LLM service returned error status: \", code)\n        return HTTP_INTERNAL_SERVER_ERROR\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ai.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require         = require\nlocal apisix          = require(\"apisix\")\nlocal core            = require(\"apisix.core\")\nlocal router          = require(\"apisix.router\")\nlocal get_global_rules = require(\"apisix.global_rules\").global_rules\nlocal event           = require(\"apisix.core.event\")\nlocal balancer        = require(\"ngx.balancer\")\nlocal ngx             = ngx\nlocal is_http         = ngx.config.subsystem == \"http\"\nlocal enable_keepalive = balancer.enable_keepalive and is_http\nlocal is_apisix_or, response = pcall(require, \"resty.apisix.response\")\nlocal ipairs          = ipairs\nlocal pcall           = pcall\nlocal loadstring      = loadstring\nlocal type            = type\nlocal pairs           = pairs\n\nlocal get_cache_key_func\nlocal get_cache_key_func_def_render\n\nlocal get_cache_key_func_def = [[\nreturn function(ctx)\n    local var = ctx.var\n    return var.uri\n        {% if route_flags[\"methods\"] then %}\n        .. \"#\" .. var.method\n        {% end %}\n        {% if route_flags[\"host\"] then %}\n        .. \"#\" .. var.host\n        {% end %}\nend\n]]\n\nlocal route_lrucache\n\nlocal schema = {}\n\nlocal plugin_name = \"ai\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 22900,\n    name = plugin_name,\n    schema = schema,\n    scope = \"global\",\n}\n\nlocal orig_router_http_matching\nlocal orig_handle_upstream\nlocal orig_http_balancer_phase\n\nlocal default_keepalive_pool = {}\n\nlocal function create_router_matching_cache(api_ctx)\n    orig_router_http_matching(api_ctx)\n    return core.table.deepcopy(api_ctx)\nend\n\n\nlocal function ai_router_http_matching(api_ctx)\n    core.log.info(\"route match mode: ai_match\")\n\n    local key = get_cache_key_func(api_ctx)\n    core.log.info(\"route cache key: \", key)\n    local api_ctx_cache = route_lrucache(key, nil,\n                                   create_router_matching_cache, api_ctx)\n    -- if the version has not changed, use the cached route\n    if api_ctx then\n        api_ctx.matched_route = api_ctx_cache.matched_route\n        if api_ctx_cache.curr_req_matched then\n            api_ctx.curr_req_matched = core.table.clone(api_ctx_cache.curr_req_matched)\n        end\n    end\nend\n\n\nlocal function gen_get_cache_key_func(route_flags)\n    if get_cache_key_func_def_render == nil then\n        local template = require(\"resty.template\")\n        get_cache_key_func_def_render = template.compile(get_cache_key_func_def)\n    end\n\n    local str = get_cache_key_func_def_render({route_flags = route_flags})\n    local func, err = loadstring(str)\n    if func == nil then\n        return false, err\n    else\n        local ok, err_or_function = pcall(func)\n        if not ok then\n            return false, err_or_function\n        end\n        get_cache_key_func = err_or_function\n    end\n\n    return true\nend\n\n\nlocal function ai_upstream()\n    core.log.info(\"enable sample upstream\")\nend\n\n\nlocal pool_opt\nlocal function ai_http_balancer_phase()\n    local api_ctx = ngx.ctx.api_ctx\n    if not api_ctx then\n        core.log.error(\"invalid api_ctx\")\n        return core.response.exit(500)\n    end\n\n    if is_apisix_or then\n        local ok, err = response.skip_body_filter_by_lua()\n        if not ok then\n            core.log.error(\"failed to skip body filter by lua: \", err)\n        end\n    end\n\n    local route = api_ctx.matched_route\n    local server = route.value.upstream.nodes[1]\n    if enable_keepalive then\n        local ok, err = balancer.set_current_peer(server.host, server.port or 80, pool_opt)\n        if not ok then\n            core.log.error(\"failed to set server peer [\", server.host, \":\",\n                           server.port, \"] err: \", err)\n            return ok, err\n        end\n        balancer.enable_keepalive(default_keepalive_pool.idle_timeout,\n                                  default_keepalive_pool.requests)\n    else\n        balancer.set_current_peer(server.host, server.port or 80)\n    end\nend\n\n\nlocal function routes_analyze(routes)\n    if orig_router_http_matching == nil then\n        orig_router_http_matching = router.router_http.matching\n    end\n\n    if orig_handle_upstream == nil then\n        orig_handle_upstream = apisix.handle_upstream\n    end\n\n    if orig_http_balancer_phase == nil then\n        orig_http_balancer_phase = apisix.http_balancer_phase\n    end\n\n    local route_flags = core.table.new(0, 16)\n    local route_up_flags = core.table.new(0, 12)\n\n    for _, route in ipairs(routes) do\n        if type(route) == \"table\" then\n            for key, value in pairs(route.value) do\n                -- collect route flags\n                if key == \"methods\" then\n                    route_flags[\"methods\"] = true\n                elseif key == \"host\" or key == \"hosts\" then\n                    route_flags[\"host\"] = true\n                elseif key == \"vars\" then\n                    route_flags[\"vars\"] = true\n                elseif key == \"filter_func\"then\n                    route_flags[\"filter_func\"] = true\n                elseif key == \"remote_addr\" or key == \"remote_addrs\" then\n                    route_flags[\"remote_addr\"] = true\n                elseif key == \"service\" then\n                    route_flags[\"service\"] = true\n                elseif key == \"enable_websocket\" then\n                    route_flags[\"enable_websocket\"] = true\n                elseif key == \"plugins\" then\n                    route_flags[\"plugins\"] = true\n                elseif key == \"upstream_id\" then\n                    route_flags[\"upstream_id\"] = true\n                elseif key == \"service_id\" then\n                    route_flags[\"service_id\"] = true\n                elseif key == \"plugin_config_id\" then\n                    route_flags[\"plugin_config_id\"] = true\n                elseif key == \"script\" then\n                    route_flags[\"script\"] = true\n                end\n\n                -- collect upstream flags\n                if key == \"upstream\" then\n                    if value.nodes and #value.nodes == 1 then\n                        for k, v in pairs(value) do\n                            if k == \"nodes\" then\n                                if (not core.utils.parse_ipv4(v[1].host)\n                                    and not core.utils.parse_ipv6(v[1].host)) then\n                                    route_up_flags[\"has_domain\"] = true\n                                end\n                            elseif k == \"pass_host\" and v ~= \"pass\" then\n                                route_up_flags[\"pass_host\"] = true\n                            elseif k == \"scheme\" and v ~= \"http\" then\n                                route_up_flags[\"scheme\"] = true\n                            elseif k == \"checks\" then\n                                route_up_flags[\"checks\"] = true\n                            elseif k == \"retries\" then\n                                route_up_flags[\"retries\"] = true\n                            elseif k == \"timeout\" then\n                                route_up_flags[\"timeout\"] = true\n                            elseif k == \"tls\" then\n                                route_up_flags[\"tls\"] = true\n                            elseif k == \"keepalive_pool\" then\n                                route_up_flags[\"keepalive_pool\"] = true\n                            elseif k == \"service_name\" then\n                                route_up_flags[\"service_name\"] = true\n                            end\n                        end\n                    else\n                        route_up_flags[\"more_nodes\"] = true\n                    end\n                end\n            end\n        end\n    end\n\n    local global_rules, _ = get_global_rules()\n    local global_rules_flag = global_rules and #global_rules ~= 0\n\n    if route_flags[\"vars\"] or route_flags[\"filter_func\"]\n         or route_flags[\"remote_addr\"]\n         or route_flags[\"service_id\"]\n         or route_flags[\"plugin_config_id\"]\n         or global_rules_flag then\n        router.router_http.matching = orig_router_http_matching\n    else\n        core.log.info(\"use ai plane to match route\")\n        router.router_http.matching = ai_router_http_matching\n\n        local count = #routes + 3000\n        core.log.info(\"renew route cache: count=\", count)\n        route_lrucache = core.lrucache.new({\n            count = count\n        })\n\n        local ok, err = gen_get_cache_key_func(route_flags)\n        if not ok then\n            core.log.error(\"generate get_cache_key_func failed:\", err)\n            router.router_http.matching = orig_router_http_matching\n        end\n    end\n\n    if route_flags[\"service\"]\n         or route_flags[\"script\"]\n         or route_flags[\"service_id\"]\n         or route_flags[\"upstream_id\"]\n         or route_flags[\"enable_websocket\"]\n         or route_flags[\"plugins\"]\n         or route_flags[\"plugin_config_id\"]\n         or route_up_flags[\"has_domain\"]\n         or route_up_flags[\"pass_host\"]\n         or route_up_flags[\"scheme\"]\n         or route_up_flags[\"checks\"]\n         or route_up_flags[\"retries\"]\n         or route_up_flags[\"timeout\"]\n         or route_up_flags[\"tls\"]\n         or route_up_flags[\"keepalive_pool\"]\n         or route_up_flags[\"service_name\"]\n         or route_up_flags[\"more_nodes\"]\n         or global_rules_flag then\n        apisix.handle_upstream = orig_handle_upstream\n        apisix.http_balancer_phase = orig_http_balancer_phase\n    else\n        -- replace the upstream and balancer module\n        apisix.handle_upstream = ai_upstream\n        apisix.http_balancer_phase = ai_http_balancer_phase\n    end\nend\n\n\nfunction _M.init()\n    event.register(event.CONST.BUILD_ROUTER, routes_analyze)\n    local local_conf = core.config.local_conf()\n    local up_keepalive_conf =\n                        core.table.try_read_attr(local_conf, \"nginx_config\",\n                                                 \"http\", \"upstream\")\n    default_keepalive_pool.idle_timeout =\n        core.config_util.parse_time_unit(up_keepalive_conf.keepalive_timeout)\n    default_keepalive_pool.size = up_keepalive_conf.keepalive\n    default_keepalive_pool.requests = up_keepalive_conf.keepalive_requests\n\n    pool_opt = { pool_size = default_keepalive_pool.size }\nend\n\n\nfunction _M.destroy()\n    if orig_router_http_matching then\n        router.router_http.matching = orig_router_http_matching\n        orig_router_http_matching = nil\n    end\n\n    if orig_handle_upstream then\n        apisix.handle_upstream = orig_handle_upstream\n        orig_handle_upstream = nil\n    end\n\n    if orig_http_balancer_phase then\n        apisix.http_balancer_phase = orig_http_balancer_phase\n        orig_http_balancer_phase = nil\n    end\n\n    event.unregister(event.CONST.BUILD_ROUTER)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/api-breaker.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal plugin_name = \"api-breaker\"\nlocal ngx = ngx\nlocal math = math\nlocal error = error\nlocal ipairs = ipairs\n\n\nlocal shared_buffer = ngx.shared[\"plugin-\".. plugin_name]\nif not shared_buffer then\n    error(\"failed to get ngx.shared dict when load plugin \" .. plugin_name)\nend\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        break_response_code = {\n            type = \"integer\",\n            minimum = 200,\n            maximum = 599,\n        },\n        break_response_body = {\n            type = \"string\"\n        },\n        break_response_headers = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    key = {\n                        type = \"string\",\n                        minLength = 1\n                    },\n                    value = {\n                        type = \"string\",\n                        minLength = 1\n                    }\n                },\n                required = {\"key\", \"value\"},\n            }\n        },\n        max_breaker_sec = {\n            type = \"integer\",\n            minimum = 3,\n            default = 300,\n        },\n        unhealthy = {\n            type = \"object\",\n            properties = {\n                http_statuses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"integer\",\n                        minimum = 500,\n                        maximum = 599,\n                    },\n                    uniqueItems = true,\n                    default = {500}\n                },\n                failures = {\n                    type = \"integer\",\n                    minimum = 1,\n                    default = 3,\n                }\n            },\n            default = {http_statuses = {500}, failures = 3}\n        },\n        healthy = {\n            type = \"object\",\n            properties = {\n                http_statuses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"integer\",\n                        minimum = 200,\n                        maximum = 499,\n                    },\n                    uniqueItems = true,\n                    default = {200}\n                },\n                successes = {\n                    type = \"integer\",\n                    minimum = 1,\n                    default = 3,\n                }\n            },\n            default = {http_statuses = {200}, successes = 3}\n        }\n    },\n    required = {\"break_response_code\"},\n}\n\n\nlocal function gen_healthy_key(ctx)\n    return \"healthy-\" .. core.request.get_host(ctx) .. ctx.var.uri\nend\n\n\nlocal function gen_unhealthy_key(ctx)\n    return \"unhealthy-\" .. core.request.get_host(ctx) .. ctx.var.uri\nend\n\n\nlocal function gen_lasttime_key(ctx)\n    return \"unhealthy-lasttime\" .. core.request.get_host(ctx) .. ctx.var.uri\nend\n\n\nlocal _M = {\n    version = 0.1,\n    name = plugin_name,\n    priority = 1005,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    local unhealthy_key = gen_unhealthy_key(ctx)\n    -- unhealthy counts\n    local unhealthy_count, err = shared_buffer:get(unhealthy_key)\n    if err then\n        core.log.warn(\"failed to get unhealthy_key: \",\n                      unhealthy_key, \" err: \", err)\n        return\n    end\n\n    if not unhealthy_count then\n        return\n    end\n\n    -- timestamp of the last time a unhealthy state was triggered\n    local lasttime_key = gen_lasttime_key(ctx)\n    local lasttime, err = shared_buffer:get(lasttime_key)\n    if err then\n        core.log.warn(\"failed to get lasttime_key: \",\n                      lasttime_key, \" err: \", err)\n        return\n    end\n\n    if not lasttime then\n        return\n    end\n\n    local failure_times = math.floor(unhealthy_count / conf.unhealthy.failures)\n    if failure_times < 1 then\n        failure_times = 1\n    end\n\n    -- cannot exceed the maximum value of the user configuration\n    local breaker_time = 2 ^ failure_times\n    if breaker_time > conf.max_breaker_sec then\n        breaker_time = conf.max_breaker_sec\n    end\n    core.log.info(\"breaker_time: \", breaker_time)\n\n    -- breaker\n    if lasttime + breaker_time >= ngx.time() then\n        if conf.break_response_body then\n            if conf.break_response_headers then\n                for _, value in ipairs(conf.break_response_headers) do\n                    local val = core.utils.resolve_var(value.value, ctx.var)\n                    core.response.add_header(value.key, val)\n                end\n            end\n            return conf.break_response_code, conf.break_response_body\n        end\n        return conf.break_response_code\n    end\n\n    return\nend\n\n\nfunction _M.log(conf, ctx)\n    local unhealthy_key = gen_unhealthy_key(ctx)\n    local healthy_key = gen_healthy_key(ctx)\n    local upstream_status = core.response.get_upstream_status(ctx)\n\n    if not upstream_status then\n        return\n    end\n\n    -- unhealthy process\n    if core.table.array_find(conf.unhealthy.http_statuses,\n                             upstream_status)\n    then\n        local unhealthy_count, err = shared_buffer:incr(unhealthy_key, 1, 0)\n        if err then\n            core.log.warn(\"failed to incr unhealthy_key: \", unhealthy_key,\n                          \" err: \", err)\n        end\n        core.log.info(\"unhealthy_key: \", unhealthy_key, \" count: \",\n                      unhealthy_count)\n\n        shared_buffer:delete(healthy_key)\n\n        -- whether the user-configured number of failures has been reached,\n        -- and if so, the timestamp for entering the unhealthy state.\n        if unhealthy_count % conf.unhealthy.failures == 0 then\n            shared_buffer:set(gen_lasttime_key(ctx), ngx.time(),\n                              conf.max_breaker_sec)\n            core.log.info(\"update unhealthy_key: \", unhealthy_key, \" to \",\n                          unhealthy_count)\n        end\n\n        return\n    end\n\n    -- health process\n    if not core.table.array_find(conf.healthy.http_statuses, upstream_status) then\n        return\n    end\n\n    local unhealthy_count, err = shared_buffer:get(unhealthy_key)\n    if err then\n        core.log.warn(\"failed to `get` unhealthy_key: \", unhealthy_key,\n                      \" err: \", err)\n    end\n\n    if not unhealthy_count then\n        return\n    end\n\n    local healthy_count, err = shared_buffer:incr(healthy_key, 1, 0)\n    if err then\n        core.log.warn(\"failed to `incr` healthy_key: \", healthy_key,\n                      \" err: \", err)\n    end\n\n    -- clear related status\n    if healthy_count >= conf.healthy.successes then\n        -- stat change to normal\n        core.log.info(\"change to normal, \", healthy_key, \" \", healthy_count)\n        shared_buffer:delete(gen_lasttime_key(ctx))\n        shared_buffer:delete(unhealthy_key)\n        shared_buffer:delete(healthy_key)\n    end\n\n    return\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/attach-consumer-label.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal pairs = pairs\nlocal plugin_name = \"attach-consumer-label\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        headers = {\n            type = \"object\",\n            additionalProperties = {\n                type = \"string\",\n                pattern = \"^\\\\$.*\"\n            },\n            minProperties = 1\n        },\n    },\n    required = {\"headers\"},\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2399,\n    name = plugin_name,\n    schema = schema,\n}\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\nfunction _M.before_proxy(conf, ctx)\n    -- check if the consumer is exists in the context\n    if not ctx.consumer then\n        return\n    end\n\n    local labels = ctx.consumer.labels\n    core.log.info(\"consumer username: \", ctx.consumer.username, \" labels: \",\n            core.json.delay_encode(labels))\n    if not labels then\n        return\n    end\n\n    for header, label_key in pairs(conf.headers) do\n        -- remove leading $ character\n        local label_value = labels[label_key:sub(2)]\n        core.request.set_header(ctx, header, label_value)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/authz-casbin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal casbin          = require(\"casbin\")\nlocal core            = require(\"apisix.core\")\nlocal plugin          = require(\"apisix.plugin\")\n\nlocal plugin_name = \"authz-casbin\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        model_path = { type = \"string\" },\n        policy_path = { type = \"string\" },\n        model = { type = \"string\" },\n        policy = { type = \"string\" },\n        username = { type = \"string\"}\n    },\n    oneOf = {\n        {required = {\"model_path\", \"policy_path\", \"username\"}},\n        {required = {\"model\", \"policy\", \"username\"}}\n    },\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        model = {type = \"string\"},\n        policy = {type = \"string\"},\n    },\n    required = {\"model\", \"policy\"},\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2560,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema\n}\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    local ok, err = core.schema.check(schema, conf)\n    if ok then\n        return true\n    else\n        local metadata = plugin.plugin_metadata(plugin_name)\n        if metadata and metadata.value and conf.username then\n            return true\n        end\n    end\n    return false, err\nend\n\nlocal casbin_enforcer\n\nlocal function new_enforcer_if_need(conf)\n    if conf.model_path and conf.policy_path then\n        local model_path = conf.model_path\n        local policy_path = conf.policy_path\n        if not conf.casbin_enforcer then\n            conf.casbin_enforcer = casbin:new(model_path, policy_path)\n        end\n        return true\n    end\n\n    if conf.model and conf.policy then\n        local model = conf.model\n        local policy = conf.policy\n        if not conf.casbin_enforcer then\n            conf.casbin_enforcer = casbin:newEnforcerFromText(model, policy)\n        end\n        return true\n    end\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if not (metadata and metadata.value.model and metadata.value.policy) then\n        return nil, \"not enough configuration to create enforcer\"\n    end\n\n    local modifiedIndex = metadata.modifiedIndex\n    if not casbin_enforcer or casbin_enforcer.modifiedIndex ~= modifiedIndex then\n        local model = metadata.value.model\n        local policy = metadata.value.policy\n        casbin_enforcer = casbin:newEnforcerFromText(model, policy)\n        casbin_enforcer.modifiedIndex = modifiedIndex\n    end\n    return true\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    -- creates an enforcer when request sent for the first time\n    local ok, err = new_enforcer_if_need(conf)\n    if not ok then\n        core.log.error(err)\n        return 503\n    end\n\n    local path = ctx.var.uri\n    local method = ctx.var.method\n    local headers = core.request.headers(ctx)\n    local username = headers[conf.username] or \"anonymous\"\n\n    if conf.casbin_enforcer then\n        if not conf.casbin_enforcer:enforce(username, path, method) then\n            return 403, {message = \"Access Denied\"}\n        end\n    else\n        if not casbin_enforcer:enforce(username, path, method) then\n            return 403, {message = \"Access Denied\"}\n        end\n    end\nend\n\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/authz-casdoor.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal http = require(\"resty.http\")\nlocal session = require(\"resty.session\")\nlocal ngx = ngx\nlocal rand = math.random\nlocal tostring = tostring\n\n\nlocal plugin_name = \"authz-casdoor\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        -- Note: endpoint_addr and callback_url should not end with '/'\n        endpoint_addr = {type = \"string\", pattern = \"^[^%?]+[^/]$\"},\n        client_id = {type = \"string\"},\n        client_secret = {type = \"string\"},\n        callback_url = {type = \"string\", pattern = \"^[^%?]+[^/]$\"}\n    },\n    encrypt_fields = {\"client_secret\"},\n    required = {\n        \"callback_url\", \"endpoint_addr\", \"client_id\", \"client_secret\"\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2559,\n    name = plugin_name,\n    schema = schema\n}\n\nlocal function fetch_access_token(code, conf)\n    local client = http.new()\n    local url = conf.endpoint_addr .. \"/api/login/oauth/access_token\"\n    local res, err = client:request_uri(url, {\n        method = \"POST\",\n        body =  ngx.encode_args({\n            code = code,\n            grant_type = \"authorization_code\",\n            client_id = conf.client_id,\n            client_secret = conf.client_secret\n        }),\n        headers = {\n            [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n        }\n    })\n\n    if not res then\n        return nil, nil, err\n    end\n    local data, err = core.json.decode(res.body)\n\n    if err or not data then\n        err = \"failed to parse casdoor response data: \" .. err .. \", body: \" .. res.body\n        return nil, nil, err\n    end\n\n    if not data.access_token then\n        return nil, nil,\n               \"failed when accessing token: no access_token contained\"\n    end\n    -- In the reply of casdoor, setting expires_in to 0 indicates that the access_token is invalid.\n    if not data.expires_in or data.expires_in == 0 then\n        return nil, nil, \"failed when accessing token: invalid access_token\"\n    end\n\n    return data.access_token, data.expires_in, nil\nend\n\n\nfunction _M.check_schema(conf)\n    local check = {\"endpoint_addr\", \"callback_url\"}\n    core.utils.check_https(check, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    local current_uri = ctx.var.uri\n    local session_obj, sess_err, session_present = session.open()\n    -- step 1: check whether hits the callback\n    local m, err = ngx.re.match(conf.callback_url, \".+//[^/]+(/.*)\", \"jo\")\n    if err or not m then\n        core.log.error(err)\n        return 503\n    end\n    local real_callback_url = m[1]\n    if current_uri == real_callback_url then\n        if not session_present then\n            err = \"no session found: \" .. sess_err\n            core.log.error(err)\n            return 503\n        end\n        local state_in_session = session_obj:get(\"state\")\n        if not state_in_session then\n            err = \"no state found in session\"\n            core.log.error(err)\n            return 503\n        end\n        local args = core.request.get_uri_args(ctx)\n        if not args or not args.code or not args.state then\n            err = \"failed when accessing token. Invalid code or state\"\n            core.log.error(err)\n            return 400, err\n        end\n        if args.state ~= tostring(state_in_session) then\n            err = \"invalid state\"\n            core.log.error(err)\n            return 400, err\n        end\n        if not args.code then\n            err = \"invalid code\"\n            core.log.error(err)\n            return 400, err\n        end\n        local access_token, lifetime, err =\n            fetch_access_token(args.code, conf)\n        if not access_token then\n            core.log.error(err)\n            return 503\n        end\n        local original_url = session_obj:get(\"original_uri\")\n        if not original_url then\n            err = \"no original_url found in session\"\n            core.log.error(err)\n            return 503\n        end\n        local session_obj_write = session.new {\n            cookie = {lifetime = lifetime}\n        }\n        session_obj_write:open()\n        session_obj_write:set(\"access_token\", access_token)\n        session_obj_write:save()\n        core.response.set_header(\"Location\", original_url)\n        return 302\n    end\n\n    -- step 2: check whether session exists\n    if not (session_present and session_obj:get(\"access_token\")) then\n        -- session not exists, redirect to login page\n        local state = rand(0x7fffffff)\n        local session_obj_write = session.start()\n        session_obj_write:set(\"original_uri\", current_uri)\n        session_obj_write:set(\"state\", state)\n        session_obj_write:save()\n\n        local redirect_url = conf.endpoint_addr .. \"/login/oauth/authorize?\" .. ngx.encode_args({\n            response_type = \"code\",\n            scope = \"read\",\n            state = state,\n            client_id = conf.client_id,\n            redirect_uri = conf.callback_url\n        })\n        core.response.set_header(\"Location\", redirect_url)\n        return 302\n    end\n\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/authz-keycloak.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core      = require(\"apisix.core\")\nlocal http      = require \"resty.http\"\nlocal sub_str   = string.sub\nlocal type      = type\nlocal ngx       = ngx\nlocal plugin_name = \"authz-keycloak\"\nlocal fetch_secrets    = require(\"apisix.secret\").fetch_secrets\n\nlocal log = core.log\nlocal pairs = pairs\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        discovery = {type = \"string\", minLength = 1, maxLength = 4096},\n        token_endpoint = {type = \"string\", minLength = 1, maxLength = 4096},\n        resource_registration_endpoint = {type = \"string\", minLength = 1, maxLength = 4096},\n        client_id = {type = \"string\", minLength = 1, maxLength = 100},\n        client_secret = {type = \"string\", minLength = 1, maxLength = 100},\n        grant_type = {\n            type = \"string\",\n            default=\"urn:ietf:params:oauth:grant-type:uma-ticket\",\n            enum = {\"urn:ietf:params:oauth:grant-type:uma-ticket\"},\n            minLength = 1, maxLength = 100\n        },\n        policy_enforcement_mode = {\n            type = \"string\",\n            enum = {\"ENFORCING\", \"PERMISSIVE\"},\n            default = \"ENFORCING\"\n        },\n        permissions = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                minLength = 1, maxLength = 100\n            },\n            uniqueItems = true,\n            default = {}\n        },\n        lazy_load_paths = {type = \"boolean\", default = false},\n        http_method_as_scope = {type = \"boolean\", default = false},\n        timeout = {type = \"integer\", minimum = 1000, default = 3000},\n        ssl_verify = {type = \"boolean\", default = true},\n        cache_ttl_seconds = {type = \"integer\", minimum = 1, default = 24 * 60 * 60},\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {type = \"integer\", minimum = 1000, default = 60000},\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 5},\n        access_denied_redirect_uri = {type = \"string\", minLength = 1, maxLength = 2048},\n        access_token_expires_in = {type = \"integer\", minimum = 1, default = 300},\n        access_token_expires_leeway = {type = \"integer\", minimum = 0, default = 0},\n        refresh_token_expires_in = {type = \"integer\", minimum = 1, default = 3600},\n        refresh_token_expires_leeway = {type = \"integer\", minimum = 0, default = 0},\n        password_grant_token_generation_incoming_uri = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 4096\n        },\n    },\n    encrypt_fields = {\"client_secret\"},\n    required = {\"client_id\"},\n    allOf = {\n        -- Require discovery or token endpoint.\n        {\n            anyOf = {\n                {required = {\"discovery\"}},\n                {required = {\"token_endpoint\"}}\n            }\n        },\n        -- If lazy_load_paths is true, require discovery or resource registration endpoint.\n        {\n            anyOf = {\n                {\n                    properties = {\n                        lazy_load_paths = {enum = {false}},\n                    }\n                },\n                {\n                    properties = {\n                        lazy_load_paths = {enum = {true}},\n                    },\n                    anyOf = {\n                        {required = {\"discovery\"}},\n                        {required = {\"resource_registration_endpoint\"}}\n                    }\n                }\n            }\n        }\n    }\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2000,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local check = {\"discovery\", \"token_endpoint\", \"resource_registration_endpoint\",\n                    \"access_denied_redirect_uri\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n\n    return core.schema.check(schema, conf)\nend\n\n\n-- Some auxiliary functions below heavily inspired by the excellent\n-- lua-resty-openidc module; see https://github.com/zmartzone/lua-resty-openidc\n\n\n-- Retrieve value from server-wide cache, if available.\nlocal function authz_keycloak_cache_get(type, key)\n    local dict = ngx.shared[type]\n    local value\n    if dict then\n        value = dict:get(key)\n        if value then log.debug(\"cache hit: type=\", type, \" key=\", key) end\n    end\n    return value\nend\n\n\n-- Set value in server-wide cache, if available.\nlocal function authz_keycloak_cache_set(type, key, value, exp)\n    local dict = ngx.shared[type]\n    if dict and (exp > 0) then\n        local success, err, forcible = dict:set(key, value, exp)\n        if err then\n            log.error(\"cache set: success=\", success, \" err=\", err, \" forcible=\", forcible)\n        else\n            log.debug(\"cache set: success=\", success, \" err=\", err, \" forcible=\", forcible)\n        end\n    end\nend\n\n\n-- Configure request parameters.\nlocal function authz_keycloak_configure_params(params, conf)\n    -- Keepalive options.\n    if conf.keepalive then\n        params.keepalive_timeout = conf.keepalive_timeout\n        params.keepalive_pool = conf.keepalive_pool\n    else\n        params.keepalive = conf.keepalive\n    end\n\n    -- TLS verification.\n    params.ssl_verify = conf.ssl_verify\n\n    -- Decorate parameters, maybe, and return.\n    return conf.http_request_decorator and conf.http_request_decorator(params) or params\nend\n\n\n-- Configure timeouts.\nlocal function authz_keycloak_configure_timeouts(httpc, timeout)\n    if timeout then\n        if type(timeout) == \"table\" then\n            httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0)\n        else\n            httpc:set_timeout(timeout)\n        end\n    end\nend\n\n\n-- Set outgoing proxy options.\nlocal function authz_keycloak_configure_proxy(httpc, proxy_opts)\n    if httpc and proxy_opts and type(proxy_opts) == \"table\" then\n        log.debug(\"authz_keycloak_configure_proxy : use http proxy\")\n        httpc:set_proxy_options(proxy_opts)\n    else\n        log.debug(\"authz_keycloak_configure_proxy : don't use http proxy\")\n    end\nend\n\n\n-- Get and configure HTTP client.\nlocal function authz_keycloak_get_http_client(conf)\n    local httpc = http.new()\n    authz_keycloak_configure_timeouts(httpc, conf.timeout)\n    authz_keycloak_configure_proxy(httpc, conf.proxy_opts)\n    return httpc\nend\n\n\n-- Parse the JSON result from a call to the OP.\nlocal function authz_keycloak_parse_json_response(response)\n    local err\n    local res\n\n    -- Check the response from the OP.\n    if response.status ~= 200 then\n        err = \"response indicates failure, status=\" .. response.status .. \", body=\" .. response.body\n    else\n        -- Decode the response and extract the JSON object.\n        res, err = core.json.decode(response.body)\n\n        if not res then\n            err = \"JSON decoding failed: \" .. err\n        end\n    end\n\n    return res, err\nend\n\n\n-- Get the Discovery metadata from the specified URL.\nlocal function authz_keycloak_discover(conf)\n    log.debug(\"authz_keycloak_discover: URL is: \" .. conf.discovery)\n\n    local json, err\n    local v = authz_keycloak_cache_get(\"discovery\", conf.discovery)\n\n    if not v then\n        log.debug(\"Discovery data not in cache, making call to discovery endpoint.\")\n\n        -- Make the call to the discovery endpoint.\n        local httpc = authz_keycloak_get_http_client(conf)\n\n        local params = authz_keycloak_configure_params({}, conf)\n\n        local res, error = httpc:request_uri(conf.discovery, params)\n\n        if not res then\n            err = \"Accessing discovery URL (\" .. conf.discovery .. \") failed: \" .. error\n            log.error(err)\n        else\n            log.debug(\"Response data: \" .. res.body)\n            json, err = authz_keycloak_parse_json_response(res)\n            if json then\n                authz_keycloak_cache_set(\"discovery\", conf.discovery, core.json.encode(json),\n                                         conf.cache_ttl_seconds)\n            else\n                err = \"could not decode JSON from Discovery data\" .. (err and (\": \" .. err) or '')\n                log.error(err)\n            end\n        end\n    else\n        json = core.json.decode(v)\n    end\n\n    return json, err\nend\n\n\n-- Turn a discovery url set in the conf dictionary into the discovered information.\nlocal function authz_keycloak_ensure_discovered_data(conf)\n    local err\n    if type(conf.discovery) == \"string\" then\n        local discovery\n        discovery, err = authz_keycloak_discover(conf)\n        if not err then\n            conf.discovery = discovery\n        end\n    end\n    return err\nend\n\n\n-- Get an endpoint from the configuration.\nlocal function authz_keycloak_get_endpoint(conf, endpoint)\n    if conf and conf[endpoint] then\n        -- Use explicit entry.\n        return conf[endpoint]\n    elseif conf and conf.discovery and type(conf.discovery) == \"table\" then\n        -- Use discovery data.\n        return conf.discovery[endpoint]\n    end\n\n    -- Unable to obtain endpoint.\n    return nil\nend\n\n\n-- Return the token endpoint from the configuration.\nlocal function authz_keycloak_get_token_endpoint(conf)\n    return authz_keycloak_get_endpoint(conf, \"token_endpoint\")\nend\n\n\n-- Return the resource registration endpoint from the configuration.\nlocal function authz_keycloak_get_resource_registration_endpoint(conf)\n    return authz_keycloak_get_endpoint(conf, \"resource_registration_endpoint\")\nend\n\n\n-- Return access_token expires_in value (in seconds).\nlocal function authz_keycloak_access_token_expires_in(conf, expires_in)\n    return (expires_in or conf.access_token_expires_in)\n           - 1 - conf.access_token_expires_leeway\nend\n\n\n-- Return refresh_token expires_in value (in seconds).\nlocal function authz_keycloak_refresh_token_expires_in(conf, expires_in)\n    return (expires_in or conf.refresh_token_expires_in)\n           - 1 - conf.refresh_token_expires_leeway\nend\n\n\n-- Ensure a valid service account access token is available for the configured client.\nlocal function authz_keycloak_ensure_sa_access_token(conf)\n    local client_id = conf.client_id\n    local ttl = conf.cache_ttl_seconds\n    local token_endpoint = authz_keycloak_get_token_endpoint(conf)\n\n    if not token_endpoint then\n        log.error(\"Unable to determine token endpoint.\")\n        return 503, \"Unable to determine token endpoint.\"\n    end\n\n    local session = authz_keycloak_cache_get(\"access-tokens\", token_endpoint .. \":\"\n                                             .. client_id)\n\n    if session then\n        -- Decode session string.\n        local err\n        session, err = core.json.decode(session)\n\n        if not session then\n            -- Should never happen.\n            return 500, err\n        end\n\n        local current_time = ngx.time()\n\n        if current_time < session.access_token_expiration then\n            -- Access token is still valid.\n            log.debug(\"Access token is still valid.\")\n            return session.access_token\n        else\n            -- Access token has expired.\n            log.debug(\"Access token has expired.\")\n            if session.refresh_token\n               and (not session.refresh_token_expiration\n                    or current_time < session.refresh_token_expiration) then\n                -- Try to get a new access token, using the refresh token.\n                log.debug(\"Trying to get new access token using refresh token.\")\n\n                local httpc = authz_keycloak_get_http_client(conf)\n\n                local params = {\n                    method = \"POST\",\n                    body = ngx.encode_args({\n                        grant_type = \"refresh_token\",\n                        client_id = client_id,\n                        client_secret = conf.client_secret,\n                        refresh_token = session.refresh_token,\n                    }),\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                }\n\n                params = authz_keycloak_configure_params(params, conf)\n\n                local res, err = httpc:request_uri(token_endpoint, params)\n\n                if not res then\n                    err = \"Accessing token endpoint URL (\" .. token_endpoint\n                          .. \") failed: \" .. err\n                    log.error(err)\n                    return nil, err\n                end\n\n                log.debug(\"Response data: \" .. res.body)\n                local json, err = authz_keycloak_parse_json_response(res)\n\n                if not json then\n                    err = \"Could not decode JSON from token endpoint\"\n                          .. (err and (\": \" .. err) or '.')\n                    log.error(err)\n                    return nil, err\n                end\n\n                if not json.access_token then\n                    -- Clear session.\n                    log.debug(\"Answer didn't contain a new access token. Clearing session.\")\n                    session = nil\n                else\n                    log.debug(\"Got new access token.\")\n                    -- Save access token.\n                    session.access_token = json.access_token\n\n                    -- Calculate and save access token expiry time.\n                    session.access_token_expiration = current_time\n                            + authz_keycloak_access_token_expires_in(conf, json.expires_in)\n\n                    -- Save refresh token, maybe.\n                    if json.refresh_token ~= nil then\n                        log.debug(\"Got new refresh token.\")\n                        session.refresh_token = json.refresh_token\n\n                        -- Calculate and save refresh token expiry time.\n                        session.refresh_token_expiration = current_time\n                                + authz_keycloak_refresh_token_expires_in(conf,\n                                                                          json.refresh_expires_in)\n                    end\n\n                    authz_keycloak_cache_set(\"access-tokens\",\n                                             token_endpoint .. \":\" .. client_id,\n                                             core.json.encode(session), ttl)\n                end\n            else\n                -- No refresh token available, or it has expired. Clear session.\n                log.debug(\"No or expired refresh token. Clearing session.\")\n                session = nil\n            end\n        end\n    end\n\n    if not session then\n        -- No session available. Create a new one.\n\n        log.debug(\"Getting access token for Protection API from token endpoint.\")\n        local httpc = authz_keycloak_get_http_client(conf)\n\n        local params = {\n            method = \"POST\",\n            body = ngx.encode_args({\n                grant_type = \"client_credentials\",\n                client_id = client_id,\n                client_secret = conf.client_secret,\n            }),\n            headers = {\n                [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n            }\n        }\n\n        params = authz_keycloak_configure_params(params, conf)\n\n        local current_time = ngx.time()\n\n        local res, err = httpc:request_uri(token_endpoint, params)\n\n        if not res then\n            err = \"Accessing token endpoint URL (\" .. token_endpoint .. \") failed: \" .. err\n            log.error(err)\n            return nil, err\n        end\n\n        log.debug(\"Response data: \" .. res.body)\n        local json, err = authz_keycloak_parse_json_response(res)\n\n        if not json then\n          err = \"Could not decode JSON from token endpoint\" .. (err and (\": \" .. err) or '.')\n          log.error(err)\n          return nil, err\n        end\n\n        if not json.access_token then\n            err = \"Response does not contain access_token field.\"\n            log.error(err)\n            return nil, err\n        end\n\n        session = {}\n\n        -- Save access token.\n        session.access_token = json.access_token\n\n        -- Calculate and save access token expiry time.\n        session.access_token_expiration = current_time\n                + authz_keycloak_access_token_expires_in(conf, json.expires_in)\n\n        -- Save refresh token, maybe.\n        if json.refresh_token ~= nil then\n            session.refresh_token = json.refresh_token\n\n            -- Calculate and save refresh token expiry time.\n            session.refresh_token_expiration = current_time\n                    + authz_keycloak_refresh_token_expires_in(conf, json.refresh_expires_in)\n        end\n\n        authz_keycloak_cache_set(\"access-tokens\", token_endpoint .. \":\" .. client_id,\n                                 core.json.encode(session), ttl)\n    end\n\n    return session.access_token\nend\n\n\n-- Resolve a URI to one or more resource IDs.\nlocal function authz_keycloak_resolve_resource(conf, uri, sa_access_token)\n    -- Get resource registration endpoint URL.\n    local resource_registration_endpoint = authz_keycloak_get_resource_registration_endpoint(conf)\n\n    if not resource_registration_endpoint then\n        local err = \"Unable to determine registration endpoint.\"\n        log.error(err)\n        return nil, err\n    end\n\n    log.debug(\"Resource registration endpoint: \", resource_registration_endpoint)\n\n    local httpc = authz_keycloak_get_http_client(conf)\n\n    local params = {\n        method = \"GET\",\n        query = {uri = uri, matchingUri = \"true\"},\n        headers = {\n            [\"Authorization\"] = \"Bearer \" .. sa_access_token\n        }\n    }\n\n    params = authz_keycloak_configure_params(params, conf)\n\n    local res, err = httpc:request_uri(resource_registration_endpoint, params)\n\n    if not res then\n        err = \"Accessing resource registration endpoint URL (\" .. resource_registration_endpoint\n              .. \") failed: \" .. err\n        log.error(err)\n        return nil, err\n    end\n\n    log.debug(\"Response data: \" .. res.body)\n    res.body = '{\"resources\": ' .. res.body .. '}'\n    local json, err = authz_keycloak_parse_json_response(res)\n\n    if not json then\n      err = \"Could not decode JSON from resource registration endpoint\"\n            .. (err and (\": \" .. err) or '.')\n      log.error(err)\n      return nil, err\n    end\n\n    return json.resources\nend\n\n\nlocal function evaluate_permissions(conf, ctx, token)\n    -- Ensure discovered data.\n    local err = authz_keycloak_ensure_discovered_data(conf)\n    if err then\n        return 503, err\n    end\n\n    local permission\n\n    if conf.lazy_load_paths then\n        -- Ensure service account access token.\n        local sa_access_token, err = authz_keycloak_ensure_sa_access_token(conf)\n        if err then\n            log.error(err)\n            return 503, err\n        end\n\n        -- Resolve URI to resource(s).\n        permission, err = authz_keycloak_resolve_resource(conf, ctx.var.uri,\n                                                          sa_access_token)\n\n        -- Check result.\n        if permission == nil then\n            -- No result back from resource registration endpoint.\n            log.error(err)\n            return 503, err\n        end\n    else\n        -- Use statically configured permissions.\n        permission = conf.permissions\n    end\n\n    -- Return 403 or 307 if permission is empty and enforcement mode is \"ENFORCING\".\n    if #permission == 0 and conf.policy_enforcement_mode == \"ENFORCING\" then\n        -- Return Keycloak-style message for consistency.\n        if conf.access_denied_redirect_uri then\n            core.response.set_header(\"Location\", conf.access_denied_redirect_uri)\n            return 307\n        end\n        return 403, '{\"error\":\"access_denied\",\"error_description\":\"not_authorized\"}'\n    end\n\n    -- Determine scope from HTTP method, maybe.\n    local scope\n    if conf.http_method_as_scope then\n        scope = ctx.var.request_method\n    end\n\n    if scope then\n        -- Loop over permissions and add scope.\n        for k, v in pairs(permission) do\n            if v:find(\"#\", 1, true) then\n                -- Already contains scope.\n                permission[k] = v .. \", \" .. scope\n            else\n                -- Doesn't contain scope yet.\n                permission[k] = v .. \"#\" .. scope\n            end\n        end\n    end\n\n    for k, v in pairs(permission) do\n        log.debug(\"Requesting permission \", v, \".\")\n    end\n\n    -- Get token endpoint URL.\n    local token_endpoint = authz_keycloak_get_token_endpoint(conf)\n    if not token_endpoint then\n        err = \"Unable to determine token endpoint.\"\n        log.error(err)\n        return 503, err\n    end\n    log.debug(\"Token endpoint: \", token_endpoint)\n\n    local httpc = authz_keycloak_get_http_client(conf)\n\n    local params = {\n        method = \"POST\",\n        body = ngx.encode_args({\n            grant_type = conf.grant_type,\n            audience = conf.client_id,\n            response_mode = \"decision\",\n            permission = permission\n        }),\n        headers = {\n            [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n            [\"Authorization\"] = token\n        }\n    }\n\n    params = authz_keycloak_configure_params(params, conf)\n\n    local res, err = httpc:request_uri(token_endpoint, params)\n\n    if not res then\n        err = \"Error while sending authz request to \" .. token_endpoint .. \": \" .. err\n        log.error(err)\n        return 503\n    end\n\n    log.debug(\"Response status: \", res.status, \", data: \", res.body)\n\n    if res.status == 403 then\n        -- Request permanently denied, e.g. due to lacking permissions.\n        log.debug('Request denied: HTTP 403 Forbidden. Body: ', res.body)\n        if conf.access_denied_redirect_uri then\n            core.response.set_header(\"Location\", conf.access_denied_redirect_uri)\n            return 307\n        end\n\n        return res.status, res.body\n    elseif res.status == 401 then\n        -- Request temporarily denied, e.g access token not valid.\n        log.debug('Request denied: HTTP 401 Unauthorized. Body: ', res.body)\n        return res.status, res.body\n    elseif res.status >= 400 then\n        -- Some other error. Log full response.\n        log.error('Request denied: Token endpoint returned an error (status: ',\n                  res.status, ', body: ', res.body, ').')\n        return res.status, res.body\n    end\n\n    -- Request accepted.\nend\n\n\nlocal function fetch_jwt_token(ctx)\n    local token = core.request.header(ctx, \"Authorization\")\n    if not token then\n        return nil, \"authorization header not available\"\n    end\n\n    local prefix = sub_str(token, 1, 7)\n    if prefix ~= 'Bearer ' and prefix ~= 'bearer ' then\n        return \"Bearer \" .. token\n    end\n    return token\nend\n\n-- To get new access token by calling get token api\nlocal function generate_token_using_password_grant(conf,ctx)\n    log.debug(\"generate_token_using_password_grant Function Called\")\n\n    local body, err = core.request.get_body()\n    if err or not body then\n        log.error(\"Failed to get request body: \", err)\n        return 503\n    end\n    local parameters = core.string.decode_args(body)\n\n    local username = parameters[\"username\"]\n    local password = parameters[\"password\"]\n\n    if not username then\n        local err = \"username is missing.\"\n        log.warn(err)\n        return 422, {message = err}\n    end\n    if not password then\n        local err = \"password is missing.\"\n        log.warn(err)\n        return 422, {message = err}\n    end\n\n    local client_id = conf.client_id\n\n    local token_endpoint = authz_keycloak_get_token_endpoint(conf)\n\n    if not token_endpoint then\n        local err = \"Unable to determine token endpoint.\"\n        log.error(err)\n        return 503, {message = err}\n    end\n    local httpc = authz_keycloak_get_http_client(conf)\n\n    local params = {\n        method = \"POST\",\n        body = ngx.encode_args({\n            grant_type = \"password\",\n            client_id = client_id,\n            client_secret = conf.client_secret,\n            username = username,\n            password = password\n        }),\n        headers = {\n            [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n        }\n    }\n\n    params = authz_keycloak_configure_params(params, conf)\n\n    local res, err = httpc:request_uri(token_endpoint, params)\n\n    if not res then\n        err = \"Accessing token endpoint URL (\" .. token_endpoint\n              .. \") failed: \" .. err\n        log.error(err)\n        return 401, {message = \"Accessing token endpoint URL failed.\"}\n    end\n\n    log.debug(\"Response data: \" .. res.body)\n    local json, err = authz_keycloak_parse_json_response(res)\n\n    if not json then\n        err = \"Could not decode JSON from response\"\n              .. (err and (\": \" .. err) or '.')\n        log.error(err)\n        return 401, {message = \"Could not decode JSON from response.\"}\n    end\n\n    return res.status, res.body\nend\n\nfunction _M.access(conf, ctx)\n    -- resolve secrets\n    conf = fetch_secrets(conf, true)\n    local headers = core.request.headers(ctx)\n    local need_grant_token = conf.password_grant_token_generation_incoming_uri and\n        ctx.var.request_uri == conf.password_grant_token_generation_incoming_uri and\n        headers[\"content-type\"] == \"application/x-www-form-urlencoded\" and\n        core.request.get_method() == \"POST\"\n    if need_grant_token then\n        return generate_token_using_password_grant(conf,ctx)\n    end\n    log.debug(\"hit keycloak-auth access\")\n    local jwt_token, err = fetch_jwt_token(ctx)\n    if not jwt_token then\n        log.error(\"failed to fetch JWT token: \", err)\n        return 401, {message = \"Missing JWT token in request\"}\n    end\n\n    local status, body = evaluate_permissions(conf, ctx, jwt_token)\n    if status then\n        return status, body\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/aws-lambda.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n\nlocal ngx = ngx\nlocal hmac = require(\"resty.hmac\")\nlocal hex_encode = require(\"resty.string\").to_hex\nlocal resty_sha256 = require(\"resty.sha256\")\nlocal str_strip = require(\"pl.stringx\").strip\nlocal norm_path = require(\"pl.path\").normpath\nlocal pairs = pairs\nlocal tab_concat = table.concat\nlocal tab_sort = table.sort\nlocal os = os\n\n\nlocal plugin_name = \"aws-lambda\"\nlocal plugin_version = 0.1\nlocal priority = -1899\n\nlocal ALGO = \"AWS4-HMAC-SHA256\"\n\nlocal function hmac256(key, msg)\n    return hmac:new(key, hmac.ALGOS.SHA256):final(msg)\nend\n\nlocal function sha256(msg)\n    local hash = resty_sha256:new()\n    hash:update(msg)\n    local digest = hash:final()\n    return hex_encode(digest)\nend\n\nlocal function get_signature_key(key, datestamp, region, service)\n    local kDate = hmac256(\"AWS4\" .. key, datestamp)\n    local kRegion = hmac256(kDate, region)\n    local kService = hmac256(kRegion, service)\n    local kSigning = hmac256(kService, \"aws4_request\")\n    return kSigning\nend\n\nlocal aws_authz_schema = {\n    type = \"object\",\n    properties = {\n        -- API Key based authorization\n        apikey = {type = \"string\"},\n        -- IAM role based authorization, works via aws v4 request signing\n        -- more at https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html\n        iam = {\n            type = \"object\",\n            properties = {\n                accesskey = {\n                    type = \"string\",\n                    description = \"access key id from from aws iam console\"\n                },\n                secretkey = {\n                    type = \"string\",\n                    description = \"secret access key from from aws iam console\"\n                },\n                aws_region = {\n                    type = \"string\",\n                    default = \"us-east-1\",\n                    description = \"the aws region that is receiving the request\"\n                },\n                service = {\n                    type = \"string\",\n                    default = \"execute-api\",\n                    description = \"the service that is receiving the request\"\n                }\n            },\n            required = {\"accesskey\", \"secretkey\"}\n        }\n    }\n}\n\nlocal function request_processor(conf, ctx, params)\n    local headers = params.headers\n    -- set authorization headers if not already set by the client\n    -- we are following not to overwrite the authz keys\n    if not headers[\"x-api-key\"] then\n        if conf.authorization and conf.authorization.apikey then\n            headers[\"x-api-key\"] = conf.authorization.apikey\n            return\n        end\n    end\n\n    -- performing aws v4 request signing for IAM authorization\n    -- visit https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html\n    -- to look at the pseudocode in python.\n    if headers[\"authorization\"] or not conf.authorization or not conf.authorization.iam then\n        return\n    end\n\n    -- create a date for headers and the credential string\n    local t = ngx.time()\n    local amzdate =  os.date(\"!%Y%m%dT%H%M%SZ\", t)\n    local datestamp = os.date(\"!%Y%m%d\", t) -- Date w/o time, used in credential scope\n    headers[\"X-Amz-Date\"] = amzdate\n\n    -- computing canonical uri\n    local canonical_uri = norm_path(params.path)\n    if canonical_uri ~= \"/\" then\n        if canonical_uri:sub(-1, -1) == \"/\" then\n            canonical_uri = canonical_uri:sub(1, -2)\n        end\n        if canonical_uri:sub(1, 1) ~= \"/\" then\n            canonical_uri = \"/\" .. canonical_uri\n        end\n    end\n\n    -- computing canonical query string\n    local canonical_qs = {}\n    local canonical_qs_i = 0\n    for k, v in pairs(params.query) do\n        canonical_qs_i = canonical_qs_i + 1\n        canonical_qs[canonical_qs_i] = ngx.unescape_uri(k) .. \"=\" .. ngx.unescape_uri(v)\n    end\n\n    tab_sort(canonical_qs)\n    canonical_qs = tab_concat(canonical_qs, \"&\")\n\n    -- computing canonical and signed headers\n\n    local canonical_headers, signed_headers = {}, {}\n    local signed_headers_i = 0\n    for k, v in pairs(headers) do\n        k = k:lower()\n        if k ~= \"connection\" then\n            signed_headers_i = signed_headers_i + 1\n            signed_headers[signed_headers_i] = k\n            -- strip starting and trailing spaces including strip multiple spaces into single space\n            canonical_headers[k] =  str_strip(v)\n        end\n    end\n    tab_sort(signed_headers)\n\n    for i = 1, #signed_headers do\n        local k = signed_headers[i]\n        canonical_headers[i] = k .. \":\" .. canonical_headers[k] .. \"\\n\"\n    end\n    canonical_headers = tab_concat(canonical_headers, nil, 1, #signed_headers)\n    signed_headers = tab_concat(signed_headers, \";\")\n\n    -- combining elements to form the canonical request (step-1)\n    local canonical_request = params.method:upper() .. \"\\n\"\n                        .. canonical_uri .. \"\\n\"\n                        .. (canonical_qs or \"\") .. \"\\n\"\n                        .. canonical_headers .. \"\\n\"\n                        .. signed_headers .. \"\\n\"\n                        .. sha256(params.body or \"\")\n\n    -- creating the string to sign for aws signature v4 (step-2)\n    local iam = conf.authorization.iam\n    local credential_scope = datestamp .. \"/\" .. iam.aws_region .. \"/\"\n                            .. iam.service .. \"/aws4_request\"\n    local string_to_sign = ALGO .. \"\\n\"\n                        .. amzdate .. \"\\n\"\n                        .. credential_scope .. \"\\n\"\n                        .. sha256(canonical_request)\n\n    -- calculate the signature (step-3)\n    local signature_key = get_signature_key(iam.secretkey, datestamp, iam.aws_region, iam.service)\n    local signature = hex_encode(hmac256(signature_key, string_to_sign))\n\n    -- add info to the headers (step-4)\n    headers[\"authorization\"] = ALGO .. \" Credential=\" .. iam.accesskey\n                            .. \"/\" .. credential_scope\n                            .. \", SignedHeaders=\" .. signed_headers\n                            .. \", Signature=\" .. signature\nend\n\n\nlocal serverless_obj = require(\"apisix.plugins.serverless.generic-upstream\")\n\nreturn serverless_obj(plugin_name, plugin_version, priority, request_processor, aws_authz_schema)\n"
  },
  {
    "path": "apisix/plugins/azure-functions.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n\nlocal plugin = require(\"apisix.plugin\")\nlocal plugin_name, plugin_version, priority = \"azure-functions\", 0.1, -1900\n\nlocal azure_authz_schema = {\n    type = \"object\",\n    properties = {\n        apikey = {type = \"string\"},\n        clientid = {type = \"string\"}\n    }\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        master_apikey = {type = \"string\", default = \"\"},\n        master_clientid = {type = \"string\", default = \"\"}\n    }\n}\n\nlocal function request_processor(conf, ctx, params)\n    local headers = params.headers or {}\n    -- set authorization headers if not already set by the client\n    -- we are following not to overwrite the authz keys\n    if not headers[\"x-functions-key\"] and\n            not headers[\"x-functions-clientid\"] then\n        if conf.authorization then\n            headers[\"x-functions-key\"] = conf.authorization.apikey\n            headers[\"x-functions-clientid\"] = conf.authorization.clientid\n        else\n            -- If neither api keys are set with the client request nor inside the plugin attributes\n            -- plugin will fallback to the master key (if any) present inside the metadata.\n            local metadata = plugin.plugin_metadata(plugin_name)\n            if metadata then\n                headers[\"x-functions-key\"] = metadata.value.master_apikey\n                headers[\"x-functions-clientid\"] = metadata.value.master_clientid\n            end\n        end\n    end\n\n    params.headers = headers\nend\n\n\nreturn require(\"apisix.plugins.serverless.generic-upstream\")(plugin_name,\n        plugin_version, priority, request_processor, azure_authz_schema, metadata_schema)\n"
  },
  {
    "path": "apisix/plugins/basic-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx = ngx\nlocal ngx_re = require(\"ngx.re\")\nlocal consumer = require(\"apisix.consumer\")\nlocal schema_def = require(\"apisix.schema_def\")\nlocal auth_utils = require(\"apisix.utils.auth\")\n\nlocal lrucache = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\nlocal schema = {\n    type = \"object\",\n    title = \"work with route or service object\",\n    properties = {\n        hide_credentials = {\n            type = \"boolean\",\n            default = false,\n        },\n        realm = schema_def.get_realm_schema(\"basic\"),\n    },\n    anonymous_consumer = schema_def.anonymous_consumer_schema,\n}\n\nlocal consumer_schema = {\n    type = \"object\",\n    title = \"work with consumer object\",\n    properties = {\n        username = { type = \"string\" },\n        password = { type = \"string\" },\n    },\n    encrypt_fields = {\"password\"},\n    required = {\"username\", \"password\"},\n}\n\nlocal plugin_name = \"basic-auth\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2520,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n    consumer_schema = consumer_schema\n}\n\nfunction _M.check_schema(conf, schema_type)\n    local ok, err\n    if schema_type == core.schema.TYPE_CONSUMER then\n        ok, err = core.schema.check(consumer_schema, conf)\n    else\n        ok, err = core.schema.check(schema, conf)\n    end\n\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\nlocal function extract_auth_header(authorization)\n\n    local function do_extract(auth)\n        local obj = { username = \"\", password = \"\" }\n\n        local m, err = ngx.re.match(auth, \"(?i:basic)\\\\s(.+)\", \"jo\")\n        if err then\n            -- error authorization\n            return nil, err\n        end\n\n        if not m then\n            return nil, \"Invalid authorization header format\"\n        end\n\n        local decoded = ngx.decode_base64(m[1])\n\n        if not decoded then\n            return nil, \"Failed to decode authentication header: \" .. m[1]\n        end\n\n        local res\n        res, err = ngx_re.split(decoded, \":\")\n        if err then\n            return nil, \"Split authorization err:\" .. err\n        end\n        if #res < 2 then\n            return nil, \"Split authorization err: invalid decoded data: \" .. decoded\n        end\n\n        obj.username = ngx.re.gsub(res[1], \"\\\\s+\", \"\", \"jo\")\n        obj.password = ngx.re.gsub(res[2], \"\\\\s+\", \"\", \"jo\")\n        return obj, nil\n    end\n\n    local matcher, err = lrucache(authorization, nil, do_extract, authorization)\n\n    if matcher then\n        return matcher.username, matcher.password, err\n    else\n        return \"\", \"\", err\n    end\n\nend\n\n\nlocal function find_consumer(ctx)\n    local auth_header = core.request.header(ctx, \"Authorization\")\n    if not auth_header then\n        return nil, nil, \"Missing authorization in request\"\n    end\n\n    local username, password, err = extract_auth_header(auth_header)\n    if err then\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.warn(err)\n        return nil, nil, \"Invalid authorization in request\"\n    end\n\n    local cur_consumer, consumer_conf, err = consumer.find_consumer(plugin_name,\n                                             \"username\", username)\n    if not cur_consumer then\n        err = \"failed to find user: \" .. (err or \"invalid user\")\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.warn(err)\n        return nil, nil, \"Invalid user authorization\"\n    end\n\n    if cur_consumer.auth_conf.password ~= password then\n        return nil, nil, \"Invalid user authorization\"\n    end\n\n    return cur_consumer, consumer_conf, err\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local cur_consumer, consumer_conf, err = find_consumer(ctx, conf)\n    if not cur_consumer then\n        if not conf.anonymous_consumer then\n            core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = err }\n        end\n        cur_consumer, consumer_conf, err = consumer.get_anonymous_consumer(conf.anonymous_consumer)\n        if not cur_consumer then\n            err = \"basic-auth failed to authenticate the request, code: 401. error: \" .. err\n            core.log.error(err)\n            core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = \"Invalid user authorization\" }\n        end\n    end\n    if conf.hide_credentials then\n        core.request.set_header(ctx, \"Authorization\", nil)\n    end\n\n    consumer.attach_consumer(ctx, cur_consumer, consumer_conf)\n\n    core.log.info(\"hit basic-auth access\")\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/batch-requests.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core      = require(\"apisix.core\")\nlocal http      = require(\"resty.http\")\nlocal plugin    = require(\"apisix.plugin\")\nlocal ngx       = ngx\nlocal ipairs    = ipairs\nlocal pairs     = pairs\nlocal str_find  = core.string.find\nlocal str_lower = string.lower\n\n\nlocal plugin_name = \"batch-requests\"\n\nlocal default_uri = \"/apisix/batch-requests\"\n\nlocal attr_schema = {\n    type = \"object\",\n    properties = {\n        uri = {\n            type = \"string\",\n            description = \"uri for batch-requests\",\n            default = default_uri\n        }\n    },\n}\n\nlocal schema = {\n    type = \"object\",\n}\n\nlocal default_max_body_size = 1024 * 1024 -- 1MiB\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        max_body_size = {\n            description = \"max pipeline body size in bytes\",\n            type = \"integer\",\n            exclusiveMinimum = 0,\n            default = default_max_body_size,\n        },\n    },\n}\n\nlocal method_schema = core.table.clone(core.schema.method_schema)\nmethod_schema.default = \"GET\"\n\nlocal req_schema = {\n    type = \"object\",\n    properties = {\n        query = {\n            description = \"pipeline query string\",\n            type = \"object\"\n        },\n        headers = {\n            description = \"pipeline header\",\n            type = \"object\"\n        },\n        timeout = {\n            description = \"pipeline timeout(ms)\",\n            type = \"integer\",\n            default = 30000,\n        },\n        pipeline = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"object\",\n                properties = {\n                    version = {\n                        description = \"HTTP version\",\n                        type = \"number\",\n                        enum = {1.0, 1.1},\n                        default = 1.1,\n                    },\n                    method = method_schema,\n                    path = {\n                        type = \"string\",\n                        minLength = 1,\n                    },\n                    query = {\n                        description = \"request header\",\n                        type = \"object\",\n                    },\n                    headers = {\n                        description = \"request query string\",\n                        type = \"object\",\n                    },\n                    ssl_verify = {\n                        type = \"boolean\",\n                        default = false\n                    },\n                }\n            }\n        }\n    },\n    anyOf = {\n        {required = {\"pipeline\"}},\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 4010,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n    attr_schema = attr_schema,\n    scope = \"global\",\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function check_input(data)\n    local ok, err = core.schema.check(req_schema, data)\n    if not ok then\n        return 400, {error_msg = \"bad request body: \" .. err}\n    end\nend\n\nlocal function lowercase_key_or_init(obj)\n    if not obj then\n        return {}\n    end\n\n    local lowercase_key_obj = {}\n    for k, v in pairs(obj) do\n        lowercase_key_obj[str_lower(k)] = v\n    end\n\n    return lowercase_key_obj\nend\n\nlocal function ensure_header_lowercase(data)\n    data.headers = lowercase_key_or_init(data.headers)\n\n    for i,req in ipairs(data.pipeline) do\n        req.headers = lowercase_key_or_init(req.headers)\n    end\nend\n\n\nlocal function set_common_header(data)\n    local local_conf = core.config.local_conf()\n    local real_ip_hdr = core.table.try_read_attr(local_conf, \"nginx_config\", \"http\",\n                                                 \"real_ip_header\")\n    -- we don't need to handle '_' to '-' as Nginx won't treat 'X_REAL_IP' as 'X-Real-IP'\n    real_ip_hdr = str_lower(real_ip_hdr)\n\n    local outer_headers = core.request.headers(nil)\n    for i,req in ipairs(data.pipeline) do\n        for k, v in pairs(data.headers) do\n            if not req.headers[k] then\n                req.headers[k] = v\n            end\n        end\n\n        if outer_headers then\n            for k, v in pairs(outer_headers) do\n                local is_content_header = str_find(k, \"content-\") == 1\n                -- skip header start with \"content-\"\n                if not req.headers[k] and not is_content_header then\n                    req.headers[k] = v\n                end\n            end\n        end\n\n        req.headers[real_ip_hdr] = core.request.get_remote_client_ip()\n    end\nend\n\n\nlocal function set_common_query(data)\n    if not data.query then\n        return\n    end\n\n    for i,req in ipairs(data.pipeline) do\n        if not req.query then\n            req.query = data.query\n        else\n            for k, v in pairs(data.query) do\n                if not req.query[k] then\n                    req.query[k] = v\n                end\n            end\n        end\n    end\nend\n\n\nlocal function batch_requests(ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    core.log.info(\"metadata: \", core.json.delay_encode(metadata))\n\n    local max_body_size\n    if metadata then\n        max_body_size = metadata.value.max_body_size\n    else\n        max_body_size = default_max_body_size\n    end\n\n    local req_body, err = core.request.get_body(max_body_size, ctx)\n    if err then\n        -- Nginx doesn't support 417: https://trac.nginx.org/nginx/ticket/2062\n        -- So always return 413 instead\n        return 413, { error_msg = err }\n    end\n    if not req_body then\n        return 400, {\n            error_msg = \"no request body, you should give at least one pipeline setting\"\n        }\n    end\n\n    local data, err = core.json.decode(req_body)\n    if not data then\n        return 400, {\n            error_msg = \"invalid request body: \" .. req_body .. \", err: \" .. err\n        }\n    end\n\n    local code, body = check_input(data)\n    if code then\n        return code, body\n    end\n\n    local httpc = http.new()\n    httpc:set_timeout(data.timeout)\n    local ok, err = httpc:connect(\"127.0.0.1\", ngx.var.server_port)\n    if not ok then\n        return 500, {error_msg = \"connect to apisix failed: \" .. err}\n    end\n\n    ensure_header_lowercase(data)\n    set_common_header(data)\n    set_common_query(data)\n\n    local responses, err = httpc:request_pipeline(data.pipeline)\n    if not responses then\n        return 400, {error_msg = \"request failed: \" .. err}\n    end\n\n    local aggregated_resp = {}\n    for _, resp in ipairs(responses) do\n        if not resp.status then\n            core.table.insert(aggregated_resp, {\n                status = 504,\n                reason = \"upstream timeout\"\n            })\n            goto CONTINUE\n        end\n        local sub_resp = {\n            status  = resp.status,\n            reason  = resp.reason,\n            headers = resp.headers,\n        }\n        if resp.has_body then\n            local err\n            sub_resp.body, err = resp:read_body()\n            if err then\n                sub_resp.read_body_err = err\n                core.log.error(\"read pipeline response body failed: \", err)\n            else\n                resp:read_trailers()\n            end\n        end\n        core.table.insert(aggregated_resp, sub_resp)\n        ::CONTINUE::\n    end\n    return 200, aggregated_resp\nend\n\n\nfunction _M.api()\n    local uri = default_uri\n    local attr = plugin.plugin_attr(plugin_name)\n    if attr then\n        uri = attr.uri or default_uri\n    end\n    return {\n        {\n            methods = {\"POST\"},\n            uri = uri,\n            handler = batch_requests,\n        }\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/body-transformer.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core              = require(\"apisix.core\")\nlocal xml2lua           = require(\"xml2lua\")\nlocal xmlhandler        = require(\"xmlhandler.tree\")\nlocal template          = require(\"resty.template\")\nlocal ngx               = ngx\nlocal decode_base64     = ngx.decode_base64\nlocal req_set_body_data = ngx.req.set_body_data\nlocal req_get_uri_args  = ngx.req.get_uri_args\nlocal str_format        = string.format\nlocal decode_args       = ngx.decode_args\nlocal str_find          = core.string.find\nlocal type              = type\nlocal pcall             = pcall\nlocal pairs             = pairs\nlocal next              = next\nlocal multipart         = require(\"multipart\")\nlocal setmetatable      = setmetatable\n\nlocal transform_schema = {\n    type = \"object\",\n    properties = {\n        input_format = { type = \"string\",\n                         enum = {\"xml\", \"json\", \"encoded\", \"args\", \"plain\", \"multipart\",}},\n        template = { type = \"string\" },\n        template_is_base64 = { type = \"boolean\" },\n    },\n    required = {\"template\"},\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        request = transform_schema,\n        response = transform_schema,\n    },\n    anyOf = {\n        {required = {\"request\"}},\n        {required = {\"response\"}},\n        {required = {\"request\", \"response\"}},\n    },\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 1080,\n    name     = \"body-transformer\",\n    schema   = schema,\n}\n\n\nlocal function escape_xml(s)\n    return s:gsub(\"&\", \"&amp;\")\n        :gsub(\"<\", \"&lt;\")\n        :gsub(\">\", \"&gt;\")\n        :gsub(\"'\", \"&apos;\")\n        :gsub('\"', \"&quot;\")\nend\n\n\nlocal function escape_json(s)\n    return core.json.encode(s)\nend\n\n\nlocal function remove_namespace(tbl)\n    for k, v in pairs(tbl) do\n        if type(v) == \"table\" and next(v) == nil then\n            v = \"\"\n            tbl[k] = v\n        end\n        if type(k) == \"string\" then\n            local newk = k:match(\".*:(.*)\")\n            if newk then\n                tbl[newk] = v\n                tbl[k] = nil\n            end\n            if type(v) == \"table\" then\n                remove_namespace(v)\n            end\n        end\n    end\n    return tbl\nend\n\n\nlocal decoders = {\n    xml = function(data)\n        local handler = xmlhandler:new()\n        local parser = xml2lua.parser(handler)\n        local ok, err = pcall(parser.parse, parser, data)\n        if ok then\n            return remove_namespace(handler.root)\n        else\n            return nil, err\n        end\n    end,\n    json = function(data)\n        return core.json.decode(data)\n    end,\n    encoded = function(data)\n        return decode_args(data)\n    end,\n    args = function()\n        return req_get_uri_args()\n    end,\n    multipart = function (data, content_type_header)\n        local res = multipart(data, content_type_header)\n        return res\n    end\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function transform(conf, body, typ, ctx, request_method)\n    local out = {}\n    local _multipart\n    local format = conf[typ].input_format\n    local ct = ctx.var.http_content_type\n    if typ == \"response\" then\n        ct = ngx.header.content_type\n    end\n    if (body or request_method == \"GET\") and format ~= \"plain\" then\n        local err\n        if format then\n            out, err = decoders[format](body, ct)\n            if format == \"multipart\" then\n                _multipart = out\n                out = out:get_all_with_arrays()\n            end\n            if not out then\n                err = str_format(\"%s body decode: %s\", typ, err)\n                core.log.error(err, \", body=\", body)\n                return nil, 400, err\n            end\n        else\n            core.log.warn(\"no input format to parse \", typ, \" body\")\n        end\n    end\n\n    local text = conf[typ].template\n    if (conf[typ].template_is_base64 or (format and format ~= \"encoded\" and format ~= \"args\")) then\n        text = decode_base64(text) or text\n    end\n    local ok, render = pcall(template.compile, text)\n    if not ok then\n        local err = render\n        err = str_format(\"%s template compile: %s\", typ, err)\n        core.log.error(err)\n        return nil, 503, err\n    end\n\n    setmetatable(out, {__index = {\n        _ctx = ctx,\n        _body = body,\n        _escape_xml = escape_xml,\n        _escape_json = escape_json,\n        _multipart = _multipart\n    }})\n\n    local ok, render_out = pcall(render, out)\n    if not ok then\n        local err = str_format(\"%s template rendering: %s\", typ, render_out)\n        core.log.error(err)\n        return nil, 503, err\n    end\n\n    core.log.info(typ, \" body transform output=\", render_out)\n    return render_out\nend\n\n\nlocal function set_input_format(conf, typ, ct, method)\n    if method == \"GET\" then\n        conf[typ].input_format = \"args\"\n    end\n    if conf[typ].input_format == nil and ct then\n        if ct:find(\"text/xml\") then\n            conf[typ].input_format = \"xml\"\n        elseif ct:find(\"application/json\") then\n            conf[typ].input_format = \"json\"\n        elseif str_find(ct:lower(), \"application/x-www-form-urlencoded\", nil, true) then\n            conf[typ].input_format = \"encoded\"\n        elseif str_find(ct:lower(), \"multipart/\", nil, true) then\n            conf[typ].input_format = \"multipart\"\n        end\n    end\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    if conf.request then\n        local request_method  = ngx.var.request_method\n        conf = core.table.deepcopy(conf)\n        ctx.body_transformer_conf = conf\n        local body = core.request.get_body()\n        set_input_format(conf, \"request\", ctx.var.http_content_type, request_method)\n        local out, status, err = transform(conf, body, \"request\", ctx, request_method)\n        if not out then\n            return status, { message = err }\n        end\n        req_set_body_data(out)\n    end\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    if conf.response then\n        if not ctx.body_transformer_conf then\n            conf = core.table.deepcopy(conf)\n            ctx.body_transformer_conf = conf\n        end\n        set_input_format(conf, \"response\", ngx.header.content_type)\n        core.response.clear_header_as_body_modified()\n    end\nend\n\n\nfunction _M.body_filter(_, ctx)\n    local conf = ctx.body_transformer_conf\n    if not conf then\n        return\n    end\n    if conf.response then\n        local body = core.response.hold_body_chunk(ctx)\n        if ngx.arg[2] == false and not body then\n            return\n        end\n\n        local out = transform(conf, body, \"response\", ctx)\n        if not out then\n            core.log.error(\"failed to transform response body: \", body)\n            return\n        end\n\n        ngx.arg[1] = out\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/brotli.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx = ngx\nlocal ngx_re_gmatch = ngx.re.gmatch\nlocal ngx_header = ngx.header\nlocal req_http_version = ngx.req.http_version\nlocal str_sub = string.sub\nlocal ipairs = ipairs\nlocal tonumber = tonumber\nlocal type = type\nlocal is_loaded, brotli = pcall(require, \"brotli\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        types = {\n            anyOf = {\n                {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"string\",\n                        minLength = 1,\n                    },\n                },\n                {\n                    enum = {\"*\"}\n                }\n            },\n            default = {\"text/html\"}\n        },\n        min_length = {\n            type = \"integer\",\n            minimum = 1,\n            default = 20,\n        },\n        mode = {\n            type = \"integer\",\n            minimum = 0,\n            maximum = 2,\n            default = 0,\n            -- 0: MODE_GENERIC (default),\n            -- 1: MODE_TEXT (for UTF-8 format text input)\n            -- 2: MODE_FONT (for WOFF 2.0)\n        },\n        comp_level = {\n            type = \"integer\",\n            minimum = 0,\n            maximum = 11,\n            default = 6,\n            -- follow the default value from ngx_brotli brotli_comp_level\n        },\n        lgwin = {\n            type = \"integer\",\n            default = 19,\n            -- follow the default value from ngx_brotli brotli_window\n            enum = {0,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24},\n        },\n        lgblock = {\n            type = \"integer\",\n            default = 0,\n            enum = {0,16,17,18,19,20,21,22,23,24},\n        },\n        http_version = {\n            enum = {1.1, 1.0},\n            default = 1.1,\n        },\n        vary = {\n            type = \"boolean\",\n        }\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 996,\n    name = \"brotli\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function create_brotli_compressor(mode, comp_level, lgwin, lgblock)\n    local options = {\n        mode = mode,\n        quality = comp_level,\n        lgwin = lgwin,\n        lgblock = lgblock,\n    }\n    return brotli.compressor:new(options)\nend\n\n\nlocal function check_accept_encoding(ctx)\n    local accept_encoding = core.request.header(ctx, \"Accept-Encoding\")\n    -- no Accept-Encoding\n    if not accept_encoding then\n        return false\n    end\n\n    -- single Accept-Encoding\n    if accept_encoding == \"*\" or accept_encoding == \"br\" then\n        return true\n    end\n\n    -- multi Accept-Encoding\n    local iterator, err = ngx_re_gmatch(accept_encoding,\n                                        [[([a-z\\*]+)(;q=)?([0-9.]*)?]], \"jo\")\n    if not iterator then\n        core.log.error(\"gmatch failed, error: \", err)\n        return false\n    end\n\n    local captures\n    while true do\n        captures, err = iterator()\n        if not captures then\n            break\n        end\n        if err then\n            core.log.error(\"iterator failed, error: \", err)\n            return false\n        end\n        if (captures[1] == \"br\" or captures[1] == \"*\") and\n           (not captures[2] or captures[3] ~= \"0\") then\n            return true\n        end\n    end\n\n    return false\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    if not is_loaded then\n        core.log.error(\"please check the brotli library\")\n        return\n    end\n\n    local allow_encoding = check_accept_encoding(ctx)\n    if not allow_encoding then\n        return\n    end\n\n    local content_encoded = ngx_header[\"Content-Encoding\"]\n    if content_encoded then\n        -- Don't compress if Content-Encoding is present in upstream data\n        return\n    end\n\n    local types = conf.types\n    local content_type = ngx_header[\"Content-Type\"]\n    if not content_type then\n        -- Like Nginx, don't compress if Content-Type is missing\n        return\n    end\n\n    if type(types) == \"table\" then\n        local matched = false\n        local from = core.string.find(content_type, \";\")\n        if from then\n            content_type = str_sub(content_type, 1, from - 1)\n        end\n\n        for _, ty in ipairs(types) do\n            if content_type == ty then\n                matched = true\n                break\n            end\n        end\n\n        if not matched then\n            return\n        end\n    end\n\n    local content_length = tonumber(ngx_header[\"Content-Length\"])\n    if content_length then\n        local min_length = conf.min_length\n        if content_length < min_length then\n            return\n        end\n        -- Like Nginx, don't check min_length if Content-Length is missing\n    end\n\n    local http_version = req_http_version()\n    if http_version < conf.http_version then\n        return\n    end\n\n    if conf.vary then\n        core.response.add_header(\"Vary\", \"Accept-Encoding\")\n    end\n\n    local compressor = create_brotli_compressor(conf.mode, conf.comp_level,\n                                                conf.lgwin, conf.lgblock)\n    if not compressor then\n        core.log.error(\"failed to create brotli compressor\")\n        return\n    end\n\n    ctx.brotli_matched = true\n    ctx.compressor = compressor\n    core.response.clear_header_as_body_modified()\n    core.response.add_header(\"Content-Encoding\", \"br\")\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    if not ctx.brotli_matched then\n        return\n    end\n\n    local chunk, eof = ngx.arg[1], ngx.arg[2]\n    if type(chunk) == \"string\" and chunk ~= \"\" then\n        local encode_chunk = ctx.compressor:compress(chunk)\n        ngx.arg[1] = encode_chunk .. ctx.compressor:flush()\n    end\n\n    if eof then\n        -- overwriting the arg[1], results into partial response\n        ngx.arg[1] = ngx.arg[1] .. ctx.compressor:finish()\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/cas-auth.lua",
    "content": "--\n---- Licensed to the Apache Software Foundation (ASF) under one or more\n---- contributor license agreements.  See the NOTICE file distributed with\n---- this work for additional information regarding copyright ownership.\n---- The ASF licenses this file to You under the Apache License, Version 2.0\n---- (the \"License\"); you may not use this file except in compliance with\n---- the License.  You may obtain a copy of the License at\n----\n----     http://www.apache.org/licenses/LICENSE-2.0\n----\n---- Unless required by applicable law or agreed to in writing, software\n---- distributed under the License is distributed on an \"AS IS\" BASIS,\n---- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n---- See the License for the specific language governing permissions and\n---- limitations under the License.\n----\nlocal core = require(\"apisix.core\")\nlocal http = require(\"resty.http\")\nlocal ngx = ngx\nlocal ngx_re_match = ngx.re.match\n\nlocal CAS_REQUEST_URI = \"CAS_REQUEST_URI\"\nlocal COOKIE_NAME = \"CAS_SESSION\"\nlocal COOKIE_PARAMS = \"; Path=/; HttpOnly\"\nlocal SESSION_LIFETIME = 3600\nlocal STORE_NAME = \"cas_sessions\"\n\nlocal store = ngx.shared[STORE_NAME]\n\n\nlocal plugin_name = \"cas-auth\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        idp_uri = {type = \"string\"},\n        cas_callback_uri = {type = \"string\"},\n        logout_uri = {type = \"string\"},\n    },\n    required = {\n        \"idp_uri\", \"cas_callback_uri\", \"logout_uri\"\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2597,\n    name = plugin_name,\n    schema = schema\n}\n\nfunction _M.check_schema(conf)\n    local check = {\"idp_uri\"}\n    core.utils.check_https(check, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\nlocal function uri_without_ticket(conf, ctx)\n    return ctx.var.scheme .. \"://\" .. ctx.var.host .. \":\" ..\n        ctx.var.server_port .. conf.cas_callback_uri\nend\n\nlocal function get_session_id(ctx)\n    return ctx.var[\"cookie_\" .. COOKIE_NAME]\nend\n\nlocal function set_our_cookie(name, val)\n    core.response.add_header(\"Set-Cookie\", name .. \"=\" .. val .. COOKIE_PARAMS)\nend\n\nlocal function first_access(conf, ctx)\n    local login_uri = conf.idp_uri .. \"/login?\" ..\n        ngx.encode_args({ service = uri_without_ticket(conf, ctx) })\n    core.log.info(\"first access: \", login_uri,\n        \", cookie: \", ctx.var.http_cookie, \", request_uri: \", ctx.var.request_uri)\n    set_our_cookie(CAS_REQUEST_URI, ctx.var.request_uri)\n    core.response.set_header(\"Location\", login_uri)\n    return ngx.HTTP_MOVED_TEMPORARILY\nend\n\nlocal function with_session_id(conf, ctx, session_id)\n    -- does the cookie exist in our store?\n    local user = store:get(session_id);\n    core.log.info(\"ticket=\", session_id, \", user=\", user)\n    if user == nil then\n        set_our_cookie(COOKIE_NAME, \"deleted; Max-Age=0\")\n        return first_access(conf, ctx)\n    else\n        -- refresh the TTL\n        store:set(session_id, user, SESSION_LIFETIME)\n    end\nend\n\nlocal function set_store_and_cookie(session_id, user)\n    -- place cookie into cookie store\n    local success, err, forcible = store:add(session_id, user, SESSION_LIFETIME)\n    if success then\n        if forcible then\n            core.log.info(\"CAS cookie store is out of memory\")\n        end\n        set_our_cookie(COOKIE_NAME, session_id)\n    else\n        if err == \"no memory\" then\n            core.log.emerg(\"CAS cookie store is out of memory\")\n        elseif err == \"exists\" then\n            core.log.error(\"Same CAS ticket validated twice, this should never happen!\")\n        else\n            core.log.error(\"CAS cookie store: \", err)\n        end\n    end\n    return success\nend\n\nlocal function validate(conf, ctx, ticket)\n    -- send a request to CAS to validate the ticket\n    local httpc = http.new()\n    local res, err = httpc:request_uri(conf.idp_uri ..\n        \"/serviceValidate\",\n        { query = { ticket = ticket, service = uri_without_ticket(conf, ctx) } })\n\n    if res and res.status == ngx.HTTP_OK and res.body ~= nil then\n        if core.string.find(res.body, \"<cas:authenticationSuccess>\") then\n            local m = ngx_re_match(res.body, \"<cas:user>(.*?)</cas:user>\", \"jo\");\n            if m then\n                return m[1]\n            end\n        else\n            core.log.info(\"CAS serviceValidate failed: \", res.body)\n        end\n    else\n        core.log.error(\"validate ticket failed: status=\", (res and res.status),\n            \", has_body=\", (res and res.body ~= nil or false), \", err=\", err)\n    end\n    return nil\nend\n\nlocal function validate_with_cas(conf, ctx, ticket)\n    local user = validate(conf, ctx, ticket)\n    if user and set_store_and_cookie(ticket, user) then\n        local request_uri = ctx.var[\"cookie_\" .. CAS_REQUEST_URI]\n        set_our_cookie(CAS_REQUEST_URI, \"deleted; Max-Age=0\")\n        core.log.info(\"ticket: \", ticket,\n            \", cookie: \", ctx.var.http_cookie, \", request_uri: \", request_uri, \", user=\", user)\n        core.response.set_header(\"Location\", request_uri)\n        return ngx.HTTP_MOVED_TEMPORARILY\n    else\n        return ngx.HTTP_UNAUTHORIZED, {message = \"invalid ticket\"}\n    end\nend\n\nlocal function logout(conf, ctx)\n    local session_id = get_session_id(ctx)\n    if session_id == nil then\n        return ngx.HTTP_UNAUTHORIZED\n    end\n\n    core.log.info(\"logout: ticket=\", session_id, \", cookie=\", ctx.var.http_cookie)\n    store:delete(session_id)\n    set_our_cookie(COOKIE_NAME, \"deleted; Max-Age=0\")\n\n    core.response.set_header(\"Location\", conf.idp_uri .. \"/logout\")\n    return ngx.HTTP_MOVED_TEMPORARILY\nend\n\nfunction _M.access(conf, ctx)\n    local method = core.request.get_method()\n    local uri = ctx.var.uri\n\n    if method == \"GET\" and uri == conf.logout_uri then\n        return logout(conf, ctx)\n    end\n\n    if method == \"POST\" and uri == conf.cas_callback_uri then\n        local data = core.request.get_body()\n        local ticket = data:match(\"<samlp:SessionIndex>(.*)</samlp:SessionIndex>\")\n        if ticket == nil then\n            return ngx.HTTP_BAD_REQUEST,\n                {message = \"invalid logout request from IdP, no ticket\"}\n        end\n        core.log.info(\"Back-channel logout (SLO) from IdP: LogoutRequest: \", data)\n        local session_id = ticket\n        local user = store:get(session_id);\n        if user then\n            store:delete(session_id)\n            core.log.info(\"SLO: user=\", user, \", tocket=\", ticket)\n        end\n    else\n        local session_id = get_session_id(ctx)\n        if session_id ~= nil then\n            return with_session_id(conf, ctx, session_id)\n        end\n\n        local ticket = ctx.var.arg_ticket\n        if ticket ~= nil and uri == conf.cas_callback_uri then\n            return validate_with_cas(conf, ctx, ticket)\n        else\n            return first_access(conf, ctx)\n        end\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/chaitin-waf.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal rr_balancer = require(\"apisix.balancer.roundrobin\")\nlocal plugin = require(\"apisix.plugin\")\nlocal t1k = require \"resty.t1k\"\nlocal expr = require(\"resty.expr.v1\")\n\nlocal ngx = ngx\nlocal ngx_now = ngx.now\nlocal string = string\nlocal fmt = string.format\nlocal tostring = tostring\nlocal tonumber = tonumber\nlocal ipairs = ipairs\n\nlocal plugin_name = \"chaitin-waf\"\n\nlocal vars_schema = {\n    type = \"array\",\n}\n\nlocal lrucache = core.lrucache.new({\n    ttl = 300, count = 1024\n})\n\nlocal match_schema = {\n    type = \"array\",\n    items = {\n        type = \"object\",\n        properties = {\n            vars = vars_schema\n        }\n    },\n}\n\nlocal plugin_schema = {\n    type = \"object\",\n    properties = {\n        mode = {\n            type = \"string\",\n            enum = { \"off\", \"monitor\", \"block\" }\n        },\n        match = match_schema,\n        append_waf_resp_header = {\n            type = \"boolean\",\n            default = true\n        },\n        append_waf_debug_header = {\n            type = \"boolean\",\n            default = false\n        },\n        config = {\n            type = \"object\",\n            properties = {\n                connect_timeout = {\n                    type = \"integer\",\n                },\n                send_timeout = {\n                    type = \"integer\",\n                },\n                read_timeout = {\n                    type = \"integer\",\n                },\n                req_body_size = {\n                    type = \"integer\",\n                },\n                keepalive_size = {\n                    type = \"integer\",\n                },\n                keepalive_timeout = {\n                    type = \"integer\",\n                },\n                real_client_ip = {\n                    type = \"boolean\"\n                }\n            },\n        },\n    },\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        mode = {\n            type = \"string\",\n            enum = { \"off\", \"monitor\", \"block\" }\n        },\n        nodes = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    host = {\n                        type = \"string\",\n                        pattern = \"^\\\\*?[0-9a-zA-Z-._\\\\[\\\\]:/]+$\"\n                    },\n                    port = {\n                        type = \"integer\",\n                        minimum = 1,\n                        default = 80\n                    },\n                },\n                required = { \"host\" }\n            },\n            minItems = 1,\n        },\n        config = {\n            type = \"object\",\n            properties = {\n                connect_timeout = {\n                    type = \"integer\",\n                    default = 1000 -- milliseconds\n                },\n                send_timeout = {\n                    type = \"integer\",\n                    default = 1000 -- milliseconds\n                },\n                read_timeout = {\n                    type = \"integer\",\n                    default = 1000 -- milliseconds\n                },\n                req_body_size = {\n                    type = \"integer\",\n                    default = 1024 -- milliseconds\n                },\n                -- maximum concurrent idle connections to\n                -- the SafeLine WAF detection service\n                keepalive_size = {\n                    type = \"integer\",\n                    default = 256\n                },\n                keepalive_timeout = {\n                    type = \"integer\",\n                    default = 60000 -- milliseconds\n                },\n                real_client_ip = {\n                    type = \"boolean\",\n                    default = true\n                }\n            },\n            default = {},\n        },\n    },\n    required = { \"nodes\" },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2700,\n    name = plugin_name,\n    schema = plugin_schema,\n    metadata_schema = metadata_schema\n}\n\nlocal global_server_picker\n\nlocal HEADER_CHAITIN_WAF = \"X-APISIX-CHAITIN-WAF\"\nlocal HEADER_CHAITIN_WAF_ERROR = \"X-APISIX-CHAITIN-WAF-ERROR\"\nlocal HEADER_CHAITIN_WAF_TIME = \"X-APISIX-CHAITIN-WAF-TIME\"\nlocal HEADER_CHAITIN_WAF_STATUS = \"X-APISIX-CHAITIN-WAF-STATUS\"\nlocal HEADER_CHAITIN_WAF_ACTION = \"X-APISIX-CHAITIN-WAF-ACTION\"\nlocal HEADER_CHAITIN_WAF_SERVER = \"X-APISIX-CHAITIN-WAF-SERVER\"\nlocal blocked_message = [[{\"code\": %s, \"success\":false, ]] ..\n        [[\"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"%s\"}]]\nlocal warning_message = \"chaitin-waf monitor mode: request would have been rejected, event_id: \"\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local ok, err = core.schema.check(plugin_schema, conf)\n\n    if not ok then\n        return false, err\n    end\n\n    if conf.match then\n        for _, m in ipairs(conf.match) do\n            local ok, err = expr.new(m.vars)\n            if not ok then\n                return false, \"failed to validate the 'vars' expression: \" .. err\n            end\n        end\n    end\n\n    return true\nend\n\n\nlocal function get_healthy_chaitin_server_nodes(metadata, checker)\n    local nodes = metadata.nodes\n    local new_nodes = core.table.new(0, #nodes)\n\n    for i = 1, #nodes do\n        local host, port = nodes[i].host, nodes[i].port\n        new_nodes[host .. \":\" .. tostring(port)] = 1\n    end\n\n    return new_nodes\nend\n\n\nlocal function get_chaitin_server(metadata, ctx)\n    if not global_server_picker or global_server_picker.upstream ~= metadata.value.nodes then\n        local up_nodes = get_healthy_chaitin_server_nodes(metadata.value)\n        if core.table.nkeys(up_nodes) == 0 then\n            return nil, nil, \"no healthy nodes\"\n        end\n        core.log.info(\"chaitin-waf nodes: \", core.json.delay_encode(up_nodes))\n\n        global_server_picker = rr_balancer.new(up_nodes, metadata.value.nodes)\n    end\n\n    local server = global_server_picker.get(ctx)\n    local host, port, err = core.utils.parse_addr(server)\n    if err then\n        return nil, nil, err\n    end\n\n    return host, port, nil\nend\n\n\nlocal function check_match(conf, ctx)\n    if not conf.match or #conf.match == 0 then\n        return true\n    end\n\n    for _, match in ipairs(conf.match) do\n        local cache_key = tostring(match.vars)\n\n        local exp, err = lrucache(cache_key, nil, function(vars)\n            return expr.new(vars)\n        end, match.vars)\n\n        if not exp then\n            local msg = \"failed to create match expression for \" ..\n                        tostring(match.vars) .. \", err: \" .. tostring(err)\n            return false, msg\n        end\n\n        local matched = exp:eval(ctx.var)\n        if matched then\n            return true\n        end\n    end\n\n    return false\nend\n\n\nlocal function get_conf(conf, metadata)\n    local t = {\n        mode = \"monitor\",\n        real_client_ip = true,\n    }\n\n    if metadata.config then\n        t.connect_timeout = metadata.config.connect_timeout\n        t.send_timeout = metadata.config.send_timeout\n        t.read_timeout = metadata.config.read_timeout\n        t.req_body_size = metadata.config.req_body_size\n        t.keepalive_size = metadata.config.keepalive_size\n        t.keepalive_timeout = metadata.config.keepalive_timeout\n        t.real_client_ip = metadata.config.real_client_ip or t.real_client_ip\n    end\n\n    if conf.config then\n        t.connect_timeout = conf.config.connect_timeout\n        t.send_timeout = conf.config.send_timeout\n        t.read_timeout = conf.config.read_timeout\n        t.req_body_size = conf.config.req_body_size\n        t.keepalive_size = conf.config.keepalive_size\n        t.keepalive_timeout = conf.config.keepalive_timeout\n        t.real_client_ip = conf.config.real_client_ip or t.real_client_ip\n    end\n\n    t.mode = conf.mode or metadata.mode or t.mode\n\n    return t\nend\n\n\nlocal function do_access(conf, ctx)\n    local extra_headers = {}\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if not core.table.try_read_attr(metadata, \"value\", \"nodes\") then\n        extra_headers[HEADER_CHAITIN_WAF] = \"err\"\n        extra_headers[HEADER_CHAITIN_WAF_ERROR] = \"missing metadata\"\n        return 500, nil, extra_headers\n    end\n\n    local host, port, err = get_chaitin_server(metadata, ctx)\n    if err then\n        extra_headers[HEADER_CHAITIN_WAF] = \"unhealthy\"\n        extra_headers[HEADER_CHAITIN_WAF_ERROR] = tostring(err)\n\n        return 500, nil, extra_headers\n    end\n\n    core.log.info(\"picked chaitin-waf server: \", host, \":\", port)\n    local t = get_conf(conf, metadata.value)\n    t.host = host\n    t.port = port\n\n    extra_headers[HEADER_CHAITIN_WAF_SERVER] = host\n\n    local mode = t.mode or \"monitor\"\n    if mode == \"off\" then\n        extra_headers[HEADER_CHAITIN_WAF] = \"off\"\n        return nil, nil, extra_headers\n    end\n\n    local match, err = check_match(conf, ctx)\n    if not match then\n        if err then\n            extra_headers[HEADER_CHAITIN_WAF] = \"err\"\n            extra_headers[HEADER_CHAITIN_WAF_ERROR] = tostring(err)\n            return 500, nil, extra_headers\n        else\n            extra_headers[HEADER_CHAITIN_WAF] = \"no\"\n            return nil, nil, extra_headers\n        end\n    end\n\n    if t.real_client_ip then\n        t.client_ip = ctx.var.http_x_forwarded_for or ctx.var.remote_addr\n    else\n        t.client_ip = ctx.var.remote_addr\n    end\n\n    local start_time = ngx_now() * 1000\n    local ok, err, result = t1k.do_access(t, false)\n\n    extra_headers[HEADER_CHAITIN_WAF_TIME] = ngx_now() * 1000 - start_time\n\n    if not ok then\n        extra_headers[HEADER_CHAITIN_WAF] = \"waf-err\"\n        local err_msg = tostring(err)\n        if core.string.find(err_msg, \"timeout\") then\n            extra_headers[HEADER_CHAITIN_WAF] = \"timeout\"\n        end\n        extra_headers[HEADER_CHAITIN_WAF_ERROR] = tostring(err)\n\n        if mode == \"monitor\" then\n            core.log.warn(\"chaitin-waf monitor mode: detected waf error - \", err_msg)\n            return nil, nil, extra_headers\n        end\n\n        return 500, nil, extra_headers\n    else\n        extra_headers[HEADER_CHAITIN_WAF] = \"yes\"\n        extra_headers[HEADER_CHAITIN_WAF_ACTION] = \"pass\"\n    end\n\n    local code = 200\n    extra_headers[HEADER_CHAITIN_WAF_STATUS] = code\n\n    if result and result.status and result.status ~= 200 and result.event_id then\n        extra_headers[HEADER_CHAITIN_WAF_STATUS] = result.status\n        extra_headers[HEADER_CHAITIN_WAF_ACTION] = \"reject\"\n\n        if mode == \"monitor\" then\n            core.log.warn(warning_message, result.event_id)\n            return nil, nil, extra_headers\n        end\n\n        core.log.error(\"request rejected by chaitin-waf, event_id: \" .. result.event_id)\n\n        return tonumber(result.status),\n               fmt(blocked_message, result.status, result.event_id) .. \"\\n\",\n               extra_headers\n    end\n\n    return nil, nil, extra_headers\nend\n\n\nfunction _M.access(conf, ctx)\n    local code, msg, extra_headers = do_access(conf, ctx)\n\n    if not conf.append_waf_debug_header then\n        extra_headers[HEADER_CHAITIN_WAF_ERROR] = nil\n        extra_headers[HEADER_CHAITIN_WAF_SERVER] = nil\n    end\n\n    if conf.append_waf_resp_header then\n        core.response.set_header(extra_headers)\n    end\n\n    return code, msg\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    t1k.do_header_filter()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/clickhouse-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal fetch_secrets   = require(\"apisix.secret\").fetch_secrets\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal core            = require(\"apisix.core\")\nlocal http            = require(\"resty.http\")\nlocal url             = require(\"net.url\")\nlocal math_random     = math.random\n\nlocal tostring = tostring\n\nlocal plugin_name = \"clickhouse-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        -- deprecated, use \"endpoint_addrs\" instead\n        endpoint_addr = core.schema.uri_def,\n        endpoint_addrs = {items = core.schema.uri_def, type = \"array\", minItems = 1},\n        user = {type = \"string\", default = \"\"},\n        password = {type = \"string\", default = \"\"},\n        database = {type = \"string\", default = \"\"},\n        logtable = {type = \"string\", default = \"\"},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        name = {type = \"string\", default = \"clickhouse logger\"},\n        ssl_verify = {type = \"boolean\", default = true},\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    oneOf = {\n        {required = {\"endpoint_addr\", \"user\", \"password\", \"database\", \"logtable\"}},\n        {required = {\"endpoint_addrs\", \"user\", \"password\", \"database\", \"logtable\"}}\n    },\n    encrypt_fields = {\"password\"},\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 398,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    local check = {\"endpoint_addrs\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function send_http_data(conf, log_message)\n    conf = fetch_secrets(conf, true)\n\n    local err_msg\n    local res = true\n    local selected_endpoint_addr\n    if conf.endpoint_addr then\n        selected_endpoint_addr = conf.endpoint_addr\n    else\n        selected_endpoint_addr = conf.endpoint_addrs[math_random(#conf.endpoint_addrs)]\n    end\n    local url_decoded = url.parse(selected_endpoint_addr)\n    local host = url_decoded.host\n    local port = url_decoded.port\n\n    core.log.info(\"sending a batch logs to \", selected_endpoint_addr)\n\n    if not port then\n        if url_decoded.scheme == \"https\" then\n            port = 443\n        else\n            port = 80\n        end\n    end\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout * 1000)\n    local ok, err = httpc:connect(host, port)\n\n    if not ok then\n        return false, \"failed to connect to host[\" .. host .. \"] port[\"\n            .. tostring(port) .. \"] \" .. err\n    end\n\n    if url_decoded.scheme == \"https\" then\n        ok, err = httpc:ssl_handshake(true, host, conf.ssl_verify)\n        if not ok then\n            return false, \"failed to perform SSL with host[\" .. host .. \"] \"\n                .. \"port[\" .. tostring(port) .. \"] \" .. err\n        end\n    end\n\n    local httpc_res, httpc_err = httpc:request({\n        method = \"POST\",\n        path = url_decoded.path,\n        query = url_decoded.query,\n        body = \"INSERT INTO \" .. conf.logtable ..\" FORMAT JSONEachRow \" .. log_message,\n        headers = {\n            [\"Host\"] = url_decoded.host,\n            [\"Content-Type\"] = \"application/json\",\n            [\"X-ClickHouse-User\"] = conf.user,\n            [\"X-ClickHouse-Key\"] = conf.password,\n            [\"X-ClickHouse-Database\"] = conf.database\n        }\n    })\n\n    if not httpc_res then\n        return false, \"error while sending data to [\" .. host .. \"] port[\"\n            .. tostring(port) .. \"] \" .. httpc_err\n    end\n\n    -- some error occurred in the server\n    if httpc_res.status >= 400 then\n        res =  false\n        err_msg = \"server returned status code[\" .. httpc_res.status .. \"] host[\"\n            .. host .. \"] port[\" .. tostring(port) .. \"] \"\n            .. \"body[\" .. httpc_res:read_body() .. \"]\"\n    end\n\n    return res, err_msg\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err\n\n        if batch_max_size == 1 then\n            data, err = core.json.encode(entries[1]) -- encode as single {}\n        else\n            local log_table = {}\n            for i = 1, #entries do\n                core.table.insert(log_table, core.json.encode(entries[i]))\n            end\n            data = core.table.concat(log_table, \" \")  -- assemble multi items as string \"{} {}\"\n        end\n\n        if not data then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        return send_http_data(conf, data)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/client-control.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal ok, apisix_ngx_client = pcall(require, \"resty.apisix.client\")\nlocal tonumber = tonumber\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        max_body_size = {\n            type = \"integer\",\n            minimum = 0,\n            description = \"Maximum message body size in bytes. No restriction when set to 0.\"\n        },\n    },\n}\n\n\nlocal plugin_name = \"client-control\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 22000,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    if not ok then\n        core.log.error(\"need to build APISIX-Runtime to support client control\")\n        return 501\n    end\n\n    if conf.max_body_size then\n        local len = tonumber(core.request.header(ctx, \"Content-Length\"))\n        if len then\n            -- if length is given in the header, check it immediately\n            if conf.max_body_size ~= 0 and len > conf.max_body_size then\n                return 413\n            end\n        end\n\n        -- then check it when reading the body\n        local ok, err = apisix_ngx_client.set_client_max_body_size(conf.max_body_size)\n        if not ok then\n            core.log.error(\"failed to set client max body size: \", err)\n            return 503\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/consumer-restriction.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ipairs    = ipairs\nlocal core      = require(\"apisix.core\")\nlocal ngx       = ngx\nlocal schema = {\n    type = \"object\",\n    properties = {\n        type = {\n            type = \"string\",\n            enum = {\"consumer_name\", \"service_id\", \"route_id\", \"consumer_group_id\"},\n            default = \"consumer_name\"\n        },\n        blacklist = {\n            type = \"array\",\n            minItems = 1,\n            items = {type = \"string\"}\n        },\n        whitelist = {\n            type = \"array\",\n            minItems = 1,\n            items = {type = \"string\"}\n        },\n        allowed_by_methods = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    user = {\n                        type = \"string\"\n                    },\n                    methods = {\n                        type = \"array\",\n                        minItems = 1,\n                        items = core.schema.method_schema,\n                    }\n                }\n            }\n        },\n        rejected_code = {type = \"integer\", minimum = 200, default = 403},\n        rejected_msg = {type = \"string\"}\n    },\n    anyOf = {\n        {required = {\"blacklist\"}},\n        {required = {\"whitelist\"}},\n        {required = {\"allowed_by_methods\"}}\n    },\n}\n\nlocal plugin_name = \"consumer-restriction\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 2400,\n    name = plugin_name,\n    schema = schema,\n}\n\nlocal fetch_val_funcs = {\n    [\"route_id\"] = function(ctx)\n        return ctx.route_id\n    end,\n    [\"service_id\"] = function(ctx)\n        return ctx.service_id\n    end,\n    [\"consumer_name\"] = function(ctx)\n        return ctx.consumer_name\n    end,\n    [\"consumer_group_id\"] = function (ctx)\n        return ctx.consumer_group_id\n    end\n}\n\nlocal function is_include(value, tab)\n    for k,v in ipairs(tab) do\n        if v == value then\n            return true\n        end\n    end\n    return false\nend\n\nlocal function is_method_allowed(allowed_methods, method, user)\n    for _, value in ipairs(allowed_methods) do\n        if value.user == user then\n            for _, allowed_method in ipairs(value.methods) do\n                if allowed_method == method then\n                    return true\n                end\n            end\n            return false\n        end\n    end\n    return true\nend\n\nlocal function reject(conf)\n    if conf.rejected_msg then\n        return conf.rejected_code , { message = conf.rejected_msg }\n    end\n    return conf.rejected_code , { message = \"The \" .. conf.type .. \" is forbidden.\"}\nend\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n   if not ok then\n        return false, err\n   end\n   return true\nend\n\nfunction _M.access(conf, ctx)\n    local value = fetch_val_funcs[conf.type](ctx)\n    local method = ngx.req.get_method()\n\n    if not value then\n        local err_msg = \"The request is rejected, please check the \"\n                        .. conf.type .. \" for this request\"\n        return 401, { message = err_msg}\n    end\n    core.log.info(\"value: \", value)\n\n    local block = false\n    local whitelisted = false\n\n    if conf.blacklist and #conf.blacklist > 0 then\n        if is_include(value, conf.blacklist) then\n            return reject(conf)\n        end\n    end\n\n    if conf.whitelist and #conf.whitelist > 0 then\n        whitelisted = is_include(value, conf.whitelist)\n        if not whitelisted then\n            block = true\n        end\n    end\n\n    if conf.allowed_by_methods and #conf.allowed_by_methods > 0 and not whitelisted then\n        if not is_method_allowed(conf.allowed_by_methods, method, value) then\n            block = true\n        end\n    end\n\n    if block then\n        return reject(conf)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/cors.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core        = require(\"apisix.core\")\nlocal plugin      = require(\"apisix.plugin\")\nlocal ngx         = ngx\nlocal plugin_name = \"cors\"\nlocal str_find    = core.string.find\nlocal re_gmatch   = ngx.re.gmatch\nlocal re_compile = require(\"resty.core.regex\").re_match_compile\nlocal re_find = ngx.re.find\nlocal ipairs = ipairs\nlocal origins_pattern = [[^(\\*|\\*\\*|null|\\w+://[^,]+(,\\w+://[^,]+)*)$]]\n\nlocal TYPE_ACCESS_CONTROL_ALLOW_ORIGIN = \"ACAO\"\nlocal TYPE_TIMING_ALLOW_ORIGIN = \"TAO\"\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        allow_origins = {\n            type = \"object\",\n            additionalProperties = {\n                type = \"string\",\n                pattern = origins_pattern\n            }\n        },\n    },\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        allow_origins = {\n            description =\n                \"you can use '*' to allow all origins when no credentials,\" ..\n                \"'**' to allow forcefully(it will bring some security risks, be carefully),\" ..\n                \"multiple origin use ',' to split. default: *.\",\n            type = \"string\",\n            pattern = origins_pattern,\n            default = \"*\"\n        },\n        allow_methods = {\n            description =\n                \"you can use '*' to allow all methods when no credentials,\" ..\n                \"'**' to allow forcefully(it will bring some security risks, be carefully),\" ..\n                \"multiple method use ',' to split. default: *.\",\n            type = \"string\",\n            default = \"*\"\n        },\n        allow_headers = {\n            description =\n                \"you can use '*' to allow all header when no credentials,\" ..\n                \"'**' to allow forcefully(it will bring some security risks, be carefully),\" ..\n                \"multiple header use ',' to split. default: *.\",\n            type = \"string\",\n            default = \"*\"\n        },\n        expose_headers = {\n            description =\n                \"multiple header use ',' to split.\" ..\n                \"If not specified, no custom headers are exposed.\",\n            type = \"string\"\n        },\n        max_age = {\n            description =\n                \"maximum number of seconds the results can be cached.\" ..\n                \"-1 means no cached, the max value is depend on browser,\" ..\n                \"more details plz check MDN. default: 5.\",\n            type = \"integer\",\n            default = 5\n        },\n        allow_credential = {\n            description =\n                \"allow client append credential. according to CORS specification,\" ..\n                \"if you set this option to 'true', you can not use '*' for other options.\",\n            type = \"boolean\",\n            default = false\n        },\n        allow_origins_by_regex = {\n            type = \"array\",\n            description =\n                \"you can use regex to allow specific origins when no credentials,\" ..\n                \"for example use [.*\\\\.test.com$] to allow a.test.com and b.test.com\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n                maxLength = 4096,\n            },\n            minItems = 1,\n            uniqueItems = true,\n        },\n        allow_origins_by_metadata = {\n            type = \"array\",\n            description =\n                \"set allowed origins by referencing origins in plugin metadata\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n                maxLength = 4096,\n            },\n            minItems = 1,\n            uniqueItems = true,\n        },\n        timing_allow_origins = {\n            description =\n                \"you can use '*' to allow all origins which can view timing information \" ..\n                \"when no credentials,\" ..\n                \"'**' to allow forcefully (it will bring some security risks, be careful),\" ..\n                \"multiple origin use ',' to split. default: nil\",\n            type = \"string\",\n            pattern = origins_pattern\n        },\n        timing_allow_origins_by_regex = {\n            type = \"array\",\n            description =\n                \"you can use regex to allow specific origins which can view timing information,\" ..\n                \"for example use [.*\\\\.test.com] to allow a.test.com and b.test.com\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n                maxLength = 4096,\n            },\n            minItems = 1,\n            uniqueItems = true,\n        },\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 4000,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n}\n\n\nlocal function create_multiple_origin_cache(allow_origins)\n    if not str_find(allow_origins, \",\") then\n        return nil\n    end\n    local origin_cache = {}\n    local iterator, err = re_gmatch(allow_origins, \"([^,]+)\", \"jiox\")\n    if not iterator then\n        core.log.error(\"match origins failed: \", err)\n        return nil\n    end\n    while true do\n        local origin, err = iterator()\n        if err then\n            core.log.error(\"iterate origins failed: \", err)\n            return nil\n        end\n        if not origin then\n            break\n        end\n        origin_cache[origin[0]] = true\n    end\n    return origin_cache\nend\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n    if conf.allow_credential then\n        if conf.allow_origins == \"*\" or conf.allow_methods == \"*\" or\n            conf.allow_headers == \"*\" or conf.expose_headers == \"*\" or\n            conf.timing_allow_origins == \"*\" then\n            return false, \"you can not set '*' for other option when 'allow_credential' is true\"\n        end\n    end\n    if conf.allow_origins_by_regex then\n        for i, re_rule in ipairs(conf.allow_origins_by_regex) do\n            local ok, err = re_compile(re_rule, \"j\")\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    if conf.timing_allow_origins_by_regex then\n        for i, re_rule in ipairs(conf.timing_allow_origins_by_regex) do\n            local ok, err = re_compile(re_rule, \"j\")\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nlocal function set_cors_headers(conf, ctx)\n    local allow_methods = conf.allow_methods\n    if allow_methods == \"**\" then\n        allow_methods = \"GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\"\n    end\n\n    core.response.set_header(\"Access-Control-Allow-Origin\", ctx.cors_allow_origins)\n    core.response.set_header(\"Access-Control-Allow-Methods\", allow_methods)\n    core.response.set_header(\"Access-Control-Max-Age\", conf.max_age)\n    if conf.expose_headers ~= nil and conf.expose_headers ~= \"\" then\n        core.response.set_header(\"Access-Control-Expose-Headers\", conf.expose_headers)\n    end\n    if conf.allow_headers == \"**\" then\n        core.response.set_header(\"Access-Control-Allow-Headers\",\n            core.request.header(ctx, \"Access-Control-Request-Headers\"))\n    else\n        core.response.set_header(\"Access-Control-Allow-Headers\", conf.allow_headers)\n    end\n    if conf.allow_credential then\n        core.response.set_header(\"Access-Control-Allow-Credentials\", true)\n    end\nend\n\nlocal function set_timing_headers(conf, ctx)\n    if ctx.timing_allow_origin then\n        core.response.set_header(\"Timing-Allow-Origin\", ctx.timing_allow_origin)\n    end\nend\n\n\nlocal function process_with_allow_origins(allow_origin_type, allow_origins, ctx, req_origin,\n                                          cache_key, cache_version)\n    if allow_origins == \"**\" then\n        allow_origins = req_origin or '*'\n    end\n\n    local multiple_origin, err\n    if cache_key and cache_version then\n        multiple_origin, err = lrucache(\n                cache_key, cache_version, create_multiple_origin_cache, allow_origins\n        )\n    else\n        multiple_origin, err = core.lrucache.plugin_ctx(\n                lrucache, ctx, allow_origin_type, create_multiple_origin_cache, allow_origins\n        )\n    end\n\n    if err then\n        return 500, {message = \"get multiple origin cache failed: \" .. err}\n    end\n\n    if multiple_origin then\n        if multiple_origin[req_origin] then\n            allow_origins = req_origin\n        else\n            return\n        end\n    end\n\n    return allow_origins\nend\n\nlocal function process_with_allow_origins_by_regex(allow_origin_type,\n                                                   allow_origins_by_regex, conf, ctx, req_origin)\n\n    local allow_origins_by_regex_rules_concat_conf_key =\n            \"allow_origins_by_regex_rules_concat_\" .. allow_origin_type\n\n    if not conf[allow_origins_by_regex_rules_concat_conf_key] then\n        local allow_origins_by_regex_rules = {}\n        for i, re_rule in ipairs(allow_origins_by_regex) do\n            allow_origins_by_regex_rules[i] = re_rule\n        end\n        conf[allow_origins_by_regex_rules_concat_conf_key] = core.table.concat(\n            allow_origins_by_regex_rules, \"|\")\n    end\n\n    -- core.log.warn(\"regex: \", conf[allow_origins_by_regex_rules_concat_conf_key], \"\\n \")\n    local matched = re_find(req_origin, conf[allow_origins_by_regex_rules_concat_conf_key], \"jo\")\n    if matched then\n        return req_origin\n    end\nend\n\n\nlocal function match_origins(req_origin, allow_origins)\n    return req_origin == allow_origins or allow_origins == '*'\nend\n\nlocal function process_with_allow_origins_by_metadata(allow_origin_type, allow_origins_by_metadata,\n                                                      ctx, req_origin)\n\n    if allow_origins_by_metadata == nil then\n        return\n    end\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if metadata and metadata.value.allow_origins then\n        local allow_origins_map = metadata.value.allow_origins\n        for _, key in ipairs(allow_origins_by_metadata) do\n            local allow_origins_conf = allow_origins_map[key]\n            local allow_origins = process_with_allow_origins(\n                allow_origin_type, allow_origins_conf, ctx, req_origin,\n                plugin_name .. \"#\" .. key, metadata.modifiedIndex\n            )\n            if match_origins(req_origin, allow_origins) then\n                return req_origin\n            end\n        end\n    end\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    -- save the original request origin as it may be changed at other phase\n    ctx.original_request_origin = core.request.header(ctx, \"Origin\")\n    if ctx.var.request_method == \"OPTIONS\" then\n        return 200\n    end\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    local req_origin =  ctx.original_request_origin\n    -- If allow_origins_by_regex is not nil, should be matched to it only\n    local allow_origins\n    local allow_origins_local = false\n    if conf.allow_origins_by_metadata then\n        allow_origins = process_with_allow_origins_by_metadata(\n            TYPE_ACCESS_CONTROL_ALLOW_ORIGIN, conf.allow_origins_by_metadata, ctx, req_origin\n        )\n        if not match_origins(req_origin, allow_origins) then\n            if conf.allow_origins and conf.allow_origins ~= \"*\" then\n                allow_origins_local = true\n            end\n        end\n    else\n        allow_origins_local = true\n    end\n    if conf.allow_origins_by_regex == nil then\n        if allow_origins_local then\n            allow_origins = process_with_allow_origins(\n                TYPE_ACCESS_CONTROL_ALLOW_ORIGIN, conf.allow_origins, ctx, req_origin\n            )\n        end\n    else\n        if allow_origins_local then\n            allow_origins = process_with_allow_origins_by_regex(\n                TYPE_ACCESS_CONTROL_ALLOW_ORIGIN, conf.allow_origins_by_regex,\n                conf, ctx, req_origin\n            )\n        end\n    end\n    if not match_origins(req_origin, allow_origins) then\n        allow_origins = process_with_allow_origins_by_metadata(\n            TYPE_ACCESS_CONTROL_ALLOW_ORIGIN, conf.allow_origins_by_metadata, ctx, req_origin\n        )\n    end\n    if conf.allow_origins ~= \"*\" then\n        core.response.add_header(\"Vary\", \"Origin\")\n    end\n    if allow_origins then\n        ctx.cors_allow_origins = allow_origins\n        set_cors_headers(conf, ctx)\n    end\n\n    local timing_allow_origins\n    if conf.timing_allow_origins_by_regex == nil and conf.timing_allow_origins then\n        timing_allow_origins = process_with_allow_origins(\n            TYPE_TIMING_ALLOW_ORIGIN, conf.timing_allow_origins, ctx, req_origin\n        )\n    elseif conf.timing_allow_origins_by_regex then\n        timing_allow_origins = process_with_allow_origins_by_regex(\n            TYPE_TIMING_ALLOW_ORIGIN, conf.timing_allow_origins_by_regex,\n            conf, ctx, req_origin\n        )\n    end\n    if timing_allow_origins and match_origins(req_origin, timing_allow_origins) then\n        ctx.timing_allow_origin = timing_allow_origins\n        set_timing_headers(conf, ctx)\n    end\n\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/csrf.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal resty_sha256 = require(\"resty.sha256\")\nlocal str = require(\"resty.string\")\nlocal ngx = ngx\nlocal ngx_encode_base64 = ngx.encode_base64\nlocal ngx_decode_base64 = ngx.decode_base64\nlocal ngx_time = ngx.time\nlocal ngx_cookie_time = ngx.cookie_time\nlocal math = math\nlocal SAFE_METHODS = {\"GET\", \"HEAD\", \"OPTIONS\"}\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        key = {\n            description = \"use to generate csrf token\",\n            type = \"string\",\n        },\n        expires = {\n            description = \"expires time(s) for csrf token\",\n            type = \"integer\",\n            default = 7200\n        },\n        name = {\n            description = \"the csrf token name\",\n            type = \"string\",\n            default = \"apisix-csrf-token\"\n        }\n    },\n    encrypt_fields = {\"key\"},\n    required = {\"key\"}\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2980,\n    name = \"csrf\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function gen_sign(random, expires, key)\n    local sha256 = resty_sha256:new()\n\n    local sign = \"{expires:\" .. expires .. \",random:\" .. random .. \",key:\" .. key .. \"}\"\n\n    sha256:update(sign)\n    local digest = sha256:final()\n\n    return str.to_hex(digest)\nend\n\n\nlocal function gen_csrf_token(conf)\n    local random = math.random()\n    local timestamp = ngx_time()\n    local sign = gen_sign(random, timestamp, conf.key)\n\n    local token = {\n        random = random,\n        expires = timestamp,\n        sign = sign,\n    }\n\n    local cookie = ngx_encode_base64(core.json.encode(token))\n    return cookie\nend\n\n\nlocal function check_csrf_token(conf, ctx, token)\n    local token_str = ngx_decode_base64(token)\n    if not token_str then\n        core.log.error(\"csrf token base64 decode error\")\n        return false\n    end\n\n    local token_table, err = core.json.decode(token_str)\n    if err then\n        core.log.error(\"decode token error: \", err)\n        return false\n    end\n\n    local random = token_table[\"random\"]\n    if not random then\n        core.log.error(\"no random in token\")\n        return false\n    end\n\n    local expires = token_table[\"expires\"]\n    if not expires then\n        core.log.error(\"no expires in token\")\n        return false\n    end\n    local time_now = ngx_time()\n    if conf.expires > 0 and time_now - expires > conf.expires then\n        core.log.error(\"token has expired\")\n        return false\n    end\n\n    local sign = gen_sign(random, expires, conf.key)\n    if token_table[\"sign\"] ~= sign then\n        core.log.error(\"Invalid signatures\")\n        return false\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    local method = core.request.get_method(ctx)\n    if core.table.array_find(SAFE_METHODS, method) then\n        return\n    end\n\n    local header_token = core.request.header(ctx, conf.name)\n    if not header_token or header_token == \"\" then\n        return 401, {error_msg = \"no csrf token in headers\"}\n    end\n\n    local cookie_token = ctx.var[\"cookie_\" .. conf.name]\n    if not cookie_token then\n        return 401, {error_msg = \"no csrf cookie\"}\n    end\n\n    if header_token ~= cookie_token then\n        return 401, {error_msg = \"csrf token mismatch\"}\n    end\n\n    local result = check_csrf_token(conf, ctx, cookie_token)\n    if not result then\n        return 401, {error_msg = \"Failed to verify the csrf token signature\"}\n    end\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    local csrf_token = gen_csrf_token(conf)\n    local cookie = conf.name .. \"=\" .. csrf_token .. \";path=/;SameSite=Lax;Expires=\"\n                   .. ngx_cookie_time(ngx_time() + conf.expires)\n    core.response.add_header(\"Set-Cookie\", cookie)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/datadog.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal fetch_log = require(\"apisix.utils.log-util\").get_full_log\nlocal service_fetch = require(\"apisix.http.service\").get\nlocal ngx = ngx\nlocal udp = ngx.socket.udp\nlocal format = string.format\nlocal concat = table.concat\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal floor = math.floor\nlocal unpack = unpack\n\nlocal plugin_name = \"datadog\"\nlocal defaults = {\n    host = \"127.0.0.1\",\n    port = 8125,\n    namespace = \"apisix\",\n    constant_tags = {\"source:apisix\"}\n}\n\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\n\n-- Shared schema for individual tag strings.\nlocal tag_schema = {\n    type = \"string\",\n    -- Tags must be between 1 and 200 characters.\n    minLength = 1,\n    maxLength = 200,\n    -- Tags must follow the Datadog tag format:\n    --   - `^[\\p{L}]`: Must start with any kind of Unicode letter.\n    --   - `[\\p{L}\\p{N}_.:/-]*`: Followed by Unicode letters (\\p{L}), numbers (\\p{N}),\n    --     or the allowed special characters (underscore, hyphen, colon,\n    --     period, and slash).\n    --   - `(?<!:)$`: Must not end with a colon.\n    pattern = [[^[\\p{L}][\\p{L}\\p{N}_.:/-]*(?<!:)$]]\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        prefer_name = {type = \"boolean\", default = true},\n        include_path = {type = \"boolean\", default = false},\n        include_method = {type = \"boolean\", default = false},\n        constant_tags = {\n            type = \"array\",\n            items = tag_schema,\n            default = {}\n        }\n    }\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        host = {type = \"string\", default= defaults.host},\n        port = {type = \"integer\", minimum = 0, default = defaults.port},\n        namespace = {type = \"string\", default = defaults.namespace},\n        constant_tags = {\n            type = \"array\",\n            items = tag_schema,\n            default = defaults.constant_tags\n        }\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 495,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function generate_tag(entry, const_tags)\n    local tags\n    if const_tags and #const_tags > 0 then\n        tags = core.table.clone(const_tags)\n    else\n        tags = {}\n    end\n\n    if entry.constant_tags and #entry.constant_tags > 0 then\n        for _, tag in ipairs(entry.constant_tags) do\n            core.table.insert(tags, tag)\n        end\n    end\n\n    local variable_tags = {\n        {\"route_name\", entry.route_id},\n        {\"path\", entry.path},\n        {\"method\", entry.method},\n        {\"service_name\", entry.service_id},\n        {\"consumer\", entry.consumer and entry.consumer.username},\n        {\"balancer_ip\", entry.balancer_ip},\n        {\"response_status\", entry.response.status},\n        {\n            \"response_status_class\",\n            entry.response.status and floor(entry.response.status / 100) .. \"xx\"\n        },\n        {\"scheme\", entry.scheme}\n    }\n\n    for _, tag in ipairs(variable_tags) do\n        local key, value = unpack(tag)\n        if value and value ~= \"\" then\n            core.table.insert(tags, key .. \":\" .. value)\n        end\n    end\n\n    if #tags > 0 then\n        return \"|#\" .. concat(tags, ',')\n    end\n\n    return \"\"\nend\n\n\nlocal function send_metric_over_udp(entry, metadata)\n    local err_msg\n    local sock = udp()\n    local host, port = metadata.value.host, metadata.value.port\n\n    local ok, err = sock:setpeername(host, port)\n    if not ok then\n        return false, \"failed to connect to UDP server: host[\" .. host\n                      .. \"] port[\" .. tostring(port) .. \"] err: \" .. err\n    end\n\n    -- Generate prefix & suffix according dogstatsd udp data format.\n    local suffix = generate_tag(entry, metadata.value.constant_tags)\n    local prefix = metadata.value.namespace\n    if prefix ~= \"\" then\n        prefix = prefix .. \".\"\n    end\n\n    -- request counter\n    ok, err = sock:send(format(\"%s:%s|%s%s\", prefix .. \"request.counter\", 1, \"c\", suffix))\n    if not ok then\n        err_msg = \"error sending request.counter: \" .. err\n        core.log.error(\"failed to report request count to dogstatsd server: host[\" .. host\n                       .. \"] port[\" .. tostring(port) .. \"] err: \" .. err)\n    end\n\n    -- request latency histogram\n    ok, err = sock:send(format(\"%s:%s|%s%s\", prefix .. \"request.latency\",\n                               entry.latency, \"h\", suffix))\n    if not ok then\n        err_msg = \"error sending request.latency: \" .. err\n        core.log.error(\"failed to report request latency to dogstatsd server: host[\"\n                       .. host .. \"] port[\" .. tostring(port) .. \"] err: \" .. err)\n    end\n\n    -- upstream latency\n    if entry.upstream_latency then\n        ok, err = sock:send(format(\"%s:%s|%s%s\", prefix .. \"upstream.latency\",\n                                   entry.upstream_latency, \"h\", suffix))\n        if not ok then\n            err_msg = \"error sending upstream.latency: \" .. err\n            core.log.error(\"failed to report upstream latency to dogstatsd server: host[\"\n                           .. host .. \"] port[\" .. tostring(port) .. \"] err: \" .. err)\n        end\n    end\n\n    -- apisix_latency\n    ok, err = sock:send(format(\"%s:%s|%s%s\", prefix .. \"apisix.latency\",\n                               entry.apisix_latency, \"h\", suffix))\n    if not ok then\n        err_msg = \"error sending apisix.latency: \" .. err\n        core.log.error(\"failed to report apisix latency to dogstatsd server: host[\" .. host\n                       .. \"] port[\" .. tostring(port) .. \"] err: \" .. err)\n    end\n\n    -- request body size timer\n    ok, err = sock:send(format(\"%s:%s|%s%s\", prefix .. \"ingress.size\",\n                               entry.request.size, \"ms\", suffix))\n    if not ok then\n        err_msg = \"error sending ingress.size: \" .. err\n        core.log.error(\"failed to report req body size to dogstatsd server: host[\" .. host\n                       .. \"] port[\" .. tostring(port) .. \"] err: \" .. err)\n    end\n\n    -- response body size timer\n    ok, err = sock:send(format(\"%s:%s|%s%s\", prefix .. \"egress.size\",\n                               entry.response.size, \"ms\", suffix))\n    if not ok then\n        err_msg = \"error sending egress.size: \" .. err\n        core.log.error(\"failed to report response body size to dogstatsd server: host[\"\n                       .. host .. \"] port[\" .. tostring(port) .. \"] err: \" .. err)\n    end\n\n    ok, err = sock:close()\n    if not ok then\n        core.log.error(\"failed to close the UDP connection, host[\",\n                       host, \"] port[\", port, \"] \", err)\n    end\n\n    if not err_msg then\n        return true\n    end\n\n    return false, err_msg\nend\n\n\nlocal function push_metrics(entries)\n    -- Fetching metadata details\n    local metadata = plugin.plugin_metadata(plugin_name)\n    core.log.info(\"metadata: \", core.json.delay_encode(metadata))\n\n    if not metadata then\n        core.log.info(\"received nil metadata: using metadata defaults: \",\n                      core.json.delay_encode(defaults, true))\n        metadata = {}\n        metadata.value = defaults\n    end\n    core.log.info(\"sending batch metrics to dogstatsd: \", metadata.value.host,\n                  \":\", metadata.value.port)\n\n    for i = 1, #entries do\n        local ok, err = send_metric_over_udp(entries[i], metadata)\n        if not ok then\n            return false, err, i\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.log(conf, ctx)\n    local entry = fetch_log(ngx, {})\n    entry.balancer_ip = ctx.balancer_ip or \"\"\n    entry.scheme = ctx.upstream_scheme or \"\"\n\n    -- if prefer_name is set, fetch the service/route name. If the name is nil, fall back to id.\n    if conf.prefer_name then\n        if entry.service_id and entry.service_id ~= \"\" then\n            local svc = service_fetch(entry.service_id)\n\n            if svc and svc.value.name ~= \"\" then\n                entry.service_id =  svc.value.name\n            end\n        end\n\n        if ctx.route_name and ctx.route_name ~= \"\" then\n            entry.route_id = ctx.route_name\n        end\n    end\n\n    if conf.include_path then\n        if ctx.curr_req_matched and ctx.curr_req_matched._path then\n            entry.path = ctx.curr_req_matched._path\n        end\n    end\n\n    if conf.include_method then\n        entry.method = ctx.var.method\n    end\n\n    if conf.constant_tags and #conf.constant_tags > 0 then\n        entry.constant_tags = core.table.clone(conf.constant_tags)\n    end\n\n    if batch_processor_manager:add_entry(conf, entry) then\n        return\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, push_metrics)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/degraphql.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal gq_parse = require(\"graphql\").parse\nlocal req_set_body_data = ngx.req.set_body_data\nlocal ipairs = ipairs\nlocal pcall = pcall\nlocal type = type\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        query = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 1024,\n        },\n        variables = {\n            type = \"array\",\n            items = {\n                type = \"string\"\n            },\n            minItems = 1,\n        },\n        operation_name = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 1024\n        },\n    },\n    required = {\"query\"},\n}\n\nlocal plugin_name = \"degraphql\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 509,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    local ok, res = pcall(gq_parse, conf.query)\n    if not ok then\n        return false, \"failed to parse query: \" .. res\n    end\n\n    if #res.definitions > 1 and not conf.operation_name then\n        return false, \"operation_name is required if multiple operations are present in the query\"\n    end\n    return true\nend\n\n\nlocal function fetch_post_variables(conf)\n    local req_body, err = core.request.get_body()\n    if err ~= nil then\n        core.log.error(\"failed to get request body: \", err)\n        return nil, 503\n    end\n\n    if not req_body then\n        core.log.error(\"missing request body\")\n        return nil, 400\n    end\n\n    -- JSON as the default content type\n    req_body, err = core.json.decode(req_body)\n    if type(req_body) ~= \"table\" then\n        core.log.error(\"invalid request body can't be decoded: \", err or \"bad type\")\n        return nil, 400\n    end\n\n    local variables = {}\n    for _, v in ipairs(conf.variables) do\n        variables[v] = req_body[v]\n    end\n\n    return variables\nend\n\n\nlocal function fetch_get_variables(conf)\n    local args = core.request.get_uri_args()\n    local variables = {}\n    for _, v in ipairs(conf.variables) do\n        variables[v] = args[v]\n    end\n\n    return variables\nend\n\n\nfunction _M.access(conf, ctx)\n    local meth = core.request.get_method()\n    if meth ~= \"POST\" and meth ~= \"GET\" then\n        return 405\n    end\n\n    local new_body = core.table.new(0, 3)\n\n    if conf.variables then\n        local variables, code\n        if meth == \"POST\" then\n            variables, code = fetch_post_variables(conf)\n        else\n            variables, code = fetch_get_variables(conf)\n        end\n\n        if not variables then\n            return code\n        end\n\n        if meth == \"POST\" then\n            new_body[\"variables\"] = variables\n        else\n            new_body[\"variables\"] = core.json.encode(variables)\n        end\n    end\n\n    new_body[\"operationName\"] = conf.operation_name\n    new_body[\"query\"] = conf.query\n\n    if meth == \"POST\" then\n        if not conf.variables then\n            -- the set_body_data requires to read the body first\n            core.request.get_body()\n        end\n\n        core.request.set_header(ctx, \"Content-Type\", \"application/json\")\n        req_set_body_data(core.json.encode(new_body))\n    else\n        core.request.set_uri_args(ctx, new_body)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/dubbo-proxy.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx_var = ngx.var\n\n\nlocal plugin_name = \"dubbo-proxy\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        service_name = {\n            type = \"string\",\n            minLength = 1,\n        },\n        service_version = {\n            type = \"string\",\n            pattern = [[^\\d+\\.\\d+\\.\\d+]],\n        },\n        method = {\n            type = \"string\",\n            minLength = 1,\n        },\n    },\n    required = { \"service_name\", \"service_version\"},\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 507,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    ctx.dubbo_proxy_enabled = true\n\n    ngx_var.dubbo_service_name = conf.service_name\n    ngx_var.dubbo_service_version = conf.service_version\n    if not conf.method then\n        -- remove the prefix '/' from $uri\n        ngx_var.dubbo_method = core.string.sub(ngx_var.uri, 2)\n    else\n        ngx_var.dubbo_method = conf.method\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/echo.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal pairs       = pairs\nlocal type        = type\nlocal ngx         = ngx\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        before_body = {\n            description = \"body before the filter phase.\",\n            type = \"string\"\n        },\n        body = {\n            description = \"body to replace upstream response.\",\n            type = \"string\"\n        },\n        after_body = {\n            description = \"body after the modification of filter phase.\",\n            type = \"string\"\n        },\n        headers = {\n            description = \"new headers for response\",\n            type = \"object\",\n            minProperties = 1,\n        },\n    },\n    anyOf = {\n        {required = {\"before_body\"}},\n        {required = {\"body\"}},\n        {required = {\"after_body\"}}\n    },\n    minProperties = 1,\n}\n\nlocal plugin_name = \"echo\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 412,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    if conf.body then\n        ngx.arg[1] = conf.body\n        ngx.arg[2] = true\n    end\n\n    if conf.before_body and not ctx.plugin_echo_body_set then\n        ngx.arg[1] = conf.before_body ..  ngx.arg[1]\n        ctx.plugin_echo_body_set = true\n    end\n\n    if ngx.arg[2] and conf.after_body then\n        ngx.arg[1] = ngx.arg[1] .. conf.after_body\n    end\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    if conf.body or conf.before_body or conf.after_body then\n        core.response.clear_header_as_body_modified()\n    end\n\n    if not conf.headers then\n        return\n    end\n\n    if not conf.headers_arr then\n        conf.headers_arr = {}\n\n        for field, value in pairs(conf.headers) do\n            if type(field) == 'string'\n                    and (type(value) == 'string' or type(value) == 'number') then\n                if #field == 0 then\n                    return false, 'invalid field length in header'\n                end\n                core.table.insert(conf.headers_arr, field)\n                core.table.insert(conf.headers_arr, value)\n            else\n                return false, 'invalid type as header value'\n            end\n        end\n    end\n\n    local field_cnt = #conf.headers_arr\n    for i = 1, field_cnt, 2 do\n        ngx.header[conf.headers_arr[i]] = conf.headers_arr[i+1]\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/elasticsearch-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core            = require(\"apisix.core\")\nlocal http            = require(\"resty.http\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal ngx             = ngx\nlocal str_format      = core.string.format\nlocal math_random     = math.random\nlocal pairs           = pairs\n\nlocal plugin_name = \"elasticsearch-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        -- deprecated, use \"endpoint_addrs\" instead\n        endpoint_addr = {\n            type = \"string\",\n            pattern = \"[^/]$\",\n        },\n        endpoint_addrs = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                pattern = \"[^/]$\",\n            },\n        },\n        field = {\n            type = \"object\",\n            properties = {\n                index = { type = \"string\"},\n            },\n            required = {\"index\"}\n        },\n        log_format = {type = \"object\"},\n        auth = {\n            type = \"object\",\n            properties = {\n                username = {\n                    type = \"string\",\n                    minLength = 1\n                },\n                password = {\n                    type = \"string\",\n                    minLength = 1\n                }\n            },\n            oneOf = {\n                {required = {\"username\", \"password\"}},\n            }\n        },\n        headers = {\n            type = \"object\",\n            minProperties = 1,\n            patternProperties = {\n                [\"^[^:]+$\"] = {\n                    type = \"string\",\n                    minLength = 1\n                }\n            },\n            additionalProperties = false\n        },\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            default = 10\n        },\n        ssl_verify = {\n            type = \"boolean\",\n            default = true\n        },\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = { type = \"integer\", minimum = 1, default = 524288 },\n        max_resp_body_bytes = { type = \"integer\", minimum = 1, default = 524288 },\n    },\n    encrypt_fields = {\"auth.password\"},\n    oneOf = {\n        {required = {\"endpoint_addr\", \"field\"}},\n        {required = {\"endpoint_addrs\", \"field\"}}\n    },\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 413,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local check = {\"endpoint_addrs\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function get_es_major_version(uri, conf)\n    local httpc = http.new()\n    if not httpc then\n        return nil, \"failed to create http client\"\n    end\n\n    local headers = {}\n    if conf.auth then\n        local authorization = \"Basic \" .. ngx.encode_base64(\n            conf.auth.username .. \":\" .. conf.auth.password\n        )\n        headers[\"Authorization\"] = authorization\n    end\n\n    if conf.headers then\n        for k, v in pairs(conf.headers) do\n            headers[k] = v\n        end\n    end\n\n    httpc:set_timeout(conf.timeout * 1000)\n    local res, err = httpc:request_uri(uri, {\n        ssl_verify = conf.ssl_verify,\n        method = \"GET\",\n        headers = headers,\n    })\n    if not res then\n        return false, err\n    end\n    if res.status ~= 200 then\n        return nil, str_format(\"server returned status: %d, body: %s\",\n            res.status, res.body or \"\")\n    end\n    local json_body, err = core.json.decode(res.body)\n    if not json_body then\n        return nil, \"failed to decode response body: \" .. err\n    end\n    if not json_body.version or not json_body.version.number then\n        return nil, \"failed to get version from response body\"\n    end\n\n    local major_version = json_body.version.number:match(\"^(%d+)%.\")\n    if not major_version then\n        return nil, \"invalid version format: \" .. json_body.version.number\n    end\n\n    return major_version\nend\n\n\nlocal function get_logger_entry(conf, ctx)\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    local body = {\n        index = {\n            _index = conf.field.index\n        }\n    }\n    -- for older version type is required\n    if conf._version == \"6\" or conf._version == \"5\" then\n        body.index._type = \"_doc\"\n    end\n    return core.json.encode(body) .. \"\\n\" ..\n        core.json.encode(entry) .. \"\\n\"\nend\n\n\nlocal function fetch_and_update_es_version(conf)\n    if conf._version then\n        return\n    end\n    local selected_endpoint_addr\n    if conf.endpoint_addr then\n        selected_endpoint_addr = conf.endpoint_addr\n    else\n        selected_endpoint_addr = conf.endpoint_addrs[math_random(#conf.endpoint_addrs)]\n    end\n    local major_version, err = get_es_major_version(selected_endpoint_addr, conf)\n    if err then\n        core.log.error(\"failed to get Elasticsearch version: \", err)\n        return\n    end\n    conf._version = major_version\nend\n\n\nlocal function send_to_elasticsearch(conf, entries)\n    local httpc, err = http.new()\n    if not httpc then\n        return false, str_format(\"create http error: %s\", err)\n    end\n    fetch_and_update_es_version(conf)\n    local selected_endpoint_addr\n    if conf.endpoint_addr then\n        selected_endpoint_addr = conf.endpoint_addr\n    else\n        selected_endpoint_addr = conf.endpoint_addrs[math_random(#conf.endpoint_addrs)]\n    end\n    local uri = selected_endpoint_addr .. \"/_bulk\"\n    local body = core.table.concat(entries, \"\")\n    local headers = {\n        [\"Content-Type\"] = \"application/x-ndjson\",\n        [\"Accept\"] = \"application/vnd.elasticsearch+json\"\n    }\n    if conf.auth then\n        local authorization = \"Basic \" .. ngx.encode_base64(\n            conf.auth.username .. \":\" .. conf.auth.password\n        )\n        headers[\"Authorization\"] = authorization\n    end\n\n    if conf.headers then\n        for k, v in pairs(conf.headers) do\n            headers[k] = v\n        end\n    end\n\n    core.log.info(\"uri: \", uri, \", body: \", body)\n\n    httpc:set_timeout(conf.timeout * 1000)\n    local resp, err = httpc:request_uri(uri, {\n        ssl_verify = conf.ssl_verify,\n        method = \"POST\",\n        headers = headers,\n        body = body\n    })\n    if not resp then\n        return false, err\n    end\n\n    if resp.status ~= 200 then\n        return false, str_format(\"elasticsearch server returned status: %d, body: %s\",\n        resp.status, resp.body or \"\")\n    end\n\n    return true\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.access(conf, ctx)\n    -- fetch_and_update_es_version will call ES server only the first time\n    -- so this should not amount to considerable overhead\n    fetch_and_update_es_version(conf)\n\n    log_util.check_and_read_req_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = get_logger_entry(conf, ctx)\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    local process = function(entries)\n        return send_to_elasticsearch(conf, entries)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx,\n                                                       process, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/error-log-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal errlog = require(\"ngx.errlog\")\nlocal batch_processor = require(\"apisix.utils.batch-processor\")\nlocal plugin = require(\"apisix.plugin\")\nlocal timers = require(\"apisix.timers\")\nlocal http = require(\"resty.http\")\nlocal producer = require(\"resty.kafka.producer\")\nlocal plugin_name = \"error-log-logger\"\nlocal table = core.table\nlocal schema_def = core.schema\nlocal ngx = ngx\nlocal tcp = ngx.socket.tcp\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal string = require(\"string\")\nlocal lrucache = core.lrucache.new({\n    ttl = 300, count = 32\n})\nlocal kafka_prod_lrucache = core.lrucache.new({\n    ttl = 300, count = 32\n})\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        tcp = {\n            type = \"object\",\n            properties = {\n                host = schema_def.host_def,\n                port = {type = \"integer\", minimum = 0},\n                tls = {type = \"boolean\", default = false},\n                tls_server_name = {type = \"string\"},\n            },\n            required = {\"host\", \"port\"}\n        },\n        skywalking = {\n            type = \"object\",\n            properties = {\n                endpoint_addr = {schema_def.uri, default = \"http://127.0.0.1:12900/v3/logs\"},\n                service_name = {type = \"string\", default = \"APISIX\"},\n                service_instance_name = {type=\"string\", default = \"APISIX Service Instance\"},\n            },\n        },\n        clickhouse = {\n            type = \"object\",\n            properties = {\n                endpoint_addr = {schema_def.uri_def, default=\"http://127.0.0.1:8123\"},\n                user = {type = \"string\", default = \"default\"},\n                password = {type = \"string\", default = \"\"},\n                database = {type = \"string\", default = \"\"},\n                logtable = {type = \"string\", default = \"\"},\n            },\n            required = {\"endpoint_addr\", \"user\", \"password\", \"database\", \"logtable\"}\n        },\n        kafka = {\n            type = \"object\",\n            properties = {\n                brokers = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"object\",\n                        properties = {\n                            host = {\n                                type = \"string\",\n                                description = \"the host of kafka broker\",\n                            },\n                            port = {\n                                type = \"integer\",\n                                minimum = 1,\n                                maximum = 65535,\n                                description = \"the port of kafka broker\",\n                            },\n                            sasl_config = {\n                                type = \"object\",\n                                description = \"sasl config\",\n                                properties = {\n                                    mechanism = {\n                                        type = \"string\",\n                                        default = \"PLAIN\",\n                                        enum = {\"PLAIN\"},\n                                    },\n                                    user = { type = \"string\", description = \"user\" },\n                                    password =  { type = \"string\", description = \"password\" },\n                                },\n                                required = {\"user\", \"password\"},\n                            },\n                        },\n                        required = {\"host\", \"port\"},\n                    },\n                    uniqueItems = true,\n                },\n                kafka_topic = {type = \"string\"},\n                producer_type = {\n                    type = \"string\",\n                    default = \"async\",\n                    enum = {\"async\", \"sync\"},\n                },\n                required_acks = {\n                    type = \"integer\",\n                    default = 1,\n                    enum = { 0, 1, -1 },\n                },\n                key = {type = \"string\"},\n                -- in lua-resty-kafka, cluster_name is defined as number\n                -- see https://github.com/doujiang24/lua-resty-kafka#new-1\n                cluster_name = {type = \"integer\", minimum = 1, default = 1},\n                meta_refresh_interval = {type = \"integer\", minimum = 1, default = 30},\n            },\n            required = {\"brokers\", \"kafka_topic\"},\n        },\n        name = {type = \"string\", default = plugin_name},\n        level = {type = \"string\", default = \"WARN\", enum = {\"STDERR\", \"EMERG\", \"ALERT\", \"CRIT\",\n                \"ERR\", \"ERROR\", \"WARN\", \"NOTICE\", \"INFO\", \"DEBUG\"}},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        keepalive = {type = \"integer\", minimum = 1, default = 30},\n        batch_max_size = {type = \"integer\", minimum = 0, default = 1000},\n        max_retry_count = {type = \"integer\", minimum = 0, default = 0},\n        retry_delay = {type = \"integer\", minimum = 0, default = 1},\n        buffer_duration = {type = \"integer\", minimum = 1, default = 60},\n        inactive_timeout = {type = \"integer\", minimum = 1, default = 3},\n    },\n    oneOf = {\n        {required = {\"skywalking\"}},\n        {required = {\"tcp\"}},\n        {required = {\"clickhouse\"}},\n        {required = {\"kafka\"}},\n        -- for compatible with old schema\n        {required = {\"host\", \"port\"}}\n    },\n    encrypt_fields = {\"clickhouse.password\"},\n}\n\n\nlocal schema = {\n    type = \"object\",\n}\n\n\nlocal log_level = {\n    STDERR =    ngx.STDERR,\n    EMERG  =    ngx.EMERG,\n    ALERT  =    ngx.ALERT,\n    CRIT   =    ngx.CRIT,\n    ERR    =    ngx.ERR,\n    ERROR  =    ngx.ERR,\n    WARN   =    ngx.WARN,\n    NOTICE =    ngx.NOTICE,\n    INFO   =    ngx.INFO,\n    DEBUG  =    ngx.DEBUG\n}\n\n\nlocal config = {}\nlocal log_buffer\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 1091,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n    scope = \"global\",\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local check = {\"skywalking.endpoint_addr\", \"clickhouse.endpoint_addr\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"tcp.tls\"}, conf, plugin_name)\n\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function send_to_tcp_server(data)\n    local sock, soc_err = tcp()\n\n    if not sock then\n        return false, \"failed to init the socket \" .. soc_err\n    end\n\n    sock:settimeout(config.timeout * 1000)\n\n    local tcp_config = config.tcp\n    local ok, err = sock:connect(tcp_config.host, tcp_config.port)\n    if not ok then\n        return false, \"failed to connect the TCP server: host[\" .. tcp_config.host\n            .. \"] port[\" .. tostring(tcp_config.port) .. \"] err: \" .. err\n    end\n\n    if tcp_config.tls then\n        ok, err = sock:sslhandshake(false, tcp_config.tls_server_name, false)\n        if not ok then\n            sock:close()\n            return false, \"failed to perform TLS handshake to TCP server: host[\"\n                .. tcp_config.host .. \"] port[\" .. tostring(tcp_config.port) .. \"] err: \" .. err\n        end\n    end\n\n    local bytes, err = sock:send(data)\n    if not bytes then\n        sock:close()\n        return false, \"failed to send data to TCP server: host[\" .. tcp_config.host\n            .. \"] port[\" .. tostring(tcp_config.port) .. \"] err: \" .. err\n    end\n\n    sock:setkeepalive(config.keepalive * 1000)\n    return true\nend\n\n\nlocal function send_to_skywalking(log_message)\n    local err_msg\n    local res = true\n    core.log.info(\"sending a batch logs to \", config.skywalking.endpoint_addr)\n\n    local httpc = http.new()\n    httpc:set_timeout(config.timeout * 1000)\n\n    local entries = {}\n    local service_instance_name = config.skywalking.service_instance_name\n    if service_instance_name == \"$hostname\" then\n        service_instance_name = core.utils.gethostname()\n    end\n\n    for i = 1, #log_message, 2 do\n        local content = {\n            service = config.skywalking.service_name,\n            serviceInstance = service_instance_name,\n            endpoint = \"\",\n            body = {\n                text = {\n                    text = log_message[i]\n                }\n           }\n        }\n        table.insert(entries, content)\n    end\n\n    local httpc_res, httpc_err = httpc:request_uri(\n        config.skywalking.endpoint_addr,\n        {\n            method = \"POST\",\n            body = core.json.encode(entries),\n            keepalive_timeout = config.keepalive * 1000,\n            headers = {\n                [\"Content-Type\"] = \"application/json\",\n            }\n        }\n    )\n\n    if not httpc_res then\n        return false, \"error while sending data to skywalking[\"\n            .. config.skywalking.endpoint_addr .. \"] \" .. httpc_err\n    end\n\n    -- some error occurred in the server\n    if httpc_res.status >= 400 then\n        res =  false\n        err_msg = string.format(\n            \"server returned status code[%s] skywalking[%s] body[%s]\",\n            httpc_res.status,\n            config.skywalking.endpoint_addr.endpoint_addr,\n            httpc_res:read_body()\n        )\n    end\n\n    return res, err_msg\nend\n\n\nlocal function send_to_clickhouse(log_message)\n    local err_msg\n    local res = true\n    core.log.info(\"sending a batch logs to \", config.clickhouse.endpoint_addr)\n\n    local httpc = http.new()\n    httpc:set_timeout(config.timeout * 1000)\n\n    local entries = {}\n    for i = 1, #log_message, 2 do\n        -- TODO Here save error log as a whole string to clickhouse 'data' column.\n        -- We will add more columns in the future.\n        table.insert(entries, core.json.encode({data=log_message[i]}))\n    end\n\n    local httpc_res, httpc_err = httpc:request_uri(\n        config.clickhouse.endpoint_addr,\n        {\n            method = \"POST\",\n            body = \"INSERT INTO \" .. config.clickhouse.logtable ..\" FORMAT JSONEachRow \"\n                   .. table.concat(entries, \" \"),\n            keepalive_timeout = config.keepalive * 1000,\n            headers = {\n                [\"Content-Type\"] = \"application/json\",\n                [\"X-ClickHouse-User\"] = config.clickhouse.user,\n                [\"X-ClickHouse-Key\"] = config.clickhouse.password,\n                [\"X-ClickHouse-Database\"] = config.clickhouse.database\n            }\n        }\n    )\n\n    if not httpc_res then\n        return false, \"error while sending data to clickhouse[\"\n            .. config.clickhouse.endpoint_addr .. \"] \" .. httpc_err\n    end\n\n    -- some error occurred in the server\n    if httpc_res.status >= 400 then\n        res =  false\n        err_msg = string.format(\n            \"server returned status code[%s] clickhouse[%s] body[%s]\",\n            httpc_res.status,\n            config.clickhouse.endpoint_addr.endpoint_addr,\n            httpc_res:read_body()\n        )\n    end\n\n    return res, err_msg\nend\n\n\nlocal function update_filter(value)\n    local level = log_level[value.level]\n    local status, err = errlog.set_filter_level(level)\n    if not status then\n        return nil, \"failed to set filter level by ngx.errlog, the error is :\" .. err\n    else\n        core.log.notice(\"set the filter_level to \", value.level)\n    end\n\n    return value\nend\n\n\nlocal function create_producer(broker_list, broker_config, cluster_name)\n    core.log.info(\"create new kafka producer instance\")\n    return producer:new(broker_list, broker_config, cluster_name)\nend\n\n\nlocal function send_to_kafka(log_message)\n    -- avoid race of the global config\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if not (metadata and metadata.value and metadata.modifiedIndex) then\n        return false, \"please set the correct plugin_metadata for \" .. plugin_name\n    end\n    local config, err = lrucache(plugin_name, metadata.modifiedIndex, update_filter, metadata.value)\n    if not config then\n        return false, \"get config failed: \" .. err\n    end\n\n    core.log.info(\"sending a batch logs to kafka brokers: \",\n                  core.json.delay_encode(config.kafka.brokers))\n\n    local broker_config = {}\n    broker_config[\"request_timeout\"] = config.timeout * 1000\n    broker_config[\"producer_type\"] = config.kafka.producer_type\n    broker_config[\"required_acks\"] = config.kafka.required_acks\n    broker_config[\"refresh_interval\"] = config.kafka.meta_refresh_interval * 1000\n\n    -- reuse producer via kafka_prod_lrucache to avoid unbalanced partitions of messages in kafka\n    local prod, err = kafka_prod_lrucache(plugin_name, metadata.modifiedIndex,\n                                          create_producer, config.kafka.brokers, broker_config,\n                                          config.kafka.cluster_name)\n    if not prod then\n        return false, \"get kafka producer failed: \" .. err\n    end\n    core.log.info(\"kafka cluster name \", config.kafka.cluster_name, \", broker_list[1] port \",\n                  prod.client.broker_list[1].port)\n\n    local ok\n    for i = 1, #log_message, 2 do\n        ok, err = prod:send(config.kafka.kafka_topic,\n                            config.kafka.key, core.json.encode(log_message[i]))\n        if not ok then\n            return false, \"failed to send data to Kafka topic: \" .. err ..\n                          \", brokers: \" .. core.json.encode(config.kafka.brokers)\n        end\n        core.log.info(\"send data to kafka: \", core.json.delay_encode(log_message[i]))\n    end\n\n    return true\nend\n\n\nlocal function send(data)\n    if config.skywalking then\n        return send_to_skywalking(data)\n    elseif config.clickhouse then\n        return send_to_clickhouse(data)\n    elseif config.kafka then\n        return send_to_kafka(data)\n    end\n    return send_to_tcp_server(data)\nend\n\n\nlocal function process()\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if not (metadata and metadata.value and metadata.modifiedIndex) then\n        core.log.info(\"please set the correct plugin_metadata for \", plugin_name)\n        return\n    else\n        local err\n        config, err = lrucache(plugin_name, metadata.modifiedIndex, update_filter, metadata.value)\n        if not config then\n            core.log.warn(\"set log filter failed for \", err)\n            return\n        end\n        if not (config.tcp or config.skywalking or config.clickhouse or config.kafka) then\n            config.tcp = {\n                host = config.host,\n                port =  config.port,\n                tls = config.tls,\n                tls_server_name = config.tls_server_name\n            }\n            core.log.warn(\n                string.format(\"The schema is out of date. Please update to the new configuration, \"\n                    .. \"for example: {\\\"tcp\\\": {\\\"host\\\": \\\"%s\\\", \\\"port\\\": \\\"%s\\\"}}\",\n                    config.host, config.port\n                ))\n        end\n    end\n\n    local err_level = log_level[metadata.value.level]\n    local entries = {}\n    local logs = errlog.get_logs(9)\n    while ( logs and #logs>0 ) do\n        for i = 1, #logs, 3 do\n            -- There will be some stale error logs after the filter level changed.\n            -- We should avoid reporting them.\n            if logs[i] <= err_level then\n                table.insert(entries, logs[i + 2])\n                table.insert(entries, \"\\n\")\n            end\n        end\n        logs = errlog.get_logs(9)\n    end\n\n    if #entries == 0 then\n        return\n    end\n\n    if log_buffer then\n        for _, v in ipairs(entries) do\n            log_buffer:push(v)\n        end\n        return\n    end\n\n    local config_bat = {\n        name = config.name,\n        retry_delay = config.retry_delay,\n        batch_max_size = config.batch_max_size,\n        max_retry_count = config.max_retry_count,\n        buffer_duration = config.buffer_duration,\n        inactive_timeout = config.inactive_timeout,\n    }\n\n    local err\n    log_buffer, err = batch_processor:new(send, config_bat)\n\n    if not log_buffer then\n        core.log.warn(\"error when creating the batch processor: \", err)\n        return\n    end\n\n    for _, v in ipairs(entries) do\n        log_buffer:push(v)\n    end\n\nend\n\n\nfunction _M.init()\n    timers.register_timer(\"plugin#error-log-logger\", process)\nend\n\n\nfunction _M.destroy()\n    timers.unregister_timer(\"plugin#error-log-logger\")\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/example-plugin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx = ngx\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal upstream = require(\"apisix.upstream\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        i = {type = \"number\", minimum = 0},\n        s = {type = \"string\"},\n        t = {type = \"array\", minItems = 1},\n        ip = {type = \"string\"},\n        port = {type = \"integer\"},\n    },\n    required = {\"i\"},\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        ikey = {type = \"number\", minimum = 0},\n        skey = {type = \"string\"},\n    },\n    required = {\"ikey\", \"skey\"},\n}\n\nlocal plugin_name = \"example-plugin\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 0,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.init()\n    -- call this function when plugin is loaded\n    local attr = plugin.plugin_attr(plugin_name)\n    if attr then\n        core.log.info(plugin_name, \" get plugin attr val: \", attr.val)\n    end\nend\n\n\nfunction _M.destroy()\n    -- call this function when plugin is unloaded\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    core.log.warn(\"plugin rewrite phase, conf: \", core.json.encode(conf))\n    core.log.warn(\"conf_type: \", ctx.conf_type)\n    core.log.warn(\"conf_id: \", ctx.conf_id)\n    core.log.warn(\"conf_version: \", ctx.conf_version)\nend\n\n\nfunction _M.access(conf, ctx)\n    core.log.warn(\"plugin access phase, conf: \", core.json.encode(conf))\n    -- return 200, {message = \"hit example plugin\"}\n\n    if not conf.ip then\n        return\n    end\n\n    local up_conf = {\n        type = \"roundrobin\",\n        nodes = {\n            {host = conf.ip, port = conf.port, weight = 1}\n        }\n    }\n\n    local ok, err = upstream.check_schema(up_conf)\n    if not ok then\n        return 500, err\n    end\n\n    local matched_route = ctx.matched_route\n    upstream.set(ctx, up_conf.type .. \"#route_\" .. matched_route.value.id,\n                 ctx.conf_version, up_conf)\n    return\nend\n\nfunction _M.header_filter(conf, ctx)\n    core.log.warn(\"plugin header_filter phase, conf: \", core.json.encode(conf))\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    core.log.warn(\"plugin body_filter phase, eof: \", ngx.arg[2],\n                  \", conf: \", core.json.encode(conf))\nend\n\n\nfunction _M.delayed_body_filter(conf, ctx)\n    core.log.warn(\"plugin delayed_body_filter phase, eof: \", ngx.arg[2],\n                  \", conf: \", core.json.encode(conf))\nend\n\nfunction _M.log(conf, ctx)\n    core.log.warn(\"plugin log phase, conf: \", core.json.encode(conf))\nend\n\n\nlocal function hello()\n    local args = ngx.req.get_uri_args()\n    if args[\"json\"] then\n        return 200, {msg = \"world\"}\n    else\n        return 200, \"world\\n\"\n    end\nend\n\n\nfunction _M.control_api()\n    return {\n        {\n            methods = {\"GET\"},\n            uris = {\"/v1/plugin/example-plugin/hello\"},\n            handler = hello,\n        }\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ext-plugin/helper.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal core = require(\"apisix.core\")\nlocal config_local = require(\"apisix.core.config_local\")\nlocal process\nif is_http then\n    process = require \"ngx.process\"\nend\nlocal pl_path = require(\"pl.path\")\n\n\nlocal _M = {}\n\n\ndo\n    local path\n    function _M.get_path()\n        if not path then\n            local local_conf = config_local.local_conf()\n            if local_conf then\n                local test_path =\n                    core.table.try_read_attr(local_conf, \"ext-plugin\", \"path_for_test\")\n                if test_path then\n                    path = \"unix:\" .. test_path\n                end\n            end\n\n            if not path then\n                local sock = \"./conf/apisix-\" .. process.get_master_pid() .. \".sock\"\n                path = \"unix:\" .. pl_path.abspath(sock)\n            end\n        end\n\n        return path\n    end\nend\n\n\nfunction _M.get_conf_token_cache_time()\n    return 3600\nend\n\n\nfunction _M.response_reader(reader, callback, ...)\n    if not reader then\n        return \"get response reader failed\"\n    end\n\n    repeat\n        local chunk, read_err, cb_err\n        chunk, read_err = reader()\n        if read_err then\n            return \"read response failed: \".. (read_err or \"\")\n        end\n\n        if chunk then\n            cb_err = callback(chunk, ...)\n            if cb_err then\n                return cb_err\n            end\n        end\n    until not chunk\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ext-plugin/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal flatbuffers = require(\"flatbuffers\")\nlocal a6_method = require(\"A6.Method\")\nlocal prepare_conf_req = require(\"A6.PrepareConf.Req\")\nlocal prepare_conf_resp = require(\"A6.PrepareConf.Resp\")\nlocal http_req_call_req = require(\"A6.HTTPReqCall.Req\")\nlocal http_req_call_resp = require(\"A6.HTTPReqCall.Resp\")\nlocal http_req_call_action = require(\"A6.HTTPReqCall.Action\")\nlocal http_req_call_stop = require(\"A6.HTTPReqCall.Stop\")\nlocal http_req_call_rewrite = require(\"A6.HTTPReqCall.Rewrite\")\nlocal http_resp_call_req = require(\"A6.HTTPRespCall.Req\")\nlocal http_resp_call_resp = require(\"A6.HTTPRespCall.Resp\")\nlocal extra_info = require(\"A6.ExtraInfo.Info\")\nlocal extra_info_req = require(\"A6.ExtraInfo.Req\")\nlocal extra_info_var = require(\"A6.ExtraInfo.Var\")\nlocal extra_info_resp = require(\"A6.ExtraInfo.Resp\")\nlocal extra_info_reqbody = require(\"A6.ExtraInfo.ReqBody\")\nlocal extra_info_respbody = require(\"A6.ExtraInfo.RespBody\")\nlocal text_entry = require(\"A6.TextEntry\")\nlocal err_resp = require(\"A6.Err.Resp\")\nlocal err_code = require(\"A6.Err.Code\")\nlocal constants = require(\"apisix.constants\")\nlocal core = require(\"apisix.core\")\nlocal helper = require(\"apisix.plugins.ext-plugin.helper\")\nlocal process, ngx_pipe, events\nif is_http then\n    process = require(\"ngx.process\")\n    ngx_pipe = require(\"ngx.pipe\")\n    events = require(\"apisix.events\")\nend\nlocal resty_lock = require(\"resty.lock\")\nlocal resty_signal = require \"resty.signal\"\nlocal bit = require(\"bit\")\nlocal band = bit.band\nlocal lshift = bit.lshift\nlocal rshift = bit.rshift\nlocal ffi = require(\"ffi\")\nlocal ffi_str = ffi.string\nlocal socket_tcp = ngx.socket.tcp\nlocal worker_id = ngx.worker.id\nlocal ngx_timer_at = ngx.timer.at\nlocal exiting = ngx.worker.exiting\nlocal str_byte = string.byte\nlocal str_format = string.format\nlocal str_lower = string.lower\nlocal str_sub = string.sub\nlocal error = error\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal tostring = tostring\nlocal type = type\nlocal ngx = ngx\n\n\nlocal events_list\nlocal exclude_resp_header = {\n    [\"connection\"] = true,\n    [\"content-length\"] = true,\n    [\"transfer-encoding\"] = true,\n    [\"location\"] = true,\n    [\"server\"] = true,\n    [\"www-authenticate\"] = true,\n    [\"content-encoding\"] = true,\n    [\"content-type\"] = true,\n    [\"content-location\"] = true,\n    [\"content-language\"] = true,\n}\n\nlocal function new_lrucache()\n    return core.lrucache.new({\n        type = \"plugin\",\n        invalid_stale = true,\n        ttl = helper.get_conf_token_cache_time(),\n    })\nend\nlocal lrucache = new_lrucache()\n\nlocal shdict_name = \"ext-plugin\"\nlocal shdict = ngx.shared[shdict_name]\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        conf = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    name = {\n                        type = \"string\",\n                        maxLength = 128,\n                        minLength = 1\n                    },\n                    value = {\n                        type = \"string\",\n                    },\n                },\n                required = {\"name\", \"value\"}\n            },\n            minItems = 1,\n        },\n        allow_degradation = {type = \"boolean\", default = false}\n    },\n}\n\nlocal _M = {\n    schema = schema,\n}\nlocal builder = flatbuffers.Builder(0)\n\n\nlocal send\ndo\n    local hdr_buf = ffi.new(\"unsigned char[4]\")\n    local buf = core.table.new(2, 0)\n    local MAX_DATA_SIZE = lshift(1, 24) - 1\n\n    function send(sock, ty, data)\n        hdr_buf[0] = ty\n\n        local len = #data\n\n        core.log.info(\"sending rpc type: \", ty, \" data length: \", len)\n\n        if len > MAX_DATA_SIZE then\n            return nil, str_format(\"the max length of data is %d but got %d\", MAX_DATA_SIZE, len)\n        end\n\n        -- length is sent as big endian\n        for i = 3, 1, -1 do\n            hdr_buf[i] = band(len, 255)\n            len = rshift(len, 8)\n        end\n\n        buf[1] = ffi_str(hdr_buf, 4)\n        buf[2] = data\n        return sock:send(buf)\n    end\nend\n_M.send = send\n\n\nlocal err_to_msg\ndo\n    local map = {\n        [err_code.BAD_REQUEST] = \"bad request\",\n        [err_code.SERVICE_UNAVAILABLE] = \"service unavailable\",\n        [err_code.CONF_TOKEN_NOT_FOUND] = \"conf token not found\",\n    }\n\n    function err_to_msg(resp)\n        local buf = flatbuffers.binaryArray.New(resp)\n        local resp = err_resp.GetRootAsResp(buf, 0)\n        local code = resp:Code()\n        return map[code] or str_format(\"unknown err %d\", code)\n    end\nend\n\n\nlocal function receive(sock)\n    local hdr, err = sock:receive(4)\n    if not hdr then\n        return nil, err\n    end\n    if #hdr ~= 4 then\n        return nil, \"header too short\"\n    end\n\n    local ty = str_byte(hdr, 1)\n    local resp\n    local hi, mi, li = str_byte(hdr, 2, 4)\n    local len = 256 * (256 * hi + mi) + li\n\n    core.log.info(\"receiving rpc type: \", ty, \" data length: \", len)\n\n    if len > 0 then\n        resp, err = sock:receive(len)\n        if not resp then\n            return nil, err\n        end\n        if #resp ~= len then\n            return nil, \"data truncated\"\n        end\n    end\n\n    if ty == constants.RPC_ERROR then\n        return nil, err_to_msg(resp)\n    end\n\n    return ty, resp\nend\n_M.receive = receive\n\n\nlocal generate_id\ndo\n    local count = 0\n    local MAX_COUNT = lshift(1, 22)\n\n    function generate_id()\n        local wid = worker_id()\n        local id = lshift(wid, 22) + count\n        count = count + 1\n        if count == MAX_COUNT then\n            count = 0\n        end\n        return id\n    end\nend\n\n\nlocal encode_a6_method\ndo\n    local map = {\n        GET = a6_method.GET,\n        HEAD = a6_method.HEAD,\n        POST = a6_method.POST,\n        PUT = a6_method.PUT,\n        DELETE = a6_method.DELETE,\n        MKCOL = a6_method.MKCOL,\n        COPY = a6_method.COPY,\n        MOVE = a6_method.MOVE,\n        OPTIONS = a6_method.OPTIONS,\n        PROPFIND = a6_method.PROPFIND,\n        PROPPATCH = a6_method.PROPPATCH,\n        LOCK = a6_method.LOCK,\n        UNLOCK = a6_method.UNLOCK,\n        PATCH = a6_method.PATCH,\n        TRACE = a6_method.TRACE,\n    }\n\n    function encode_a6_method(name)\n        return map[name]\n    end\nend\n\n\nlocal function build_args(builder, key, val)\n    local name = builder:CreateString(key)\n    local value\n    if val ~= true then\n        value = builder:CreateString(val)\n    end\n\n    text_entry.Start(builder)\n    text_entry.AddName(builder, name)\n    if val ~= true then\n        text_entry.AddValue(builder, value)\n    end\n    return text_entry.End(builder)\nend\n\n\nlocal function build_headers(var, builder, key, val)\n    if key == \"host\" then\n        val = var.upstream_host\n    end\n\n    local name = builder:CreateString(key)\n    local value = builder:CreateString(val)\n\n    text_entry.Start(builder)\n    text_entry.AddName(builder, name)\n    text_entry.AddValue(builder, value)\n    return text_entry.End(builder)\nend\n\n\nlocal function handle_extra_info(ctx, input)\n    -- exact request\n    local buf = flatbuffers.binaryArray.New(input)\n    local req = extra_info_req.GetRootAsReq(buf, 0)\n\n    local res\n    local info_type = req:InfoType()\n    if info_type == extra_info.Var then\n        local info = req:Info()\n        local var_req = extra_info_var.New()\n        var_req:Init(info.bytes, info.pos)\n\n        local var_name = var_req:Name()\n        res = ctx.var[var_name]\n    elseif info_type == extra_info.ReqBody then\n        local info = req:Info()\n        local reqbody_req = extra_info_reqbody.New()\n        reqbody_req:Init(info.bytes, info.pos)\n\n        local err\n        res, err = core.request.get_body()\n        if err then\n            core.log.error(\"failed to read request body: \", err)\n        end\n    elseif info_type == extra_info.RespBody then\n        local ext_res = ctx.runner_ext_response\n        if ext_res then\n            local info = req:Info()\n            local respbody_req = extra_info_respbody.New()\n            respbody_req:Init(info.byte, info.pos)\n\n            local chunks = {}\n            local err = helper.response_reader(ext_res.body_reader, function (chunk, chunks)\n                -- When the upstream response is chunked type,\n                -- we will receive the complete response body\n                -- before sending it to the runner program\n                -- to reduce the number of RPC calls.\n                core.table.insert_tail(chunks, chunk)\n            end, chunks)\n            if err then\n                -- TODO: send RPC_ERROR to runner\n                core.log.error(err)\n            else\n                res = core.table.concat(chunks)\n                ctx.runner_ext_response_body = chunks\n            end\n        else\n            core.log.error(\"failed to read response body: not exits\")\n        end\n    else\n        return nil, \"unsupported info type: \" .. info_type\n    end\n\n    -- build response\n    builder:Clear()\n\n    local packed_res\n    if res then\n        -- ensure to pass the res in string type\n        res = tostring(res)\n        packed_res = builder:CreateByteVector(res)\n    end\n    extra_info_resp.Start(builder)\n    if packed_res then\n        extra_info_resp.AddResult(builder, packed_res)\n    end\n    local resp = extra_info_resp.End(builder)\n    builder:Finish(resp)\n    return builder:Output()\nend\n\n\nlocal function fetch_token(key)\n    if shdict then\n        return shdict:get(key)\n    else\n        core.log.error('shm \"ext-plugin\" not found')\n        return nil\n    end\nend\n\n\nlocal function store_token(key, token)\n    if shdict then\n        local exp = helper.get_conf_token_cache_time()\n        -- early expiry, lrucache in critical state sends prepare_conf_req as original behaviour\n        exp = exp * 0.9\n        local success, err, forcible = shdict:set(key, token, exp)\n        if not success then\n            core.log.error(\"ext-plugin:failed to set conf token, err: \", err)\n        end\n        if forcible then\n            core.log.warn(\"ext-plugin:set valid items forcibly overwritten\")\n        end\n    else\n        core.log.error('shm \"ext-plugin\" not found')\n    end\nend\n\n\nlocal function flush_token()\n    if shdict then\n        core.log.warn(\"flush conf token in shared dict\")\n        shdict:flush_all()\n    else\n        core.log.error('shm \"ext-plugin\" not found')\n    end\nend\n\n\nlocal rpc_call\nlocal rpc_handlers = {\n    nil,\n    function (conf, ctx, sock, unique_key)\n        local token = fetch_token(unique_key)\n        if token then\n            core.log.info(\"fetch token from shared dict, token: \", token)\n            return token\n        end\n\n        local lock, err = resty_lock:new(shdict_name)\n        if not lock then\n            return nil, \"failed to create lock: \" .. err\n        end\n\n        local elapsed, err = lock:lock(\"prepare_conf\")\n        if not elapsed then\n            return nil, \"failed to acquire the lock: \" .. err\n        end\n\n        local token = fetch_token(unique_key)\n        if token then\n            lock:unlock()\n            core.log.info(\"fetch token from shared dict, token: \", token)\n            return token\n        end\n\n        builder:Clear()\n\n        local key = builder:CreateString(unique_key)\n        local conf_vec\n        if conf.conf then\n            local len = #conf.conf\n            local textEntries = core.table.new(len, 0)\n            for i = 1, len do\n                local name = builder:CreateString(conf.conf[i].name)\n                local value = builder:CreateString(conf.conf[i].value)\n                text_entry.Start(builder)\n                text_entry.AddName(builder, name)\n                text_entry.AddValue(builder, value)\n                local c = text_entry.End(builder)\n                textEntries[i] = c\n            end\n            prepare_conf_req.StartConfVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            conf_vec = builder:EndVector(len)\n        end\n\n        prepare_conf_req.Start(builder)\n        prepare_conf_req.AddKey(builder, key)\n        if conf_vec then\n            prepare_conf_req.AddConf(builder, conf_vec)\n        end\n        local req = prepare_conf_req.End(builder)\n        builder:Finish(req)\n\n        local ok, err = send(sock, constants.RPC_PREPARE_CONF, builder:Output())\n        if not ok then\n            lock:unlock()\n            return nil, \"failed to send RPC_PREPARE_CONF: \" .. err\n        end\n\n        local ty, resp = receive(sock)\n        if ty == nil then\n            lock:unlock()\n            return nil, \"failed to receive RPC_PREPARE_CONF: \" .. resp\n        end\n\n        if ty ~= constants.RPC_PREPARE_CONF then\n            lock:unlock()\n            return nil, \"failed to receive RPC_PREPARE_CONF: unexpected type \" .. ty\n        end\n\n        local buf = flatbuffers.binaryArray.New(resp)\n        local pcr = prepare_conf_resp.GetRootAsResp(buf, 0)\n        token = pcr:ConfToken()\n\n        core.log.notice(\"get conf token: \", token, \" conf: \", core.json.delay_encode(conf.conf))\n        store_token(unique_key, token)\n\n        lock:unlock()\n\n        return token\n    end,\n    function (conf, ctx, sock, entry)\n        local lrucache_id = core.lrucache.plugin_ctx_id(ctx, entry)\n        local token, err = core.lrucache.plugin_ctx(lrucache, ctx, entry, rpc_call,\n                                                    constants.RPC_PREPARE_CONF, conf, ctx,\n                                                    lrucache_id)\n        if not token then\n            return nil, err\n        end\n\n        builder:Clear()\n        local var = ctx.var\n\n        local uri\n        if var.upstream_uri == \"\" then\n            -- use original uri instead of rewritten one\n            uri = var.uri\n        else\n            uri = var.upstream_uri\n\n            -- the rewritten one may contain new args\n            local index = core.string.find(uri, \"?\")\n            if index then\n                local raw_uri = uri\n                uri = str_sub(raw_uri, 1, index - 1)\n                core.request.set_uri_args(ctx, str_sub(raw_uri, index + 1))\n            end\n        end\n\n        local path = builder:CreateString(uri)\n\n        local bin_addr = var.binary_remote_addr\n        local src_ip = builder:CreateByteVector(bin_addr)\n\n        local args = core.request.get_uri_args(ctx)\n        local textEntries = {}\n        for key, val in pairs(args) do\n            local ty = type(val)\n            if ty == \"table\" then\n                for _, v in ipairs(val) do\n                    core.table.insert(textEntries, build_args(builder, key, v))\n                end\n            else\n                core.table.insert(textEntries, build_args(builder, key, val))\n            end\n        end\n        local len = #textEntries\n        http_req_call_req.StartArgsVector(builder, len)\n        for i = len, 1, -1 do\n            builder:PrependUOffsetTRelative(textEntries[i])\n        end\n        local args_vec = builder:EndVector(len)\n\n        local hdrs = core.request.headers(ctx)\n        core.table.clear(textEntries)\n        for key, val in pairs(hdrs) do\n            local ty = type(val)\n            if ty == \"table\" then\n                for _, v in ipairs(val) do\n                    core.table.insert(textEntries, build_headers(var, builder, key, v))\n                end\n            else\n                core.table.insert(textEntries, build_headers(var, builder, key, val))\n            end\n        end\n        local len = #textEntries\n        http_req_call_req.StartHeadersVector(builder, len)\n        for i = len, 1, -1 do\n            builder:PrependUOffsetTRelative(textEntries[i])\n        end\n        local hdrs_vec = builder:EndVector(len)\n\n        local id = generate_id()\n        local method = var.method\n\n        http_req_call_req.Start(builder)\n        http_req_call_req.AddId(builder, id)\n        http_req_call_req.AddConfToken(builder, token)\n        http_req_call_req.AddSrcIp(builder, src_ip)\n        http_req_call_req.AddPath(builder, path)\n        http_req_call_req.AddArgs(builder, args_vec)\n        http_req_call_req.AddHeaders(builder, hdrs_vec)\n        http_req_call_req.AddMethod(builder, encode_a6_method(method))\n\n        local req = http_req_call_req.End(builder)\n        builder:Finish(req)\n\n        local ok, err = send(sock, constants.RPC_HTTP_REQ_CALL, builder:Output())\n        if not ok then\n            return nil, \"failed to send RPC_HTTP_REQ_CALL: \" .. err\n        end\n\n        local ty, resp\n        while true do\n            ty, resp = receive(sock)\n            if ty == nil then\n                return nil, \"failed to receive RPC_HTTP_REQ_CALL: \" .. resp\n            end\n\n            if ty ~= constants.RPC_EXTRA_INFO then\n                break\n            end\n\n            local out, err = handle_extra_info(ctx, resp)\n            if not out then\n                return nil, \"failed to handle RPC_EXTRA_INFO: \" .. err\n            end\n\n            local ok, err = send(sock, constants.RPC_EXTRA_INFO, out)\n            if not ok then\n                return nil, \"failed to reply RPC_EXTRA_INFO: \" .. err\n            end\n        end\n\n        if ty ~= constants.RPC_HTTP_REQ_CALL then\n            return nil, \"failed to receive RPC_HTTP_REQ_CALL: unexpected type \" .. ty\n        end\n\n        local buf = flatbuffers.binaryArray.New(resp)\n        local call_resp = http_req_call_resp.GetRootAsResp(buf, 0)\n        local action_type = call_resp:ActionType()\n        if action_type == http_req_call_action.Stop then\n            local action = call_resp:Action()\n            local stop = http_req_call_stop.New()\n            stop:Init(action.bytes, action.pos)\n\n            local len = stop:HeadersLength()\n            if len > 0 then\n                local stop_resp_headers = {}\n                for i = 1, len do\n                    local entry = stop:Headers(i)\n                    local name = str_lower(entry:Name())\n                    if stop_resp_headers[name] == nil then\n                        core.response.set_header(name, entry:Value())\n                        stop_resp_headers[name] = true\n                    else\n                        core.response.add_header(name, entry:Value())\n                    end\n                end\n            end\n\n            local body\n            local len = stop:BodyLength()\n            if len > 0 then\n                -- TODO: support empty body\n                body = stop:BodyAsString()\n            end\n            local code = stop:Status()\n            -- avoid using 0 as the default http status code\n            if code == 0 then\n                 code = 200\n            end\n            return true, nil, code, body\n        end\n\n        if action_type == http_req_call_action.Rewrite then\n            local action = call_resp:Action()\n            local rewrite = http_req_call_rewrite.New()\n            rewrite:Init(action.bytes, action.pos)\n\n            local path = rewrite:Path()\n            if path then\n                path = core.utils.uri_safe_encode(path)\n                var.upstream_uri = path\n            end\n\n            local len = rewrite:HeadersLength()\n            if len > 0 then\n                for i = 1, len do\n                    local entry = rewrite:Headers(i)\n                    local name = entry:Name()\n                    core.request.set_header(ctx, name, entry:Value())\n\n                    if str_lower(name) == \"host\" then\n                        var.upstream_host = entry:Value()\n                    end\n                end\n            end\n\n            local body_len = rewrite:BodyLength()\n            if body_len > 0 then\n                local body = rewrite:BodyAsString()\n                ngx.req.read_body()\n                ngx.req.set_body_data(body)\n            end\n\n            local len = rewrite:RespHeadersLength()\n            if len > 0 then\n                local rewrite_resp_headers = {}\n                for i = 1, len do\n                    local entry = rewrite:RespHeaders(i)\n                    local name = str_lower(entry:Name())\n                    if exclude_resp_header[name] == nil then\n                        if rewrite_resp_headers[name] == nil then\n                            core.response.set_header(name, entry:Value())\n                            rewrite_resp_headers[name] = true\n                        else\n                            core.response.add_header(name, entry:Value())\n                        end\n                    end\n                end\n            end\n\n            local len = rewrite:ArgsLength()\n            if len > 0 then\n                local changed = {}\n                for i = 1, len do\n                    local entry = rewrite:Args(i)\n                    local name = entry:Name()\n                    local value = entry:Value()\n                    if value == nil then\n                        args[name] = nil\n\n                    else\n                        if changed[name] then\n                            if type(args[name]) == \"table\" then\n                                core.table.insert(args[name], value)\n                            else\n                                args[name] = {args[name], entry:Value()}\n                            end\n                        else\n                            args[name] = entry:Value()\n                        end\n\n                        changed[name] = true\n                    end\n                end\n\n                core.request.set_uri_args(ctx, args)\n            end\n\n            if path then\n                var.upstream_uri = path .. (var.is_args or '') .. (var.args or '')\n            end\n        end\n\n        return true\n    end,\n    nil, -- ignore RPC_EXTRA_INFO, already processed during RPC_HTTP_REQ_CALL interaction\n    function (conf, ctx, sock, entry)\n        local lrucache_id = core.lrucache.plugin_ctx_id(ctx, entry)\n        local token, err = core.lrucache.plugin_ctx(lrucache, ctx, entry, rpc_call,\n                                                    constants.RPC_PREPARE_CONF, conf, ctx,\n                                                    lrucache_id)\n        if not token then\n            return nil, err\n        end\n\n        builder:Clear()\n        local var = ctx.var\n\n        local res = ctx.runner_ext_response\n        local textEntries = {}\n        local hdrs = res.headers\n        for key, val in pairs(hdrs) do\n            local ty = type(val)\n            if ty == \"table\" then\n                for _, v in ipairs(val) do\n                    core.table.insert(textEntries, build_headers(var, builder, key, v))\n                end\n            else\n                core.table.insert(textEntries, build_headers(var, builder, key, val))\n            end\n        end\n        local len = #textEntries\n        http_resp_call_req.StartHeadersVector(builder, len)\n        for i = len, 1, -1 do\n            builder:PrependUOffsetTRelative(textEntries[i])\n        end\n        local hdrs_vec = builder:EndVector(len)\n\n        local id = generate_id()\n        local status = res.status\n\n        http_resp_call_req.Start(builder)\n        http_resp_call_req.AddId(builder, id)\n        http_resp_call_req.AddStatus(builder, status)\n        http_resp_call_req.AddConfToken(builder, token)\n        http_resp_call_req.AddHeaders(builder, hdrs_vec)\n\n        local req = http_resp_call_req.End(builder)\n        builder:Finish(req)\n\n        local ok, err = send(sock, constants.RPC_HTTP_RESP_CALL, builder:Output())\n        if not ok then\n            return nil, \"failed to send RPC_HTTP_RESP_CALL: \" .. err\n        end\n\n        local ty, resp\n        while true do\n            ty, resp = receive(sock)\n            if ty == nil then\n                return nil, \"failed to receive RPC_HTTP_REQ_CALL: \" .. resp\n            end\n\n            if ty ~= constants.RPC_EXTRA_INFO then\n                break\n            end\n\n            local out, err = handle_extra_info(ctx, resp)\n            if not out then\n                return nil, \"failed to handle RPC_EXTRA_INFO: \" .. err\n            end\n\n            local ok, err = send(sock, constants.RPC_EXTRA_INFO, out)\n            if not ok then\n                return nil, \"failed to reply RPC_EXTRA_INFO: \" .. err\n            end\n        end\n\n        if ty ~= constants.RPC_HTTP_RESP_CALL then\n            return nil, \"failed to receive RPC_HTTP_RESP_CALL: unexpected type \" .. ty\n        end\n\n        local buf = flatbuffers.binaryArray.New(resp)\n        local call_resp = http_resp_call_resp.GetRootAsResp(buf, 0)\n        local len = call_resp:HeadersLength()\n        if len > 0 then\n            local resp_headers = {}\n            for i = 1, len do\n                local entry = call_resp:Headers(i)\n                local name = str_lower(entry:Name())\n                if resp_headers[name] == nil then\n                    core.response.set_header(name, entry:Value())\n                    resp_headers[name] = true\n                else\n                    core.response.add_header(name, entry:Value())\n                end\n            end\n        else\n            -- Filter out origin headeres\n            for k, v in pairs(res.headers) do\n                if not exclude_resp_header[str_lower(k)] then\n                    core.response.set_header(k, v)\n                end\n            end\n        end\n\n        local body\n        local len = call_resp:BodyLength()\n        if len > 0 then\n            -- TODO: support empty body\n            body = call_resp:BodyAsString()\n        end\n        local code = call_resp:Status()\n        core.log.info(\"recv resp, code: \", code, \" body: \", body, \" len: \", len)\n\n        if code == 0 then\n            -- runner changes body only, we should set code.\n            code = body and res.status or nil\n        end\n\n        return true, nil, code, body\n    end\n}\n\n\nrpc_call = function (ty, conf, ctx, ...)\n    local path = helper.get_path()\n\n    local sock = socket_tcp()\n    sock:settimeouts(1000, 60000, 60000)\n    local ok, err = sock:connect(path)\n    if not ok then\n        return nil, \"failed to connect to the unix socket \" .. path .. \": \" .. err\n    end\n\n    local res, err, code, body = rpc_handlers[ty + 1](conf, ctx, sock, ...)\n    if not res then\n        sock:close()\n        return nil, err\n    end\n\n    local ok, err = sock:setkeepalive(180 * 1000, 32)\n    if not ok then\n        core.log.info(\"failed to setkeepalive: \", err)\n    end\n\n    return res, nil, code, body\nend\n\n\nlocal function recreate_lrucache()\n    flush_token()\n\n    if lrucache then\n        core.log.warn(\"flush conf token lrucache\")\n    end\n\n    lrucache = new_lrucache()\nend\n\n\nfunction _M.communicate(conf, ctx, plugin_name, rpc_cmd)\n    local ok, err, code, body\n    local tries = 0\n    local ty = rpc_cmd and rpc_cmd or constants.RPC_HTTP_REQ_CALL\n    while tries < 3 do\n        tries = tries + 1\n        ok, err, code, body = rpc_call(ty, conf, ctx, plugin_name)\n        if ok then\n            if code then\n                return code, body\n            end\n\n            return\n        end\n\n        if not core.string.find(err, \"conf token not found\") then\n            core.log.error(err)\n            if conf.allow_degradation then\n                core.log.warn(\"Plugin Runner is wrong, allow degradation\")\n                return\n            end\n            return 503\n        end\n\n        core.log.warn(\"refresh cache and try again\")\n        recreate_lrucache()\n    end\n\n    core.log.error(err)\n    if conf.allow_degradation then\n        core.log.warn(\"Plugin Runner is wrong after \" .. tries .. \" times retry, allow degradation\")\n        return\n    end\n    return 503\nend\n\n\nlocal function must_set(env, value)\n    local ok, err = core.os.setenv(env, value)\n    if not ok then\n        error(str_format(\"failed to set %s: %s\", env, err), 2)\n    end\nend\n\n\nlocal function spawn_proc(cmd)\n    must_set(\"APISIX_CONF_EXPIRE_TIME\", helper.get_conf_token_cache_time())\n    must_set(\"APISIX_LISTEN_ADDRESS\", helper.get_path())\n\n    local opt = {\n        merge_stderr = true,\n    }\n    local proc, err = ngx_pipe.spawn(cmd, opt)\n    if not proc then\n        error(str_format(\"failed to start %s: %s\", core.json.encode(cmd), err))\n        -- TODO: add retry\n    end\n\n    proc:set_timeouts(nil, nil, nil, 0)\n    return proc\nend\n\n\nlocal runner\nlocal function setup_runner(cmd)\n\n    ngx_timer_at(0, function(premature)\n        if premature then\n            return\n        end\n\n        runner = spawn_proc(cmd)\n\n        while not exiting() do\n            while true do\n                -- drain output\n                local max = 3800 -- smaller than Nginx error log length limit\n                local data, err = runner:stdout_read_any(max)\n                if not data then\n                    if exiting() then\n                        return\n                    end\n\n                    if err == \"closed\" then\n                        break\n                    end\n                else\n                    -- we log stdout here just for debug or test\n                    -- the runner itself should log to a file\n                    core.log.warn(data)\n                end\n            end\n\n            local ok, reason, status = runner:wait()\n            if not ok then\n                core.log.warn(\"runner exited with reason: \", reason, \", status: \", status)\n            end\n\n            runner = nil\n            local ok, err = events:post(events_list._source, events_list.runner_exit)\n            if not ok then\n                core.log.error(\"post event failure with \", events_list._source, \", error: \", err)\n            end\n\n            core.log.warn(\"respawn runner 3 seconds later with cmd: \", core.json.encode(cmd))\n            core.utils.sleep(3)\n            core.log.warn(\"respawning new runner...\")\n            runner = spawn_proc(cmd)\n        end\n    end)\nend\n\n\nfunction _M.init_worker()\n    local local_conf = core.config.local_conf()\n    local cmd = core.table.try_read_attr(local_conf, \"ext-plugin\", \"cmd\")\n    if not cmd then\n        return\n    end\n\n    events_list = events:event_list(\n        \"process_runner_exit_event\",\n        \"runner_exit\"\n    )\n\n    -- flush cache when runner exited\n    events:register(recreate_lrucache, events_list._source, events_list.runner_exit)\n\n    -- note that the runner is run under the same user as the Nginx master\n    if process.type() == \"privileged agent\" then\n        setup_runner(cmd)\n    end\nend\n\n\nfunction _M.exit_worker()\n    if process.type() == \"privileged agent\" and runner then\n        -- We need to send SIGTERM in the exit_worker phase, as:\n        -- 1. privileged agent doesn't support graceful exiting when I write this\n        -- 2. better to make it work without graceful exiting\n        local pid = runner:pid()\n        core.log.notice(\"terminate runner \", pid, \" with SIGTERM\")\n        local num = resty_signal.signum(\"TERM\")\n        runner:kill(num)\n\n        -- give 1s to clean up the mess\n        core.os.waitpid(pid, 1)\n        -- then we KILL it via gc finalizer\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ext-plugin-post-req.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ext = require(\"apisix.plugins.ext-plugin.init\")\n\n\nlocal name = \"ext-plugin-post-req\"\nlocal _M = {\n    version = 0.1,\n    priority = -3000,\n    name = name,\n    schema = ext.schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(_M.schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    return ext.communicate(conf, ctx, name)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ext-plugin-post-resp.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ext = require(\"apisix.plugins.ext-plugin.init\")\nlocal helper = require(\"apisix.plugins.ext-plugin.helper\")\nlocal constants = require(\"apisix.constants\")\nlocal http = require(\"resty.http\")\n\nlocal ngx       = ngx\nlocal ngx_print = ngx.print\nlocal ngx_flush = ngx.flush\nlocal string    = string\nlocal str_sub   = string.sub\n\n\nlocal name = \"ext-plugin-post-resp\"\nlocal _M = {\n    version = 0.1,\n    priority = -4000,\n    name = name,\n    schema = ext.schema,\n}\n\n\nlocal function include_req_headers(ctx)\n    -- TODO: handle proxy_set_header\n    return core.request.headers(ctx)\nend\n\n\nlocal function close(http_obj)\n    -- TODO: keepalive\n    local ok, err = http_obj:close()\n    if not ok then\n        core.log.error(\"close http object failed: \", err)\n    end\nend\n\n\nlocal function get_response(ctx, http_obj)\n    local ok, err = http_obj:connect({\n        scheme = ctx.upstream_scheme,\n        host = ctx.picked_server.host,\n        port = ctx.picked_server.port,\n    })\n\n    if not ok then\n        return nil, err\n    end\n    -- TODO: set timeout\n    local uri, args\n    if ctx.var.upstream_uri == \"\" then\n        -- use original uri instead of rewritten one\n        uri = ctx.var.uri\n    else\n        uri = ctx.var.upstream_uri\n\n        -- the rewritten one may contain new args\n        local index = core.string.find(uri, \"?\")\n        if index then\n            local raw_uri = uri\n            uri = str_sub(raw_uri, 1, index - 1)\n            args = str_sub(raw_uri, index + 1)\n        end\n    end\n    local params = {\n        path = uri,\n        query = args or ctx.var.args,\n        headers = include_req_headers(ctx),\n        method = core.request.get_method(),\n    }\n\n    local body, err = core.request.get_body()\n    if err then\n        return nil, err\n    end\n\n    if body then\n        params[\"body\"] = body\n    end\n\n    local res, err = http_obj:request(params)\n    if not res then\n        return nil, err\n    end\n\n    return res, err\nend\n\nlocal function send_chunk(chunk)\n    if not chunk then\n        return nil\n    end\n\n    local ok, print_err = ngx_print(chunk)\n    if not ok then\n        return \"output response failed: \".. (print_err or \"\")\n    end\n    local ok, flush_err = ngx_flush(true)\n    if not ok then\n        core.log.warn(\"flush response failed: \", flush_err)\n    end\n\n    return nil\nend\n\n-- TODO: response body is empty (304 or HEAD)\n-- If the upstream returns 304 or the request method is HEAD,\n-- there is no response body. In this case,\n-- we need to send a response to the client in the plugin,\n-- instead of continuing to execute the subsequent plugin.\nlocal function send_response(ctx, res, code)\n    ngx.status = code or res.status\n\n    local chunks = ctx.runner_ext_response_body\n    if chunks then\n        for i=1, #chunks do\n            local err = send_chunk(chunks[i])\n            if err then\n                return err\n            end\n        end\n        return\n    end\n\n    return helper.response_reader(res.body_reader, send_chunk)\nend\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(_M.schema, conf)\nend\n\n\nfunction _M.before_proxy(conf, ctx)\n    local http_obj = http.new()\n    local res, err = get_response(ctx, http_obj)\n    if not res or err then\n        core.log.error(\"failed to request: \", err or \"\")\n        close(http_obj)\n        return 502\n    end\n    ctx.runner_ext_response = res\n\n    core.log.info(\"response info, status: \", res.status)\n    core.log.info(\"response info, headers: \", core.json.delay_encode(res.headers))\n\n    local code, body = ext.communicate(conf, ctx, name, constants.RPC_HTTP_RESP_CALL)\n    if body then\n        close(http_obj)\n        -- if the body is changed, the code will be set.\n        return code, body\n    end\n    core.log.info(\"ext-plugin will send response\")\n\n    -- send origin response, status maybe changed.\n    err = send_response(ctx, res, code)\n    close(http_obj)\n\n    if err then\n        core.log.error(err)\n        return not ngx.headers_sent and 502 or nil\n    end\n\n    core.log.info(\"ext-plugin send response succefully\")\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ext-plugin-pre-req.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ext = require(\"apisix.plugins.ext-plugin.init\")\n\n\nlocal name = \"ext-plugin-pre-req\"\nlocal _M = {\n    version = 0.1,\n    priority = 12000,\n    name = name,\n    schema = ext.schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(_M.schema, conf)\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    return ext.communicate(conf, ctx, name)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/fault-injection.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal expr = require(\"resty.expr.v1\")\n\nlocal sleep = core.sleep\nlocal random = math.random\nlocal ipairs = ipairs\nlocal ngx = ngx\nlocal pairs = pairs\nlocal type = type\n\nlocal plugin_name   = \"fault-injection\"\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        abort = {\n            type = \"object\",\n            properties = {\n                http_status = {type = \"integer\", minimum = 200},\n                body = {type = \"string\", minLength = 0},\n                headers = {\n                    type = \"object\",\n                    minProperties = 1,\n                    patternProperties = {\n                        [\"^[^:]+$\"] = {\n                            oneOf = {\n                                { type = \"string\" },\n                                { type = \"number\" }\n                            }\n                        }\n                    }\n                },\n                percentage = {type = \"integer\", minimum = 0, maximum = 100},\n                vars = {\n                    type = \"array\",\n                    maxItems = 20,\n                    items = {\n                        type = \"array\",\n                    },\n                }\n            },\n            required = {\"http_status\"},\n        },\n        delay = {\n            type = \"object\",\n            properties = {\n                duration = {type = \"number\", minimum = 0},\n                percentage = {type = \"integer\", minimum = 0, maximum = 100},\n                vars = {\n                    type = \"array\",\n                    maxItems = 20,\n                    items = {\n                        type = \"array\",\n                    },\n                }\n            },\n            required = {\"duration\"},\n        }\n    },\n    minProperties = 1,\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 11000,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nlocal function sample_hit(percentage)\n    if not percentage then\n        return true\n    end\n\n    return random(1, 100) <= percentage\nend\n\n\nlocal function vars_match(vars, ctx)\n    local match_result\n    for _, var in ipairs(vars) do\n        local expr, _ = expr.new(var)\n        match_result = expr:eval(ctx.var)\n        if match_result then\n            break\n        end\n    end\n\n    return match_result\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.abort and conf.abort.vars then\n        for _, var in ipairs(conf.abort.vars) do\n            local _, err = expr.new(var)\n            if err then\n                core.log.error(\"failed to create vars expression: \", err)\n                return false, err\n            end\n        end\n    end\n\n    if conf.delay and conf.delay.vars then\n        for _, var in ipairs(conf.delay.vars) do\n            local _, err = expr.new(var)\n            if err then\n                core.log.error(\"failed to create vars expression: \", err)\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    core.log.info(\"plugin rewrite phase, conf: \", core.json.delay_encode(conf))\n\n    local abort_vars = true\n    if conf.abort and conf.abort.vars then\n        abort_vars = vars_match(conf.abort.vars, ctx)\n    end\n    core.log.info(\"abort_vars: \", abort_vars)\n\n    local delay_vars = true\n    if conf.delay and conf.delay.vars then\n        delay_vars = vars_match(conf.delay.vars, ctx)\n    end\n    core.log.info(\"delay_vars: \", delay_vars)\n\n    if conf.delay and sample_hit(conf.delay.percentage) and delay_vars then\n        sleep(conf.delay.duration)\n    end\n\n    if conf.abort and sample_hit(conf.abort.percentage) and abort_vars then\n        if conf.abort.headers then\n            for header_name, header_value in pairs(conf.abort.headers) do\n                if type(header_value) == \"string\" then\n                    header_value = core.utils.resolve_var(header_value, ctx.var)\n                end\n                ngx.header[header_name] = header_value\n            end\n        end\n        return conf.abort.http_status, core.utils.resolve_var(conf.abort.body, ctx.var)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/file-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal log_util     =   require(\"apisix.utils.log-util\")\nlocal core         =   require(\"apisix.core\")\nlocal plugin       =   require(\"apisix.plugin\")\nlocal expr         =   require(\"resty.expr.v1\")\nlocal ngx          =   ngx\nlocal io_open      =   io.open\nlocal is_apisix_or, process = pcall(require, \"resty.apisix.process\")\n\n\nlocal plugin_name = \"file-logger\"\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        path = {\n            type = \"string\"\n        },\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        match = {\n            type = \"array\",\n            maxItems = 20,\n            items = {\n                type = \"array\",\n            },\n        }\n    },\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        path = {\n            type = \"string\"\n        },\n        log_format = {\n            type = \"object\"\n        }\n    }\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 399,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema\n}\n\n\nlocal function get_configured_path(conf)\n    if conf.path then\n        return conf.path\n    end\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if metadata and metadata.value and metadata.value.path then\n        return metadata.value.path\n    end\n\n    return nil, \"property \\\"path\\\" is not set in either the plugin conf or the metadata\"\nend\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    if conf.match then\n        local ok, err = expr.new(conf.match)\n        if not ok then\n            return nil, \"failed to validate the 'match' expression: \" .. err\n        end\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return ok, err\n    end\n\n    local path, err = get_configured_path(conf)\n    if not path then\n        return nil, err\n    end\n\n    return true\nend\n\n\nlocal open_file_cache\nif is_apisix_or then\n    -- TODO: switch to a cache which supports inactive time,\n    -- so that unused files would not be cached\n    local path_to_file = core.lrucache.new({\n        type = \"plugin\",\n    })\n\n    local function open_file_handler(conf, handler)\n        local file, err = io_open(conf.path, 'a+')\n        if not file then\n            return nil, err\n        end\n\n        -- it will case output problem with buffer when log is larger than buffer\n        file:setvbuf(\"no\")\n\n        handler.file = file\n        handler.open_time = ngx.now() * 1000\n        return handler\n    end\n\n    function open_file_cache(conf)\n        local last_reopen_time = process.get_last_reopen_ms()\n\n        local handler, err = path_to_file(conf.path, 0, open_file_handler, conf, {})\n        if not handler then\n            return nil, err\n        end\n\n        if handler.open_time < last_reopen_time then\n            core.log.notice(\"reopen cached log file: \", conf.path)\n            handler.file:close()\n\n            local ok, err = open_file_handler(conf, handler)\n            if not ok then\n                return nil, err\n            end\n        end\n\n        return handler.file\n    end\nend\n\n\nlocal function write_file_data(conf, log_message)\n    local path, err = get_configured_path(conf)\n    if not path then\n        core.log.error(err)\n        return\n    end\n\n    local msg = core.json.encode(log_message)\n\n    local file, err\n    local file_conf = conf.path and conf or {path = path}\n    if open_file_cache then\n        file, err = open_file_cache(file_conf)\n    else\n        file, err = io_open(path, 'a+')\n    end\n\n    if not file then\n        core.log.error(\"failed to open file: \", path, \", error info: \", err)\n    else\n        -- file:write(msg, \"\\n\") will call fwrite several times\n        -- which will cause problem with the log output\n        -- it should be atomic\n        msg = msg .. \"\\n\"\n        -- write to file directly, no need flush\n        local ok, err = file:write(msg)\n        if not ok then\n            core.log.error(\"failed to write file: \", path, \", error info: \", err)\n        end\n\n        -- file will be closed by gc, if open_file_cache exists\n        if not open_file_cache then\n            file:close()\n        end\n    end\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    if entry == nil then\n        return\n    end\n    write_file_data(conf, entry)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/forward-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ipairs   = ipairs\nlocal core     = require(\"apisix.core\")\nlocal http     = require(\"resty.http\")\nlocal pairs    = pairs\nlocal type     = type\nlocal tostring = tostring\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        uri = {type = \"string\"},\n        allow_degradation = {type = \"boolean\", default = false},\n        status_on_error = {type = \"integer\", minimum = 200, maximum = 599, default = 403},\n        ssl_verify = {\n            type = \"boolean\",\n            default = true,\n        },\n        request_method = {\n            type = \"string\",\n            default = \"GET\",\n            enum = {\"GET\", \"POST\"},\n            description = \"the method for client to request the authorization service\"\n        },\n        request_headers = {\n            type = \"array\",\n            default = {},\n            items = {type = \"string\"},\n            description = \"client request header that will be sent to the authorization service\"\n        },\n        extra_headers = {\n            type = \"object\",\n            minProperties = 1,\n            patternProperties = {\n                [\"^[^:]+$\"] = {\n                    type = \"string\",\n                    description = \"header value as a string; may contain variables\"\n                                  .. \"like $remote_addr, $request_uri\"\n                }\n            },\n            description = \"extra headers sent to the authorization service; \"\n                        .. \"values must be strings and can include variables\"\n                        .. \"like $remote_addr, $request_uri.\"\n        },\n        upstream_headers = {\n            type = \"array\",\n            default = {},\n            items = {type = \"string\"},\n            description = \"authorization response header that will be sent to the upstream\"\n        },\n        client_headers = {\n            type = \"array\",\n            default = {},\n            items = {type = \"string\"},\n            description = \"authorization response header that will be sent to\"\n                           .. \"the client when authorizing failed\"\n        },\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 60000,\n            default = 3000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {type = \"integer\", minimum = 1000, default = 60000},\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 5},\n    },\n    required = {\"uri\"}\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2002,\n    name = \"forward-auth\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local check = {\"uri\"}\n    core.utils.check_https(check, conf, _M.name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, _M.name)\n\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    local auth_headers = {\n        [\"X-Forwarded-Proto\"] = core.request.get_scheme(ctx),\n        [\"X-Forwarded-Method\"] = core.request.get_method(),\n        [\"X-Forwarded-Host\"] = core.request.get_host(ctx),\n        [\"X-Forwarded-Uri\"] = ctx.var.request_uri,\n        [\"X-Forwarded-For\"] = core.request.get_remote_client_ip(ctx),\n    }\n\n    if conf.request_method == \"POST\" then\n        auth_headers[\"Content-Length\"] = core.request.header(ctx, \"content-length\")\n        auth_headers[\"Expect\"] = core.request.header(ctx, \"expect\")\n        auth_headers[\"Transfer-Encoding\"] = core.request.header(ctx, \"transfer-encoding\")\n        auth_headers[\"Content-Encoding\"] = core.request.header(ctx, \"content-encoding\")\n    end\n\n    if conf.extra_headers then\n        for header, value in pairs(conf.extra_headers) do\n            if type(value) == \"number\" then\n                value = tostring(value)\n            end\n            local resolve_value, err = core.utils.resolve_var(value, ctx.var)\n            if not err then\n                auth_headers[header] = resolve_value\n            end\n            if err then\n                core.log.error(\"failed to resolve variable in extra header '\",\n                                header, \"': \",value,\": \",err)\n            end\n        end\n    end\n\n    -- append headers that need to be get from the client request header\n    if #conf.request_headers > 0 then\n        for _, header in ipairs(conf.request_headers) do\n            if not auth_headers[header] then\n                auth_headers[header] = core.request.header(ctx, header)\n            end\n        end\n    end\n\n    local params = {\n        headers = auth_headers,\n        keepalive = conf.keepalive,\n        ssl_verify = conf.ssl_verify,\n        method = conf.request_method\n    }\n\n    if params.method == \"POST\" then\n        params.body = core.request.get_body()\n    end\n\n    if conf.keepalive then\n        params.keepalive_timeout = conf.keepalive_timeout\n        params.keepalive_pool = conf.keepalive_pool\n    end\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout)\n\n    local res, err = httpc:request_uri(conf.uri, params)\n    if not res and conf.allow_degradation then\n        return\n    elseif not res then\n        core.log.warn(\"failed to process forward auth, err: \", err)\n        return conf.status_on_error\n    end\n\n    if res.status >= 300 then\n        local client_headers = {}\n\n        if #conf.client_headers > 0 then\n            for _, header in ipairs(conf.client_headers) do\n                client_headers[header] = res.headers[header]\n            end\n        end\n\n        core.response.set_header(client_headers)\n        return res.status, res.body\n    end\n\n    -- append headers that need to be get from the auth response header\n    for _, header in ipairs(conf.upstream_headers) do\n        local header_value = res.headers[header]\n        if header_value then\n            core.request.set_header(ctx, header, header_value)\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/gm.lua",
    "content": "-- Licensed to the Apache Software Foundation (ASF) under one\n-- or more contributor license agreements.  See the NOTICE file\n-- distributed with this work for additional information\n-- regarding copyright ownership.  The ASF licenses this file\n-- to you under the Apache License, Version 2.0 (the\n-- \"License\"); you may not use this file except in compliance\n-- with the License.  You may obtain a copy of the License at\n--\n--   http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing,\n-- software distributed under the License is distributed on an\n-- \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n-- KIND, either express or implied.  See the License for the\n-- specific language governing permissions and limitations\n-- under the License.\n\n-- local common libs\nlocal require = require\nlocal pcall = pcall\nlocal ffi = require(\"ffi\")\nlocal C = ffi.C\nlocal get_request = require(\"resty.core.base\").get_request\nlocal core = require(\"apisix.core\")\nlocal radixtree_sni = require(\"apisix.ssl.router.radixtree_sni\")\nlocal apisix_ssl = require(\"apisix.ssl\")\nlocal _, ssl = pcall(require, \"resty.apisix.ssl\")\nlocal error = error\n\n\nffi.cdef[[\nunsigned long Tongsuo_version_num(void)\n]]\n\n\n-- local function\nlocal function set_pem_ssl_key(sni, enc_cert, enc_pkey, sign_cert, sign_pkey)\n    local r = get_request()\n    if r == nil then\n        return false, \"no request found\"\n    end\n\n    local parsed_enc_cert, err = apisix_ssl.fetch_cert(sni, enc_cert)\n    if not parsed_enc_cert then\n        return false, \"failed to parse enc PEM cert: \" .. err\n    end\n\n    local parsed_sign_cert, err = apisix_ssl.fetch_cert(sni, sign_cert)\n    if not parsed_sign_cert then\n        return false, \"failed to parse sign PEM cert: \" .. err\n    end\n\n    local ok, err = ssl.set_gm_cert(parsed_enc_cert, parsed_sign_cert)\n    if not ok then\n        return false, \"failed to set PEM cert: \" .. err\n    end\n\n    local parsed_enc_pkey, err = apisix_ssl.fetch_pkey(sni, enc_pkey)\n    if not parsed_enc_pkey then\n        return false, \"failed to parse enc PEM priv key: \" .. err\n    end\n\n    local parsed_sign_pkey, err = apisix_ssl.fetch_pkey(sni, sign_pkey)\n    if not parsed_sign_pkey then\n        return false, \"failed to parse sign PEM priv key: \" .. err\n    end\n\n    ok, err = ssl.set_gm_priv_key(parsed_enc_pkey, parsed_sign_pkey)\n    if not ok then\n        return false, \"failed to set PEM priv key: \" .. err\n    end\n\n    return true\nend\n\n\nlocal original_set_cert_and_key\nlocal function set_cert_and_key(sni, value)\n    if value.gm then\n        -- process as GM certificate\n        -- For GM dual certificate, the `cert` and `key` will be encryption cert/key.\n        -- The first item in `certs` and `keys` will be sign cert/key.\n        local enc_cert = value.cert\n        local enc_pkey = value.key\n        local sign_cert = value.certs[1]\n        local sign_pkey = value.keys[1]\n        return set_pem_ssl_key(sni, enc_cert, enc_pkey, sign_cert, sign_pkey)\n    end\n    return original_set_cert_and_key(sni, value)\nend\n\n\nlocal original_check_ssl_conf\nlocal function check_ssl_conf(in_dp, conf)\n    if conf.gm then\n        -- process as GM certificate\n        -- For GM dual certificate, the `cert` and `key` will be encryption cert/key.\n        -- The first item in `certs` and `keys` will be sign cert/key.\n        local ok, err = original_check_ssl_conf(in_dp, conf)\n        -- check cert/key first in the original method\n        if not ok then\n            return nil, err\n        end\n\n        -- Currently, APISIX doesn't check the cert type (ECDSA / RSA). So we skip the\n        -- check for now in this plugin.\n        local num_certs = conf.certs and #conf.certs or 0\n        local num_keys = conf.keys and #conf.keys or 0\n        if num_certs ~= 1 or num_keys ~= 1 then\n            return nil, \"sign cert/key are required\"\n        end\n        return true\n    end\n    return original_check_ssl_conf(in_dp, conf)\nend\n\n\n-- module define\nlocal plugin_name = \"gm\"\n\n-- plugin schema\nlocal plugin_schema = {\n    type = \"object\",\n    properties = {\n    },\n}\n\nlocal _M = {\n    version  = 0.1,            -- plugin version\n    priority = -43,\n    name     = plugin_name,    -- plugin name\n    schema   = plugin_schema,  -- plugin schema\n}\n\n\nfunction _M.init()\n    if not pcall(function () return C.Tongsuo_version_num end) then\n        error(\"need to build Tongsuo (https://github.com/Tongsuo-Project/Tongsuo) \" ..\n              \"into the APISIX-Runtime\")\n    end\n\n    ssl.enable_ntls()\n    original_set_cert_and_key = radixtree_sni.set_cert_and_key\n    radixtree_sni.set_cert_and_key = set_cert_and_key\n    original_check_ssl_conf = apisix_ssl.check_ssl_conf\n    apisix_ssl.check_ssl_conf = check_ssl_conf\n\n    if core.schema.ssl.properties.gm ~= nil then\n        error(\"Field 'gm' is occupied\")\n    end\n\n    -- inject a mark to distinguish GM certificate\n    core.schema.ssl.properties.gm = {\n        type = \"boolean\"\n    }\nend\n\n\nfunction _M.destroy()\n    ssl.disable_ntls()\n    radixtree_sni.set_cert_and_key = original_set_cert_and_key\n    apisix_ssl.check_ssl_conf = original_check_ssl_conf\n    core.schema.ssl.properties.gm = nil\nend\n\n-- module interface for schema check\n-- @param `conf` user defined conf data\n-- @param `schema_type` defined in `apisix/core/schema.lua`\n-- @return <boolean>\nfunction _M.check_schema(conf, schema_type)\n    return core.schema.check(plugin_schema, conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/google-cloud-logging.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core            = require(\"apisix.core\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal tostring        = tostring\nlocal http            = require(\"resty.http\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal google_oauth    = require(\"apisix.utils.google-cloud-oauth\")\n\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal plugin_name = \"google-cloud-logging\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\nlocal schema = {\n    type = \"object\",\n    properties = {\n        auth_config = {\n            type = \"object\",\n            properties = {\n                client_email = { type = \"string\" },\n                private_key = { type = \"string\" },\n                project_id = { type = \"string\" },\n                token_uri = {\n                    type = \"string\",\n                    default = \"https://oauth2.googleapis.com/token\"\n                },\n                -- https://developers.google.com/identity/protocols/oauth2/scopes#logging\n                scope = {\n                    type = \"array\",\n                    items = {\n                        description = \"Google OAuth2 Authorization Scopes\",\n                        type = \"string\",\n                    },\n                    minItems = 1,\n                    uniqueItems = true,\n                    default = {\n                        \"https://www.googleapis.com/auth/logging.read\",\n                        \"https://www.googleapis.com/auth/logging.write\",\n                        \"https://www.googleapis.com/auth/logging.admin\",\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    }\n                },\n                scopes = {\n                    type = \"array\",\n                    items = {\n                        description = \"Google OAuth2 Authorization Scopes\",\n                        type = \"string\",\n                    },\n                    minItems = 1,\n                    uniqueItems = true\n                },\n                entries_uri = {\n                    type = \"string\",\n                    default = \"https://logging.googleapis.com/v2/entries:write\"\n                },\n            },\n            required = { \"client_email\", \"private_key\", \"project_id\", \"token_uri\" }\n        },\n        ssl_verify = {\n            type = \"boolean\",\n            default = true\n        },\n        auth_file = { type = \"string\" },\n        -- https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource\n        resource = {\n            type = \"object\",\n            properties = {\n                type = { type = \"string\" },\n                labels = { type = \"object\" }\n            },\n            default = {\n                type = \"global\"\n            },\n            required = { \"type\" }\n        },\n        -- https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry\n        log_id = {\n            type = \"string\",\n            default = \"apisix.apache.org%2Flogs\"\n        },\n        log_format = {type = \"object\"},\n    },\n    oneOf = {\n        { required = { \"auth_config\" } },\n        { required = { \"auth_file\" } },\n    },\n    encrypt_fields = {\"auth_config.private_key\"},\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal function send_to_google(oauth, entries)\n    local http_new = http.new()\n    local access_token = oauth:generate_access_token()\n    if not access_token then\n        return nil, \"failed to get google oauth token\"\n    end\n\n    local res, err = http_new:request_uri(oauth.entries_uri, {\n        ssl_verify = oauth.ssl_verify,\n        method = \"POST\",\n        body = core.json.encode({\n            entries = entries,\n            partialSuccess = false,\n        }),\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n            [\"Authorization\"] = (oauth.access_token_type or \"Bearer\") .. \" \" .. access_token,\n        },\n    })\n\n    if not res then\n        return nil, \"failed to write log to google, \" .. err\n    end\n\n    if res.status ~= 200 then\n        return nil, res.body\n    end\n\n    return res.body\nend\n\n\nlocal function fetch_oauth_conf(conf)\n    if conf.auth_config then\n        return conf.auth_config\n    end\n\n    if not conf.auth_file then\n        return nil, \"configuration is not defined\"\n    end\n\n    local file_content, err = core.io.get_file(conf.auth_file)\n    if not file_content then\n        return nil, \"failed to read configuration, file: \" .. conf.auth_file .. \" err: \" .. err\n    end\n\n    local config_tab\n    config_tab, err = core.json.decode(file_content)\n    if not config_tab then\n        return nil, \"config parse failure, data: \" .. file_content .. \" , err: \" .. err\n    end\n\n    return config_tab\nend\n\n\nlocal function create_oauth_object(conf)\n    local auth_conf, err = fetch_oauth_conf(conf)\n    if not auth_conf then\n        return nil, err\n    end\n\n    auth_conf.scope = auth_conf.scopes or auth_conf.scope\n\n    return google_oauth.new(auth_conf, conf.ssl_verify)\nend\n\n\nlocal function get_logger_entry(conf, ctx, oauth)\n    local entry, customized = log_util.get_log_entry(plugin_name, conf, ctx)\n    local google_entry\n    if not customized then\n        google_entry = {\n            httpRequest = {\n                requestMethod = entry.request.method,\n                requestUrl = entry.request.url,\n                requestSize = entry.request.size,\n                status = entry.response.status,\n                responseSize = entry.response.size,\n                userAgent = entry.request.headers and entry.request.headers[\"user-agent\"],\n                remoteIp = entry.client_ip,\n                serverIp = entry.upstream,\n                latency = tostring(core.string.format(\"%0.3f\", entry.latency / 1000)) .. \"s\"\n            },\n            jsonPayload = {\n                route_id = entry.route_id,\n                service_id = entry.service_id,\n            },\n        }\n    else\n        google_entry = {\n            jsonPayload = entry,\n        }\n    end\n\n    google_entry.labels = {\n        source = \"apache-apisix-google-cloud-logging\"\n    }\n    google_entry.timestamp = log_util.get_rfc3339_zulu_timestamp()\n    google_entry.resource = conf.resource\n    google_entry.insertId = ctx.var.request_id\n    google_entry.logName = core.string.format(\"projects/%s/logs/%s\", oauth.project_id, conf.log_id)\n\n    return google_entry\nend\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 407,\n    name = plugin_name,\n    metadata_schema = metadata_schema,\n    schema = batch_processor_manager:wrap_schema(schema),\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local oauth, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,\n                                                create_oauth_object, conf)\n    if not oauth then\n        core.log.error(\"failed to fetch google-cloud-logging.oauth object: \", err)\n        return\n    end\n\n    local entry = get_logger_entry(conf, ctx, oauth)\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    local process = function(entries)\n        return send_to_google(oauth, entries)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx,\n                                                       process, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/grpc-transcode/proto.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core          = require(\"apisix.core\")\nlocal config_util   = require(\"apisix.core.config_util\")\nlocal pb            = require(\"pb\")\nlocal protoc        = require(\"protoc\")\nlocal pcall         = pcall\nlocal ipairs        = ipairs\nlocal decode_base64 = ngx.decode_base64\n\n\nlocal protos\nlocal lrucache_proto = core.lrucache.new({\n    ttl = 300, count = 100\n})\n\nlocal proto_fake_file = \"filename for loaded\"\n\nlocal function compile_proto_text(content)\n    protoc.reload()\n    local _p  = protoc.new()\n    -- the loaded proto won't appears in _p.loaded without a file name after lua-protobuf=0.3.2,\n    -- which means _p.loaded after _p:load(content) is always empty, so we can pass a fake file\n    -- name to keep the code below unchanged, or we can create our own load function with returning\n    -- the loaded DescriptorProto table additionally, see more details in\n    -- https://github.com/apache/apisix/pull/4368\n    local ok, res = pcall(_p.load, _p, content, proto_fake_file)\n    if not ok then\n        return nil, res\n    end\n\n    if not res or not _p.loaded then\n        return nil, \"failed to load proto content\"\n    end\n\n    local compiled = _p.loaded\n\n    local index = {}\n    for _, s in ipairs(compiled[proto_fake_file].service or {}) do\n        local method_index = {}\n        for _, m in ipairs(s.method) do\n            method_index[m.name] = m\n        end\n\n        index[compiled[proto_fake_file].package .. '.' .. s.name] = method_index\n    end\n\n    compiled[proto_fake_file].index = index\n\n    return compiled\nend\n\n\nlocal function compile_proto_bin(content)\n    content = decode_base64(content)\n    if not content then\n        return nil\n    end\n\n    -- pb.load doesn't return err\n    local ok = pb.load(content)\n    if not ok then\n        return nil\n    end\n\n    local files = pb.decode(\"google.protobuf.FileDescriptorSet\", content).file\n    local index = {}\n    for _, f in ipairs(files) do\n        for _, s in ipairs(f.service or {}) do\n            local method_index = {}\n            for _, m in ipairs(s.method) do\n                method_index[m.name] = m\n            end\n\n            index[f.package .. '.' .. s.name] = method_index\n        end\n    end\n\n    local compiled = {}\n    compiled[proto_fake_file] = {}\n    compiled[proto_fake_file].index = index\n    return compiled\nend\n\n\nlocal function compile_proto(content)\n    -- clear pb state\n    local old_pb_state = pb.state(nil)\n\n    local compiled, err = compile_proto_text(content)\n    if not compiled then\n        compiled = compile_proto_bin(content)\n        if not compiled then\n            return nil, err\n        end\n    end\n\n    -- fetch pb state\n    compiled.pb_state = pb.state(old_pb_state)\n    return compiled\nend\n\n\nlocal _M = {\n    version = 0.1,\n    compile_proto = compile_proto,\n    proto_fake_file = proto_fake_file\n}\n\nlocal function create_proto_obj(proto_id)\n    if protos.values == nil then\n        return nil\n    end\n\n    local content\n    for _, proto in config_util.iterate_values(protos.values) do\n        if proto_id == proto.value.id then\n            content = proto.value.content\n            break\n        end\n    end\n\n    if not content then\n        return nil, \"failed to find proto by id: \" .. proto_id\n    end\n\n    return compile_proto(content)\nend\n\n\nfunction _M.fetch(proto_id)\n    return lrucache_proto(proto_id, protos.conf_version,\n                          create_proto_obj, proto_id)\nend\n\n\nfunction _M.protos()\n    if not protos then\n        return nil, nil\n    end\n\n    return protos.values, protos.conf_version\nend\n\n\nlocal grpc_status_proto = [[\n    syntax = \"proto3\";\n\n    package grpc_status;\n\n    message Any {\n      // A URL/resource name that uniquely identifies the type of the serialized\n      // protocol buffer message. This string must contain at least\n      // one \"/\" character. The last segment of the URL's path must represent\n      // the fully qualified name of the type (as in\n      // `path/google.protobuf.Duration`). The name should be in a canonical form\n      // (e.g., leading \".\" is not accepted).\n      //\n      // In practice, teams usually precompile into the binary all types that they\n      // expect it to use in the context of Any. However, for URLs which use the\n      // scheme `http`, `https`, or no scheme, one can optionally set up a type\n      // server that maps type URLs to message definitions as follows:\n      //\n      // * If no scheme is provided, `https` is assumed.\n      // * An HTTP GET on the URL must yield a [google.protobuf.Type][]\n      //   value in binary format, or produce an error.\n      // * Applications are allowed to cache lookup results based on the\n      //   URL, or have them precompiled into a binary to avoid any\n      //   lookup. Therefore, binary compatibility needs to be preserved\n      //   on changes to types. (Use versioned type names to manage\n      //   breaking changes.)\n      //\n      // Note: this functionality is not currently available in the official\n      // protobuf release, and it is not used for type URLs beginning with\n      // type.googleapis.com.\n      //\n      // Schemes other than `http`, `https` (or the empty scheme) might be\n      // used with implementation specific semantics.\n      //\n      string type_url = 1;\n\n      // Must be a valid serialized protocol buffer of the above specified type.\n      bytes value = 2;\n    }\n\n    // The `Status` type defines a logical error model that is suitable for\n    // different programming environments, including REST APIs and RPC APIs. It is\n    // used by [gRPC](https://github.com/grpc). Each `Status` message contains\n    // three pieces of data: error code, error message, and error details.\n    //\n    // You can find out more about this error model and how to work with it in the\n    // [API Design Guide](https://cloud.google.com/apis/design/errors).\n    message ErrorStatus {\n        // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].\n        int32 code = 1;\n\n        // A developer-facing error message, which should be in English. Any\n        // user-facing error message should be localized and sent in the\n        // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.\n        string message = 2;\n\n        // A list of messages that carry the error details.  There is a common set of\n        // message types for APIs to use.\n        repeated Any details = 3;\n    }\n]]\n\n\nlocal status_pb_state\nlocal function init_status_pb_state()\n    if not status_pb_state then\n        -- clear current pb state\n        local old_pb_state = pb.state(nil)\n\n        -- initialize protoc compiler\n        protoc.reload()\n        local status_protoc = protoc.new()\n        -- do not use loadfile here, it can not load the proto file when using a relative address\n        -- after luarocks install apisix\n        local ok, err = status_protoc:load(grpc_status_proto, \"grpc_status.proto\")\n        if not ok then\n            status_protoc:reset()\n            pb.state(old_pb_state)\n            return \"failed to load grpc status protocol: \" .. err\n        end\n\n        status_pb_state = pb.state(old_pb_state)\n    end\nend\n\n\nfunction _M.fetch_status_pb_state()\n    return status_pb_state\nend\n\n\nfunction _M.init()\n    local err\n    protos, err = core.config.new(\"/protos\", {\n        automatic = true,\n        item_schema = core.schema.proto\n    })\n    if not protos then\n        core.log.error(\"failed to create etcd instance for fetching protos: \",\n                       err)\n        return\n    end\n\n    if not status_pb_state then\n        err = init_status_pb_state()\n        if err then\n            core.log.error(\"failed to init grpc status proto: \",\n                            err)\n            return\n        end\n    end\nend\n\nfunction _M.destroy()\n    if protos then\n        protos:close()\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/grpc-transcode/request.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal util   = require(\"apisix.plugins.grpc-transcode.util\")\nlocal core   = require(\"apisix.core\")\nlocal pb     = require(\"pb\")\nlocal bit    = require(\"bit\")\nlocal ngx    = ngx\nlocal string = string\nlocal table  = table\nlocal pcall = pcall\nlocal tonumber = tonumber\nlocal req_read_body = ngx.req.read_body\n\nreturn function (proto, service, method, pb_option, deadline, default_values)\n    core.log.info(\"proto: \", core.json.delay_encode(proto, true))\n    local m = util.find_method(proto, service, method)\n    if not m then\n        return false, \"Undefined service method: \" .. service .. \"/\" .. method\n                      .. \" end\", 503\n    end\n\n    req_read_body()\n\n    local pb_old_state = pb.state(proto.pb_state)\n    util.set_options(proto, pb_option)\n\n    local map_message = util.map_message(m.input_type, default_values or {})\n    local ok, encoded = pcall(pb.encode, m.input_type, map_message)\n    pb.state(pb_old_state)\n\n    if not ok or not encoded then\n        return false, \"failed to encode request data to protobuf\", 400\n    end\n\n    local size = #encoded\n    local prefix = {\n        string.char(0),\n        string.char(bit.band(bit.rshift(size, 24), 0xFF)),\n        string.char(bit.band(bit.rshift(size, 16), 0xFF)),\n        string.char(bit.band(bit.rshift(size, 8), 0xFF)),\n        string.char(bit.band(size, 0xFF))\n    }\n\n    local message = table.concat(prefix, \"\") .. encoded\n\n    ngx.req.set_method(ngx.HTTP_POST)\n    ngx.req.set_uri(\"/\" .. service .. \"/\" .. method, false)\n    ngx.req.set_uri_args({})\n    ngx.req.set_body_data(message)\n\n    local dl = tonumber(deadline)\n    if dl~= nil and dl > 0 then\n        ngx.req.set_header(\"grpc-timeout\",  dl .. \"m\")\n    end\n\n    return true\nend\n"
  },
  {
    "path": "apisix/plugins/grpc-transcode/response.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal util        = require(\"apisix.plugins.grpc-transcode.util\")\nlocal grpc_proto  = require(\"apisix.plugins.grpc-transcode.proto\")\nlocal core   = require(\"apisix.core\")\nlocal pb     = require(\"pb\")\nlocal ngx    = ngx\nlocal string = string\nlocal ngx_decode_base64 = ngx.decode_base64\nlocal ipairs = ipairs\nlocal pcall  = pcall\n\n\nlocal function handle_error_response(status_detail_type, proto)\n    local err_msg\n\n    local grpc_status = ngx.header[\"grpc-status-details-bin\"]\n    if grpc_status then\n        grpc_status = ngx_decode_base64(grpc_status)\n        if grpc_status == nil then\n            err_msg = \"grpc-status-details-bin is not base64 format\"\n            ngx.arg[1] = err_msg\n            return err_msg\n        end\n\n        local status_pb_state = grpc_proto.fetch_status_pb_state()\n        local old_pb_state = pb.state(status_pb_state)\n\n        local ok, decoded_grpc_status = pcall(pb.decode, \"grpc_status.ErrorStatus\", grpc_status)\n        pb.state(old_pb_state)\n        if not ok then\n            err_msg = \"failed to call pb.decode to decode grpc-status-details-bin\"\n            ngx.arg[1] = err_msg\n            return err_msg .. \", err: \" .. decoded_grpc_status\n        end\n\n        if not decoded_grpc_status then\n            err_msg = \"failed to decode grpc-status-details-bin\"\n            ngx.arg[1] = err_msg\n            return err_msg\n        end\n\n        local details = decoded_grpc_status.details\n        if status_detail_type and details then\n            local decoded_details = {}\n            for _, detail in ipairs(details) do\n                local pb_old_state = pb.state(proto.pb_state)\n                local ok, err_or_value = pcall(pb.decode, status_detail_type, detail.value)\n                pb.state(pb_old_state)\n                if not ok then\n                    err_msg = \"failed to call pb.decode to decode details in \"\n                           .. \"grpc-status-details-bin\"\n                    ngx.arg[1] = err_msg\n                    return err_msg .. \", err: \" .. err_or_value\n                end\n\n                if not err_or_value then\n                    err_msg = \"failed to decode details in grpc-status-details-bin\"\n                    ngx.arg[1] = err_msg\n                    return err_msg\n                end\n\n                core.table.insert(decoded_details, err_or_value)\n            end\n\n            decoded_grpc_status.details = decoded_details\n        end\n\n        local resp_body = {error = decoded_grpc_status}\n        local response, err = core.json.encode(resp_body)\n        if not response then\n            err_msg = \"failed to json_encode response body\"\n            ngx.arg[1] = err_msg\n            return err_msg .. \", error: \" .. err\n        end\n\n        ngx.arg[1] = response\n    end\nend\n\n\nreturn function(ctx, proto, service, method, pb_option, show_status_in_body, status_detail_type)\n    local buffer = core.response.hold_body_chunk(ctx)\n    if not buffer then\n        return nil\n    end\n\n    -- handle error response after the last response chunk\n    if ngx.status >= 300 and show_status_in_body then\n        return handle_error_response(status_detail_type, proto)\n    end\n\n    -- when body has already been read by other plugin\n    -- the buffer is an empty string\n    if buffer == \"\" and ctx.resp_body then\n        buffer = ctx.resp_body\n    end\n\n    local m = util.find_method(proto, service, method)\n    if not m then\n        return false, \"2.Undefined service method: \" .. service .. \"/\" .. method\n                      .. \" end.\"\n    end\n\n    if not ngx.req.get_headers()[\"X-Grpc-Web\"] then\n        buffer = string.sub(buffer, 6)\n    end\n\n    local pb_old_state = pb.state(proto.pb_state)\n    util.set_options(proto, pb_option)\n\n    local err_msg\n    local decoded = pb.decode(m.output_type, buffer)\n    pb.state(pb_old_state)\n    if not decoded then\n        err_msg = \"failed to decode response data by protobuf\"\n        ngx.arg[1] = err_msg\n        return err_msg\n    end\n\n    local response, err = core.json.encode(decoded)\n    if not response then\n        err_msg = \"failed to json_encode response body\"\n        ngx.arg[1] = err_msg\n        return err_msg .. \", err: \" .. err\n    end\n\n    ngx.arg[1] = response\n    return nil\nend\n"
  },
  {
    "path": "apisix/plugins/grpc-transcode/util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core              = require(\"apisix.core\")\nlocal proto_fake_file   = require(\"apisix.plugins.grpc-transcode.proto\").proto_fake_file\nlocal json              = core.json\nlocal pb                = require(\"pb\")\nlocal ngx               = ngx\nlocal string            = string\nlocal table             = table\nlocal ipairs            = ipairs\nlocal pairs             = pairs\nlocal tonumber          = tonumber\nlocal type              = type\n\n\nlocal _M = {version = 0.1}\n\n\nfunction _M.find_method(proto, service, method)\n    local loaded = proto[proto_fake_file]\n    if type(loaded) ~= \"table\" then\n        core.log.error(\"compiled proto not found\")\n        return nil\n    end\n\n    if type(loaded.index[service]) ~= \"table\" then\n        core.log.error(\"compiled proto service not found\")\n        return nil\n    end\n\n    local res = loaded.index[service][method]\n    if not res then\n        core.log.error(\"compiled proto method not found\")\n        return nil\n    end\n\n    return res\nend\n\n\nfunction _M.set_options(proto, options)\n    local cur_opts = proto.options\n    if cur_opts then\n        if cur_opts == options then\n            -- same route\n            return\n        end\n\n        local same = true\n        table.sort(options)\n        for i, v in ipairs(options) do\n            if cur_opts[i] ~= v then\n                same = false\n                break\n            end\n        end\n\n        if same then\n            -- Routes have the same configuration, usually the default one.\n            -- As this is a small optimization, we don't care about routes have different\n            -- configuration but have the same effect eventually.\n            return\n        end\n    else\n        table.sort(options)\n    end\n\n    for _, opt in ipairs(options) do\n        pb.option(opt)\n    end\n\n    proto.options = options\nend\n\n\nlocal function get_request_table()\n    local method = ngx.req.get_method()\n    local content_type = ngx.req.get_headers()[\"Content-Type\"] or \"\"\n    if string.find(content_type, \"application/json\", 1, true) and\n        (method == \"POST\" or method == \"PUT\" or method == \"PATCH\")\n    then\n        local req_body, _ = core.request.get_body()\n        if req_body then\n            local data, _ = json.decode(req_body)\n            if data then\n                return data\n            end\n        end\n    end\n\n    if method == \"POST\" then\n        return ngx.req.get_post_args()\n    end\n\n    return ngx.req.get_uri_args()\nend\n\n\nlocal function get_from_request(request_table, name, kind)\n    if not request_table then\n        return nil\n    end\n\n    local prefix = kind:sub(1, 3)\n    if prefix == \"int\" then\n        if request_table[name] then\n            if kind == \"int64\" then\n                return request_table[name]\n            else\n                return tonumber(request_table[name])\n            end\n        end\n    end\n\n    return request_table[name]\nend\n\n\nfunction _M.map_message(field, default_values, request_table, real_key)\n    if not pb.type(field) then\n        return nil, \"Field \" .. field .. \" is not defined\"\n    end\n\n    local request = {}\n    local sub, err\n    if not request_table then\n        request_table = get_request_table()\n    end\n\n    for name, _, field_type in pb.fields(field) do\n        local _, _, ty = pb.type(field_type)\n        if ty ~= \"enum\" and field_type:sub(1, 1) == \".\" then\n            if request_table[name] == nil then\n                sub = default_values and default_values[name]\n            elseif core.table.isarray(request_table[name]) then\n                local sub_array = core.table.new(#request_table[name], 0)\n                for i, value in ipairs(request_table[name]) do\n                    local sub_array_obj\n                    if type(value) == \"table\" then\n                        sub_array_obj, err = _M.map_message(field_type,\n                                default_values and default_values[name], value)\n                        if err then\n                            return nil, err\n                        end\n                    else\n                        sub_array_obj = value\n                    end\n                    sub_array[i] = sub_array_obj\n                end\n                sub = sub_array\n            else\n                if ty == \"map\" then\n                    for k, v in pairs(request_table[name]) do\n                        local tbl, err = _M.map_message(field_type,\n                            default_values and default_values[name],\n                            request_table[name], k)\n                        if err then\n                            return nil, err\n                        end\n                        if not sub then\n                            sub = {}\n                        end\n                        sub[k] = tbl[k]\n                    end\n                else\n                    sub, err = _M.map_message(field_type,\n                        default_values and default_values[name],\n                        request_table[name])\n                    if err then\n                        return nil, err\n                    end\n                end\n            end\n\n            request[name] = sub\n        else\n            if real_key then\n                name = real_key\n            end\n            request[name] = get_from_request(request_table, name, field_type)\n                                or (default_values and default_values[name])\n        end\n    end\n    return request\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/grpc-transcode.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx         = ngx\nlocal core        = require(\"apisix.core\")\nlocal schema_def  = require(\"apisix.schema_def\")\nlocal proto       = require(\"apisix.plugins.grpc-transcode.proto\")\nlocal request     = require(\"apisix.plugins.grpc-transcode.request\")\nlocal response    = require(\"apisix.plugins.grpc-transcode.response\")\n\n\nlocal plugin_name = \"grpc-transcode\"\n\nlocal pb_option_def = {\n    {   description = \"enum as result\",\n        type = \"string\",\n        enum = {\"enum_as_name\", \"enum_as_value\"},\n    },\n    {   description = \"int64 as result\",\n        type = \"string\",\n        enum = {\"int64_as_number\", \"int64_as_string\", \"int64_as_hexstring\"},\n    },\n    {   description =\"default values option\",\n        type = \"string\",\n        enum = {\"auto_default_values\", \"no_default_values\",\n                \"use_default_values\", \"use_default_metatable\"},\n    },\n    {   description = \"hooks option\",\n        type = \"string\",\n        enum = {\"enable_hooks\", \"disable_hooks\" },\n    },\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        proto_id  = schema_def.id_schema,\n        service = {\n            description = \"the grpc service name\",\n            type        = \"string\"\n        },\n        method = {\n            description = \"the method name in the grpc service.\",\n            type    = \"string\"\n        },\n        deadline = {\n            description = \"deadline for grpc, millisecond\",\n            type        = \"number\",\n            default     = 0\n        },\n        pb_option = {\n            type = \"array\",\n            items = { type=\"string\", anyOf = pb_option_def },\n            minItems = 1,\n            default = {\n                \"enum_as_name\",\n                \"int64_as_number\",\n                \"auto_default_values\",\n                \"disable_hooks\",\n            }\n        },\n        show_status_in_body = {\n            description = \"show decoded grpc-status-details-bin in response body\",\n            type        = \"boolean\",\n            default     = false\n        },\n        -- https://github.com/googleapis/googleapis/blob/b7cb84f5d42e6dba0fdcc2d8689313f6a8c9d7b9/\n        -- google/rpc/status.proto#L46\n        status_detail_type = {\n            description = \"the message type of the grpc-status-details-bin's details part, \"\n                            .. \"if not given, the details part will not be decoded\",\n            type        = \"string\",\n        },\n    },\n    additionalProperties = true,\n    required = { \"proto_id\", \"service\", \"method\" },\n}\n\n-- Based on https://cloud.google.com/apis/design/errors#handling_errors\nlocal status_rel = {\n    [\"1\"] = 499,    -- CANCELLED\n    [\"2\"] = 500,    -- UNKNOWN\n    [\"3\"] = 400,    -- INVALID_ARGUMENT\n    [\"4\"] = 504,    -- DEADLINE_EXCEEDED\n    [\"5\"] = 404,    -- NOT_FOUND\n    [\"6\"] = 409,    -- ALREADY_EXISTS\n    [\"7\"] = 403,    -- PERMISSION_DENIED\n    [\"8\"] = 429,    -- RESOURCE_EXHAUSTED\n    [\"9\"] = 400,    -- FAILED_PRECONDITION\n    [\"10\"] = 409,   -- ABORTED\n    [\"11\"] = 400,   -- OUT_OF_RANGE\n    [\"12\"] = 501,   -- UNIMPLEMENTED\n    [\"13\"] = 500,   -- INTERNAL\n    [\"14\"] = 503,   -- UNAVAILABLE\n    [\"15\"] = 500,   -- DATA_LOSS\n    [\"16\"] = 401,   -- UNAUTHENTICATED\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 506,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.init()\n    proto.init()\nend\n\n\nfunction _M.destroy()\n    proto.destroy()\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    core.log.info(\"conf: \", core.json.delay_encode(conf))\n\n    local proto_id = conf.proto_id\n    if not proto_id then\n        core.log.error(\"proto id miss: \", proto_id)\n        return\n    end\n\n    local proto_obj, err = proto.fetch(proto_id)\n    if err then\n        core.log.error(\"proto load error: \", err)\n        return\n    end\n\n    local ok, err, err_code = request(proto_obj, conf.service,\n                                      conf.method, conf.pb_option, conf.deadline)\n    if not ok then\n        core.log.error(\"transform request error: \", err)\n        return err_code\n    end\n\n    ctx.proto_obj = proto_obj\n\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    if ngx.status >= 300 then\n        return\n    end\n\n    ngx.header[\"Content-Type\"] = \"application/json\"\n    ngx.header.content_length = nil\n\n    local headers = ngx.resp.get_headers()\n\n    if headers[\"grpc-status\"] ~= nil and headers[\"grpc-status\"] ~= \"0\" then\n        local http_status = status_rel[headers[\"grpc-status\"]]\n        if http_status ~= nil then\n            ngx.status = http_status\n        else\n            ngx.status = 599\n        end\n    else\n        -- The error response body does not contain grpc-status and grpc-message\n        ngx.header[\"Trailer\"] = {\"grpc-status\", \"grpc-message\"}\n    end\n\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    if ngx.status >= 300 and not conf.show_status_in_body then\n        return\n    end\n\n    local proto_obj = ctx.proto_obj\n    if not proto_obj then\n        return\n    end\n\n    local err = response(ctx, proto_obj, conf.service, conf.method, conf.pb_option,\n                         conf.show_status_in_body, conf.status_detail_type)\n    if err then\n        core.log.error(\"transform response error: \", err)\n        return\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/grpc-web.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n\nlocal ngx               = ngx\nlocal ngx_arg           = ngx.arg\nlocal core              = require(\"apisix.core\")\nlocal req_set_uri       = ngx.req.set_uri\nlocal req_set_body_data = ngx.req.set_body_data\nlocal decode_base64     = ngx.decode_base64\nlocal encode_base64     = ngx.encode_base64\nlocal bit               = require(\"bit\")\nlocal string            = string\n\n\nlocal ALLOW_METHOD_OPTIONS = \"OPTIONS\"\nlocal ALLOW_METHOD_POST = \"POST\"\nlocal CONTENT_ENCODING_BASE64 = \"base64\"\nlocal CONTENT_ENCODING_BINARY = \"binary\"\nlocal DEFAULT_CORS_ALLOW_ORIGIN = \"*\"\nlocal DEFAULT_CORS_ALLOW_METHODS = ALLOW_METHOD_POST\nlocal DEFAULT_CORS_ALLOW_HEADERS = \"content-type,x-grpc-web,x-user-agent\"\nlocal DEFAULT_CORS_EXPOSE_HEADERS = \"grpc-message,grpc-status\"\nlocal DEFAULT_PROXY_CONTENT_TYPE = \"application/grpc\"\n\n\nlocal plugin_name = \"grpc-web\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        cors_allow_headers = {\n            description =\n            \"multiple header use ',' to split. default: content-type,x-grpc-web,x-user-agent.\",\n            type = \"string\",\n            default = DEFAULT_CORS_ALLOW_HEADERS\n        }\n    }\n}\n\nlocal grpc_web_content_encoding = {\n    [\"application/grpc-web\"] = CONTENT_ENCODING_BINARY,\n    [\"application/grpc-web-text\"] = CONTENT_ENCODING_BASE64,\n    [\"application/grpc-web+proto\"] = CONTENT_ENCODING_BINARY,\n    [\"application/grpc-web-text+proto\"] = CONTENT_ENCODING_BASE64,\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 505,\n    name = plugin_name,\n    schema = schema,\n}\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\nlocal function exit(ctx, status)\n    ctx.grpc_web_skip_body_filter = true\n    return status\nend\n\n--- Build gRPC-Web trailer chunk\n-- grpc-web trailer format reference:\n--     envoyproxy/envoy/source/extensions/filters/http/grpc_web/grpc_web_filter.cc\n--\n-- Format for grpc-web trailer\n--     1 byte: 0x80\n--     4 bytes: length of the trailer\n--     n bytes: trailer\n-- It using upstream_trailer_* variables from nginx, it is available since NGINX version 1.13.10\n-- https://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_trailer_\n--\n-- @param grpc_status number grpc status code\n-- @param grpc_message string grpc message\n-- @return string grpc-web trailer chunk in raw string\nlocal build_trailer = function (grpc_status, grpc_message)\n    local status_str = \"grpc-status:\" .. grpc_status\n    local status_msg = \"grpc-message:\" .. ( grpc_message or \"\")\n    local grpc_web_trailer = status_str .. \"\\r\\n\" .. status_msg .. \"\\r\\n\"\n    local len = #grpc_web_trailer\n\n    -- 1 byte: 0x80\n    local trailer_buf = string.char(0x80)\n    -- 4 bytes: length of the trailer\n    trailer_buf = trailer_buf .. string.char(\n        bit.band(bit.rshift(len, 24), 0xff),\n        bit.band(bit.rshift(len, 16), 0xff),\n        bit.band(bit.rshift(len, 8), 0xff),\n        bit.band(len, 0xff)\n    )\n    -- n bytes: trailer\n    trailer_buf = trailer_buf .. grpc_web_trailer\n\n    return trailer_buf\nend\n\nfunction _M.access(conf, ctx)\n    -- set context variable mime\n    -- When processing non gRPC Web requests, `mime` can be obtained in the context\n    -- and set to the `Content-Type` of the response\n    ctx.grpc_web_mime = core.request.header(ctx, \"Content-Type\")\n\n    local method = core.request.get_method()\n    if method == ALLOW_METHOD_OPTIONS then\n        return exit(ctx, 204)\n    end\n\n    if method ~= ALLOW_METHOD_POST then\n        -- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support\n        core.log.error(\"request method: `\", method, \"` invalid\")\n        return exit(ctx, 405)\n    end\n\n    local encoding = grpc_web_content_encoding[ctx.grpc_web_mime]\n    if not encoding then\n        core.log.error(\"request Content-Type: `\", ctx.grpc_web_mime, \"` invalid\")\n        return exit(ctx, 400)\n    end\n\n    -- set context variable encoding method\n    ctx.grpc_web_encoding = encoding\n\n    local path = ctx.curr_req_matched[\":ext\"]\n    if path and path ~= \"\" then\n        if path:byte(1) ~= core.string.byte(\"/\") then\n            path = \"/\" .. path\n        end\n        req_set_uri(path)\n    end\n\n    -- set grpc body\n    local body, err = core.request.get_body()\n    if err or not body then\n        core.log.error(\"failed to read request body, err: \", err)\n        return exit(ctx, 400)\n    end\n\n    if encoding == CONTENT_ENCODING_BASE64 then\n        body = decode_base64(body)\n        if not body then\n            core.log.error(\"failed to decode request body\")\n            return exit(ctx, 400)\n        end\n    end\n\n    -- set grpc content-type\n    core.request.set_header(ctx, \"Content-Type\", DEFAULT_PROXY_CONTENT_TYPE)\n    -- set grpc body\n    req_set_body_data(body)\nend\n\nfunction _M.header_filter(conf, ctx)\n    local method = core.request.get_method()\n    if method == ALLOW_METHOD_OPTIONS then\n        core.response.set_header(\"Access-Control-Allow-Methods\", DEFAULT_CORS_ALLOW_METHODS)\n        core.response.set_header(\"Access-Control-Allow-Headers\", conf.cors_allow_headers)\n    end\n\n    if not ctx.cors_allow_origins then\n        core.response.set_header(\"Access-Control-Allow-Origin\", DEFAULT_CORS_ALLOW_ORIGIN)\n    end\n    core.response.set_header(\"Access-Control-Expose-Headers\", DEFAULT_CORS_EXPOSE_HEADERS)\n\n    if not ctx.grpc_web_skip_body_filter then\n        core.response.set_header(\"Content-Type\", ctx.grpc_web_mime)\n        core.response.set_header(\"Content-Length\", nil)\n    end\n\n    -- Set gRPC Web metadata sent flag\n    -- The RPC calls on the gRPC service may fail without returning any data,\n    -- i.e., send `grpc-status` and `grpc-message`, but send an empty response body\n    -- In this case, the grpc metadata metadata (usually sent in the trailer) may\n    -- be sent with the HTTP/2 header.\n    --\n    -- 1. Normal gRPC (HTTP/2):\n    --\n    --   => HEADER frame => DATA frame => HEADER (trailer) frame with end stream flag => [END]\n    --\n    --   There will be a DATA frame between the HEADER frame (which contains the HTTP/2\n    --   pseudo-header such as :status) and the trailer header frame, which is sent in two\n    --   separate frames, and which was handled correctly by the previous implementation.\n    --\n    -- 2. Exception gRPC (HTTP/2 or gRPC-Web over HTTP1):\n    --\n    --   => HEADER(+trailers) frame (with end header flag and end stream flag) => [END]\n    --\n    --   If the gRPC call chooses not to return any data but instead returns a gRPC error\n    --   directly (via grpc-status), the DATA frame will be omitted and the gRPC metadata field,\n    --   which is normally located in the trailer, will be sent as a header along with the\n    --   regular HTTP/2 header.\n    --   For APISIX, its nginx http grpc module will handle HTTP/2 details on gRPC connections,\n    --   and for this exception case, the gRPC metadata will no longer be treated as a trailer\n    --   but rather as a general HTTP header. So it will no longer be readable as\n    --   an `upstream_trailer_grpc_status` variable, but only with `sent_http_grpc_status`.\n    --\n    -- We must deal with the second exception carefully. As required by the gRPC Web message frame:\n    --\n    --   Trailers-only responses: no change to the gRPC protocol spec. Trailers may be\n    --   sent together with response headers, with no message in the body.\n    --   ref: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md\n    --\n    -- Therefore, when this situation exists, we confirm here that the `grpc-status` response header\n    -- has been sent and set a flag.\n    -- In subsequent body_filter phases, if it finds that this flag has been set, it skips attempts\n    -- to append additional trailer encoding to the end of the response body.\n    -- This is compliant, i.e., this is a trailers-only response, send the gRPC metadata contained\n    -- in the trailers in the HTTP response header and ensure that an empty response body is\n    -- sent (no trailers block in the end of response body).\n    ctx.grpc_web_metadata_sent = ctx.var.sent_http_grpc_status ~= nil\nend\n\nfunction _M.body_filter(conf, ctx)\n    if ctx.grpc_web_skip_body_filter then\n        return\n    end\n\n    -- If the MIME extension type description of the gRPC-Web standard is not obtained,\n    -- indicating that the request is not based on the gRPC Web specification,\n    -- the processing of the request body will be ignored\n    -- https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md\n    -- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support\n    if not ctx.grpc_web_mime then\n        return\n    end\n\n    if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then\n        local chunk = ngx_arg[1]\n        chunk = encode_base64(chunk)\n        ngx_arg[1] = chunk\n    end\n\n    if ngx_arg[2] then -- if eof\n        -- If the gRPC metadata has already been sent through the response header,\n        -- the subsequent logic is skipped and no trailers chunk is appended.\n        if ctx.grpc_web_metadata_sent then\n            return\n        end\n\n        local status = ctx.var.upstream_trailer_grpc_status\n        local message = ctx.var.upstream_trailer_grpc_message\n\n        -- When the response body completes and still does not receive the grpc status\n        local resp_ok = status ~= nil and status ~= \"\"\n        local trailer_buf = build_trailer(\n            resp_ok and status  or 2,\n            resp_ok and message or \"upstream grpc status not received\"\n        )\n        if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then\n            trailer_buf = encode_base64(trailer_buf)\n        end\n\n        ngx_arg[1] = ngx_arg[1] .. trailer_buf\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/gzip.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal is_apisix_or, response = pcall(require, \"resty.apisix.response\")\nlocal ngx_header = ngx.header\nlocal req_http_version = ngx.req.http_version\nlocal str_sub = string.sub\nlocal ipairs = ipairs\nlocal tonumber = tonumber\nlocal type = type\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        types = {\n            anyOf = {\n                {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"string\",\n                        minLength = 1,\n                    },\n                },\n                {\n                    enum = {\"*\"}\n                }\n            },\n            default = {\"text/html\"}\n        },\n        min_length = {\n            type = \"integer\",\n            minimum = 1,\n            default = 20,\n        },\n        comp_level = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 9,\n            default = 1,\n        },\n        http_version = {\n            enum = {1.1, 1.0},\n            default = 1.1,\n        },\n        buffers = {\n            type = \"object\",\n            properties = {\n                number = {\n                    type = \"integer\",\n                    minimum = 1,\n                    default = 32,\n                },\n                size = {\n                    type = \"integer\",\n                    minimum = 1,\n                    default = 4096,\n                }\n            },\n            default = {\n                number = 32,\n                size = 4096,\n            }\n        },\n        vary = {\n            type = \"boolean\",\n        }\n    },\n}\n\n\nlocal plugin_name = \"gzip\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 995,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    if not is_apisix_or then\n        core.log.error(\"need to build APISIX-Runtime to support setting gzip\")\n        return 501\n    end\n\n    local types = conf.types\n    local content_type = ngx_header[\"Content-Type\"]\n    if not content_type then\n        -- Like Nginx, don't gzip if Content-Type is missing\n        return\n    end\n\n    if type(types) == \"table\" then\n        local matched = false\n        local from = core.string.find(content_type, \";\")\n        if from then\n            content_type = str_sub(content_type, 1, from - 1)\n        end\n\n        for _, ty in ipairs(types) do\n            if content_type == ty then\n                matched = true\n                break\n            end\n        end\n\n        if not matched then\n            return\n        end\n    end\n\n    local content_length = tonumber(ngx_header[\"Content-Length\"])\n    if content_length then\n        local min_length = conf.min_length\n        if content_length < min_length then\n            return\n        end\n        -- Like Nginx, don't check min_length if Content-Length is missing\n    end\n\n    local http_version = req_http_version()\n    if http_version < conf.http_version then\n        return\n    end\n\n    local buffers = conf.buffers\n\n    core.log.info(\"set gzip with buffers: \", buffers.number, \" \", buffers.size,\n                  \", level: \", conf.comp_level)\n\n    local ok, err = response.set_gzip({\n        buffer_num = buffers.number,\n        buffer_size = buffers.size,\n        compress_level = conf.comp_level,\n    })\n    if not ok then\n        core.log.error(\"failed to set gzip: \", err)\n        return\n    end\n\n    if conf.vary then\n        core.response.add_header(\"Vary\", \"Accept-Encoding\")\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/hmac-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx        = ngx\nlocal abs        = math.abs\nlocal ngx_time   = ngx.time\nlocal ngx_re     = require(\"ngx.re\")\nlocal ipairs     = ipairs\nlocal hmac_sha1  = ngx.hmac_sha1\nlocal core       = require(\"apisix.core\")\nlocal hmac       = require(\"resty.hmac\")\nlocal consumer   = require(\"apisix.consumer\")\nlocal ngx_decode_base64 = ngx.decode_base64\nlocal ngx_encode_base64 = ngx.encode_base64\nlocal plugin_name   = \"hmac-auth\"\nlocal ALLOWED_ALGORITHMS = {\"hmac-sha1\", \"hmac-sha256\", \"hmac-sha512\"}\nlocal resty_sha256 = require(\"resty.sha256\")\nlocal schema_def = require(\"apisix.schema_def\")\nlocal auth_utils = require(\"apisix.utils.auth\")\n\nlocal schema = {\n    type = \"object\",\n    title = \"work with route or service object\",\n    properties = {\n        allowed_algorithms = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                enum = ALLOWED_ALGORITHMS\n            },\n            default = ALLOWED_ALGORITHMS,\n        },\n        clock_skew = {\n            type = \"integer\",\n            default = 300,\n            minimum = 1\n        },\n        signed_headers = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n                maxLength = 50,\n            }\n        },\n        validate_request_body = {\n            type = \"boolean\",\n            title = \"A boolean value telling the plugin to enable body validation\",\n            default = false,\n        },\n        hide_credentials = {type = \"boolean\", default = false},\n        realm = schema_def.get_realm_schema(\"hmac\"),\n        anonymous_consumer = schema_def.anonymous_consumer_schema,\n    },\n}\n\nlocal consumer_schema = {\n    type = \"object\",\n    title = \"work with consumer object\",\n    properties = {\n        key_id = {type = \"string\", minLength = 1, maxLength = 256},\n        secret_key = {type = \"string\", minLength = 1, maxLength = 256},\n    },\n    encrypt_fields = {\"secret_key\"},\n    required = {\"key_id\", \"secret_key\"},\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2530,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n    consumer_schema = consumer_schema\n}\n\nlocal hmac_funcs = {\n    [\"hmac-sha1\"] = function(secret_key, message)\n        return hmac_sha1(secret_key, message)\n    end,\n    [\"hmac-sha256\"] = function(secret_key, message)\n        return hmac:new(secret_key, hmac.ALGOS.SHA256):final(message)\n    end,\n    [\"hmac-sha512\"] = function(secret_key, message)\n        return hmac:new(secret_key, hmac.ALGOS.SHA512):final(message)\n    end,\n}\n\n\nlocal function array_to_map(arr)\n    local map = core.table.new(0, #arr)\n    for _, v in ipairs(arr) do\n      map[v] = true\n    end\n\n    return map\nend\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_CONSUMER then\n        return core.schema.check(consumer_schema, conf)\n    else\n        return core.schema.check(schema, conf)\n    end\nend\n\n\n\nlocal function get_consumer(key_id)\n    if not key_id then\n        return nil, \"missing key_id\"\n    end\n\n    local cur_consumer, _, err = consumer.find_consumer(plugin_name, \"key_id\", key_id)\n    if not cur_consumer then\n        return nil, err or \"Invalid key_id\"\n    end\n    return cur_consumer\nend\n\n\nlocal function generate_signature(ctx, secret_key, params)\n    local uri = ctx.var.request_uri\n    local request_method = core.request.get_method()\n\n    if uri == \"\" then\n        uri = \"/\"\n    end\n\n    local signing_string_items = {\n        params.keyId,\n    }\n\n    if params.headers then\n        for _, h in ipairs(params.headers) do\n            local canonical_header = core.request.header(ctx, h)\n            if not canonical_header then\n              if h == \"@request-target\" then\n                local request_target = request_method .. \" \" .. uri\n                core.table.insert(signing_string_items, request_target)\n                core.log.info(\"canonical_header name:\", core.json.delay_encode(h))\n                core.log.info(\"canonical_header value: \",\n                              core.json.delay_encode(request_target))\n              end\n            else\n              core.table.insert(signing_string_items,\n                                h .. \": \" .. canonical_header)\n              core.log.info(\"canonical_header name:\", core.json.delay_encode(h))\n              core.log.info(\"canonical_header value: \",\n                            core.json.delay_encode(canonical_header))\n            end\n        end\n    end\n\n    local signing_string = core.table.concat(signing_string_items, \"\\n\") .. \"\\n\"\n    return hmac_funcs[params.algorithm](secret_key, signing_string)\nend\n\n\nlocal function sha256(key)\n    local hash = resty_sha256:new()\n    hash:update(key)\n    local digest = hash:final()\n    return digest\nend\n\n\nlocal function validate(ctx, conf, params)\n    if not params then\n        return nil\n    end\n\n    if not params.keyId or not params.signature then\n        return nil, \"keyId or signature missing\"\n    end\n\n    if not params.algorithm then\n        return nil, \"algorithm missing\"\n    end\n\n    local consumer, err = get_consumer(params.keyId)\n    if err then\n        return nil, err\n    end\n\n    local consumer_conf = consumer.auth_conf\n    local found_algorithm = false\n    -- check supported algorithm used\n    if not conf.allowed_algorithms then\n        conf.allowed_algorithms = ALLOWED_ALGORITHMS\n    end\n\n    for _, algo in ipairs(conf.allowed_algorithms) do\n      if algo == params.algorithm then\n        found_algorithm = true\n        break\n      end\n    end\n\n    if not found_algorithm then\n        return nil, \"Invalid algorithm\"\n    end\n\n    core.log.info(\"clock_skew: \", conf.clock_skew)\n    if conf.clock_skew and conf.clock_skew > 0 then\n        if not params.date then\n            return nil, \"Date header missing. failed to validate clock skew\"\n        end\n\n        local time = ngx.parse_http_time(params.date)\n        core.log.info(\"params.date: \", params.date, \" time: \", time)\n        if not time then\n            return nil, \"Invalid GMT format time\"\n        end\n\n        local diff = abs(ngx_time() - time)\n\n        if diff > conf.clock_skew then\n            return nil, \"Clock skew exceeded\"\n        end\n    end\n\n    -- validate headers\n    -- All headers passed in route conf.signed_headers must be used in signing(params.headers)\n    if conf.signed_headers and #conf.signed_headers >= 1 then\n        if not params.headers then\n            return nil, \"headers missing\"\n        end\n        local params_headers_map = array_to_map(params.headers)\n        if params_headers_map then\n            for _, header in ipairs(conf.signed_headers) do\n                if not params_headers_map[header] then\n                    return nil, [[expected header \"]] .. header .. [[\" missing in signing]]\n                end\n            end\n        end\n    end\n\n    local secret_key          = consumer_conf and consumer_conf.secret_key\n    local request_signature   = ngx_decode_base64(params.signature)\n    local generated_signature = generate_signature(ctx, secret_key, params)\n    if request_signature ~= generated_signature then\n        return nil, \"Invalid signature\"\n    end\n\n    local validate_request_body = conf.validate_request_body\n    if validate_request_body then\n        local digest_header = params.body_digest\n        if not digest_header then\n            return nil, \"Invalid digest\"\n        end\n\n        local req_body, err = core.request.get_body()\n        if err then\n            return nil, err\n        end\n\n        req_body = req_body or \"\"\n        local digest_created = \"SHA-256\" .. \"=\" ..\n                ngx_encode_base64(sha256(req_body))\n        if digest_created ~= digest_header then\n            return nil, \"Invalid digest\"\n        end\n    end\n\n    return consumer\nend\n\n\nlocal function retrieve_hmac_fields(ctx)\n    local hmac_params = {}\n    local auth_string = core.request.header(ctx, \"Authorization\")\n    if not auth_string then\n        return nil, \"missing Authorization header\"\n    end\n\n    if not core.string.has_prefix(auth_string, \"Signature\") then\n        return nil, \"Authorization header does not start with 'Signature'\"\n    end\n\n    local signature_fields = auth_string:sub(10):gmatch('[^,]+')\n\n    for field in signature_fields do\n        local key, value = field:match('%s*(%w+)=\"(.-)\"')\n        if key and value then\n            if key == \"keyId\" or key == \"algorithm\" or key == \"signature\" then\n                hmac_params[key] = value\n\n            elseif key == \"headers\" then\n                hmac_params.headers = ngx_re.split(value, \" \")\n            end\n        end\n    end\n\n    -- will be required to check clock skew\n    if core.request.header(ctx, \"Date\") then\n        hmac_params.date = core.request.header(ctx, \"Date\")\n    end\n\n    if core.request.header(ctx, \"Digest\") then\n        hmac_params.body_digest = core.request.header(ctx, \"Digest\")\n    end\n\n    return hmac_params\nend\n\nlocal function find_consumer(conf, ctx)\n    local params,err = retrieve_hmac_fields(ctx)\n    if err then\n        if not auth_utils.is_running_under_multi_auth(ctx) then\n            core.log.warn(\"client request can't be validated: \", err)\n        end\n        return nil, nil, \"client request can't be validated: \" .. err\n    end\n\n    local validated_consumer, err = validate(ctx, conf, params)\n    if not validated_consumer then\n        err = \"client request can't be validated: \" .. (err or \"Invalid signature\")\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.warn(err)\n        return nil, nil, \"client request can't be validated\"\n    end\n\n    local consumers_conf = consumer.consumers_conf(plugin_name)\n    return validated_consumer, consumers_conf, err\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local cur_consumer, consumers_conf, err = find_consumer(conf, ctx)\n    if not cur_consumer then\n        if not conf.anonymous_consumer then\n            core.response.set_header(\"WWW-Authenticate\", \"hmac realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = err }\n        end\n        cur_consumer, consumers_conf, err = consumer.get_anonymous_consumer(conf.anonymous_consumer)\n        if not cur_consumer then\n            if auth_utils.is_running_under_multi_auth(ctx) then\n                core.response.set_header(\"WWW-Authenticate\", \"hmac realm=\\\"\" .. conf.realm .. \"\\\"\")\n                return 401, err\n            end\n            core.log.error(err)\n            core.response.set_header(\"WWW-Authenticate\", \"hmac realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = \"Invalid user authorization\" }\n        end\n    end\n\n    if conf.hide_credentials then\n        core.request.set_header(\"Authorization\", nil)\n    end\n\n    consumer.attach_consumer(ctx, cur_consumer, consumers_conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/http-dubbo.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal pairs = pairs\nlocal str_format = string.format\nlocal bit = require(\"bit\")\nlocal rshift = bit.rshift\nlocal band = bit.band\nlocal char = string.char\nlocal tostring = tostring\nlocal ngx = ngx\nlocal type = type\nlocal plugin_name = \"http-dubbo\"\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        service_name = {\n            type = \"string\",\n            minLength = 1,\n        },\n        service_version = {\n            type = \"string\",\n            pattern = [[^\\d+\\.\\d+\\.\\d+]],\n            default =\"0.0.0\"\n        },\n        method = {\n            type = \"string\",\n            minLength = 1,\n        },\n        params_type_desc = {\n            type = \"string\",\n            default = \"\"\n        },\n        serialization_header_key = {\n            type = \"string\"\n        },\n        serialized = {\n            type = \"boolean\",\n            default = false\n        },\n        connect_timeout={\n            type = \"number\",\n            default = 6000\n        },\n        read_timeout={\n            type = \"number\",\n            default = 6000\n        },\n        send_timeout={\n            type = \"number\",\n            default = 6000\n        }\n    },\n    required = { \"service_name\", \"method\" },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 504,\n    name = plugin_name,\n    schema = schema,\n}\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function str_int32(int)\n    return char(band(rshift(int, 24), 0xff),\n            band(rshift(int, 16), 0xff),\n            band(rshift(int, 8), 0xff),\n            band(int, 0xff))\nend\n\n\nlocal function parse_dubbo_header(header)\n    for i = 1, 16 do\n        local currentByte = header:byte(i)\n        if not currentByte then\n            return nil\n        end\n    end\n\n    local magic_number = str_format(\"%04x\", header:byte(1) * 256 + header:byte(2))\n    local message_flag = header:byte(3)\n    local status = header:byte(4)\n    local request_id = 0\n    for i = 5, 12 do\n        request_id = request_id * 256 + header:byte(i)\n    end\n\n    local byte13Val = header:byte(13) * 256 * 256 * 256\n    local byte14Val = header:byte(14) * 256 * 256\n    local data_length = byte13Val + byte14Val + header:byte(15) * 256 + header:byte(16)\n\n    local is_request = bit.band(bit.rshift(message_flag, 7), 0x01) == 1 and 1 or 0\n    local is_two_way = bit.band(bit.rshift(message_flag, 6), 0x01) == 1 and 1 or 0\n    local is_event = bit.band(bit.rshift(message_flag, 5), 0x01) == 1 and 1 or 0\n\n    return {\n        magic_number = magic_number,\n        message_flag = message_flag,\n        is_request = is_request,\n        is_two_way = is_two_way,\n        is_event = is_event,\n        status = status,\n        request_id = request_id,\n        data_length = data_length\n    }\nend\n\n\nlocal function string_to_json_string(str)\n    local result = \"\\\"\"\n    for i = 1, #str do\n        local byte = core.string.sub(str, i, i)\n        if byte == \"\\\\\" then\n            result = result .. \"\\\\\\\\\"\n        elseif byte == \"\\n\" then\n            result = result .. \"\\\\n\"\n        elseif byte == \"\\t\" then\n            result = result .. \"\\\\t\"\n        elseif byte == \"\\r\" then\n            result = result .. \"\\\\r\"\n        elseif byte == \"\\b\" then\n            result = result .. \"\\\\b\"\n        elseif byte == \"\\f\" then\n            result = result .. \"\\\\f\"\n        elseif byte == \"\\\"\" then\n            result = result .. \"\\\\\\\"\"\n        else\n            result = result .. byte\n        end\n    end\n    return result .. \"\\\"\"\nend\n\n\nlocal function get_dubbo_request(conf, ctx)\n    -- use dubbo and fastjson\n    local first_byte4 = \"\\xda\\xbb\\xc6\\x00\"\n\n    local requestId = \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"\n    local version = \"\\\"2.0.2\\\"\\n\"\n    local service = \"\\\"\" .. conf.service_name .. \"\\\"\" .. \"\\n\"\n\n    local service_version = \"\\\"\" ..  conf.service_version .. \"\\\"\" .. \"\\n\"\n    local method_name = \"\\\"\" .. conf.method .. \"\\\"\" .. \"\\n\"\n\n    local params_desc = \"\\\"\" .. conf.params_type_desc .. \"\\\"\" .. \"\\n\"\n    local params = \"\"\n    local serialized = conf.serialized\n    if conf.serialization_header_key then\n        local serialization_header = core.request.header(ctx, conf.serialization_header_key)\n        serialized = serialization_header == \"true\"\n    end\n    if serialized then\n        params = core.request.get_body()\n        if params then\n            local end_of_params = core.string.sub(params, -1)\n            if end_of_params ~= \"\\n\" then\n                params = params .. \"\\n\"\n            end\n        end\n    else\n        local body_data = core.request.get_body()\n        if body_data then\n            local lua_object = core.json.decode(body_data);\n            for _, v in pairs(lua_object) do\n                local pt = type(v)\n                if pt == \"nil\" then\n                    params = params .. \"null\" .. \"\\n\"\n                elseif pt == \"string\" then\n                    params = params .. string_to_json_string(v) .. \"\\n\"\n                elseif pt == \"number\" then\n                    params = params .. tostring(v) .. \"\\n\"\n                else\n                    params = params .. core.json.encode(v) .. \"\\n\"\n                end\n            end\n        end\n\n    end\n    local attachments = \"{}\\n\"\n    if params == nil then\n        params = \"\"\n    end\n    local payload = #version + #service + #service_version\n            + #method_name + #params_desc + #params + #attachments\n    return {\n        first_byte4,\n        requestId,\n        str_int32(payload),\n        version,\n        service,\n        service_version,\n        method_name,\n        params_desc,\n        params,\n        attachments\n    }\nend\n\n\nfunction _M.before_proxy(conf, ctx)\n    local sock = ngx.socket.tcp()\n\n    sock:settimeouts(conf.connect_timeout, conf.send_timeout, conf.read_timeout)\n    local ok, err = sock:connect(ctx.picked_server.host, ctx.picked_server.port)\n    if not ok then\n        sock:close()\n        core.log.error(\"failed to connect to upstream \", err)\n        return 502\n    end\n    local request = get_dubbo_request(conf, ctx)\n    local bytes, _ = sock:send(request)\n    if bytes > 0 then\n        local header, _ = sock:receiveany(16);\n        if header then\n            local header_info = parse_dubbo_header(header)\n            if header_info and header_info.status == 20 then\n                local readline = sock:receiveuntil(\"\\n\")\n                local body_status, _, _ = readline()\n                if body_status then\n                    local response_status = core.string.sub(body_status, 1, 1)\n                    if response_status == \"2\" or response_status == \"5\" then\n                        sock:close()\n                        return 200\n                    elseif response_status == \"1\" or response_status == \"4\" then\n                        local body, _, _ = readline()\n                        sock:close()\n                        return 200, body\n                    end\n                end\n            end\n        end\n    end\n    sock:close()\n    return 500\n\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/http-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal core            = require(\"apisix.core\")\nlocal http            = require(\"resty.http\")\nlocal url             = require(\"net.url\")\n\nlocal tostring = tostring\nlocal ipairs   = ipairs\n\nlocal plugin_name = \"http-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(\"http logger\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        uri = core.schema.uri_def,\n        auth_header = {type = \"string\"},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        concat_method = {type = \"string\", default = \"json\",\n                         enum = {\"json\", \"new_line\"}},\n        ssl_verify = {type = \"boolean\", default = false},\n    },\n    required = {\"uri\"}\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 410,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local check = {\"uri\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, err\n    end\n    return log_util.check_log_schema(conf)\nend\n\n\nlocal function send_http_data(conf, log_message)\n    local err_msg\n    local res = true\n    local url_decoded = url.parse(conf.uri)\n    local host = url_decoded.host\n    local port = url_decoded.port\n\n    core.log.info(\"sending a batch logs to \", conf.uri)\n\n    if ((not port) and url_decoded.scheme == \"https\") then\n        port = 443\n    elseif not port then\n        port = 80\n    end\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout * 1000)\n    local ok, err = httpc:connect(host, port)\n\n    if not ok then\n        return false, \"failed to connect to host[\" .. host .. \"] port[\"\n            .. tostring(port) .. \"] \" .. err\n    end\n\n    if url_decoded.scheme == \"https\" then\n        ok, err = httpc:ssl_handshake(true, host, conf.ssl_verify)\n        if not ok then\n            return false, \"failed to perform SSL with host[\" .. host .. \"] \"\n                .. \"port[\" .. tostring(port) .. \"] \" .. err\n        end\n    end\n\n    local content_type\n    if conf.concat_method == \"json\" then\n        content_type = \"application/json\"\n    else\n        content_type = \"text/plain\"\n    end\n\n    local httpc_res, httpc_err = httpc:request({\n        method = \"POST\",\n        path = #url_decoded.path ~= 0 and url_decoded.path or \"/\",\n        query = url_decoded.query,\n        body = log_message,\n        headers = {\n            [\"Host\"] = url_decoded.host,\n            [\"Content-Type\"] = content_type,\n            [\"Authorization\"] = conf.auth_header\n        }\n    })\n\n    if not httpc_res then\n        return false, \"error while sending data to [\" .. host .. \"] port[\"\n            .. tostring(port) .. \"] \" .. httpc_err\n    end\n\n    -- some error occurred in the server\n    if httpc_res.status >= 400 then\n        res =  false\n        err_msg = \"server returned status code[\" .. httpc_res.status .. \"] host[\"\n            .. host .. \"] port[\" .. tostring(port) .. \"] \"\n            .. \"body[\" .. httpc_res:read_body() .. \"]\"\n    end\n\n    return res, err_msg\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n\n    if not entry.route_id then\n        entry.route_id = \"no-matched\"\n    end\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err\n\n        if conf.concat_method == \"json\" then\n            if batch_max_size == 1 then\n                data, err = core.json.encode(entries[1]) -- encode as single {}\n            else\n                data, err = core.json.encode(entries) -- encode as array [{}]\n            end\n\n        elseif conf.concat_method == \"new_line\" then\n            if batch_max_size == 1 then\n                data, err = core.json.encode(entries[1]) -- encode as single {}\n            else\n                local t = core.table.new(#entries, 0)\n                for i, entry in ipairs(entries) do\n                    t[i], err = core.json.encode(entry)\n                    if err then\n                        core.log.warn(\"failed to encode http log: \", err, \", log data: \", entry)\n                        break\n                    end\n                end\n                data = core.table.concat(t, \"\\n\") -- encode as multiple string\n            end\n\n        else\n            -- defensive programming check\n            err = \"unknown concat_method \" .. (conf.concat_method or \"nil\")\n        end\n\n        if not data then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        return send_http_data(conf, data)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/inspect.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal inspect = require(\"apisix.inspect\")\n\n\nlocal plugin_name = \"inspect\"\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {},\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 200,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.init()\n    local attr = plugin.plugin_attr(plugin_name)\n    local delay\n    local hooks_file\n    if attr then\n        delay = attr.delay\n        hooks_file = attr.hooks_file\n    end\n    core.log.info(\"delay=\", delay, \", hooks_file=\", hooks_file)\n    return inspect.init(delay, hooks_file)\nend\n\n\nfunction _M.destroy()\n    return inspect.destroy()\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ip-restriction/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ipairs    = ipairs\nlocal core      = require(\"apisix.core\")\nlocal lrucache  = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        message = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 1024,\n            default = \"Your IP address is not allowed\"\n        },\n        response_code = {\n            type = \"integer\",\n            minimum = 403,\n            maximum = 404,\n            default = 403\n        },\n        whitelist = {\n            type = \"array\",\n            items = {anyOf = core.schema.ip_def},\n            minItems = 1\n        },\n        blacklist = {\n            type = \"array\",\n            items = {anyOf = core.schema.ip_def},\n            minItems = 1\n        },\n    },\n    oneOf = {\n        {required = {\"whitelist\"}},\n        {required = {\"blacklist\"}},\n    },\n}\n\n\nlocal plugin_name = \"ip-restriction\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 3000,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n\n    if not ok then\n        return false, err\n    end\n\n    -- we still need this as it is too complex to filter out all invalid IPv6 via regex\n    if conf.whitelist then\n        for _, cidr in ipairs(conf.whitelist) do\n            if not core.ip.validate_cidr_or_ip(cidr) then\n                return false, \"invalid ip address: \" .. cidr\n            end\n        end\n    end\n\n    if conf.blacklist then\n        for _, cidr in ipairs(conf.blacklist) do\n            if not core.ip.validate_cidr_or_ip(cidr) then\n                return false, \"invalid ip address: \" .. cidr\n            end\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.restrict(conf, ctx)\n    local block = false\n    local remote_addr = ctx.var.remote_addr\n\n    if conf.blacklist then\n        local matcher = lrucache(conf.blacklist, nil,\n                                 core.ip.create_ip_matcher, conf.blacklist)\n        if matcher then\n            block = matcher:match(remote_addr)\n        end\n    end\n\n    if conf.whitelist then\n        local matcher = lrucache(conf.whitelist, nil,\n                                 core.ip.create_ip_matcher, conf.whitelist)\n        if matcher then\n            block = not matcher:match(remote_addr)\n        end\n    end\n\n    if block then\n        return conf.response_code, { message = conf.message }\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ip-restriction.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal base = require(\"apisix.plugins.ip-restriction.init\")\n\n\n-- avoid unexpected data sharing\nlocal ip_restriction = core.table.clone(base)\nip_restriction.access = base.restrict\n\n\nreturn ip_restriction\n"
  },
  {
    "path": "apisix/plugins/jwe-decrypt.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core            = require(\"apisix.core\")\nlocal consumer_mod    = require(\"apisix.consumer\")\nlocal base64          = require(\"ngx.base64\")\nlocal aes             = require(\"resty.aes\")\nlocal ngx             = ngx\nlocal sub_str         = string.sub\nlocal cipher          = aes.cipher(256, \"gcm\")\n\nlocal plugin_name     = \"jwe-decrypt\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        header = {\n            type = \"string\",\n            default = \"Authorization\"\n        },\n        forward_header = {\n            type = \"string\",\n            default = \"Authorization\"\n        },\n        strict = {\n            type = \"boolean\",\n            default = true\n        }\n    },\n    required = { \"header\", \"forward_header\" },\n}\n\nlocal consumer_schema = {\n    type = \"object\",\n    properties = {\n        key = { type = \"string\" },\n        secret = { type = \"string\" },\n        is_base64_encoded = { type = \"boolean\" },\n    },\n    required = { \"key\", \"secret\" },\n    encrypt_fields = { \"key\", \"secret\" },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2509,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n    consumer_schema = consumer_schema\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_CONSUMER then\n        local ok, err = core.schema.check(consumer_schema, conf)\n        if not ok then\n            return false, err\n        end\n\n        local local_conf, err = core.config.local_conf(true)\n        if not local_conf then\n            return false, \"failed to load the configuration file: \" .. err\n        end\n\n        local encrypted = core.table.try_read_attr(local_conf, \"apisix\", \"data_encryption\",\n        \"enable_encrypt_fields\") and (core.config.type == \"etcd\")\n\n        -- if encrypted, the secret length will exceed 32 so don't check\n        if not encrypted then\n            -- restrict the length of secret, we use A256GCM for encryption,\n            -- so the length should be 32 chars only\n            if conf.is_base64_encoded then\n                if #base64.decode_base64url(conf.secret) ~= 32 then\n                    return false, \"the secret length after base64 decode should be 32 chars\"\n                end\n            else\n                if #conf.secret ~= 32 then\n                    return false, \"the secret length should be 32 chars\"\n                end\n            end\n        end\n\n        return true\n    end\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function get_secret(conf)\n    local secret = conf.secret\n\n    if conf.is_base64_encoded then\n        return base64.decode_base64url(secret)\n    end\n\n    return secret\nend\n\n\nlocal function load_jwe_token(jwe_token)\n    local o = { valid = false }\n    o.header, o.enckey, o.iv, o.ciphertext, o.tag = jwe_token:match(\"(.-)%.(.-)%.(.-)%.(.-)%.(.*)\")\n    if not o.header then\n        return o\n    end\n    local he = base64.decode_base64url(o.header)\n    if not he then\n        return o\n    end\n    o.header_obj = core.json.decode(he)\n    if not o.header_obj then\n        return o\n    end\n    o.valid = true\n    return o\nend\n\n\nlocal function jwe_decrypt_with_obj(o, consumer)\n    local secret = get_secret(consumer.auth_conf)\n    local dec = base64.decode_base64url\n\n    local aes_default = aes:new(\n        secret,\n        nil,\n        cipher,\n        {iv = dec(o.iv)}\n    )\n\n    local decrypted = aes_default:decrypt(dec(o.ciphertext), dec(o.tag))\n    return decrypted\nend\n\n\nlocal function jwe_encrypt(o, consumer)\n    local secret = get_secret(consumer.auth_conf)\n    local enc = base64.encode_base64url\n\n    local aes_default = aes:new(\n        secret,\n        nil,\n        cipher,\n        {iv = o.iv})\n\n    local encrypted = aes_default:encrypt(o.plaintext)\n\n    o.ciphertext = encrypted[1]\n    o.tag = encrypted[2]\n    return o.header .. \"..\" .. enc(o.iv) .. \".\" .. enc(o.ciphertext) .. \".\" .. enc(o.tag)\nend\n\n\nlocal function get_consumer(key)\n    local consumer_conf = consumer_mod.plugin(plugin_name)\n    if not consumer_conf then\n        return nil\n    end\n    local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, \"key\")\n    if not consumers then\n        return nil\n    end\n    return consumers[key]\nend\n\n\nlocal function fetch_jwe_token(conf, ctx)\n    local token = core.request.header(ctx, conf.header)\n    if token then\n        local prefix = sub_str(token, 1, 7)\n        if prefix == 'Bearer ' or prefix == 'bearer ' then\n            return sub_str(token, 8)\n        end\n\n        return token\n    end\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    -- fetch token and hide credentials if necessary\n    local jwe_token, err = fetch_jwe_token(conf, ctx)\n    if not jwe_token and conf.strict then\n        core.log.info(\"failed to fetch JWE token: \", err)\n        return 403, { message = \"missing JWE token in request\" }\n    end\n\n    local jwe_obj = load_jwe_token(jwe_token)\n    if not jwe_obj.valid then\n        return 400, { message = \"JWE token invalid\" }\n    end\n\n    if not jwe_obj.header_obj.kid then\n        return 400, { message = \"missing kid in JWE token\" }\n    end\n\n    local consumer = get_consumer(jwe_obj.header_obj.kid)\n    if not consumer then\n        return 400, { message = \"invalid kid in JWE token\" }\n    end\n\n    local plaintext, err = jwe_decrypt_with_obj(jwe_obj, consumer)\n    if err ~= nil then\n        return 400, { message = \"failed to decrypt JWE token\" }\n    end\n    core.request.set_header(ctx, conf.forward_header, plaintext)\nend\n\n\nlocal function gen_token()\n    local args = core.request.get_uri_args()\n    if not args or not args.key then\n        return core.response.exit(400)\n    end\n\n    local key = args.key\n    local payload = args.payload\n    if payload then\n        payload = ngx.unescape_uri(payload)\n    end\n\n    local consumer = get_consumer(key)\n    if not consumer then\n        return core.response.exit(404)\n    end\n\n    local iv = args.iv\n    if not iv then\n        -- TODO: random bytes\n        iv = \"123456789012\"\n    end\n\n    local obj = {\n        iv = iv,\n        plaintext = payload,\n        header_obj = {\n            kid = key,\n            alg = \"dir\",\n            enc = \"A256GCM\",\n        },\n    }\n    obj.header = base64.encode_base64url(core.json.encode(obj.header_obj))\n    local jwe_token = jwe_encrypt(obj, consumer)\n    if jwe_token then\n        return core.response.exit(200, jwe_token)\n    end\n\n    return core.response.exit(404)\nend\n\n\nfunction _M.api()\n    return {\n        {\n            methods = { \"GET\" },\n            uri = \"/apisix/plugin/jwe/encrypt\",\n            handler = gen_token,\n        }\n    }\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/jwt-auth/parser.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal buffer = require \"string.buffer\"\nlocal openssl_digest = require \"resty.openssl.digest\"\nlocal openssl_mac = require \"resty.openssl.mac\"\nlocal openssl_pkey = require \"resty.openssl.pkey\"\nlocal base64 = require \"ngx.base64\"\nlocal core = require \"apisix.core\"\nlocal jwt = require(\"resty.jwt\")\n\nlocal ngx_time = ngx.time\nlocal http_time = ngx.http_time\nlocal string_fmt = string.format\nlocal assert = assert\nlocal setmetatable = setmetatable\nlocal ipairs = ipairs\nlocal type = type\nlocal error = error\nlocal pcall = pcall\n\nlocal default_claims = {\n    \"nbf\",\n    \"exp\"\n}\n\nlocal alg_sign = {\n    HS256 = function(data, key)\n        return openssl_mac.new(key, \"HMAC\", nil, \"sha256\"):final(data)\n    end,\n    HS384 = function(data, key)\n        return openssl_mac.new(key, \"HMAC\", nil, \"sha384\"):final(data)\n    end,\n    HS512 = function(data, key)\n        return openssl_mac.new(key, \"HMAC\", nil, \"sha512\"):final(data)\n    end,\n    RS256 = function(data, key)\n        local digest = openssl_digest.new(\"sha256\")\n        assert(digest:update(data))\n        return assert(openssl_pkey.new(key):sign(digest))\n    end,\n    RS384 = function(data, key)\n        local digest = openssl_digest.new(\"sha384\")\n        assert(digest:update(data))\n        return assert(openssl_pkey.new(key):sign(digest))\n    end,\n    RS512 = function(data, key)\n        local digest = openssl_digest.new(\"sha512\")\n        assert(digest:update(data))\n        return assert(openssl_pkey.new(key):sign(digest))\n    end,\n    ES256 = function(data, key)\n        local pkey = openssl_pkey.new(key)\n        local sig = assert(pkey:sign(data, \"sha256\", nil, {ecdsa_use_raw = true}))\n        if not sig then\n            return nil\n        end\n        return sig\n    end,\n    ES384 = function(data, key)\n        local pkey = openssl_pkey.new(key)\n        local sig = assert(pkey:sign(data, \"sha384\", nil, {ecdsa_use_raw = true}))\n        if not sig then\n            return nil\n        end\n        return sig\n    end,\n    ES512 = function(data, key)\n        local pkey = openssl_pkey.new(key)\n        local sig = assert(pkey:sign(data, \"sha512\", nil, {ecdsa_use_raw = true}))\n        if not sig then\n            return nil\n        end\n        return sig\n    end,\n    PS256 = function(data, key)\n        local pkey = openssl_pkey.new(key)\n        local sig = assert(pkey:sign(data, \"sha256\", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))\n        if not sig then\n            return nil\n        end\n        return sig\n    end,\n    PS384 = function(data, key)\n        local pkey = openssl_pkey.new(key)\n        local sig = assert(pkey:sign(data, \"sha384\", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))\n        if not sig then\n            return nil\n        end\n        return sig\n    end,\n    PS512 = function(data, key)\n        local pkey = openssl_pkey.new(key)\n        local sig = assert(pkey:sign(data, \"sha512\", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING))\n        if not sig then\n            return nil\n        end\n        return sig\n    end,\n    EdDSA = function(data, key)\n        local pkey = assert(openssl_pkey.new(key))\n        return assert(pkey:sign(data))\n    end\n}\n\nlocal alg_verify = {\n    HS256 = function(data, signature, key)\n        return signature == alg_sign.HS256(data, key)\n    end,\n    HS384 = function(data, signature, key)\n        return signature == alg_sign.HS384(data, key)\n    end,\n    HS512 = function(data, signature, key)\n        return signature == alg_sign.HS512(data, key)\n    end,\n    RS256 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        return pkey:verify(signature, data, \"sha256\")\n    end,\n    RS384 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        return pkey:verify(signature, data, \"sha384\")\n    end,\n    RS512 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        return pkey:verify(signature, data, \"sha512\")\n    end,\n    ES256 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        assert(#signature == 64, \"Signature must be 64 bytes.\")\n        return pkey:verify(signature, data, \"sha256\", nil, {ecdsa_use_raw = true})\n    end,\n    ES384 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        assert(#signature == 96, \"Signature must be 96 bytes.\")\n        return pkey:verify(signature, data, \"sha384\", nil, {ecdsa_use_raw = true})\n    end,\n    ES512 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        assert(#signature == 132, \"Signature must be 132 bytes.\")\n        return pkey:verify(signature, data, \"sha512\", nil, {ecdsa_use_raw = true})\n    end,\n    PS256 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        assert(#signature == 256, \"Signature must be 256 bytes\")\n        return pkey:verify(signature, data, \"sha256\", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)\n    end,\n    PS384 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        assert(#signature == 256, \"Signature must be 256 bytes\")\n        return pkey:verify(signature, data, \"sha384\", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)\n    end,\n    PS512 = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        assert(#signature == 256, \"Signature must be 256 bytes\")\n        return pkey:verify(signature, data, \"sha512\", openssl_pkey.PADDINGS.RSA_PKCS1_PSS_PADDING)\n    end,\n    EdDSA = function(data, signature, key)\n        local pkey, _ = openssl_pkey.new(key)\n        assert(pkey, \"Consumer Public Key is Invalid\")\n        return pkey:verify(signature, data)\n    end\n}\n\nlocal claims_checker = {\n    nbf = {\n        type = \"number\",\n        check = function(nbf, conf)\n            local clock_leeway = conf and conf.lifetime_grace_period or 0\n            if nbf < ngx_time() + clock_leeway then\n                return true\n            end\n            return false, string_fmt(\"'nbf' claim not valid until %s\", http_time(nbf))\n        end\n    },\n    exp = {\n        type = \"number\",\n        check = function(exp, conf)\n            local clock_leeway = conf and conf.lifetime_grace_period or 0\n            if exp > ngx_time() - clock_leeway then\n                return true\n            end\n            return false, string_fmt(\"'exp' claim expired at %s\", http_time(exp))\n        end\n    }\n}\n\nlocal base64_encode = base64.encode_base64url\nlocal base64_decode = base64.decode_base64url\n\nlocal _M = {}\n\nfunction _M.new(token)\n    local jwt_obj = jwt:load_jwt(token)\n    if type(jwt_obj) == \"table\" and not jwt_obj.valid then\n        return nil, jwt_obj.reason\n    end\n    return setmetatable(jwt_obj, {__index = _M})\nend\n\n\nfunction _M.verify_signature(self, key)\n    return alg_verify[self.header.alg](self.raw_header .. \".\" ..\n               self.raw_payload, base64_decode(self.signature), key)\nend\n\n\nfunction _M.verify_claims(self, claims, conf)\n    if not claims then\n        claims = default_claims\n    end\n\n    for _, claim_name in ipairs(claims) do\n        local claim = self.payload[claim_name]\n        if claim then\n            local checker = claims_checker[claim_name]\n            if type(claim) ~= checker.type then\n                return false, \"claim \" .. claim_name .. \" is not a \" .. checker.type\n            end\n            local ok, err = checker.check(claim, conf)\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.encode(alg, key, header, data)\n    alg = alg or \"HS256\"\n    if not alg_sign[alg] then\n        return nil, \"algorithm not supported\"\n    end\n\n    if type(key) ~= \"string\" then\n        error(\"Argument #2 must be string\", 2)\n        return nil, \"key must be a string\"\n    end\n\n    if header and type(header) ~= \"table\" then\n        return nil, \"header must be a table\"\n    end\n\n    if type(data) ~= \"table\" then\n        return nil, \"data must be a table\"\n    end\n\n    local header = header or {typ = \"JWT\", alg = alg}\n    local buf = buffer.new()\n\n    buf:put(base64_encode(core.json.encode(header)))\n        :put(\".\")\n        :put(base64_encode(core.json.encode(data)))\n\n    local ok, signature = pcall(alg_sign[alg], buf:tostring(), key)\n    if not ok then\n        return nil, signature\n    end\n\n    buf:put(\".\"):put(base64_encode(signature))\n\n    return buf:get()\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/jwt-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core     = require(\"apisix.core\")\nlocal consumer_mod = require(\"apisix.consumer\")\nlocal new_tab = require (\"table.new\")\nlocal auth_utils = require(\"apisix.utils.auth\")\n\nlocal ngx_decode_base64 = ngx.decode_base64\nlocal ngx      = ngx\nlocal sub_str  = string.sub\nlocal table_insert = table.insert\nlocal table_concat = table.concat\nlocal ngx_re_gmatch = ngx.re.gmatch\nlocal plugin_name = \"jwt-auth\"\n\nlocal schema_def = require(\"apisix.schema_def\")\nlocal jwt_parser = require(\"apisix.plugins.jwt-auth.parser\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        header = {\n            type = \"string\",\n            default = \"authorization\"\n        },\n        query = {\n            type = \"string\",\n            default = \"jwt\"\n        },\n        cookie = {\n            type = \"string\",\n            default = \"jwt\"\n        },\n        hide_credentials = {\n            type = \"boolean\",\n            default = false\n        },\n        key_claim_name = {\n            type = \"string\",\n            default = \"key\",\n            minLength = 1,\n        },\n        store_in_ctx = {\n            type = \"boolean\",\n            default = false\n        },\n        realm = schema_def.get_realm_schema(\"jwt\"),\n        anonymous_consumer = schema_def.anonymous_consumer_schema,\n        claims_to_verify = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                enum = {\"exp\",\"nbf\"},\n            },\n            uniqueItems = true,\n        },\n    },\n}\n\nlocal consumer_schema = {\n    type = \"object\",\n    -- can't use additionalProperties with dependencies\n    properties = {\n        key = {\n            type = \"string\",\n            minLength = 1,\n        },\n        secret = {\n            type = \"string\",\n            minLength = 1,\n        },\n        algorithm = {\n            type = \"string\",\n            enum = {\n                \"HS256\",\n                \"HS384\",\n                \"HS512\",\n                \"RS256\",\n                \"RS384\",\n                \"RS512\",\n                \"ES256\",\n                \"ES384\",\n                \"ES512\",\n                \"PS256\",\n                \"PS384\",\n                \"PS512\",\n                \"EdDSA\",\n            },\n            default = \"HS256\"\n        },\n        exp = {type = \"integer\", minimum = 1, default = 86400},\n        base64_secret = {\n            type = \"boolean\",\n            default = false\n        },\n        lifetime_grace_period = {\n            type = \"integer\",\n            minimum = 0,\n            default = 0\n        }\n    },\n    dependencies = {\n        algorithm = {\n            oneOf = {\n                {\n                    properties = {\n                        algorithm = {\n                            enum = {\"HS256\", \"HS384\", \"HS512\"},\n                            default = \"HS256\"\n                        },\n                    },\n                },\n                {\n                    properties = {\n                        public_key = {\n                            type = \"string\",\n                            minLength = 1,\n                        },\n                        algorithm = {\n                            enum = {\n                                \"RS256\",\n                                \"RS384\",\n                                \"RS512\",\n                                \"ES256\",\n                                \"ES384\",\n                                \"ES512\",\n                                \"PS256\",\n                                \"PS384\",\n                                \"PS512\",\n                                \"EdDSA\",\n                            },\n                        },\n                    },\n                    required = {\"public_key\"},\n                },\n            }\n        }\n    },\n    encrypt_fields = {\"secret\"},\n    required = {\"key\"},\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2510,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n    consumer_schema = consumer_schema\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    local ok, err\n    if schema_type == core.schema.TYPE_CONSUMER then\n        ok, err = core.schema.check(consumer_schema, conf)\n    else\n        return core.schema.check(schema, conf)\n    end\n\n    if not ok then\n        return false, err\n    end\n\n    local is_hs_alg = conf.algorithm:sub(1, 2) == \"HS\"\n    if is_hs_alg and not conf.secret then\n        return false, \"property \\\"secret\\\" is required when using HS based algorithms\"\n    end\n\n    if conf.base64_secret then\n        if ngx_decode_base64(conf.secret) == nil then\n            return false, \"base64_secret required but the secret is not in base64 format\"\n        end\n    end\n\n    if not is_hs_alg and not conf.public_key then\n        return false, \"missing valid public key\"\n    end\n\n    return true\nend\n\nlocal function remove_specified_cookie(src, key)\n    local cookie_key_pattern = \"([a-zA-Z0-9-_]*)\"\n    local cookie_val_pattern = \"([a-zA-Z0-9-._]*)\"\n    local t = new_tab(1, 0)\n\n    local it, err = ngx_re_gmatch(src, cookie_key_pattern .. \"=\" .. cookie_val_pattern, \"jo\")\n    if not it then\n        core.log.error(\"match origins failed: \", err)\n        return src\n    end\n    while true do\n        local m, err = it()\n        if err then\n            core.log.error(\"iterate origins failed: \", err)\n            return src\n        end\n        if not m then\n            break\n        end\n        if m[1] ~= key then\n            table_insert(t, m[0])\n        end\n    end\n\n    return table_concat(t, \"; \")\nend\n\nlocal function fetch_jwt_token(conf, ctx)\n    local token = core.request.header(ctx, conf.header)\n    if token then\n        if conf.hide_credentials then\n            -- hide for header\n            core.request.set_header(ctx, conf.header, nil)\n        end\n\n        local prefix = sub_str(token, 1, 7)\n        if prefix == 'Bearer ' or prefix == 'bearer ' then\n            return sub_str(token, 8)\n        end\n\n        return token\n    end\n\n    local uri_args = core.request.get_uri_args(ctx) or {}\n    token = uri_args[conf.query]\n    if token then\n        if conf.hide_credentials then\n            -- hide for query\n            uri_args[conf.query] = nil\n            core.request.set_uri_args(ctx, uri_args)\n        end\n        return token\n    end\n\n    local val = ctx.var[\"cookie_\" .. conf.cookie]\n    if not val then\n        return nil, \"JWT not found in cookie\"\n    end\n\n    if conf.hide_credentials then\n        -- hide for cookie\n        local src = core.request.header(ctx, \"Cookie\")\n        local reset_val = remove_specified_cookie(src, conf.cookie)\n        core.request.set_header(ctx, \"Cookie\", reset_val)\n    end\n\n    return val\nend\n\nlocal function get_secret(conf)\n    local secret = conf.secret\n\n    if conf.base64_secret then\n        return ngx_decode_base64(secret)\n    end\n\n    return secret\nend\n\n\nlocal function get_auth_secret(consumer)\n    if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm:sub(1, 2) == \"HS\" then\n        return get_secret(consumer.auth_conf)\n    else\n        return consumer.auth_conf.public_key\n    end\nend\n\n\nlocal function find_consumer(conf, ctx)\n    -- fetch token and hide credentials if necessary\n    local jwt_token, err = fetch_jwt_token(conf, ctx)\n    if not jwt_token then\n        core.log.info(\"failed to fetch JWT token: \", err)\n        return nil, nil, \"Missing JWT token in request\"\n    end\n\n    local jwt, err = jwt_parser.new(jwt_token)\n    if not jwt then\n        err = \"JWT token invalid: \" .. err\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.warn(err)\n        return nil, nil, \"JWT token invalid\"\n    end\n    core.log.debug(\"parsed jwt object: \", core.json.delay_encode(jwt, true))\n\n    local key_claim_name = conf.key_claim_name\n    local user_key = jwt.payload and jwt.payload[key_claim_name]\n    if not user_key then\n        return nil, nil, \"missing user key in JWT token\"\n    end\n\n    local consumer, consumer_conf, err = consumer_mod.find_consumer(plugin_name, \"key\", user_key)\n    if not consumer then\n        core.log.warn(\"failed to find consumer: \", err or \"invalid user key\")\n        return nil, nil, \"Invalid user key in JWT token\"\n    end\n\n    local auth_secret, err = get_auth_secret(consumer)\n    if not auth_secret then\n        err = \"failed to retrieve secrets, err: \" .. err\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.error(err)\n        return nil, nil, \"failed to verify jwt\"\n    end\n\n    -- Now verify the JWT signature\n    if not jwt:verify_signature(auth_secret) then\n        local err = \"failed to verify jwt: signature mismatch: \" .. jwt.signature\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.warn(err)\n        return nil, nil, \"failed to verify jwt\"\n    end\n\n    -- Verify the JWT registered claims\n    local ok, err = jwt:verify_claims(conf.claims_to_verify, {\n        lifetime_grace_period = consumer.auth_conf.lifetime_grace_period\n    })\n    if not ok then\n        err = \"failed to verify jwt: \" .. err\n        if auth_utils.is_running_under_multi_auth(ctx) then\n            return nil, nil, err\n        end\n        core.log.error(err)\n        return nil, nil, \"failed to verify jwt\"\n    end\n\n    if conf.store_in_ctx then\n        ctx.jwt_auth_payload = jwt.payload\n    end\n\n    return consumer, consumer_conf\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local consumer, consumer_conf, err = find_consumer(conf, ctx)\n    if not consumer then\n        if not conf.anonymous_consumer then\n            core.response.set_header(\"WWW-Authenticate\", \"Bearer realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = err }\n        end\n        consumer, consumer_conf, err = consumer_mod.get_anonymous_consumer(conf.anonymous_consumer)\n        if not consumer then\n            err = \"jwt-auth failed to authenticate the request, code: 401. error: \" .. err\n            core.log.error(err)\n            core.response.set_header(\"WWW-Authenticate\", \"Bearer realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = \"Invalid user authorization\"}\n        end\n    end\n\n    core.log.info(\"consumer: \", core.json.delay_encode(consumer))\n\n    consumer_mod.attach_consumer(ctx, consumer, consumer_conf)\n    core.log.info(\"hit jwt-auth rewrite\")\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/kafka-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core     = require(\"apisix.core\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal producer = require (\"resty.kafka.producer\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin = require(\"apisix.plugin\")\n\nlocal math     = math\nlocal pairs    = pairs\nlocal type     = type\n\nlocal plugin_name = \"kafka-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(\"kafka logger\")\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        meta_format = {\n            type = \"string\",\n            default = \"default\",\n            enum = {\"default\", \"origin\"},\n        },\n        log_format = {type = \"object\"},\n        -- deprecated, use \"brokers\" instead\n        broker_list = {\n            type = \"object\",\n            minProperties = 1,\n            patternProperties = {\n                [\".*\"] = {\n                    description = \"the port of kafka broker\",\n                    type = \"integer\",\n                    minimum = 1,\n                    maximum = 65535,\n                },\n            },\n        },\n        brokers = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"object\",\n                properties = {\n                    host = {\n                        type = \"string\",\n                        description = \"the host of kafka broker\",\n                    },\n                    port = {\n                        type = \"integer\",\n                        minimum = 1,\n                        maximum = 65535,\n                        description = \"the port of kafka broker\",\n                    },\n                    sasl_config = {\n                        type = \"object\",\n                        description = \"sasl config\",\n                        properties = {\n                            mechanism = {\n                                type = \"string\",\n                                default = \"PLAIN\",\n                                enum = {\"PLAIN\", \"SCRAM-SHA-256\", \"SCRAM-SHA-512\"},\n                            },\n                            user = { type = \"string\", description = \"user\" },\n                            password =  { type = \"string\", description = \"password\" },\n                        },\n                        required = {\"user\", \"password\"},\n                    },\n                },\n                required = {\"host\", \"port\"},\n            },\n            uniqueItems = true,\n        },\n        kafka_topic = {type = \"string\"},\n        producer_type = {\n            type = \"string\",\n            default = \"async\",\n            enum = {\"async\", \"sync\"},\n        },\n        required_acks = {\n            type = \"integer\",\n            default = 1,\n            enum = { 1, -1 },\n        },\n        key = {type = \"string\"},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        -- in lua-resty-kafka, cluster_name is defined as number\n        -- see https://github.com/doujiang24/lua-resty-kafka#new-1\n        cluster_name = {type = \"integer\", minimum = 1, default = 1},\n        -- config for lua-resty-kafka, default value is same as lua-resty-kafka\n        producer_batch_num = {type = \"integer\", minimum = 1, default = 200},\n        producer_batch_size = {type = \"integer\", minimum = 0, default = 1048576},\n        producer_max_buffering = {type = \"integer\", minimum = 1, default = 50000},\n        producer_time_linger = {type = \"integer\", minimum = 1, default = 1},\n        meta_refresh_interval = {type = \"integer\", minimum = 1, default = 30},\n    },\n    oneOf = {\n        { required = {\"broker_list\", \"kafka_topic\"},},\n        { required = {\"brokers\", \"kafka_topic\"},},\n    }\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 403,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, err\n    end\n    return log_util.check_log_schema(conf)\nend\n\n\nlocal function get_partition_id(prod, topic, log_message)\n    if prod.async then\n        local ringbuffer = prod.ringbuffer\n        for i = 1, ringbuffer.size, 3 do\n            if ringbuffer.queue[i] == topic and\n                ringbuffer.queue[i+2] == log_message then\n                return math.floor(i / 3)\n            end\n        end\n        core.log.info(\"current topic in ringbuffer has no message\")\n        return nil\n    end\n\n    -- sync mode\n    local sendbuffer = prod.sendbuffer\n    if not sendbuffer.topics[topic] then\n        core.log.info(\"current topic in sendbuffer has no message\")\n        return nil\n    end\n    for i, message in pairs(sendbuffer.topics[topic]) do\n        if log_message == message.queue[2] then\n            return i\n        end\n    end\nend\n\n\nlocal function create_producer(broker_list, broker_config, cluster_name)\n    core.log.info(\"create new kafka producer instance\")\n    return producer:new(broker_list, broker_config, cluster_name)\nend\n\n\nlocal function send_kafka_data(conf, log_message, prod)\n    local ok, err = prod:send(conf.kafka_topic, conf.key, log_message)\n    core.log.info(\"partition_id: \",\n                  core.log.delay_exec(get_partition_id,\n                                      prod, conf.kafka_topic, log_message))\n\n    if not ok then\n        return false, \"failed to send data to Kafka topic: \" .. err ..\n                \", brokers: \" .. core.json.encode(conf.broker_list)\n    end\n\n    return true\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry\n    if conf.meta_format == \"origin\" then\n        entry = log_util.get_req_original(ctx, conf)\n        -- core.log.info(\"origin entry: \", entry)\n\n    else\n        entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    end\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- reuse producer via lrucache to avoid unbalanced partitions of messages in kafka\n    local broker_list = core.table.clone(conf.brokers or {})\n    local broker_config = {}\n\n    if conf.broker_list then\n        for host, port in pairs(conf.broker_list) do\n            local broker = {\n                host = host,\n                port = port\n            }\n            core.table.insert(broker_list, broker)\n        end\n    end\n\n    broker_config[\"request_timeout\"] = conf.timeout * 1000\n    broker_config[\"producer_type\"] = conf.producer_type\n    broker_config[\"required_acks\"] = conf.required_acks\n    broker_config[\"batch_num\"] = conf.producer_batch_num\n    broker_config[\"batch_size\"] = conf.producer_batch_size\n    broker_config[\"max_buffering\"] = conf.producer_max_buffering\n    broker_config[\"flush_time\"] = conf.producer_time_linger * 1000\n    broker_config[\"refresh_interval\"] = conf.meta_refresh_interval * 1000\n\n    local prod, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_producer,\n                                               broker_list, broker_config, conf.cluster_name)\n    core.log.info(\"kafka cluster name \", conf.cluster_name, \", broker_list[1] port \",\n                  prod.client.broker_list[1].port)\n    if err then\n        return nil, \"failed to identify the broker specified: \" .. err\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err\n        if batch_max_size == 1 then\n            data = entries[1]\n            if type(data) ~= \"string\" then\n                data, err = core.json.encode(data) -- encode as single {}\n            end\n        else\n            data, err = core.json.encode(entries) -- encode as array [{}]\n        end\n\n        if not data then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        core.log.info(\"send data to kafka: \", data)\n\n        return send_kafka_data(conf, data, prod)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/kafka-proxy.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        sasl = {\n            type = \"object\",\n            properties = {\n                username = {\n                    type = \"string\",\n                },\n                password = {\n                    type = \"string\",\n                },\n            },\n            required = {\"username\", \"password\"},\n        },\n    },\n    encrypt_fields = {\"sasl.password\"},\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 508,\n    name = \"kafka-proxy\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    if conf.sasl then\n        ctx.kafka_consumer_enable_sasl = true\n        ctx.kafka_consumer_sasl_username = conf.sasl.username\n        ctx.kafka_consumer_sasl_password = conf.sasl.password\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/key-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core     = require(\"apisix.core\")\nlocal consumer_mod = require(\"apisix.consumer\")\nlocal plugin_name = \"key-auth\"\nlocal schema_def = require(\"apisix.schema_def\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        header = {\n            type = \"string\",\n            default = \"apikey\",\n        },\n        query = {\n            type = \"string\",\n            default = \"apikey\",\n        },\n        realm = schema_def.get_realm_schema(\"key\"),\n        hide_credentials = {\n            type = \"boolean\",\n            default = false,\n        },\n        anonymous_consumer = schema_def.anonymous_consumer_schema,\n    },\n}\n\nlocal consumer_schema = {\n    type = \"object\",\n    properties = {\n        key = { type = \"string\" },\n    },\n    encrypt_fields = {\"key\"},\n    required = {\"key\"},\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2500,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n    consumer_schema = consumer_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_CONSUMER then\n        return core.schema.check(consumer_schema, conf)\n    else\n        return core.schema.check(schema, conf)\n    end\nend\n\nlocal function find_consumer(ctx, conf)\n    local from_header = true\n    local key = core.request.header(ctx, conf.header)\n\n    if not key then\n        local uri_args = core.request.get_uri_args(ctx) or {}\n        key = uri_args[conf.query]\n        from_header = false\n    end\n\n    if not key then\n        return nil, nil, \"Missing API key in request\"\n    end\n\n    local consumer, consumer_conf, err = consumer_mod.find_consumer(plugin_name, \"key\", key)\n    if not consumer then\n        core.log.warn(\"failed to find consumer: \", err or \"invalid api key\")\n        return nil, nil, \"Invalid API key in request\"\n    end\n\n    if conf.hide_credentials then\n        if from_header then\n            core.request.set_header(ctx, conf.header, nil)\n        else\n            local args = core.request.get_uri_args(ctx)\n            args[conf.query] = nil\n            core.request.set_uri_args(ctx, args)\n        end\n    end\n\n    return consumer, consumer_conf\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local consumer, consumer_conf, err = find_consumer(ctx, conf)\n    if not consumer then\n        if not conf.anonymous_consumer then\n            core.response.set_header(\"WWW-Authenticate\", \"apikey realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = err}\n        end\n        consumer, consumer_conf, err = consumer_mod.get_anonymous_consumer(conf.anonymous_consumer)\n        if not consumer then\n            err = \"key-auth failed to authenticate the request, code: 401. error: \" .. err\n            core.log.error(err)\n            core.response.set_header(\"WWW-Authenticate\", \"apikey realm=\\\"\" .. conf.realm .. \"\\\"\")\n            return 401, { message = \"Invalid user authorization\"}\n        end\n    end\n    consumer_mod.attach_consumer(ctx, consumer, consumer_conf)\n    core.log.info(\"hit key-auth rewrite\")\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/lago.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal type         = type\nlocal pairs        = pairs\nlocal math_random  = math.random\nlocal ngx          = ngx\n\nlocal http            = require(\"resty.http\")\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal core            = require(\"apisix.core\")\nlocal str_format      = core.string.format\n\nlocal plugin_name = \"lago\"\nlocal batch_processor_manager = bp_manager_mod.new(\"lago logger\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        -- core configurations\n        endpoint_addrs = {\n            type = \"array\",\n            minItems = 1,\n            items = core.schema.uri_def,\n            description = \"Lago API address, like http://127.0.0.1:3000, \"\n                        .. \"it supports both self-hosted and cloud. If multiple endpoints are\"\n                        .. \" configured, the log will be pushed to a randomly determined\"\n                        .. \" endpoint from the list.\",\n        },\n        endpoint_uri = {\n            type = \"string\",\n            minLength = 1,\n            default = \"/api/v1/events/batch\",\n            description = \"Lago API endpoint, it needs to be set to the batch send endpoint.\",\n        },\n        token = {\n            type = \"string\",\n            description = \"Lago API key, create one for your organization on dashboard.\"\n        },\n        event_transaction_id = {\n            type = \"string\",\n            description = \"Event's transaction ID, it is used to identify and de-duplicate\"\n                        .. \" the event, it supports string templates containing APISIX and\"\n                        .. \" NGINX variables, like \\\"req_${request_id}\\\", which allows you\"\n                        .. \" to use values returned by upstream services or request-id\"\n                        .. \" plugin integration\",\n        },\n        event_subscription_id = {\n            type = \"string\",\n            description = \"Event's subscription ID, which is automatically generated or\"\n                        .. \" specified by you when you assign the plan to the customer on\"\n                        .. \" Lago, used to associate API consumption to a customer subscription,\"\n                        .. \" it supports string templates containing APISIX and NGINX variables,\"\n                        .. \" like \\\"cus_${consumer_name}\\\", which allows you to use values\"\n                        .. \" returned by upstream services or APISIX consumer\",\n        },\n        event_code = {\n            type = \"string\",\n            description = \"Lago billable metric's code for associating an event to a specified\"\n                        .. \"billable item\",\n        },\n        event_properties = {\n            type = \"object\",\n            patternProperties = {\n                [\".*\"] = {\n                    type = \"string\",\n                    minLength = 1,\n                },\n            },\n            description = \"Event's properties, used to attach information to an event, this\"\n                        .. \" allows you to send certain information on a event to Lago, such\"\n                        .. \" as sending HTTP status to take a failed request off the bill, or\"\n                        .. \" sending the AI token consumption in the response body for accurate\"\n                        .. \" billing, its keys are fixed strings and its values can be string\"\n                        .. \" templates containing APISIX and NGINX variables, like \\\"${status}\\\"\"\n        },\n\n        -- connection layer configurations\n        ssl_verify = {type = \"boolean\", default = true},\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 60000,\n            default = 3000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {\n            type = \"integer\",\n            minimum = 1000,\n            default = 60000,\n            description = \"keepalive timeout in milliseconds\",\n        },\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 5},\n    },\n    required = {\"endpoint_addrs\", \"token\", \"event_transaction_id\", \"event_subscription_id\",\n                \"event_code\"},\n    encrypt_fields = {\"token\"},\n}\nschema = batch_processor_manager:wrap_schema(schema)\n\n-- According to https://getlago.com/docs/api-reference/events/batch, the maximum batch size is 100,\n-- so we have to override the default batch size to make it work out of the box，the plugin does\n-- not set a maximum limit, so if Lago relaxes the limit, then user can modify it\n-- to a larger batch size\n-- This does not affect other plugins, schema is appended after deep copy\nschema.properties.batch_max_size.default = 100\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 415,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    local check = {\"endpoint_addrs\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function send_http_data(conf, data)\n    local body, err = core.json.encode(data)\n    if not body then\n        return false, str_format(\"failed to encode json: %s\", err)\n    end\n    local params = {\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n            [\"Authorization\"] = \"Bearer \" .. conf.token,\n        },\n        keepalive = conf.keepalive,\n        ssl_verify = conf.ssl_verify,\n        method = \"POST\",\n        body = body,\n    }\n\n    if conf.keepalive then\n        params.keepalive_timeout = conf.keepalive_timeout\n        params.keepalive_pool = conf.keepalive_pool\n    end\n\n    local httpc, err = http.new()\n    if not httpc then\n        return false, str_format(\"create http client error: %s\", err)\n    end\n    httpc:set_timeout(conf.timeout)\n\n    -- select an random endpoint and build URL\n    local endpoint_url = conf.endpoint_addrs[math_random(#conf.endpoint_addrs)]..conf.endpoint_uri\n    local res, err = httpc:request_uri(endpoint_url, params)\n    if not res then\n        return false, err\n    end\n\n    if res.status >= 300 then\n        return false, str_format(\"lago api returned status: %d, body: %s\",\n            res.status, res.body or \"\")\n    end\n\n    return true\nend\n\n\nfunction _M.log(conf, ctx)\n    -- build usage event\n    local event_transaction_id, err = core.utils.resolve_var(conf.event_transaction_id, ctx.var)\n    if err then\n        core.log.error(\"failed to resolve event_transaction_id, event dropped: \", err)\n        return\n    end\n\n    local event_subscription_id, err = core.utils.resolve_var(conf.event_subscription_id, ctx.var)\n    if err then\n        core.log.error(\"failed to resolve event_subscription_id, event dropped: \", err)\n        return\n    end\n\n    local entry = {\n        transaction_id = event_transaction_id,\n        external_subscription_id = event_subscription_id,\n        code = conf.event_code,\n        timestamp = ngx.req.start_time(),\n    }\n\n    if conf.event_properties and type(conf.event_properties) == \"table\" then\n        entry.properties = core.table.deepcopy(conf.event_properties)\n        for key, value in pairs(entry.properties) do\n            local new_val, err, n_resolved = core.utils.resolve_var(value, ctx.var)\n            if not err and n_resolved > 0 then\n                entry.properties[key] = new_val\n            end\n        end\n    end\n\n    if batch_processor_manager:add_entry(conf, entry) then\n        return\n    end\n\n    -- generate a function to be executed by the batch processor\n    local func = function(entries)\n        return send_http_data(conf, {\n            events = entries,\n        })\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ldap-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx = ngx\nlocal ngx_re = require(\"ngx.re\")\nlocal consumer_mod = require(\"apisix.consumer\")\nlocal ldap = require(\"resty.ldap\")\nlocal schema_def = require(\"apisix.schema_def\")\n\n\nlocal schema = {\n    type = \"object\",\n    title = \"work with route or service object\",\n    properties = {\n        base_dn = { type = \"string\" },\n        ldap_uri = { type = \"string\" },\n        use_tls = { type = \"boolean\", default = false },\n        tls_verify = { type = \"boolean\", default = false },\n        uid = { type = \"string\", default = \"cn\" },\n        realm = schema_def.get_realm_schema(\"ldap\"),\n    },\n    required = {\"base_dn\",\"ldap_uri\"},\n}\n\nlocal consumer_schema = {\n    type = \"object\",\n    title = \"work with consumer object\",\n    properties = {\n        user_dn = { type = \"string\" },\n    },\n    required = {\"user_dn\"},\n}\n\nlocal plugin_name = \"ldap-auth\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2540,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n    consumer_schema = consumer_schema\n}\n\nfunction _M.check_schema(conf, schema_type)\n    local ok, err\n    if schema_type == core.schema.TYPE_CONSUMER then\n        ok, err = core.schema.check(consumer_schema, conf)\n    else\n        core.utils.check_tls_bool({\"use_tls\", \"tls_verify\"}, conf, plugin_name)\n        ok, err = core.schema.check(schema, conf)\n    end\n\n    return ok, err\nend\n\nlocal function extract_auth_header(authorization)\n    local obj = { username = \"\", password = \"\" }\n\n    local m, err = ngx.re.match(authorization, \"(?i:basic)\\\\s(.+)\", \"jo\")\n    if err then\n        -- error authorization\n        return nil, err\n    end\n\n    if not m then\n        return nil, \"Invalid authorization header format\"\n    end\n\n    local decoded = ngx.decode_base64(m[1])\n\n    if not decoded then\n        return nil, \"Failed to decode authentication header: \" .. m[1]\n    end\n\n    local res\n    res, err = ngx_re.split(decoded, \":\")\n    if err then\n        return nil, \"Split authorization err:\" .. err\n    end\n    if #res < 2 then\n        return nil, \"Split authorization err: invalid decoded data: \" .. decoded\n    end\n\n    obj.username = ngx.re.gsub(res[1], \"\\\\s+\", \"\", \"jo\")\n    obj.password = ngx.re.gsub(res[2], \"\\\\s+\", \"\", \"jo\")\n\n    return obj, nil\nend\n\nfunction _M.rewrite(conf, ctx)\n    core.log.info(\"plugin rewrite phase, conf: \", core.json.delay_encode(conf))\n\n    -- 1. extract authorization from header\n    local auth_header = core.request.header(ctx, \"Authorization\")\n    if not auth_header then\n        core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n        return 401, { message = \"Missing authorization in request\" }\n    end\n\n    local user, err = extract_auth_header(auth_header)\n    if err or not user then\n        if err then\n          core.log.warn(err)\n        else\n          core.log.warn(\"nil user\")\n        end\n        core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n        return 401, { message = \"Invalid authorization in request\" }\n    end\n\n    -- 2. try authenticate the user against the ldap server\n    local ldap_host, ldap_port = core.utils.parse_addr(conf.ldap_uri)\n    local ldapconf = {\n        timeout = 10000,\n        start_tls = false,\n        ldap_host = ldap_host,\n        ldap_port = ldap_port or 389,\n        ldaps = conf.use_tls,\n        tls_verify = conf.tls_verify,\n        base_dn = conf.base_dn,\n        attribute = conf.uid,\n        keepalive = 60000,\n    }\n    local res, err = ldap.ldap_authenticate(user.username, user.password, ldapconf)\n    if not res then\n        core.log.warn(\"ldap-auth failed: \", err)\n        core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n        return 401, { message = \"Invalid user authorization\" }\n    end\n\n    local user_dn =  conf.uid .. \"=\" .. user.username .. \",\" .. conf.base_dn\n\n    -- 3. Retrieve consumer for authorization plugin\n    local consumer_conf = consumer_mod.plugin(plugin_name)\n    if not consumer_conf then\n        core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n        return 401, { message = \"Missing related consumer\" }\n    end\n\n    local consumers = consumer_mod.consumers_kv(plugin_name, consumer_conf, \"user_dn\")\n    local consumer = consumers[user_dn]\n    if not consumer then\n        core.response.set_header(\"WWW-Authenticate\", \"Basic realm=\\\"\" .. conf.realm .. \"\\\"\")\n        return 401, {message = \"Invalid user authorization\"}\n    end\n    consumer_mod.attach_consumer(ctx, consumer, consumer_conf)\n\n    core.log.info(\"hit basic-auth access\")\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-conn/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal limit_conn_new = require(\"resty.limit.conn\").new\nlocal core = require(\"apisix.core\")\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal sleep = core.sleep\nlocal tonumber = tonumber\nlocal type = type\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal shdict_name = \"plugin-limit-conn\"\nif ngx.config.subsystem == \"stream\" then\n    shdict_name = shdict_name .. \"-stream\"\nend\n\nlocal redis_single_new\nlocal redis_cluster_new\ndo\n    local redis_src = \"apisix.plugins.limit-conn.limit-conn-redis\"\n    redis_single_new = require(redis_src).new\n\n    local cluster_src = \"apisix.plugins.limit-conn.limit-conn-redis-cluster\"\n    redis_cluster_new = require(cluster_src).new\nend\n\n\nlocal _M = {}\n\n\nlocal function resolve_var(ctx, value)\n    if type(value) == \"string\" then\n        local err, _\n        value, err, _ = core.utils.resolve_var(value, ctx.var)\n        if err then\n            return nil, \"could not resolve var for value: \" .. value .. \", err: \" .. err\n        end\n        value = tonumber(value)\n        if not value then\n            return nil, \"resolved value is not a number: \" .. tostring(value)\n        end\n    end\n    return value\nend\n\n\nlocal function get_rules(ctx, conf)\n    if not conf.rules then\n        local conn, err = resolve_var(ctx, conf.conn)\n        if err then\n            return nil, err\n        end\n        local burst, err2 = resolve_var(ctx, conf.burst)\n        if err2 then\n            return nil, err2\n        end\n        return {\n            {\n                conn = conn,\n                burst = burst,\n                key = conf.key,\n                key_type = conf.key_type,\n            }\n        }\n    end\n\n    local rules = {}\n    for _, rule in ipairs(conf.rules) do\n        local conn, err = resolve_var(ctx, rule.conn)\n        if err then\n            goto CONTINUE\n        end\n        local burst, err2 = resolve_var(ctx, rule.burst)\n        if err2 then\n            goto CONTINUE\n        end\n\n        local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)\n        if n_resolved == 0 then\n            goto CONTINUE\n        end\n        core.table.insert(rules, {\n            conn = conn,\n            burst = burst,\n            key_type = \"constant\",\n            key = key,\n        })\n\n        ::CONTINUE::\n    end\n    return rules\nend\n\n\nlocal function create_limit_obj(conf, rule, default_conn_delay)\n    core.log.info(\"create new limit-conn plugin instance\")\n\n    local conn = rule.conn\n    local burst = rule.burst\n\n    core.log.info(\"limit conn: \", conn, \", burst: \", burst)\n\n    if conf.policy == \"redis\" then\n        core.log.info(\"create new limit-conn redis plugin instance\")\n\n        return redis_single_new(\"plugin-limit-conn\", conf, conn, burst,\n                                default_conn_delay)\n\n    elseif conf.policy == \"redis-cluster\" then\n\n        core.log.info(\"create new limit-conn redis-cluster plugin instance\")\n\n        return redis_cluster_new(\"plugin-limit-conn\", conf, conn, burst,\n                                 default_conn_delay)\n    else\n        core.log.info(\"create new limit-conn plugin instance\")\n        return limit_conn_new(shdict_name, conn, burst,\n                              default_conn_delay)\n    end\nend\n\n\nlocal function run_limit_conn(conf, rule, ctx)\n    local lim, err = create_limit_obj(conf, rule, conf.default_conn_delay)\n    if not lim then\n        core.log.error(\"failed to instantiate a resty.limit.conn object: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    local conf_key = rule.key\n    local key\n    if rule.key_type == \"var_combination\" then\n        local err, n_resolved\n        key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var)\n        if err then\n            core.log.error(\"could not resolve vars in \", conf_key, \" error: \", err)\n        end\n\n        if n_resolved == 0 then\n            key = nil\n        end\n    elseif rule.key_type == \"constant\" then\n        key = conf_key\n    else\n        key = ctx.var[conf_key]\n    end\n\n    if key == nil then\n        core.log.info(\"The value of the configured key is empty, use client IP instead\")\n        -- When the value of key is empty, use client IP instead\n        key = ctx.var[\"remote_addr\"]\n    end\n\n    key = key .. ctx.conf_type .. ctx.conf_version\n    core.log.info(\"limit key: \", key)\n\n    local delay, err = lim:incoming(key, true)\n    if not delay then\n        if err == \"rejected\" then\n            if conf.rejected_msg then\n                return conf.rejected_code, { error_msg = conf.rejected_msg }\n            end\n            return conf.rejected_code or 503\n        end\n\n        core.log.error(\"failed to limit conn: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    if lim:is_committed() then\n        if not ctx.limit_conn then\n            ctx.limit_conn = core.tablepool.fetch(\"plugin#limit-conn\", 0, 6)\n        end\n\n        core.table.insert_tail(ctx.limit_conn, lim, key, delay, conf.only_use_default_delay)\n    end\n\n    if delay >= 0.001 then\n        sleep(delay)\n    end\nend\n\n\nfunction _M.increase(conf, ctx)\n    core.log.info(\"ver: \", ctx.conf_version)\n\n    local rules, err = get_rules(ctx, conf)\n    if not rules or #rules == 0 then\n        core.log.error(\"failed to get limit conn rules: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    for _, rule in ipairs(rules) do\n        local code, msg = run_limit_conn(conf, rule, ctx)\n        if code then\n            return code, msg\n        end\n    end\nend\n\n\nfunction _M.decrease(conf, ctx)\n    local limit_conn = ctx.limit_conn\n    if not limit_conn then\n        return\n    end\n\n    for i = 1, #limit_conn, 4 do\n        local lim = limit_conn[i]\n        local key = limit_conn[i + 1]\n        local delay = limit_conn[i + 2]\n        local use_delay =  limit_conn[i + 3]\n\n        local latency\n        if is_http then\n            if not use_delay then\n                if ctx.proxy_passed then\n                    latency = ctx.var.upstream_response_time\n                else\n                    latency = ctx.var.request_time - delay\n                end\n            end\n        end\n        core.log.debug(\"request latency is \", latency) -- for test\n\n        local conn, err = lim:leaving(key, latency)\n        if not conn then\n            core.log.error(\"failed to record the connection leaving request: \",\n                           err)\n            break\n        end\n    end\n\n    core.tablepool.release(\"plugin#limit-conn\", limit_conn)\n    ctx.limit_conn = nil\n    return\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-conn/limit-conn-redis-cluster.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal redis_cluster     = require(\"apisix.utils.rediscluster\")\nlocal core              = require(\"apisix.core\")\nlocal util              = require(\"apisix.plugins.limit-conn.util\")\nlocal setmetatable      = setmetatable\nlocal ngx_timer_at      = ngx.timer.at\nlocal ngx               = ngx\n\nlocal _M = {version = 0.1}\n\n\nlocal mt = {\n    __index = _M\n}\n\n\nfunction _M.new(plugin_name, conf, max, burst, default_conn_delay)\n\n    local red_cli, err = redis_cluster.new(conf, \"plugin-limit-conn-redis-cluster-slot-lock\")\n    if not red_cli then\n        return nil, err\n    end\n    local self = {\n        conf = conf,\n        plugin_name = plugin_name,\n        burst = burst,\n        max = max + 0,    -- just to ensure the param is good\n        unit_delay = default_conn_delay,\n        red_cli = red_cli,\n        use_evalsha = false,\n    }\n    return setmetatable(self, mt)\nend\n\n\nfunction _M.incoming(self, key, commit)\n    return util.incoming(self, self.red_cli, key, commit)\nend\n\n\nfunction _M.is_committed(self)\n    return self.committed\nend\n\n\nlocal function leaving_thread(premature, self, key, req_latency, req_id)\n    return util.leaving(self, self.red_cli, key, req_latency, req_id)\nend\n\n\nfunction _M.leaving(self, key, req_latency)\n    local req_id\n    if ngx.ctx.limit_conn_req_ids then\n        req_id = ngx.ctx.limit_conn_req_ids[key]\n    end\n\n    -- log_by_lua can't use cosocket\n    local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency, req_id)\n    if not ok then\n        core.log.error(\"failed to create timer: \", err)\n        return nil, err\n    end\n\n    return ok\n\nend\n\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-conn/limit-conn-redis.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal redis         = require(\"apisix.utils.redis\")\nlocal core          = require(\"apisix.core\")\nlocal util          = require(\"apisix.plugins.limit-conn.util\")\nlocal ngx_timer_at  = ngx.timer.at\nlocal ngx           = ngx\n\nlocal setmetatable  = setmetatable\n\n\nlocal _M = {version = 0.1}\n\n\nlocal mt = {\n    __index = _M\n}\n\nfunction _M.new(plugin_name, conf, max, burst, default_conn_delay)\n\n    local self = {\n        conf = conf,\n        plugin_name = plugin_name,\n        burst = burst,\n        max = max + 0,    -- just to ensure the param is good\n        unit_delay = default_conn_delay,\n        use_evalsha = true,\n    }\n    return setmetatable(self, mt)\nend\n\n\nfunction _M.incoming(self, key, commit)\n    local conf = self.conf\n    local red, err = redis.new(conf)\n    if not red then\n        return red, err\n    end\n    local delay, incoming_err = util.incoming(self, red, key, commit)\n    local ok, err = red:set_keepalive(conf.redis_keepalive_timeout, conf.redis_keepalive_pool)\n    if not ok then\n        core.log.error(\"set keepalive failed: \", err)\n    end\n    return delay, incoming_err\nend\n\n\nfunction _M.is_committed(self)\n    return self.committed\nend\n\n\nlocal function leaving_thread(premature, self, key, req_latency, req_id)\n\n    local conf = self.conf\n    local red, err = redis.new(conf)\n    if not red then\n        return red, err\n    end\n    local conn, leaving_err = util.leaving(self, red, key, req_latency, req_id)\n    local ok, err = red:set_keepalive(conf.redis_keepalive_timeout, conf.redis_keepalive_pool)\n    if not ok then\n        core.log.error(\"set keepalive failed: \", err)\n    end\n    return conn, leaving_err\nend\n\n\nfunction _M.leaving(self, key, req_latency)\n    local req_id\n    if ngx.ctx.limit_conn_req_ids then\n        req_id = ngx.ctx.limit_conn_req_ids[key]\n    end\n\n    -- log_by_lua can't use cosocket\n    local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency, req_id)\n    if not ok then\n        core.log.error(\"failed to create timer: \", err)\n        return nil, err\n    end\n\n    return ok\n\nend\n\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-conn/util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal assert            = assert\nlocal math              = require \"math\"\nlocal floor             = math.floor\nlocal ngx               = ngx\nlocal ngx_time          = ngx.time\nlocal uuid              = require(\"resty.jit-uuid\")\nlocal core              = require(\"apisix.core\")\n\nlocal _M = {version = 0.3}\nlocal redis_incoming_script = core.string.compress_script([=[\n    local key = KEYS[1]\n    local limit = tonumber(ARGV[1])\n    local ttl = tonumber(ARGV[2])\n    local now = tonumber(ARGV[3])\n    local req_id = ARGV[4]\n\n    redis.call('ZREMRANGEBYSCORE', key, 0, now)\n\n    local count = redis.call('ZCARD', key)\n    if count >= limit then\n        return {0, count}\n    end\n\n    redis.call('ZADD', key, now + ttl, req_id)\n    redis.call('EXPIRE', key, ttl)\n    return {1, count + 1}\n]=])\nlocal redis_incoming_script_sha\n\n\nlocal function generate_redis_sha1(red)\n    local sha1, err = red:script(\"LOAD\", redis_incoming_script)\n    if not sha1 then\n        return nil, err\n    end\n    return sha1\nend\n\n\nfunction _M.incoming(self, red, key, commit)\n    local max = self.max\n    self.committed = false\n    local raw_key = key\n    key = \"limit_conn\" .. \":\" .. key\n\n    local conn\n    if commit then\n        local req_id = ngx.ctx.request_id or uuid.generate_v4()\n        if not ngx.ctx.limit_conn_req_ids then\n            ngx.ctx.limit_conn_req_ids = {}\n        end\n        ngx.ctx.limit_conn_req_ids[raw_key] = req_id\n\n        local now = ngx_time()\n        local res, err\n\n        if self.use_evalsha then\n            if not redis_incoming_script_sha then\n                redis_incoming_script_sha, err = generate_redis_sha1(red)\n                if not redis_incoming_script_sha then\n                    core.log.error(\"failed to generate redis sha1: \", err)\n                    return nil, err\n                end\n            end\n\n            res, err = red:evalsha(redis_incoming_script_sha, 1, key,\n                                   max + self.burst, self.conf.key_ttl, now, req_id)\n\n            if err and core.string.has_prefix(err, \"NOSCRIPT\") then\n                core.log.warn(\"redis evalsha failed: \", err, \". Falling back to eval...\")\n                redis_incoming_script_sha = nil\n                res, err = red:eval(redis_incoming_script, 1, key,\n                                    max + self.burst, self.conf.key_ttl, now, req_id)\n            end\n        else\n            res, err = red:eval(redis_incoming_script, 1, key,\n                                max + self.burst, self.conf.key_ttl, now, req_id)\n        end\n\n        if not res then\n            return nil, err\n        end\n\n        local allowed = res[1]\n        conn = res[2]\n\n        if allowed == 0 then\n            return nil, \"rejected\"\n        end\n\n        self.committed = true\n\n    else\n        red:zremrangebyscore(key, 0, ngx_time())\n        local count, err = red:zcard(key)\n        if err then return nil, err end\n        conn = (count or 0) + 1\n    end\n\n    if conn > max then\n        -- make the excessive connections wait\n        return self.unit_delay * floor((conn - 1) / max), conn\n    end\n\n    -- we return a 0 delay by default\n    return 0, conn\nend\n\n\nfunction _M.leaving(self, red, key, req_latency, req_id)\n    assert(key)\n    key = \"limit_conn\" .. \":\" .. key\n\n    local conn, err\n    if req_id then\n        local res, err = red:zrem(key, req_id)\n        if not res then\n            return nil, err\n        end\n    end\n    conn, err = red:zcard(key)\n\n    if not conn then\n        return nil, err\n    end\n\n    if req_latency then\n        local unit_delay = self.unit_delay\n        self.unit_delay = (req_latency + unit_delay) / 2\n    end\n\n    return conn\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-conn.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core                              = require(\"apisix.core\")\nlocal limit_conn                        = require(\"apisix.plugins.limit-conn.init\")\nlocal redis_schema                      = require(\"apisix.utils.redis-schema\")\nlocal plugin_name                       = \"limit-conn\"\nlocal workflow                           = require(\"apisix.plugins.workflow\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        conn = {\n            oneOf = {\n                {type = \"integer\", exclusiveMinimum = 0},\n                {type = \"string\"},\n            },\n        },\n        burst = {\n            oneOf = {\n                {type = \"integer\", minimum = 0},\n                {type = \"string\"},\n            },\n        },\n        default_conn_delay = {type = \"number\", exclusiveMinimum = 0},\n        only_use_default_delay = {type = \"boolean\", default = false},\n        key = {type = \"string\"},\n        key_type = {type = \"string\",\n            enum = {\"var\", \"var_combination\"},\n            default = \"var\",\n        },\n        policy = {\n            type = \"string\",\n            enum = {\"redis\", \"redis-cluster\", \"local\"},\n            default = \"local\",\n        },\n        rejected_code = {\n            type = \"integer\", minimum = 200, maximum = 599, default = 503\n        },\n        rejected_msg = {\n            type = \"string\", minLength = 1\n        },\n        allow_degradation = {type = \"boolean\", default = false},\n        rules = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    conn = {\n                        oneOf = {\n                            {type = \"integer\", exclusiveMinimum = 0},\n                            {type = \"string\"},\n                        },\n                    },\n                    burst = {\n                        oneOf = {\n                            {type = \"integer\", minimum = 0},\n                            {type = \"string\"},\n                        },\n                    },\n                    key = {type = \"string\"},\n                },\n                required = {\"conn\", \"burst\", \"key\"},\n            },\n        },\n    },\n    oneOf = {\n        {\n            required = {\"conn\", \"burst\", \"default_conn_delay\", \"key\"},\n        },\n        {\n            required = {\"default_conn_delay\", \"rules\"},\n        }\n    },\n    [\"if\"] = {\n        properties = {\n            policy = {\n                enum = {\"redis\"},\n            },\n        },\n    },\n    [\"then\"] = redis_schema.limit_conn_redis_schema,\n    [\"else\"] = {\n        [\"if\"] = {\n            properties = {\n                policy = {\n                    enum = {\"redis-cluster\"},\n                },\n            },\n        },\n        [\"then\"] = redis_schema.limit_conn_redis_cluster_schema,\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1003,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    return limit_conn.increase(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    return limit_conn.decrease(conf, ctx)\nend\n\nfunction _M.workflow_handler()\n    workflow.register(plugin_name,\n    function (conf) -- schema validation\n        return core.schema.check(schema, conf)\n    end,\n    function (conf, ctx) -- handler run in access phase\n        return limit_conn.increase(conf, ctx, plugin_name, 1)\n    end,\n    function (conf, ctx) -- log_handler run in log phase\n        return limit_conn.decrease(conf, ctx, plugin_name, 1)\n    end)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-count/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal apisix_plugin = require(\"apisix.plugin\")\nlocal tab_insert = table.insert\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal redis_schema = require(\"apisix.utils.redis-schema\")\nlocal policy_to_additional_properties = redis_schema.schema\nlocal get_phase = ngx.get_phase\nlocal tonumber = tonumber\nlocal type = type\nlocal tostring = tostring\nlocal str_format = string.format\nlocal error = error\n\nlocal limit_redis_cluster_new\nlocal limit_redis_new\nlocal limit_local_new\ndo\n    local local_src = \"apisix.plugins.limit-count.limit-count-local\"\n    limit_local_new = require(local_src).new\n\n    local redis_src = \"apisix.plugins.limit-count.limit-count-redis\"\n    limit_redis_new = require(redis_src).new\n\n    local cluster_src = \"apisix.plugins.limit-count.limit-count-redis-cluster\"\n    limit_redis_cluster_new = require(cluster_src).new\nend\nlocal group_conf_lru = core.lrucache.new({\n    type = 'plugin',\n})\n\nlocal metadata_defaults = {\n    limit_header = \"X-RateLimit-Limit\",\n    remaining_header = \"X-RateLimit-Remaining\",\n    reset_header = \"X-RateLimit-Reset\",\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        limit_header = {\n            type = \"string\",\n            default = metadata_defaults.limit_header,\n        },\n        remaining_header = {\n            type = \"string\",\n            default = metadata_defaults.remaining_header,\n        },\n        reset_header = {\n            type = \"string\",\n            default = metadata_defaults.reset_header,\n        },\n    },\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        count = {\n            oneOf = {\n                {type = \"integer\", exclusiveMinimum = 0},\n                {type = \"string\"},\n            },\n        },\n        time_window = {\n            oneOf = {\n                {type = \"integer\", exclusiveMinimum = 0},\n                {type = \"string\"},\n            },\n        },\n        rules = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    count = {\n                        oneOf = {\n                            {type = \"integer\", exclusiveMinimum = 0},\n                            {type = \"string\"},\n                        },\n                    },\n                    time_window = {\n                        oneOf = {\n                            {type = \"integer\", exclusiveMinimum = 0},\n                            {type = \"string\"},\n                        },\n                    },\n                    key = {type = \"string\"},\n                    header_prefix = {\n                        type = \"string\",\n                        description = \"prefix for rate limit headers\"\n                    },\n                },\n                required = {\"count\", \"time_window\", \"key\"},\n            },\n        },\n        group = {type = \"string\"},\n        key = {type = \"string\", default = \"remote_addr\"},\n        key_type = {type = \"string\",\n            enum = {\"var\", \"var_combination\", \"constant\"},\n            default = \"var\",\n        },\n        rejected_code = {\n            type = \"integer\", minimum = 200, maximum = 599, default = 503\n        },\n        rejected_msg = {\n            type = \"string\", minLength = 1\n        },\n        policy = {\n            type = \"string\",\n            enum = {\"local\", \"redis\", \"redis-cluster\"},\n            default = \"local\",\n        },\n        allow_degradation = {type = \"boolean\", default = false},\n        show_limit_quota_header = {type = \"boolean\", default = true}\n    },\n    oneOf = {\n        {\n            required = {\"count\", \"time_window\"},\n        },\n        {\n            required = {\"rules\"},\n        }\n    },\n    [\"if\"] = {\n        properties = {\n            policy = {\n                enum = {\"redis\"},\n            },\n        },\n    },\n    [\"then\"] = policy_to_additional_properties.redis,\n    [\"else\"] = {\n        [\"if\"] = {\n            properties = {\n                policy = {\n                    enum = {\"redis-cluster\"},\n                },\n            },\n        },\n        [\"then\"] = policy_to_additional_properties[\"redis-cluster\"],\n    }\n}\n\nlocal schema_copy = core.table.deepcopy(schema)\n\nlocal _M = {\n    schema = schema,\n    metadata_schema = metadata_schema,\n}\n\n\nlocal function group_conf(conf)\n    return conf\nend\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.group then\n        -- means that call by some plugin not support\n        if conf._vid then\n            return false, \"group is not supported\"\n        end\n\n        local fields = {}\n        -- When the group field is configured,\n        -- we will use schema_copy to get the whitelist of properties,\n        -- so that we can avoid getting injected properties.\n        for k in pairs(schema_copy.properties) do\n            tab_insert(fields, k)\n        end\n        local extra = policy_to_additional_properties[conf.policy]\n        if extra then\n            for k in pairs(extra.properties) do\n                tab_insert(fields, k)\n            end\n        end\n\n        local prev_conf = group_conf_lru(conf.group, \"\", group_conf, conf)\n\n        for _, field in ipairs(fields) do\n            if not core.table.deep_eq(prev_conf[field], conf[field]) then\n                core.log.error(\"previous limit-count group \", prev_conf.group,\n                            \" conf: \", core.json.encode(prev_conf))\n                core.log.error(\"current limit-count group \", conf.group,\n                            \" conf: \", core.json.encode(conf))\n                return false, \"group conf mismatched\"\n            end\n        end\n    end\n\n    local keys = {}\n    for _, rule in ipairs(conf.rules or {}) do\n        if keys[rule.key] then\n            return false, str_format(\"duplicate key '%s' in rules\", rule.key)\n        end\n        keys[rule.key] = true\n    end\n\n    return true\nend\n\n\nlocal function create_limit_obj(conf, rule, plugin_name)\n    core.log.info(\"create new \", plugin_name, \" plugin instance\",\n        \", rule: \", core.json.delay_encode(rule, true))\n\n    if not conf.policy or conf.policy == \"local\" then\n        return limit_local_new(\"plugin-\" .. plugin_name, rule.count,\n                               rule.time_window)\n    end\n\n    if conf.policy == \"redis\" then\n        return limit_redis_new(\"plugin-\" .. plugin_name, rule.count, rule.time_window, conf)\n    end\n\n    if conf.policy == \"redis-cluster\" then\n        return limit_redis_cluster_new(\"plugin-\" .. plugin_name, rule.count,\n                                       rule.time_window, conf)\n    end\n\n    return nil\nend\n\n\nlocal function gen_limit_key(conf, ctx, key)\n    if conf.group then\n        return conf.group .. ':' .. key\n    end\n\n    -- here we add a separator ':' to mark the boundary of the prefix and the key itself\n    -- Here we use plugin-level conf version to prevent the counter from being resetting\n    -- because of the change elsewhere.\n    -- A route which reuses a previous route's ID will inherits its counter.\n    local parent = conf._meta and conf._meta.parent\n    if not parent or not parent.resource_key then\n        error(\"failed to generate key invalid parent: \", core.json.encode(parent))\n    end\n\n    local new_key = parent.resource_key .. ':' .. apisix_plugin.conf_version(conf)\n                    .. ':' .. key\n    if conf._vid then\n        -- conf has _vid means it's from workflow plugin, add _vid to the key\n        -- so that the counter is unique per action.\n        return new_key .. ':' .. conf._vid\n    end\n\n    return new_key\nend\n\n\nlocal function resolve_var(ctx, value)\n    if type(value) == \"string\" then\n        local err, _\n        value, err, _ = core.utils.resolve_var(value, ctx.var)\n        if err then\n            return nil, \"could not resolve var for value: \" .. value .. \", err: \" .. err\n        end\n        value = tonumber(value)\n        if not value then\n            return nil, \"resolved value is not a number: \" .. tostring(value)\n        end\n    end\n    return value\nend\n\n\nlocal function get_rules(ctx, conf)\n    if not conf.rules then\n        local count, err = resolve_var(ctx, conf.count)\n        if err then\n            return nil, err\n        end\n        local time_window, err2 = resolve_var(ctx, conf.time_window)\n        if err2 then\n            return nil, err2\n        end\n        return {\n            {\n                count = count,\n                time_window = time_window,\n                key = conf.key,\n                key_type = conf.key_type,\n            }\n        }\n    end\n\n    local rules = {}\n    for index, rule in ipairs(conf.rules) do\n        local count, err = resolve_var(ctx, rule.count)\n        if err then\n            goto CONTINUE\n        end\n        local time_window, err2 = resolve_var(ctx, rule.time_window)\n        if err2 then\n            goto CONTINUE\n        end\n        local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)\n        if n_resolved == 0 then\n            goto CONTINUE\n        end\n        core.table.insert(rules, {\n            count = count,\n            time_window = time_window,\n            key_type = \"constant\",\n            key = key,\n            header_prefix = rule.header_prefix or index,\n        })\n\n        ::CONTINUE::\n    end\n    return rules\nend\n\n\n\nlocal function construct_rate_limiting_headers(conf, name, rule, metadata)\n    local prefix = \"X-\"\n    if name == \"ai-rate-limiting\" then\n        prefix = \"X-AI-\"\n    end\n\n    if rule.header_prefix then\n        return {\n            limit_header = prefix .. rule.header_prefix .. \"-RateLimit-Limit\",\n            remaining_header = prefix .. rule.header_prefix .. \"-RateLimit-Remaining\",\n            reset_header = prefix .. rule.header_prefix .. \"-RateLimit-Reset\",\n        }\n    end\n    return  {\n        limit_header = conf.limit_header or metadata.limit_header,\n        remaining_header = conf.remaining_header or metadata.remaining_header,\n        reset_header = conf.reset_header or metadata.reset_header,\n    }\nend\n\n\nlocal function run_rate_limit(conf, rule, ctx, name, cost, dry_run)\n    local lim, err = create_limit_obj(conf, rule, name)\n\n    if not lim then\n        core.log.error(\"failed to fetch limit.count object: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    local conf_key = rule.key\n    local key\n    if rule.key_type == \"var_combination\" then\n        local err, n_resolved\n        key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var)\n        if err then\n            core.log.error(\"could not resolve vars in \", conf_key, \" error: \", err)\n        end\n\n        if n_resolved == 0 then\n            key = nil\n        end\n    elseif rule.key_type == \"constant\" then\n        key = conf_key\n    else\n        key = ctx.var[conf_key]\n    end\n\n    if key == nil then\n        core.log.info(\"The value of the configured key is empty, use client IP instead\")\n        -- When the value of key is empty, use client IP instead\n        key = ctx.var[\"remote_addr\"]\n    end\n\n    key = gen_limit_key(conf, ctx, key)\n    core.log.info(\"limit key: \", key)\n\n    local delay, remaining, reset\n    if not conf.policy or conf.policy == \"local\" then\n        delay, remaining, reset = lim:incoming(key, not dry_run, conf, cost)\n    else\n        delay, remaining, reset = lim:incoming(key, cost)\n    end\n\n    local metadata = apisix_plugin.plugin_metadata(\"limit-count\")\n    if metadata then\n        metadata = metadata.value\n    else\n        metadata = metadata_defaults\n    end\n    core.log.info(\"limit-count plugin-metadata: \", core.json.delay_encode(metadata))\n\n    local set_limit_headers = construct_rate_limiting_headers(conf, name, rule, metadata)\n    local phase = get_phase()\n    local set_header = phase ~= \"log\"\n\n    if not delay then\n        local err = remaining\n        if err == \"rejected\" then\n            -- show count limit header when rejected\n            if conf.show_limit_quota_header and set_header then\n                core.response.set_header(set_limit_headers.limit_header, lim.limit,\n                set_limit_headers.remaining_header, 0,\n                set_limit_headers.reset_header, reset)\n            end\n\n            if conf.rejected_msg then\n                return conf.rejected_code, { error_msg = conf.rejected_msg }\n            end\n            return conf.rejected_code\n        end\n\n        core.log.error(\"failed to limit count: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500, {error_msg = \"failed to limit count\"}\n    end\n\n    if conf.show_limit_quota_header and set_header then\n        core.response.set_header(set_limit_headers.limit_header, lim.limit,\n            set_limit_headers.remaining_header, remaining,\n            set_limit_headers.reset_header, reset)\n    end\nend\n\n\nfunction _M.rate_limit(conf, ctx, name, cost, dry_run)\n    core.log.info(\"ver: \", ctx.conf_version)\n\n    local rules, err = get_rules(ctx, conf)\n    if not rules or #rules == 0 then\n        core.log.error(\"failed to get rate limit rules: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    for _, rule in ipairs(rules) do\n        local code, msg = run_rate_limit(conf, rule, ctx, name, cost, dry_run)\n        if code then\n            return code, msg\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-count/limit-count-local.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal limit_count = require(\"resty.limit.count\")\n\nlocal ngx = ngx\nlocal ngx_time = ngx.time\nlocal assert = assert\nlocal setmetatable = setmetatable\nlocal core = require(\"apisix.core\")\n\nlocal _M = {}\n\nlocal mt = {\n    __index = _M\n}\n\nlocal function set_endtime(self, key, time_window)\n    -- set an end time\n    local end_time = ngx_time() + time_window\n    -- save to dict by key\n    local success, err = self.dict:set(key, end_time, time_window)\n\n    if not success then\n        core.log.error(\"dict set key \", key, \" error: \", err)\n    end\n\n    local reset = time_window\n    return reset\nend\n\nlocal function read_reset(self, key)\n    -- read from dict\n    local end_time = (self.dict:get(key) or 0)\n    local reset = end_time - ngx_time()\n    if reset < 0 then\n        reset = 0\n    end\n    return reset\nend\n\nfunction _M.new(plugin_name, limit, window)\n    assert(limit > 0 and window > 0)\n\n    local self = {\n        limit_count = limit_count.new(plugin_name, limit, window),\n        dict = ngx.shared[plugin_name .. \"-reset-header\"],\n        limit = limit,\n        window = window,\n    }\n\n    return setmetatable(self, mt)\nend\n\nfunction _M.incoming(self, key, commit, conf, cost)\n    local delay, remaining = self.limit_count:incoming(key, commit, cost)\n    local reset\n\n    if remaining == self.limit - cost then\n        reset = set_endtime(self, key, self.window)\n    else\n        reset = read_reset(self, key)\n    end\n\n    return delay, remaining, reset\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-count/limit-count-redis-cluster.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal redis_cluster = require(\"apisix.utils.rediscluster\")\nlocal core = require(\"apisix.core\")\nlocal setmetatable = setmetatable\nlocal tostring = tostring\n\nlocal _M = {}\n\n\nlocal mt = {\n    __index = _M\n}\n\n\nlocal script = core.string.compress_script([=[\n    assert(tonumber(ARGV[3]) >= 1, \"cost must be at least 1\")\n    local ttl = redis.call('ttl', KEYS[1])\n    if ttl < 0 then\n        redis.call('set', KEYS[1], ARGV[1] - ARGV[3], 'EX', ARGV[2])\n        return {ARGV[1] - ARGV[3], ARGV[2]}\n    end\n    return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl}\n]=])\n\n\nfunction _M.new(plugin_name, limit, window, conf)\n    local red_cli, err = redis_cluster.new(conf, \"plugin-limit-count-redis-cluster-slot-lock\")\n    if not red_cli then\n        return nil, err\n    end\n\n    local self = {\n        limit = limit,\n        window = window,\n        conf = conf,\n        plugin_name = plugin_name,\n        red_cli = red_cli,\n    }\n\n    return setmetatable(self, mt)\nend\n\n\nfunction _M.incoming(self, key, cost)\n    local red = self.red_cli\n    local limit = self.limit\n    local window = self.window\n    key = self.plugin_name .. tostring(key)\n\n    local ttl = 0\n    local res, err = red:eval(script, 1, key, limit, window, cost or 1)\n\n    if err then\n        return nil, err, ttl\n    end\n\n    local remaining = res[1]\n    ttl = res[2]\n\n    if remaining < 0 then\n        return nil, \"rejected\", ttl\n    end\n    return 0, remaining, ttl\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-count/limit-count-redis.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal redis     = require(\"apisix.utils.redis\")\nlocal core = require(\"apisix.core\")\nlocal assert = assert\nlocal setmetatable = setmetatable\nlocal tostring = tostring\n\n\nlocal _M = {version = 0.3}\n\n\nlocal mt = {\n    __index = _M\n}\n\n\nlocal script = core.string.compress_script([=[\n    assert(tonumber(ARGV[3]) >= 1, \"cost must be at least 1\")\n    local ttl = redis.call('ttl', KEYS[1])\n    if ttl < 0 then\n        redis.call('set', KEYS[1], ARGV[1] - ARGV[3], 'EX', ARGV[2])\n        return {ARGV[1] - ARGV[3], ARGV[2]}\n    end\n    return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl}\n]=])\n\n\nfunction _M.new(plugin_name, limit, window, conf)\n    assert(limit > 0 and window > 0)\n\n    local self = {\n        limit = limit,\n        window = window,\n        conf = conf,\n        plugin_name = plugin_name,\n    }\n    return setmetatable(self, mt)\nend\n\nfunction _M.incoming(self, key, cost)\n    local conf = self.conf\n    local red, err = redis.new(conf)\n    if not red then\n        return red, err, 0\n    end\n\n    local limit = self.limit\n    local window = self.window\n    local res\n    key = self.plugin_name .. tostring(key)\n\n    local ttl = 0\n    res, err = red:eval(script, 1, key, limit, window, cost or 1)\n\n    if err then\n        return nil, err, ttl\n    end\n\n    local remaining = res[1]\n    ttl = res[2]\n\n    local ok, err = red:set_keepalive(conf.redis_keepalive_timeout, conf.redis_keepalive_pool)\n    if not ok then\n        return nil, err, ttl\n    end\n\n    if remaining < 0 then\n        return nil, \"rejected\", ttl\n    end\n    return 0, remaining, ttl\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-count.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal fetch_secrets = require(\"apisix.secret\").fetch_secrets\nlocal limit_count = require(\"apisix.plugins.limit-count.init\")\nlocal workflow = require(\"apisix.plugins.workflow\")\n\nlocal plugin_name = \"limit-count\"\nlocal _M = {\n    version = 0.5,\n    priority = 1002,\n    name = plugin_name,\n    schema = limit_count.schema,\n    metadata_schema = limit_count.metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    return limit_count.check_schema(conf, schema_type)\nend\n\n\nfunction _M.access(conf, ctx)\n    conf = fetch_secrets(conf, true)\n    return limit_count.rate_limit(conf, ctx, plugin_name, 1)\nend\n\nfunction _M.workflow_handler()\n    workflow.register(plugin_name,\n    function (conf)\n        return limit_count.check_schema(conf)\n    end,\n    function (conf, ctx)\n        return limit_count.rate_limit(conf, ctx, plugin_name, 1)\n    end)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-req/limit-req-redis-cluster.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal redis_cluster     = require(\"apisix.utils.rediscluster\")\nlocal setmetatable      = setmetatable\nlocal util              = require(\"apisix.plugins.limit-req.util\")\n\nlocal _M = {version = 0.1}\n\n\nlocal mt = {\n    __index = _M\n}\n\n\nfunction _M.new(plugin_name, conf, rate, burst)\n    local red_cli, err = redis_cluster.new(conf, \"plugin-limit-req-redis-cluster-slot-lock\")\n    if not red_cli then\n        return nil, err\n    end\n    local self = {\n        conf = conf,\n        plugin_name = plugin_name,\n        burst = burst * 1000,\n        rate = rate * 1000,\n        red_cli = red_cli,\n    }\n    return setmetatable(self, mt)\nend\n\n\nfunction _M.incoming(self, key, commit)\n    return util.incoming(self, self.red_cli, key, commit)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-req/limit-req-redis.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal redis             = require(\"apisix.utils.redis\")\nlocal setmetatable      = setmetatable\nlocal util              = require(\"apisix.plugins.limit-req.util\")\nlocal core              = require(\"apisix.core\")\n\n\nlocal _M = {version = 0.1}\n\n\nlocal mt = {\n    __index = _M\n}\n\n\nfunction _M.new(plugin_name, conf, rate, burst)\n    local self = {\n        conf = conf,\n        plugin_name = plugin_name,\n        burst = burst * 1000,\n        rate = rate * 1000,\n    }\n    return setmetatable(self, mt)\nend\n\n\nfunction _M.incoming(self, key, commit)\n    local conf = self.conf\n    local red, err = redis.new(conf)\n    if not red then\n        return red, err\n    end\n\n    local delay, incoming_err = util.incoming(self, red, key, commit)\n    local ok, err = red:set_keepalive(conf.redis_keepalive_timeout, conf.redis_keepalive_pool)\n    if not ok then\n        core.log.error(\"set keepalive failed: \", err)\n    end\n    return delay, incoming_err\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-req/util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal math              = require \"math\"\nlocal abs               = math.abs\nlocal max               = math.max\nlocal ngx_now           = ngx.now\nlocal ngx_null          = ngx.null\nlocal tonumber          = tonumber\n\n\nlocal _M = {version = 0.1}\n\n\n-- the \"commit\" argument controls whether should we record the event in shm.\nfunction _M.incoming(self, red, key, commit)\n    local rate = self.rate\n    local now = ngx_now() * 1000\n\n    key = \"limit_req\" .. \":\" .. key\n    local excess_key = key .. \"excess\"\n    local last_key = key .. \"last\"\n\n    local excess, err = red:get(excess_key)\n    if err then\n        return nil, err\n    end\n    local last, err = red:get(last_key)\n    if err then\n        return nil, err\n    end\n\n    if excess ~= ngx_null and last ~= ngx_null then\n        excess = tonumber(excess)\n        last = tonumber(last)\n        local elapsed = now - last\n        excess = max(excess - rate * abs(elapsed) / 1000 + 1000, 0)\n\n        if excess > self.burst then\n            return nil, \"rejected\"\n        end\n    else\n        excess = 0\n    end\n\n    if commit then\n        local ttl = math.ceil(self.burst / self.rate) + 1\n        local ok, err\n\n        ok, err = red:set(excess_key, excess, \"EX\", ttl)\n        if not ok then\n            return nil, err\n        end\n\n        ok, err = red:set(last_key, now, \"EX\", ttl)\n        if not ok then\n            return nil, err\n        end\n    end\n\n    -- return the delay in seconds, as well as excess\n    return excess / rate, excess / 1000\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/limit-req.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal limit_req_new                     = require(\"resty.limit.req\").new\nlocal core                              = require(\"apisix.core\")\nlocal redis_schema                      = require(\"apisix.utils.redis-schema\")\nlocal policy_to_additional_properties   = redis_schema.schema\nlocal plugin_name                       = \"limit-req\"\nlocal sleep                             = core.sleep\nlocal apisix_plugin                     = require(\"apisix.plugin\")\nlocal error                             = error\n\nlocal redis_single_new\nlocal redis_cluster_new\ndo\n    local redis_src = \"apisix.plugins.limit-req.limit-req-redis\"\n    redis_single_new = require(redis_src).new\n\n    local cluster_src = \"apisix.plugins.limit-req.limit-req-redis-cluster\"\n    redis_cluster_new = require(cluster_src).new\nend\n\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        rate = {type = \"number\", exclusiveMinimum = 0},\n        burst = {type = \"number\",  minimum = 0},\n        key = {type = \"string\"},\n        key_type = {type = \"string\",\n            enum = {\"var\", \"var_combination\"},\n            default = \"var\",\n        },\n        policy = {\n            type = \"string\",\n            enum = {\"redis\", \"redis-cluster\", \"local\"},\n            default = \"local\",\n        },\n        rejected_code = {\n            type = \"integer\", minimum = 200, maximum = 599, default = 503\n        },\n        rejected_msg = {\n            type = \"string\", minLength = 1\n        },\n        nodelay = {\n            type = \"boolean\", default = false\n        },\n        allow_degradation = {type = \"boolean\", default = false}\n    },\n    required = {\"rate\", \"burst\", \"key\"},\n    [\"if\"] = {\n        properties = {\n            policy = {\n                enum = {\"redis\"},\n            },\n        },\n    },\n    [\"then\"] = policy_to_additional_properties.redis,\n    [\"else\"] = {\n        [\"if\"] = {\n            properties = {\n                policy = {\n                    enum = {\"redis-cluster\"},\n                },\n            },\n        },\n        [\"then\"] = policy_to_additional_properties[\"redis-cluster\"],\n    }\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 1001,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nlocal function create_limit_obj(conf)\n    if conf.policy == \"local\" then\n        core.log.info(\"create new limit-req plugin instance\")\n        return limit_req_new(\"plugin-limit-req\", conf.rate, conf.burst)\n\n    elseif conf.policy == \"redis\" then\n        core.log.info(\"create new limit-req redis plugin instance\")\n        return redis_single_new(\"plugin-limit-req\", conf, conf.rate, conf.burst)\n\n    elseif conf.policy == \"redis-cluster\" then\n        core.log.info(\"create new limit-req redis-cluster plugin instance\")\n        return redis_cluster_new(\"plugin-limit-req\", conf, conf.rate, conf.burst)\n\n    else\n        return nil, \"policy enum not match\"\n    end\nend\n\n\nlocal function gen_limit_key(conf, ctx, key)\n    local parent = conf._meta and conf._meta.parent\n    if not parent or not parent.resource_key then\n        error(\"failed to generate key invalid parent: \" .. core.json.encode(parent))\n    end\n\n    return parent.resource_key .. ':' .. apisix_plugin.conf_version(conf) .. ':' .. key\nend\n\n\nfunction _M.access(conf, ctx)\n    local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,\n                                              create_limit_obj, conf)\n    if not lim then\n        core.log.error(\"failed to instantiate a resty.limit.req object: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    local conf_key = conf.key\n    local key\n    if conf.key_type == \"var_combination\" then\n        local err, n_resolved\n        key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var)\n        if err then\n            core.log.error(\"could not resolve vars in \", conf_key, \" error: \", err)\n        end\n\n        if n_resolved == 0 then\n            key = nil\n        end\n\n    else\n        key = ctx.var[conf_key]\n    end\n\n    if key == nil then\n        core.log.info(\"The value of the configured key is empty, use client IP instead\")\n        -- When the value of key is empty, use client IP instead\n        key = ctx.var[\"remote_addr\"]\n    end\n\n    key = gen_limit_key(conf, ctx, key)\n    core.log.info(\"limit key: \", key)\n\n    local delay, err = lim:incoming(key, true)\n    if not delay then\n        if err == \"rejected\" then\n            if conf.rejected_msg then\n                return conf.rejected_code, { error_msg = conf.rejected_msg }\n            end\n            return conf.rejected_code\n        end\n\n        core.log.error(\"failed to limit req: \", err)\n        if conf.allow_degradation then\n            return\n        end\n        return 500\n    end\n\n    if delay >= 0.001 and not conf.nodelay then\n        sleep(delay)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/log-rotate.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal timers = require(\"apisix.timers\")\nlocal plugin = require(\"apisix.plugin\")\nlocal process = require(\"ngx.process\")\nlocal signal = require(\"resty.signal\")\nlocal shell = require(\"resty.shell\")\nlocal ipairs = ipairs\nlocal ngx = ngx\nlocal ngx_time = ngx.time\nlocal ngx_update_time = ngx.update_time\nlocal lfs = require(\"lfs\")\nlocal type = type\nlocal io_open = io.open\nlocal os_date = os.date\nlocal os_remove = os.remove\nlocal os_rename = os.rename\nlocal str_sub = string.sub\nlocal str_format = string.format\nlocal str_byte = string.byte\nlocal ngx_sleep = require(\"apisix.core.utils\").sleep\nlocal string_rfind = require(\"pl.stringx\").rfind\nlocal local_conf\nlocal enable_access_log\n\n\nlocal plugin_name = \"log-rotate\"\nlocal INTERVAL = 60 * 60    -- rotate interval (unit: second)\nlocal MAX_KEPT = 24 * 7     -- max number of log files will be kept\nlocal MAX_SIZE = -1         -- max size of file will be rotated\nlocal COMPRESSION_FILE_SUFFIX = \".tar.gz\" -- compression file suffix\nlocal rotate_time\nlocal default_logs\nlocal enable_compression = false\nlocal DEFAULT_ACCESS_LOG_FILENAME = \"access.log\"\nlocal DEFAULT_ERROR_LOG_FILENAME = \"error.log\"\nlocal SLASH_BYTE = str_byte(\"/\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {},\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 100,\n    name = plugin_name,\n    schema = schema,\n    scope = \"global\",\n}\n\n\nlocal function file_exists(path)\n    local file = io_open(path, \"r\")\n    if file then\n        file:close()\n    end\n    return file ~= nil\nend\n\n\nlocal function get_log_path_info(file_type)\n    local conf_path\n    if file_type == \"error.log\" then\n        conf_path = local_conf and local_conf.nginx_config and\n        local_conf.nginx_config.error_log\n    else\n        conf_path = local_conf and local_conf.nginx_config and\n        local_conf.nginx_config.http and\n        local_conf.nginx_config.http.access_log\n    end\n\n    local prefix = ngx.config.prefix()\n\n    if conf_path then\n        -- relative path\n        if str_byte(conf_path) ~= SLASH_BYTE then\n            conf_path = prefix .. conf_path\n        end\n        local n = string_rfind(conf_path, \"/\")\n        if n ~= nil and n ~= #conf_path then\n            local dir = str_sub(conf_path, 1, n)\n            local name = str_sub(conf_path, n + 1)\n            return dir, name\n        end\n    end\n\n    return prefix .. \"logs/\", file_type\nend\n\n\nlocal function tab_sort_comp(a, b)\n    return a > b\nend\n\n\nlocal function scan_log_folder(log_file_name)\n    local t = {}\n\n    local log_dir, log_name = get_log_path_info(log_file_name)\n\n    local compression_log_type = log_name .. COMPRESSION_FILE_SUFFIX\n    for file in lfs.dir(log_dir) do\n        local n = string_rfind(file, \"__\")\n        if n ~= nil then\n            local log_type = file:sub(n + 2)\n            if log_type == log_name or log_type == compression_log_type then\n                core.table.insert(t, file)\n            end\n        end\n    end\n\n    core.table.sort(t, tab_sort_comp)\n    return t, log_dir\nend\n\n\nlocal function rename_file(log, date_str)\n    local new_file\n    if not log.new_file then\n        core.log.warn(log.type, \" is off\")\n        return\n    end\n\n    new_file = str_format(log.new_file, date_str)\n    if file_exists(new_file) then\n        core.log.info(\"file exist: \", new_file)\n        return new_file\n    end\n\n    local ok, err = os_rename(log.file, new_file)\n    if not ok then\n        core.log.error(\"move file from \", log.file, \" to \", new_file,\n                       \" res:\", ok, \" msg:\", err)\n        return\n    end\n\n    return new_file\nend\n\n\nlocal function compression_file(new_file, timeout)\n    if not new_file or type(new_file) ~= \"string\" then\n        core.log.info(\"compression file: \", new_file, \" invalid\")\n        return\n    end\n\n    local n = string_rfind(new_file, \"/\")\n    local new_filepath = str_sub(new_file, 1, n)\n    local new_filename = str_sub(new_file, n + 1)\n    local com_filename = new_filename .. COMPRESSION_FILE_SUFFIX\n    local cmd = str_format(\"cd %s && tar -zcf %s %s\", new_filepath,\n            com_filename, new_filename)\n    core.log.info(\"log file compress command: \" .. cmd)\n\n    local ok, stdout, stderr, reason, status = shell.run(cmd, nil, timeout, nil)\n    if not ok then\n        core.log.error(\"compress log file from \", new_filename, \" to \", com_filename,\n                       \" fail, stdout: \", stdout, \" stderr: \", stderr, \" reason: \", reason,\n                       \" status: \", status)\n        return\n    end\n\n    ok, stderr = os_remove(new_file)\n    if stderr then\n        core.log.error(\"remove uncompressed log file: \", new_file,\n                       \" fail, err: \", stderr, \"  res:\", ok)\n    end\nend\n\n\nlocal function init_default_logs(logs_info, log_type)\n    local_conf = core.config.local_conf()\n    enable_access_log = core.table.try_read_attr(\n        local_conf, \"nginx_config\", \"http\", \"enable_access_log\")\n    local filepath, filename = get_log_path_info(log_type)\n    logs_info[log_type] = { type = log_type }\n    if filename ~= \"off\" then\n        logs_info[log_type].file = filepath .. filename\n        logs_info[log_type].new_file = filepath .. \"/%s__\" .. filename\n    end\nend\n\n\nlocal function file_size(file)\n    local attr = lfs.attributes(file)\n    if attr then\n        return attr.size\n    end\n    return 0\nend\n\n\nlocal function rotate_file(files, now_time, max_kept, timeout)\n    if core.table.isempty(files) then\n        return\n    end\n\n    local new_files = core.table.new(#files, 0)\n    -- rename the log files\n    for _, file in ipairs(files) do\n        local now_date = os_date(\"%Y-%m-%d_%H-%M-%S\", now_time)\n        local new_file = rename_file(default_logs[file], now_date)\n        if not new_file then\n            return\n        end\n\n        core.table.insert(new_files, new_file)\n    end\n\n    -- send signal to reopen log files\n    local pid = process.get_master_pid()\n    core.log.warn(\"send USR1 signal to master process [\", pid, \"] for reopening log file\")\n    local ok, err = signal.kill(pid, signal.signum(\"USR1\"))\n    if not ok then\n        core.log.error(\"failed to send USR1 signal for reopening log file: \", err)\n    end\n\n    if enable_compression then\n        -- Waiting for nginx reopen files\n        -- to avoid losing logs during compression\n        ngx_sleep(0.5)\n\n        for _, new_file in ipairs(new_files) do\n            compression_file(new_file, timeout)\n        end\n    end\n\n    for _, file in ipairs(files) do\n        -- clean the oldest file\n        local log_list, log_dir = scan_log_folder(file)\n        for i = max_kept + 1, #log_list do\n            local path = log_dir .. log_list[i]\n            local ok, err = os_remove(path)\n            if err then\n               core.log.error(\"remove old log file: \", path, \" err: \", err, \"  res:\", ok)\n            end\n        end\n    end\nend\n\n\nlocal function rotate()\n    local interval = INTERVAL\n    local max_kept = MAX_KEPT\n    local max_size = MAX_SIZE\n    local attr = plugin.plugin_attr(plugin_name)\n    local timeout = 10000 -- default timeout 10 seconds\n    if attr then\n        interval = attr.interval or interval\n        max_kept = attr.max_kept or max_kept\n        max_size = attr.max_size or max_size\n        timeout = attr.timeout or timeout\n        enable_compression = attr.enable_compression or enable_compression\n    end\n\n    core.log.info(\"rotate interval:\", interval)\n    core.log.info(\"rotate max keep:\", max_kept)\n    core.log.info(\"rotate max size:\", max_size)\n    core.log.info(\"rotate timeout:\", timeout)\n\n    if not default_logs then\n        -- first init default log filepath and filename\n        default_logs = {}\n        init_default_logs(default_logs, DEFAULT_ACCESS_LOG_FILENAME)\n        init_default_logs(default_logs, DEFAULT_ERROR_LOG_FILENAME)\n    end\n\n    ngx_update_time()\n    local now_time = ngx_time()\n    if not rotate_time then\n        -- first init rotate time\n        rotate_time = now_time + interval - (now_time % interval)\n        core.log.info(\"first init rotate time is: \", rotate_time)\n        return\n    end\n\n    if now_time >= rotate_time then\n        local files = {DEFAULT_ERROR_LOG_FILENAME}\n        if enable_access_log then\n            core.table.insert(files, DEFAULT_ACCESS_LOG_FILENAME)\n        end\n\n        rotate_file(files, now_time, max_kept, timeout)\n\n        -- reset rotate time\n        rotate_time = rotate_time + interval\n\n    elseif max_size > 0 then\n        local access_log_file_size = file_size(default_logs[DEFAULT_ACCESS_LOG_FILENAME].file)\n        local error_log_file_size = file_size(default_logs[DEFAULT_ERROR_LOG_FILENAME].file)\n        local files = {}\n\n        if enable_access_log and access_log_file_size >= max_size then\n            core.table.insert(files, DEFAULT_ACCESS_LOG_FILENAME)\n        end\n\n        if error_log_file_size >= max_size then\n            core.table.insert(files, DEFAULT_ERROR_LOG_FILENAME)\n        end\n\n        rotate_file(files, now_time, max_kept, timeout)\n    end\nend\n\n\nfunction _M.init()\n    timers.register_timer(\"plugin#log-rotate\", rotate, true)\nend\n\n\nfunction _M.destroy()\n    timers.unregister_timer(\"plugin#log-rotate\", true)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/loggly.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal path = require(\"pl.path\")\nlocal http = require(\"resty.http\")\nlocal ngx = ngx\nlocal tostring = tostring\nlocal pairs = pairs\nlocal tab_concat = table.concat\nlocal udp = ngx.socket.udp\n\nlocal plugin_name = \"loggly\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\n\n\nlocal severity = {\n    EMEGR = 0,              --  system is unusable\n    ALERT = 1,              --  action must be taken immediately\n    CRIT = 2,               --  critical conditions\n    ERR = 3,                --  error conditions\n    WARNING = 4,            --  warning conditions\n    NOTICE = 5,             --  normal but significant condition\n    INFO = 6,               --  informational\n    DEBUG = 7,              --  debug-level messages\n}\n\n\nlocal severity_enums = {}\ndo\n    for k, _ in pairs(severity) do\n        severity_enums[#severity_enums+1] = k\n        severity_enums[#severity_enums+1] = k:lower()\n    end\nend\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        customer_token = {type = \"string\"},\n        severity = {\n            type = \"string\",\n            default = \"INFO\",\n            enum = severity_enums,\n            description = \"base severity log level\",\n        },\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        tags = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                -- we prevent of having `tag=` prefix\n                pattern = \"^(?!tag=)[ -~]*\",\n            },\n            default = {\"apisix\"}\n        },\n        ssl_verify = {\n            -- applicable for https protocol\n            type = \"boolean\",\n            default = true\n        },\n        log_format = {type = \"object\"},\n        severity_map = {\n            type = \"object\",\n            description = \"upstream response code vs syslog severity mapping\",\n            patternProperties = {\n                [\"^[1-5][0-9]{2}$\"] = {\n                    description = \"keys are HTTP status code, values are severity\",\n                    type = \"string\",\n                    enum = severity_enums\n                },\n            },\n            additionalProperties = false\n        }\n    },\n    required = {\"customer_token\"}\n}\n\n\nlocal defaults = {\n    host = \"logs-01.loggly.com\",\n    port = 514,\n    protocol = \"syslog\",\n    timeout = 5000\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        host = {\n            type = \"string\",\n            default = defaults.host\n        },\n        port = {\n            type = \"integer\",\n            default = defaults.port\n        },\n        protocol = {\n            type = \"string\",\n            default = defaults.protocol,\n            -- in case of http and https, we use bulk endpoints\n            enum = {\"syslog\", \"http\", \"https\"}\n        },\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            default= defaults.timeout\n        },\n        log_format = {\n            type = \"object\",\n        }\n    }\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 411,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, err\n    end\n\n    if conf.severity_map then\n        local cache = {}\n        for k, v in pairs(conf.severity_map) do\n            cache[k] = severity[v:upper()]\n        end\n        conf._severity_cache = cache\n    end\n    return log_util.check_log_schema(conf)\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nlocal function generate_log_message(conf, ctx)\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    local json_str, err = core.json.encode(entry)\n    if not json_str then\n        core.log.error('error occurred while encoding the data: ', err)\n        return nil\n    end\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if metadata and metadata.value.protocol ~= \"syslog\" then\n        return json_str\n    end\n\n    -- generate rfc5424 compliant syslog event\n    local timestamp = log_util.get_rfc3339_zulu_timestamp()\n    local taglist = {}\n    if conf.tags then\n        for i = 1, #conf.tags do\n            core.table.insert(taglist, \"tag=\\\"\" .. conf.tags[i] .. \"\\\"\")\n        end\n    end\n\n    local message_severity = severity[conf.severity:upper()]\n    if conf._severity_cache and conf._severity_cache[tostring(ngx.status)] then\n        message_severity = conf._severity_cache[tostring(ngx.status)]\n    end\n\n    local message = {\n        -- facility LOG_USER - random user level message\n        \"<\".. tostring(8 + message_severity) .. \">1\",-- <PRIVAL>1\n        timestamp,                                                  -- timestamp\n        ctx.var.host or \"-\",                                        -- hostname\n        \"apisix\",                                                   -- appname\n        ctx.var.pid,                                                -- proc-id\n        \"-\",                                                        -- msgid\n        \"[\" .. conf.customer_token .. \"@41058 \" .. tab_concat(taglist, \" \") .. \"]\",\n        json_str\n    }\n\n    return tab_concat(message, \" \")\nend\n\n\nlocal function send_data_over_udp(message, metadata)\n    local err_msg\n    local res = true\n    local sock = udp()\n    local host, port = metadata.value.host, metadata.value.port\n    sock:settimeout(metadata.value.timeout)\n\n    local ok, err = sock:setpeername(host, port)\n\n    if not ok then\n        core.log.error(\"failed to send log: \", err)\n        return false, \"failed to connect to UDP server: host[\" .. host\n                    .. \"] port[\" .. tostring(port) .. \"] err: \" .. err\n    end\n\n    ok, err = sock:send(message)\n    if not ok then\n        res = false\n        core.log.error(\"failed to send log: \", err)\n        err_msg = \"failed to send data to UDP server: host[\" .. host\n                  .. \"] port[\" .. tostring(port) .. \"] err:\" .. err\n    end\n\n    ok, err = sock:close()\n    if not ok then\n        core.log.error(\"failed to close the UDP connection, host[\",\n                        host, \"] port[\", port, \"] \", err)\n    end\n\n    return res, err_msg\nend\n\n\nlocal function send_bulk_over_http(message, metadata, conf)\n    local endpoint = path.join(metadata.value.host, \"bulk\", conf.customer_token, \"tag\", \"bulk\")\n    local has_prefix = core.string.has_prefix(metadata.value.host, \"http\")\n    if not has_prefix then\n        if metadata.value.protocol == \"http\" then\n            endpoint = \"http://\" .. endpoint\n        else\n            endpoint = \"https://\" .. endpoint\n        end\n    end\n\n    local httpc = http.new()\n    httpc:set_timeout(metadata.value.timeout)\n    local res, err = httpc:request_uri(endpoint, {\n        ssl_verify = conf.ssl_verify,\n        method = \"POST\",\n        body = message,\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n            [\"X-LOGGLY-TAG\"] = conf.tags\n        },\n    })\n\n    if not res then\n        return false, \"failed to write log to loggly, \" .. err\n    end\n\n    if res.status ~= 200 then\n        local body = core.json.decode(res.body)\n        if not body then\n            return false, \"failed to send log to loggly, http status code: \" .. res.status\n        else\n            return false, \"failed to send log to loggly, http status code: \" .. res.status\n                          .. \" response body: \".. res.body\n        end\n    end\n\n    return true\nend\n\n\nlocal handle_http_payload\n\nlocal function handle_log(entries)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    core.log.info(\"metadata: \", core.json.delay_encode(metadata))\n\n    if not metadata then\n        core.log.info(\"received nil metadata: using metadata defaults: \",\n                            core.json.delay_encode(defaults, true))\n        metadata = {}\n        metadata.value = defaults\n    end\n    core.log.info(\"sending a batch logs to \", metadata.value.host)\n\n    if metadata.value.protocol == \"syslog\" then\n        for i = 1, #entries do\n            local ok, err = send_data_over_udp(entries[i], metadata)\n            if not ok then\n                return false, err, i\n            end\n        end\n    else\n        return handle_http_payload(entries, metadata)\n    end\n\n    return true\nend\n\n\nfunction _M.log(conf, ctx)\n    local log_data = generate_log_message(conf, ctx)\n    if not log_data then\n        return\n    end\n\n    handle_http_payload = function (entries, metadata)\n        -- loggly bulk endpoint expects entries concatenated in newline(\"\\n\")\n        local message = tab_concat(entries, \"\\n\")\n        return send_bulk_over_http(message, metadata, conf)\n    end\n\n    if batch_processor_manager:add_entry(conf, log_data) then\n        return\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, log_data, ctx, handle_log)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/loki-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal core            = require(\"apisix.core\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal http            = require(\"resty.http\")\nlocal new_tab         = require(\"table.new\")\n\nlocal pairs        = pairs\nlocal ipairs       = ipairs\nlocal tostring     = tostring\nlocal math_random  = math.random\nlocal table_insert = table.insert\nlocal ngx          = ngx\nlocal str_format   = core.string.format\n\nlocal plugin_name = \"loki-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(\"loki logger\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        -- core configurations\n        endpoint_addrs = {\n            type = \"array\",\n            minItems = 1,\n            items = core.schema.uri_def,\n        },\n        endpoint_uri = {\n            type = \"string\",\n            minLength = 1,\n            default = \"/loki/api/v1/push\"\n        },\n        tenant_id = {type = \"string\", default = \"fake\"},\n        headers = {\n            type = \"object\",\n            patternProperties = {\n                [\".*\"] = {\n                    type = \"string\",\n                    minLength = 1,\n                },\n            },\n        },\n        log_labels = {\n            type = \"object\",\n            patternProperties = {\n                [\".*\"] = {\n                    type = \"string\",\n                    minLength = 1,\n                },\n            },\n            default = {\n                job = \"apisix\",\n            },\n        },\n\n        -- connection layer configurations\n        ssl_verify = {type = \"boolean\", default = false},\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 60000,\n            default = 3000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {\n            type = \"integer\",\n            minimum = 1000,\n            default = 60000,\n            description = \"keepalive timeout in milliseconds\",\n        },\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 5},\n\n        -- logger related configurations\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    required = {\"endpoint_addrs\"}\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 414,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local check = {\"endpoint_addrs\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, err\n    end\n    return log_util.check_log_schema(conf)\nend\n\n\nlocal function send_http_data(conf, log)\n    local headers = conf.headers or {}\n    headers = core.table.clone(headers)\n    headers[\"X-Scope-OrgID\"] = conf.tenant_id\n    headers[\"Content-Type\"] = \"application/json\"\n\n    local params = {\n        headers = headers,\n        keepalive = conf.keepalive,\n        ssl_verify = conf.ssl_verify,\n        method = \"POST\",\n        body = core.json.encode(log)\n    }\n\n    if conf.keepalive then\n        params.keepalive_timeout = conf.keepalive_timeout\n        params.keepalive_pool = conf.keepalive_pool\n    end\n\n    local httpc, err = http.new()\n    if not httpc then\n        return false, str_format(\"create http client error: %s\", err)\n    end\n    httpc:set_timeout(conf.timeout)\n\n    -- select an random endpoint and build URL\n    local endpoint_url = conf.endpoint_addrs[math_random(#conf.endpoint_addrs)] .. conf.endpoint_uri\n    local res, err = httpc:request_uri(endpoint_url, params)\n    if not res then\n        return false, err\n    end\n\n    if res.status >= 300 then\n        return false, str_format(\"loki server returned status: %d, body: %s\",\n            res.status, res.body or \"\")\n    end\n\n    return true\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n\n    if not entry.route_id then\n        entry.route_id = \"no-matched\"\n    end\n\n    -- insert start time as log time, multiply to nanoseconds\n    -- use string concat to circumvent 64bit integers that LuaVM cannot handle\n    -- that is, first process the decimal part of the millisecond value\n    -- and then add 6 zeros by string concatenation\n    entry.loki_log_time = tostring(ngx.req.start_time() * 1000) .. \"000000\"\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    local labels = conf.log_labels\n\n    -- parsing possible variables in label value\n    for key, value in pairs(labels) do\n        local new_val, err, n_resolved = core.utils.resolve_var(value, ctx.var)\n        if not err and n_resolved > 0 then\n            labels[key] = new_val\n        end\n    end\n\n    -- generate a function to be executed by the batch processor\n    local func = function(entries)\n        -- build loki request data\n        local data = {\n            streams = {\n                {\n                    stream = labels,\n                    values = new_tab(1, 0),\n                }\n            }\n        }\n\n        -- add all entries to the batch\n        for _, entry in ipairs(entries) do\n            local log_time = entry.loki_log_time\n            entry.loki_log_time = nil -- clean logger internal field\n\n            table_insert(data.streams[1].values, {\n                log_time, core.json.encode(entry)\n            })\n        end\n\n        return send_http_data(conf, data)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mcp/broker/shared_dict.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal type           = type\nlocal setmetatable   = setmetatable\nlocal ngx            = ngx\nlocal ngx_sleep      = ngx.sleep\nlocal thread_spawn   = ngx.thread.spawn\nlocal thread_kill    = ngx.thread.kill\nlocal worker_exiting = ngx.worker.exiting\nlocal shared_dict    = ngx.shared[\"mcp-session\"] -- TODO: rename to something like mcp-broker\nlocal core           = require(\"apisix.core\")\nlocal broker_utils   = require(\"apisix.plugins.mcp.broker.utils\")\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\nlocal STORAGE_SUFFIX_QUEUE = \":queue\"\n\n\nfunction _M.new(opts)\n    return setmetatable({\n        session_id = opts.session_id,\n        event_handler = {}\n    }, mt)\nend\n\n\nfunction _M.on(self, event, cb)\n    self.event_handler[event] = cb\nend\n\n\nfunction _M.push(self, message)\n    if not message then\n        return nil, \"message is nil\"\n    end\n    local ok, err = shared_dict:rpush(self.session_id .. STORAGE_SUFFIX_QUEUE, message)\n    if not ok then\n        return nil, \"failed to push message to queue: \" .. err\n    end\n    return true\nend\n\n\nfunction _M.start(self)\n    self.thread = thread_spawn(function()\n        while not worker_exiting() do\n            local item, err = shared_dict:lpop(self.session_id .. STORAGE_SUFFIX_QUEUE)\n            if err then\n                core.log.info(\"session \", self.session_id,\n                              \" exit, failed to pop message from queue: \", err)\n                break\n            end\n            if item and type(item) == \"string\"\n                and type(self.event_handler[broker_utils.EVENT_MESSAGE]) == \"function\" then\n                self.event_handler[broker_utils.EVENT_MESSAGE](\n                    core.json.decode(item), { raw = item }\n                )\n            end\n\n            ngx_sleep(0.1) -- yield to other light threads\n        end\n    end)\nend\n\n\nfunction _M.close(self)\n    if self.thread then\n        thread_kill(self.thread)\n        self.thread = nil\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mcp/broker/utils.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal _M = {}\n\n_M.EVENT_MESSAGE = \"message\"\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mcp/server.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require        = require\nlocal setmetatable   = setmetatable\nlocal ngx            = ngx\nlocal ngx_sleep      = ngx.sleep\nlocal thread_spwan   = ngx.thread.spawn\nlocal thread_wait    = ngx.thread.wait\nlocal thread_kill    = ngx.thread.kill\nlocal worker_exiting = ngx.worker.exiting\nlocal core           = require(\"apisix.core\")\nlocal broker_utils   = require(\"apisix.plugins.mcp.broker.utils\")\n\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\n_M.EVENT_CLIENT_MESSAGE = \"event:client_message\"\n\n\n-- TODO: ping requester and handler\nfunction _M.new(opts)\n    local session_id = opts.session_id or core.id.gen_uuid_v4()\n\n    -- TODO: configurable broker type\n    local message_broker = require(\"apisix.plugins.mcp.broker.shared_dict\").new({\n        session_id = session_id,\n    })\n\n    -- TODO: configurable transport type\n    local transport = require(\"apisix.plugins.mcp.transport.sse\").new()\n\n    local obj = setmetatable({\n        opts = opts,\n        session_id = session_id,\n        next_ping_id = 0,\n        transport = transport,\n        message_broker = message_broker,\n        event_handler = {},\n        need_exit = false,\n    }, mt)\n\n    message_broker:on(broker_utils.EVENT_MESSAGE, function (message, additional)\n        if obj.event_handler[_M.EVENT_CLIENT_MESSAGE] then\n            obj.event_handler[_M.EVENT_CLIENT_MESSAGE](message, additional)\n        end\n    end)\n\n    return obj\nend\n\n\nfunction _M.on(self, event, cb)\n    self.event_handler[event] = cb\nend\n\n\nfunction _M.start(self)\n    self.message_broker:start()\n\n    -- ping loop\n    local ping = thread_spwan(function()\n        while not worker_exiting() do\n            if self.need_exit then\n                break\n            end\n\n            self.next_ping_id = self.next_ping_id + 1\n            local ok, err = self.transport:send(\n                '{\"jsonrpc\": \"2.0\",\"method\": \"ping\",\"id\":\"ping:' .. self.next_ping_id .. '\"}')\n            if not ok then\n                core.log.info(\"session \", self.session_id,\n                               \" exit, failed to send ping message: \", err)\n                self.need_exit = true\n                break\n            end\n            ngx_sleep(30)\n        end\n    end)\n    thread_wait(ping)\n    thread_kill(ping)\nend\n\n\nfunction _M.close(self)\n    if self.message_broker then\n        self.message_broker:close()\n    end\nend\n\n\nfunction _M.push_message(self, message)\n    local ok, err = self.message_broker:push(message)\n    if not ok then\n        return nil, \"failed to push message to broker: \" .. err\n    end\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mcp/server_wrapper.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx          = ngx\nlocal ngx_exit     = ngx.exit\nlocal re_match     = ngx.re.match\nlocal core         = require(\"apisix.core\")\nlocal mcp_server   = require(\"apisix.plugins.mcp.server\")\n\nlocal _M = {}\n\nlocal V241105_ENDPOINT_SSE     = \"sse\"\nlocal V241105_ENDPOINT_MESSAGE = \"message\"\n\n\nlocal function sse_handler(conf, ctx, opts)\n    -- send SSE headers and first chunk\n    core.response.set_header(\"Content-Type\", \"text/event-stream\")\n    core.response.set_header(\"Cache-Control\", \"no-cache\")\n\n    local server = opts.server\n\n    -- send endpoint event to advertise the message endpoint\n    server.transport:send(conf.base_uri .. \"/message?sessionId=\" .. server.session_id, \"endpoint\")\n\n    if opts.event_handler and opts.event_handler.on_client_message then\n        server:on(mcp_server.EVENT_CLIENT_MESSAGE, function(message, additional)\n            additional.server = server\n            opts.event_handler.on_client_message(message, additional)\n        end)\n    end\n\n    if opts.event_handler and opts.event_handler.on_connect then\n        local code, body = opts.event_handler.on_connect({ server = server })\n        if code then\n            return code, body\n        end\n        server:start() -- this is a sync call that only returns when the client disconnects\n    end\n\n    if opts.event_handler.on_disconnect then\n        opts.event_handler.on_disconnect({ server = server })\n        server:close()\n    end\n\n    ngx_exit(0) -- exit current phase, skip the upstream module\nend\n\n\nlocal function message_handler(conf, ctx, opts)\n    local body = core.request.get_body(nil, ctx)\n    if not body then\n        return 400\n    end\n\n    local ok, err = opts.server:push_message(body)\n    if not ok then\n        core.log.error(\"failed to add task to queue: \", err)\n        return 500\n    end\n\n    return 202\nend\n\n\nfunction _M.access(conf, ctx, opts)\n    local m, err = re_match(ctx.var.uri, \"^\" .. conf.base_uri .. \"/(.*)\", \"jo\")\n    if err then\n        core.log.info(\"failed to mcp base uri: \", err)\n        return core.response.exit(404)\n    end\n    local action = m and m[1] or false\n    if not action then\n        return core.response.exit(404)\n    end\n\n    if action == V241105_ENDPOINT_SSE and core.request.get_method() == \"GET\" then\n        opts.server = mcp_server.new({})\n        return sse_handler(conf, ctx, opts)\n    end\n\n    if action == V241105_ENDPOINT_MESSAGE and core.request.get_method() == \"POST\" then\n        -- TODO: check ctx.var.arg_sessionId\n        -- recover server instead of create\n        opts.server = mcp_server.new({ session_id = ctx.var.arg_sessionId })\n        return core.response.exit(message_handler(conf, ctx, opts))\n    end\n\n    return core.response.exit(404)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mcp/transport/sse.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal setmetatable = setmetatable\nlocal type         = type\nlocal ngx          = ngx\nlocal ngx_print    = ngx.print\nlocal ngx_flush    = ngx.flush\nlocal core         = require(\"apisix.core\")\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\nfunction _M.new()\n    return setmetatable({}, mt)\nend\n\n\nfunction _M.send(self, message, event_type)\n    local data = type(message) == \"table\" and core.json.encode(message) or message\n    local ok, err = ngx_print(\"event: \" .. (event_type or \"message\") ..\n                                \"\\ndata: \" .. data .. \"\\n\\n\")\n    if not ok then\n        return ok, \"failed to write buffer: \" .. err\n    end\n    return ngx_flush(true)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mcp-bridge.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal unpack         = unpack\nlocal ngx            = ngx\nlocal thread_spawn   = ngx.thread.spawn\nlocal thread_kill    = ngx.thread.kill\nlocal worker_exiting = ngx.worker.exiting\nlocal resty_signal   = require(\"resty.signal\")\nlocal core           = require(\"apisix.core\")\nlocal pipe           = require(\"ngx.pipe\")\n\nlocal mcp_server_wrapper  = require(\"apisix.plugins.mcp.server_wrapper\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        base_uri = {\n            type = \"string\",\n            minLength = 1,\n            default = \"\",\n        },\n        command = {\n            type = \"string\",\n            minLength = 1,\n        },\n        args = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n            },\n            minItems = 0,\n        },\n    },\n    required = {\n        \"command\"\n    },\n}\n\nlocal plugin_name = \"mcp-bridge\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 510,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function on_connect(conf, ctx)\n    return function(additional)\n        local proc, err = pipe.spawn({conf.command, unpack(conf.args or {})})\n        if not proc then\n            core.log.error(\"failed to spawn mcp process: \", err)\n            return 500\n        end\n        proc:set_timeouts(nil, 100, 100)\n        ctx.mcp_bridge_proc = proc\n\n        local server = additional.server\n\n        -- ngx_pipe is a yield operation, so we no longer need\n        -- to explicitly yield to other threads by ngx_sleep\n        ctx.mcp_bridge_proc_event_loop = thread_spawn(function ()\n            local stdout_partial, stderr_partial, need_exit\n            while not worker_exiting() do\n                -- read all the messages in stdout's pipe, line by line\n                -- if there is an incomplete message it is buffered and\n                -- spliced before the next message\n                repeat\n                    local line, _\n                    line, _, stdout_partial = proc:stdout_read_line()\n                    if line then\n                        local ok, err = server.transport:send(\n                            stdout_partial and stdout_partial .. line or line\n                        )\n                        if not ok then\n                            core.log.info(\"session \", server.session_id,\n                                          \" exit, failed to send response message: \", err)\n                            need_exit = true\n                            break\n                        end\n                        stdout_partial = nil -- luacheck: ignore\n                    end\n                until not line\n                if need_exit then\n                    break\n                end\n\n                repeat\n                    local line, _\n                    line, _, stderr_partial = proc:stderr_read_line()\n                    if line then\n                        local ok, err = server.transport:send(\n                           '{\"jsonrpc\":\"2.0\",\"method\":\"notifications/stderr\",\"params\":{\"content\":\"'\n                            .. (stderr_partial and stderr_partial .. line or line) .. '\"}}')\n                        if not ok then\n                            core.log.info(\"session \", server.session_id,\n                                          \" exit, failed to send response message: \", err)\n                            need_exit = true\n                            break\n                        end\n                        stderr_partial = \"\" -- luacheck: ignore\n                    end\n                until not line\n                if need_exit then\n                    break\n                end\n            end\n        end)\n    end\nend\n\n\nlocal function on_client_message(conf, ctx)\n    return function(message, additional)\n        core.log.info(\"session \", additional.server.session_id,\n                      \" send message to mcp server: \", additional.raw)\n        ctx.mcp_bridge_proc:write(additional.raw .. \"\\n\")\n    end\nend\n\n\nlocal function on_disconnect(conf, ctx)\n    return function()\n        if ctx.mcp_bridge_proc_event_loop then\n            thread_kill(ctx.mcp_bridge_proc_event_loop)\n            ctx.mcp_bridge_proc_event_loop = nil\n        end\n\n        local proc = ctx.mcp_bridge_proc\n        if proc then\n            proc:shutdown(\"stdin\")\n            proc:wait()\n            local _, err = proc:wait() -- check if process not exited then kill it\n            if err ~= \"exited\" then\n                proc:kill(resty_signal.signum(\"KILL\") or 9)\n            end\n        end\n    end\nend\n\n\nfunction _M.access(conf, ctx)\n    return mcp_server_wrapper.access(conf, ctx, {\n        event_handler = {\n            on_connect = on_connect(conf, ctx),\n            on_client_message = on_client_message(conf, ctx),\n            on_disconnect = on_disconnect(conf, ctx),\n        },\n    })\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/mocking.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal xml2lua = require(\"xml2lua\")\n\nlocal json = core.json\nlocal math = math\nlocal ngx = ngx\nlocal ngx_re = ngx.re\nlocal pairs = pairs\nlocal string = string\nlocal table = table\nlocal type = type\n\nlocal support_content_type = {\n    [\"application/xml\"] = true,\n    [\"application/json\"] = true,\n    [\"text/plain\"] = true,\n    [\"text/html\"] = true,\n    [\"text/xml\"] = true\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        -- specify response delay time,default 0ms\n        delay = { type = \"integer\", default = 0 },\n        -- specify response status,default 200\n        response_status = { type = \"integer\", default = 200, minimum = 100 },\n        -- specify response content type, support application/xml, text/plain\n        -- and application/json, default application/json\n        content_type = { type = \"string\", default = \"application/json;charset=utf8\" },\n        -- specify response body.\n        response_example = { type = \"string\" },\n        -- specify response json schema, if response_example is not nil, this conf will be ignore.\n        -- generate random response by json schema.\n        response_schema = { type = \"object\" },\n        with_mock_header = { type = \"boolean\", default = true },\n        response_headers = {\n            type = \"object\",\n            minProperties = 1,\n            patternProperties = {\n                [\"^[^:]+$\"] = {\n                    oneOf = {\n                        { type = \"string\" },\n                        { type = \"number\" }\n                    }\n                }\n            },\n        }\n    },\n    anyOf = {\n        { required = { \"response_example\" } },\n        { required = { \"response_schema\" } }\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 10900,\n    name = \"mocking\",\n    schema = schema,\n}\n\nlocal function parse_content_type(content_type)\n    if not content_type then\n        return \"\"\n    end\n    local m = ngx_re.match(content_type, \"([ -~]*);([ -~]*)\", \"jo\")\n    if m and #m == 2 then\n        return m[1], m[2]\n    end\n    return content_type\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    local typ = parse_content_type(conf.content_type)\n    if not support_content_type[typ] then\n        return false, \"unsupported content type!\"\n    end\n    return true\nend\n\n\nlocal function gen_string(example)\n    if example and type(example) == \"string\" then\n        return example\n    end\n    local n = math.random(1, 10)\n    local list = {}\n    for i = 1, n do\n        table.insert(list, string.char(math.random(97, 122)))\n    end\n    return table.concat(list)\nend\n\n\nlocal function gen_number(example)\n    if example and type(example) == \"number\" then\n        return example\n    end\n    return math.random() * 10000\nend\n\n\nlocal function gen_integer(example)\n    if example and type(example) == \"number\" then\n        return math.floor(example)\n    end\n    return math.random(1, 10000)\nend\n\n\nlocal function gen_boolean(example)\n    if example and type(example) == \"boolean\" then\n        return example\n    end\n    local r = math.random(0, 1)\n    if r == 0 then\n        return false\n    end\n    return true\nend\n\n\nlocal gen_array, gen_object, gen_by_property\n\nfunction gen_array(property)\n    local output = {}\n    if property.items == nil then\n        return nil\n    end\n    local v = property.items\n    local n = math.random(1, 3)\n    for i = 1, n do\n        table.insert(output, gen_by_property(v))\n    end\n    return output\nend\n\n\nfunction gen_object(property)\n    local output = {}\n    if not property.properties then\n        return output\n    end\n    for k, v in pairs(property.properties) do\n        output[k] = gen_by_property(v)\n    end\n    return output\nend\n\n\nfunction gen_by_property(property)\n    local typ = string.lower(property.type)\n    local example = property.example\n\n    if typ == \"array\" then\n        return gen_array(property)\n    end\n\n    if typ == \"object\" then\n        return gen_object(property)\n    end\n\n    if typ == \"string\" then\n        return gen_string(example)\n    end\n\n    if typ == \"number\" then\n        return gen_number(example)\n    end\n\n    if typ == \"integer\" then\n        return gen_integer(example)\n    end\n\n    if typ == \"boolean\" then\n        return gen_boolean(example)\n    end\n\n    return nil\nend\n\n\nfunction _M.access(conf, ctx)\n    local response_content = \"\"\n\n    if conf.response_example then\n        response_content = conf.response_example\n    else\n        local output = gen_object(conf.response_schema)\n        local typ = parse_content_type(conf.content_type)\n        if typ == \"application/xml\" or typ == \"text/xml\" then\n            response_content = xml2lua.toXml(output, \"data\")\n\n        elseif typ == \"application/json\" or typ == \"text/plain\" then\n            response_content = json.encode(output)\n\n        else\n            core.log.error(\"json schema body only support xml and json content type\")\n        end\n    end\n\n    ngx.header[\"Content-Type\"] = conf.content_type\n    if conf.with_mock_header then\n        ngx.header[\"x-mock-by\"] = \"APISIX/\" .. core.version.VERSION\n    end\n\n    if conf.response_headers then\n        for key, value in pairs(conf.response_headers) do\n            value = core.utils.resolve_var(value, ctx.var)\n            core.response.add_header(key, value)\n        end\n    end\n\n    if conf.delay > 0 then\n        ngx.sleep(conf.delay)\n    end\n    return conf.response_status, core.utils.resolve_var(response_content, ctx.var)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/multi-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal require = require\nlocal pairs = pairs\nlocal type = type\nlocal plugin = require(\"apisix.plugin\")\n\nlocal schema = {\n    type = \"object\",\n    title = \"work with route or service object\",\n    properties = {\n        auth_plugins = { type = \"array\", minItems = 2 }\n    },\n    required = { \"auth_plugins\" },\n}\n\n\nlocal plugin_name = \"multi-auth\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 2600,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema\n}\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    local auth_plugins = conf.auth_plugins\n    for k, auth_plugin in pairs(auth_plugins) do\n        for auth_plugin_name, auth_plugin_conf in pairs(auth_plugin) do\n            local auth = plugin.get(auth_plugin_name)\n            if auth == nil then\n                return false, auth_plugin_name .. \" plugin did not found\"\n            else\n                if auth.type ~= 'auth' then\n                    return false, auth_plugin_name .. \" plugin is not supported\"\n                end\n                local ok, err = auth.check_schema(auth_plugin_conf, auth.schema)\n                if not ok then\n                    return false, \"plugin \" .. auth_plugin_name .. \" check schema failed: \" .. err\n                end\n            end\n        end\n    end\n\n    return true\nend\n\nfunction _M.rewrite(conf, ctx)\n    local auth_plugins = conf.auth_plugins\n    local status_code\n    local errors = {}\n\n    for k, auth_plugin in pairs(auth_plugins) do\n        for auth_plugin_name, auth_plugin_conf in pairs(auth_plugin) do\n            local auth = plugin.get(auth_plugin_name)\n            -- returns 401 HTTP status code if authentication failed, otherwise returns nothing.\n            local auth_code, err = auth.rewrite(auth_plugin_conf, ctx)\n            if type(err) == \"table\" then\n                err = err.message  -- compat\n            end\n\n            status_code = auth_code\n            if auth_code == nil then\n                core.log.debug(auth_plugin_name .. \" succeed to authenticate the request\")\n                goto authenticated\n            else\n                core.table.insert(errors, auth_plugin_name ..\n                        \" failed to authenticate the request, code: \"\n                        .. auth_code .. \". error: \" .. err)\n            end\n        end\n    end\n\n    :: authenticated ::\n    if status_code ~= nil then\n        for _, error in pairs(errors) do\n            core.log.warn(error)\n        end\n        return 401, { message = \"Authorization Failed\" }\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/node-status.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx = ngx\nlocal re_gmatch = ngx.re.gmatch\nlocal ngx_capture = ngx.location.capture\nlocal plugin_name = \"node-status\"\nlocal apisix_id = core.id.get()\nlocal ipairs = ipairs\n\n\nlocal schema = {\n    type = \"object\",\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 1000,\n    name = plugin_name,\n    schema = schema,\n    scope = \"global\",\n}\n\n\nlocal ngx_status = {}\nlocal ngx_status_items = {\n    \"active\", \"accepted\", \"handled\", \"total\",\n    \"reading\", \"writing\", \"waiting\"\n}\n\n\nlocal function collect()\n    local res = ngx_capture(\"/apisix/nginx_status\")\n    if res.status ~= 200 then\n        return res.status\n    end\n\n    -- Active connections: 2\n    -- server accepts handled requests\n    --   26 26 84\n    -- Reading: 0 Writing: 1 Waiting: 1\n\n    local iterator, err = re_gmatch(res.body, [[(\\d+)]], \"jmo\")\n    if not iterator then\n        return 500, \"failed to re.gmatch Nginx status: \" .. err\n    end\n\n    core.table.clear(ngx_status)\n    for _, name in ipairs(ngx_status_items) do\n        local val = iterator()\n        if not val then\n            break\n        end\n\n        ngx_status[name] = val[0]\n    end\n\n    return 200, core.json.encode({id = apisix_id, status = ngx_status})\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.api()\n    return {\n        {\n            methods = {\"GET\"},\n            uri = \"/apisix/status\",\n            handler = collect,\n        }\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ocsp-stapling.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one\n-- or more contributor license agreements.  See the NOTICE file\n-- distributed with this work for additional information\n-- regarding copyright ownership.  The ASF licenses this file\n-- to you under the Apache License, Version 2.0 (the\n-- \"License\"); you may not use this file except in compliance\n-- with the License.  You may obtain a copy of the License at\n--\n--   http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing,\n-- software distributed under the License is distributed on an\n-- \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n-- KIND, either express or implied.  See the License for the\n-- specific language governing permissions and limitations\n-- under the License.\n--\n\nlocal require = require\nlocal http = require(\"resty.http\")\nlocal ngx = ngx\nlocal ngx_ocsp = require(\"ngx.ocsp\")\nlocal ngx_ssl = require(\"ngx.ssl\")\nlocal radixtree_sni = require(\"apisix.ssl.router.radixtree_sni\")\nlocal core = require(\"apisix.core\")\n\nlocal plugin_name = \"ocsp-stapling\"\nlocal ocsp_resp_cache = ngx.shared[plugin_name]\n\nlocal plugin_schema = {\n    type = \"object\",\n    properties = {},\n}\n\nlocal _M = {\n    name = plugin_name,\n    schema = plugin_schema,\n    version = 0.1,\n    priority = -44,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(plugin_schema, conf)\nend\n\n\nlocal function fetch_ocsp_resp(der_cert_chain)\n    core.log.info(\"fetch ocsp response from remote\")\n    local ocsp_url, err = ngx_ocsp.get_ocsp_responder_from_der_chain(der_cert_chain)\n\n    if not ocsp_url then\n        -- if cert not support ocsp, the report error is nil\n        if not err then\n            err = \"cert not contains authority_information_access extension\"\n        end\n        return nil, \"failed to get ocsp url: \" .. err\n    end\n\n    local ocsp_req, err = ngx_ocsp.create_ocsp_request(der_cert_chain)\n    if not ocsp_req then\n        return nil, \"failed to create ocsp request: \" .. err\n    end\n\n    local httpc = http.new()\n    local res, err = httpc:request_uri(ocsp_url, {\n        method = \"POST\",\n        headers = {\n            [\"Content-Type\"] = \"application/ocsp-request\",\n        },\n        body = ocsp_req\n    })\n\n    if not res then\n        return nil, \"ocsp responder query failed: \" .. err\n    end\n\n    local http_status = res.status\n    if http_status ~= 200 then\n        return nil, \"ocsp responder returns bad http status code: \"\n               .. http_status\n    end\n\n    if res.body and #res.body > 0 then\n        return res.body, nil\n    end\n\n    return nil, \"ocsp responder returns empty body\"\nend\n\n\nlocal function set_ocsp_resp(full_chain_pem_cert, skip_verify, cache_ttl)\n    local der_cert_chain, err = ngx_ssl.cert_pem_to_der(full_chain_pem_cert)\n    if not der_cert_chain then\n        return false, \"failed to convert certificate chain from PEM to DER: \", err\n    end\n\n    local ocsp_resp = ocsp_resp_cache:get(der_cert_chain)\n    if ocsp_resp == nil then\n        core.log.info(\"not ocsp resp cache found, fetch from ocsp responder\")\n        ocsp_resp, err = fetch_ocsp_resp(der_cert_chain)\n        if ocsp_resp == nil then\n            return false, err\n        end\n        core.log.info(\"fetch ocsp resp ok, cache it\")\n        ocsp_resp_cache:set(der_cert_chain, ocsp_resp, cache_ttl)\n    end\n\n    if not skip_verify then\n        local ok, err = ngx_ocsp.validate_ocsp_response(ocsp_resp, der_cert_chain)\n        if not ok then\n            return false, \"failed to validate ocsp response: \" .. err\n        end\n    end\n\n    -- set the OCSP stapling\n    local ok, err = ngx_ocsp.set_ocsp_status_resp(ocsp_resp)\n    if not ok then\n        return false, \"failed to set ocsp status response: \" .. err\n    end\n\n    return true\nend\n\n\nlocal original_set_cert_and_key\nlocal function set_cert_and_key(sni, value)\n    if value.gm then\n        -- should not run with gm plugin\n        core.log.warn(\"gm plugin enabled, no need to run ocsp-stapling plugin\")\n        return original_set_cert_and_key(sni, value)\n    end\n\n    if not value.ocsp_stapling then\n        core.log.info(\"no 'ocsp_stapling' field found, no need to run ocsp-stapling plugin\")\n        return original_set_cert_and_key(sni, value)\n    end\n\n    if not value.ocsp_stapling.enabled then\n        return original_set_cert_and_key(sni, value)\n    end\n\n    if not ngx.ctx.tls_ext_status_req then\n        core.log.info(\"no status request required, no need to send ocsp response\")\n        return original_set_cert_and_key(sni, value)\n    end\n\n    local ok, err = radixtree_sni.set_pem_ssl_key(sni, value.cert, value.key)\n    if not ok then\n        return false, err\n    end\n    local fin_pem_cert = value.cert\n\n    -- multiple certificates support.\n    if value.certs then\n        for i = 1, #value.certs do\n            local cert = value.certs[i]\n            local key = value.keys[i]\n            ok, err = radixtree_sni.set_pem_ssl_key(sni, cert, key)\n            if not ok then\n                return false, err\n            end\n            fin_pem_cert = cert\n        end\n    end\n\n    local ok, err = set_ocsp_resp(fin_pem_cert,\n                                  value.ocsp_stapling.skip_verify,\n                                  value.ocsp_stapling.cache_ttl)\n    if not ok then\n        core.log.error(\"no ocsp response send: \", err)\n    end\n\n    return true\nend\n\n\nfunction _M.init()\n    if core.schema.ssl.properties.gm ~= nil then\n        core.log.error(\"ocsp-stapling plugin should not run with gm plugin\")\n    end\n\n    original_set_cert_and_key = radixtree_sni.set_cert_and_key\n    radixtree_sni.set_cert_and_key = set_cert_and_key\n\n    if core.schema.ssl.properties.ocsp_stapling ~= nil then\n        core.log.error(\"Field 'ocsp_stapling' is occupied\")\n    end\n\n    core.schema.ssl.properties.ocsp_stapling = {\n        type = \"object\",\n        properties = {\n            enabled = {\n                type = \"boolean\",\n                default = false,\n            },\n            skip_verify = {\n                type = \"boolean\",\n                default = false,\n            },\n            cache_ttl = {\n                type = \"integer\",\n                minimum = 60,\n                default = 3600,\n            },\n        }\n    }\n\nend\n\n\nfunction _M.destroy()\n    radixtree_sni.set_cert_and_key = original_set_cert_and_key\n    core.schema.ssl.properties.ocsp_stapling = nil\n    ocsp_resp_cache:flush_all()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/opa/helper.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core        = require(\"apisix.core\")\nlocal get_service = require(\"apisix.http.service\").get\nlocal ngx_time    = ngx.time\n\nlocal _M = {}\n\n\n-- build a table of Nginx variables with some generality\n-- between http subsystem and stream subsystem\nlocal function build_var(conf, ctx)\n    return {\n        server_addr = ctx.var.server_addr,\n        server_port = ctx.var.server_port,\n        remote_addr = ctx.var.remote_addr,\n        remote_port = ctx.var.remote_port,\n        timestamp   = ngx_time(),\n    }\nend\n\n\nlocal function build_http_request(conf, ctx)\n    return {\n        scheme  = core.request.get_scheme(ctx),\n        method  = core.request.get_method(),\n        host    = core.request.get_host(ctx),\n        port    = core.request.get_port(ctx),\n        path    = ctx.var.uri,\n        headers = core.request.headers(ctx),\n        query   = core.request.get_uri_args(ctx),\n    }\nend\n\n\nlocal function build_http_route(conf, ctx, remove_upstream)\n    local route = core.table.deepcopy(ctx.matched_route).value\n\n    if remove_upstream and route and route.upstream then\n        -- unimportant to send upstream info to OPA\n        route.upstream = nil\n    end\n\n    return route\nend\n\n\nlocal function build_http_service(conf, ctx)\n    local service_id = ctx.service_id\n\n    -- possible that there is no service bound to the route\n    if service_id then\n        local service = core.table.clone(get_service(service_id)).value\n\n        if service then\n            if service.upstream then\n                service.upstream = nil\n            end\n            return service\n        end\n    end\n\n    return nil\nend\n\n\nlocal function build_http_consumer(conf, ctx)\n    -- possible that there is no consumer bound to the route\n    if ctx.consumer then\n        return core.table.clone(ctx.consumer)\n    end\n\n    return nil\nend\n\n\nfunction _M.build_opa_input(conf, ctx, subsystem)\n    local data = {\n        type    = subsystem,\n        request = build_http_request(conf, ctx),\n        var     = build_var(conf, ctx)\n    }\n\n    if conf.with_route then\n        data.route = build_http_route(conf, ctx, true)\n    end\n\n    if conf.with_consumer then\n        data.consumer = build_http_consumer(conf, ctx)\n    end\n\n    if conf.with_service then\n        data.service = build_http_service(conf, ctx)\n    end\n\n    return {\n        input = data,\n    }\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/opa.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core   = require(\"apisix.core\")\nlocal http   = require(\"resty.http\")\nlocal helper = require(\"apisix.plugins.opa.helper\")\nlocal type   = type\nlocal ipairs = ipairs\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        host = {type = \"string\"},\n        ssl_verify = {\n            type = \"boolean\",\n            default = true,\n        },\n        policy = {type = \"string\"},\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 60000,\n            default = 3000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        send_headers_upstream = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\"\n            },\n            description = \"list of headers to pass to upstream in request\"\n        },\n        keepalive_timeout = {type = \"integer\", minimum = 1000, default = 60000},\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 5},\n        with_route = {type = \"boolean\", default = false},\n        with_service = {type = \"boolean\", default = false},\n        with_consumer = {type = \"boolean\", default = false},\n    },\n    required = {\"host\", \"policy\"}\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2001,\n    name = \"opa\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local check = {\"host\"}\n    core.utils.check_https(check, conf, _M.name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, _M.name)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    local body = helper.build_opa_input(conf, ctx, \"http\")\n\n    local params = {\n        method = \"POST\",\n        body = core.json.encode(body),\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n        },\n        keepalive = conf.keepalive,\n        ssl_verify = conf.ssl_verify\n    }\n\n    if conf.keepalive then\n        params.keepalive_timeout = conf.keepalive_timeout\n        params.keepalive_pool = conf.keepalive_pool\n    end\n\n    local endpoint = conf.host .. \"/v1/data/\" .. conf.policy\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout)\n\n    local res, err = httpc:request_uri(endpoint, params)\n\n    -- block by default when decision is unavailable\n    if not res then\n        core.log.error(\"failed to process OPA decision, err: \", err)\n        return 403\n    end\n\n    -- parse the results of the decision\n    local data, err = core.json.decode(res.body)\n\n    if not data then\n        core.log.error(\"invalid response body: \", res.body, \" err: \", err)\n        return 503\n    end\n\n    if not data.result then\n        core.log.error(\"invalid OPA decision format: \", res.body,\n                       \" err: `result` field does not exist\")\n        return 503\n    end\n\n    local result = data.result\n\n    if not result.allow then\n        if result.headers then\n            core.response.set_header(result.headers)\n        end\n\n        local status_code = 403\n        if result.status_code then\n            status_code = result.status_code\n        end\n\n        local reason = nil\n        if result.reason then\n            reason = type(result.reason) == \"table\"\n                and core.json.encode(result.reason)\n                or result.reason\n        end\n\n        return status_code, reason\n    else if result.headers and conf.send_headers_upstream then\n        for _, name in ipairs(conf.send_headers_upstream) do\n            local value = result.headers[name]\n            if value then\n                core.request.set_header(ctx, name, value)\n            end\n        end\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/openfunction.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ngx_encode_base64 = ngx.encode_base64\nlocal plugin_name, plugin_version, priority = \"openfunction\", 0.1, -1902\n\nlocal openfunction_authz_schema = {\n    service_token = {type = \"string\"}\n}\n\nlocal function request_processor(conf, ctx, params)\n    local headers = params.headers or {}\n    -- setting authorization headers if authorization.service_token exists\n    if  conf.authorization and conf.authorization.service_token then\n        headers[\"authorization\"] = \"Basic \" .. ngx_encode_base64(conf.authorization.service_token)\n    end\n\n    params.headers = headers\nend\n\nreturn require(\"apisix.plugins.serverless.generic-upstream\")(plugin_name,\n        plugin_version, priority, request_processor, openfunction_authz_schema)\n"
  },
  {
    "path": "apisix/plugins/openid-connect.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core              = require(\"apisix.core\")\nlocal ngx_re            = require(\"ngx.re\")\nlocal openidc           = require(\"resty.openidc\")\nlocal fetch_secrets     = require(\"apisix.secret\").fetch_secrets\nlocal jsonschema        = require('jsonschema')\nlocal string            = string\nlocal ngx               = ngx\nlocal ipairs            = ipairs\nlocal type              = type\nlocal tostring          = tostring\nlocal pcall             = pcall\nlocal concat            = table.concat\n\nlocal ngx_encode_base64 = ngx.encode_base64\n\nlocal plugin_name       = \"openid-connect\"\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        client_id = {type = \"string\"},\n        client_secret = {type = \"string\"},\n        discovery = {type = \"string\"},\n        scope = {\n            type = \"string\",\n            default = \"openid\",\n        },\n        ssl_verify = {\n            type = \"boolean\",\n            default = true,\n        },\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            default = 3,\n            description = \"timeout in seconds\",\n        },\n        introspection_endpoint = {\n            type = \"string\"\n        },\n        introspection_endpoint_auth_method = {\n            type = \"string\",\n            default = \"client_secret_basic\"\n        },\n        token_endpoint_auth_method = {\n            type = \"string\",\n            default = \"client_secret_basic\"\n        },\n        bearer_only = {\n            type = \"boolean\",\n            default = false,\n        },\n        session = {\n            type = \"object\",\n            properties = {\n                secret = {\n                    type = \"string\",\n                    description = \"the key used for the encrypt and HMAC calculation\",\n                    minLength = 16,\n                },\n                cookie = {\n                    type = \"object\",\n                    properties = {\n                        lifetime = {\n                            type = \"integer\",\n                            description = \"it holds the cookie lifetime in seconds in the future\",\n                        }\n                    }\n                },\n                storage = {\n                    type = \"string\",\n                    enum = {\"cookie\", \"redis\"},\n                    default = \"cookie\",\n                },\n                redis = {\n                    type = \"object\",\n                    properties = {\n                        host = {\n                            type = \"string\", minLength = 2, default = \"127.0.0.1\"\n                        },\n                        port = {\n                            type = \"integer\", minimum = 1, default = 6379,\n                        },\n                        username = {\n                            type = \"string\", minLength = 1,\n                        },\n                        password = {\n                            type = \"string\", minLength = 0,\n                        },\n                        database = {\n                            type = \"integer\", minimum = 0, default = 0,\n                            description = \"redis database index\",\n                        },\n                        prefix = {\n                            type = \"string\",\n                            default = \"sessions\",\n                            description = \"prefix for keys stored in redis\"\n                        },\n                        ssl = {\n                            type = \"boolean\", default = false,\n                            description = \"enable ssl\",\n                        },\n                        ssl_verify = {\n                            type = \"boolean\", default = true,\n                            description = \"verify ssl certificate\",\n                        },\n                        server_name = {\n                            type = \"string\",\n                            description = \"The server name for the new TLS SNI extension.\",\n                        },\n                        connect_timeout = {\n                            type = \"integer\", minimum = 1, default = 1000,\n                            description = \"connect timeout in milliseconds\",\n                        },\n                        send_timeout = {\n                            type = \"integer\", minimum = 1, default = 1000,\n                            description = \"send timeout in milliseconds\",\n                        },\n                        read_timeout = {\n                            type = \"integer\", minimum = 1, default = 1000,\n                            description = \"read timeout in milliseconds\",\n                        },\n                        keepalive_timeout = {\n                            type = \"integer\", minimum = 1000, default = 10000,\n                            description = \"keepalive timeout in milliseconds\",\n                        },\n                    }\n                }\n            },\n            required = {\"secret\"},\n            [\"if\"] = {\n                properties = {\n                    storage = { enum = {\"redis\"} },\n                },\n            },\n            [\"then\"] = {\n                required = {\"redis\"},\n            },\n            additionalProperties = false,\n        },\n        realm = {\n            type = \"string\",\n            default = \"apisix\",\n        },\n        claim_validator = {\n            type = \"object\",\n            properties = {\n                issuer = {\n                    description = [[Whitelist the vetted issuers of the jwt.\n                    When not passed by the user, the issuer returned by\n                    discovery endpoint will be used. In case both are missing,\n                    the issuer will not be validated.]],\n                    type = \"object\",\n                    properties = {\n                        valid_issuers = {\n                            type = \"array\",\n                            items = {\n                                type = \"string\"\n                            }\n                        }\n                    }\n                },\n                audience = {\n                    type = \"object\",\n                    description = \"audience claim value to validate\",\n                    properties = {\n                        claim = {\n                            type = \"string\",\n                            description = \"custom claim name\",\n                            default = \"aud\",\n                        },\n                        required = {\n                            type = \"boolean\",\n                            description = \"audience claim is required\",\n                            default = false,\n                        },\n                        match_with_client_id = {\n                            type = \"boolean\",\n                            description = \"audience must euqal to or includes client_id\",\n                            default = false,\n                        }\n                    },\n                },\n            },\n        },\n        logout_path = {\n            type = \"string\",\n            default = \"/logout\",\n        },\n        redirect_uri = {\n            type = \"string\",\n            description = \"auto append '.apisix/redirect' to ngx.var.uri if not configured\"\n        },\n        post_logout_redirect_uri = {\n            type = \"string\",\n            description = \"the URI will be redirect when request logout_path\",\n        },\n        unauth_action = {\n            type = \"string\",\n            default = \"auth\",\n            enum = {\"auth\", \"deny\", \"pass\"},\n            description = \"The action performed when client is not authorized. Use auth to \" ..\n                \"redirect user to identity provider, deny to respond with 401 Unauthorized, and \" ..\n                \"pass to allow the request regardless.\"\n        },\n        public_key = {type = \"string\"},\n        use_jwks = {\n            type = \"boolean\",\n            default = false,\n            description = \"If true and if `public_key` is not set, use the JWKS to verify JWT \" ..\n                \"signature and skip token introspection in client credentials flow. The JWKS \" ..\n                \"endpoint is parsed from the discovery document.\"\n        },\n        token_signing_alg_values_expected = {type = \"string\"},\n        use_pkce = {\n            description = \"when set to true the PKCE(Proof Key for Code Exchange) will be used.\",\n            type = \"boolean\",\n            default = false\n        },\n        set_access_token_header = {\n            description = \"Whether the access token should be added as a header to the request \" ..\n                \"for downstream\",\n            type = \"boolean\",\n            default = true\n        },\n        access_token_in_authorization_header = {\n            description = \"Whether the access token should be added in the Authorization \" ..\n                \"header as opposed to the X-Access-Token header.\",\n            type = \"boolean\",\n            default = false\n        },\n        set_id_token_header = {\n            description = \"Whether the ID token should be added in the X-ID-Token header to \" ..\n                \"the request for downstream.\",\n            type = \"boolean\",\n            default = true\n        },\n        set_userinfo_header = {\n            description = \"Whether the user info token should be added in the X-Userinfo \" ..\n                \"header to the request for downstream.\",\n            type = \"boolean\",\n            default = true\n        },\n        set_refresh_token_header = {\n            description = \"Whether the refresh token should be added in the X-Refresh-Token \" ..\n                \"header to the request for downstream.\",\n            type = \"boolean\",\n            default = false\n        },\n        proxy_opts = {\n            description = \"HTTP proxy server be used to access identity server.\",\n            type = \"object\",\n            properties = {\n                http_proxy = {\n                    type = \"string\",\n                    description = \"HTTP proxy like: http://proxy-server:80.\",\n                },\n                https_proxy = {\n                    type = \"string\",\n                    description = \"HTTPS proxy like: http://proxy-server:80.\",\n                },\n                http_proxy_authorization = {\n                    type = \"string\",\n                    description = \"Basic [base64 username:password].\",\n                },\n                https_proxy_authorization = {\n                    type = \"string\",\n                    description = \"Basic [base64 username:password].\",\n                },\n                no_proxy = {\n                    type = \"string\",\n                    description = \"Comma separated list of hosts that should not be proxied.\",\n                }\n            },\n        },\n        authorization_params = {\n            description = \"Extra authorization params to the authorize endpoint\",\n            type = \"object\"\n        },\n        client_rsa_private_key = {\n            description = \"Client RSA private key used to sign JWT.\",\n            type = \"string\"\n        },\n        client_rsa_private_key_id = {\n            description = \"Client RSA private key ID used to compute a signed JWT.\",\n            type = \"string\"\n        },\n        client_jwt_assertion_expires_in = {\n            description = \"Life duration of the signed JWT in seconds.\",\n            type = \"integer\",\n            default = 60\n        },\n        renew_access_token_on_expiry = {\n            description = \"Whether to attempt silently renewing the access token.\",\n            type = \"boolean\",\n            default = true\n        },\n        access_token_expires_in = {\n            description = \"Lifetime of the access token in seconds if expires_in is not present.\",\n            type = \"integer\"\n        },\n        refresh_session_interval = {\n            description = \"Time interval to refresh user ID token without re-authentication.\",\n            type = \"integer\"\n        },\n        iat_slack = {\n            description = \"Tolerance of clock skew in seconds with the iat claim in an ID token.\",\n            type = \"integer\",\n            default = 120\n        },\n        accept_none_alg = {\n            description = \"Set to true if the OpenID provider does not sign its ID token.\",\n            type = \"boolean\",\n            default = false\n        },\n        accept_unsupported_alg = {\n            description = \"Ignore ID token signature to accept unsupported signature algorithm.\",\n            type = \"boolean\",\n            default = true\n        },\n        access_token_expires_leeway = {\n            description = \"Expiration leeway in seconds for access token renewal.\",\n            type = \"integer\",\n            default = 0\n        },\n        force_reauthorize = {\n            description = \"Whether to execute the authorization flow when a token has been cached.\",\n            type = \"boolean\",\n            default = false\n        },\n        use_nonce = {\n            description = \"Whether to include nonce parameter in authorization request.\",\n            type = \"boolean\",\n            default = false\n        },\n        revoke_tokens_on_logout = {\n            description = \"Notify authorization server a previous token is no longer needed.\",\n            type = \"boolean\",\n            default = false\n        },\n        jwk_expires_in = {\n            description = \"Expiration time for JWK cache in seconds.\",\n            type = \"integer\",\n            default = 86400\n        },\n        jwt_verification_cache_ignore = {\n            description = \"Whether to ignore cached verification and re-verify.\",\n            type = \"boolean\",\n            default = false\n        },\n        cache_segment = {\n            description = \"Name of a cache segment to differentiate caches.\",\n            type = \"string\"\n        },\n        introspection_interval = {\n            description = \"TTL of the cached and introspected access token in seconds.\",\n            type = \"integer\",\n            default = 0\n        },\n        introspection_expiry_claim = {\n            description = \"Name of the expiry claim that controls the cached access token TTL.\",\n            type = \"string\"\n        },\n        introspection_addon_headers = {\n            description = \"Extra http headers in introspection\",\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                pattern = \"^[^:]+$\"\n            }\n        },\n        required_scopes = {\n            description = \"List of scopes that are required to be granted to the access token\",\n            type = \"array\",\n            items = {\n                type = \"string\"\n            }\n        },\n        claim_schema = {\n            description = \"JSON schema of OIDC response claim\",\n            type = \"object\",\n            default = nil,\n        }\n    },\n    encrypt_fields = {\"client_secret\", \"client_rsa_private_key\"},\n    required = {\"client_id\", \"client_secret\", \"discovery\"}\n}\n\n\nlocal _M = {\n    version = 0.2,\n    priority = 2599,\n    name = plugin_name,\n    schema = schema,\n}\n\nfunction _M.check_schema(conf)\n    if conf.ssl_verify == \"no\" then\n        -- we used to set 'ssl_verify' to \"no\"\n        conf.ssl_verify = false\n    end\n\n    if not conf.bearer_only and not conf.session then\n        return false, \"property \\\"session.secret\\\" is required when \\\"bearer_only\\\" is false\"\n    end\n\n    local check = {\"discovery\", \"introspection_endpoint\", \"redirect_uri\",\n                    \"post_logout_redirect_uri\", \"proxy_opts.http_proxy\", \"proxy_opts.https_proxy\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, plugin_name)\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.claim_schema then\n        local ok, res = pcall(jsonschema.generate_validator, conf.claim_schema)\n        if not ok then\n            return false, \"check claim_schema failed: \" .. tostring(res)\n        end\n    end\n\n    return true\nend\n\nlocal function get_bearer_access_token(ctx)\n    -- Get Authorization header, maybe.\n    local auth_header = core.request.header(ctx, \"Authorization\")\n    if not auth_header then\n        -- No Authorization header, get X-Access-Token header, maybe.\n        local access_token_header = core.request.header(ctx, \"X-Access-Token\")\n        if not access_token_header then\n            -- No X-Access-Token header neither.\n            return false, nil, nil\n        end\n\n        -- Return extracted header value.\n        return true, access_token_header, nil\n    end\n\n    -- Check format of Authorization header.\n    local res, err = ngx_re.split(auth_header, \" \", nil, nil, 2)\n\n    if not res then\n        -- No result was returned.\n        return false, nil, err\n    elseif #res < 2 then\n        -- Header doesn't split into enough tokens.\n        return false, nil, \"Invalid Authorization header format.\"\n    end\n\n    if string.lower(res[1]) == \"bearer\" then\n        -- Return extracted token.\n        return true, res[2], nil\n    end\n\n    return false, nil, nil\nend\n\n\nlocal function introspect(ctx, conf)\n    -- Extract token, maybe.\n    local has_token, token, err = get_bearer_access_token(ctx)\n\n    if err then\n        return ngx.HTTP_BAD_REQUEST, err, nil, nil\n    end\n\n    if not has_token then\n        -- Could not find token.\n\n        if conf.bearer_only then\n            -- Token strictly required in request.\n            ngx.header[\"WWW-Authenticate\"] = 'Bearer realm=\"' .. conf.realm .. '\"'\n            return ngx.HTTP_UNAUTHORIZED, \"No bearer token found in request.\", nil, nil\n        else\n            -- Return empty result.\n            return nil, nil, nil, nil\n        end\n    end\n\n    if conf.public_key or conf.use_jwks then\n        local opts = {}\n        -- Validate token against public key or jwks document of the oidc provider.\n        -- TODO: In the called method, the openidc module will try to extract\n        --  the token by itself again -- from a request header or session cookie.\n        --  It is inefficient that we also need to extract it (just from headers)\n        --  so we can add it in the configured header. Find a way to use openidc\n        --  module's internal methods to extract the token.\n        local valid_issuers\n        if conf.claim_validator and conf.claim_validator.issuer then\n            valid_issuers = conf.claim_validator.issuer.valid_issuers\n        end\n        if not valid_issuers then\n            local discovery, discovery_err = openidc.get_discovery_doc(conf)\n            if discovery_err then\n                core.log.warn(\"OIDC access discovery url failed : \", discovery_err)\n            else\n                core.log.info(\"valid_issuers not provided explicitly,\" ..\n                              \" using issuer from discovery doc: \",\n                              discovery.issuer)\n                valid_issuers = {discovery.issuer}\n            end\n        end\n        if valid_issuers then\n            opts.valid_issuers = valid_issuers\n        end\n        local res, err = openidc.bearer_jwt_verify(conf, opts)\n        if err then\n            -- Error while validating or token invalid.\n            ngx.header[\"WWW-Authenticate\"] = 'Bearer realm=\"' .. conf.realm ..\n                '\", error=\"invalid_token\", error_description=\"' .. err .. '\"'\n            return ngx.HTTP_UNAUTHORIZED, err, nil, nil\n        end\n\n        -- Token successfully validated.\n        local method = (conf.public_key and \"public_key\") or (conf.use_jwks and \"jwks\")\n        core.log.debug(\"token validate successfully by \", method)\n        return res, err, token, res\n    else\n        -- Validate token against introspection endpoint.\n        -- TODO: Same as above for public key validation.\n        if conf.introspection_addon_headers then\n            -- http_request_decorator option provided by lua-resty-openidc\n            conf.http_request_decorator = function(req)\n                local h = req.headers or {}\n                for _, name in ipairs(conf.introspection_addon_headers) do\n                    local value = core.request.header(ctx, name)\n                    if value then\n                        h[name] = value\n                    end\n                end\n                req.headers = h\n                return req\n            end\n        end\n\n        local res, err = openidc.introspect(conf)\n        conf.http_request_decorator = nil\n\n        if err then\n            ngx.header[\"WWW-Authenticate\"] = 'Bearer realm=\"' .. conf.realm ..\n                '\", error=\"invalid_token\", error_description=\"' .. err .. '\"'\n            return ngx.HTTP_UNAUTHORIZED, err, nil, nil\n        end\n\n        -- Token successfully validated and response from the introspection\n        -- endpoint contains the userinfo.\n        core.log.debug(\"token validate successfully by introspection\")\n        return res, err, token, res\n    end\nend\n\n\nlocal function add_access_token_header(ctx, conf, token)\n    if token then\n        -- Add Authorization or X-Access-Token header, respectively, if not already set.\n        if conf.set_access_token_header then\n            if conf.access_token_in_authorization_header then\n                if not core.request.header(ctx, \"Authorization\") then\n                    -- Add Authorization header.\n                    core.request.set_header(ctx, \"Authorization\", \"Bearer \" .. token)\n                end\n            else\n                if not core.request.header(ctx, \"X-Access-Token\") then\n                    -- Add X-Access-Token header.\n                    core.request.set_header(ctx, \"X-Access-Token\", token)\n                end\n            end\n        end\n    end\nend\n\n-- Function to split the scope string into a table\nlocal function split_scopes_by_space(scope_string)\n    local scopes = {}\n    for scope in string.gmatch(scope_string, \"%S+\") do\n        scopes[scope] = true\n    end\n    return scopes\nend\n\n-- Function to check if all required scopes are present\nlocal function required_scopes_present(required_scopes, http_scopes)\n    for _, scope in ipairs(required_scopes) do\n        if not http_scopes[scope] then\n            return false\n        end\n    end\n    return true\nend\n\nlocal function validate_claims_in_oidcauth_response(resp, conf)\n    if not conf.claim_schema then\n        return true\n    end\n    local data = {\n        user         = resp.user,\n        access_token = resp.access_token,\n        id_token     = resp.id_token,\n    }\n    return core.schema.check(conf.claim_schema, data)\nend\n\nfunction _M.rewrite(plugin_conf, ctx)\n    local conf_clone = core.table.clone(plugin_conf)\n    local conf = fetch_secrets(conf_clone, true)\n\n    -- Previously, we multiply conf.timeout before storing it in etcd.\n    -- If the timeout is too large, we should not multiply it again.\n    if not (conf.timeout >= 1000 and conf.timeout % 1000 == 0) then\n        conf.timeout = conf.timeout * 1000\n    end\n\n    local path = ctx.var.request_uri\n\n    if not conf.redirect_uri then\n        -- NOTE: 'lua-resty-openidc' requires that 'redirect_uri' be\n        --       different from 'uri'.  So default to append the\n        --       '.apisix/redirect' suffix if not configured.\n        local suffix = \"/.apisix/redirect\"\n        local uri = ctx.var.uri\n        if core.string.has_suffix(uri, suffix) then\n            -- This is the redirection response from the OIDC provider.\n            conf.redirect_uri = uri\n        else\n            if string.sub(uri, -1, -1) == \"/\" then\n                conf.redirect_uri = string.sub(uri, 1, -2) .. suffix\n            else\n                conf.redirect_uri = uri .. suffix\n            end\n        end\n        core.log.debug(\"auto set redirect_uri: \", conf.redirect_uri)\n    end\n\n    if not conf.ssl_verify then\n        -- openidc use \"no\" to disable ssl verification\n        conf.ssl_verify = \"no\"\n    end\n\n    if path == (conf.logout_path or \"/logout\") then\n        local discovery, discovery_err = openidc.get_discovery_doc(conf)\n        if discovery_err then\n            core.log.error(\"OIDC access discovery url failed : \", discovery_err)\n            return 503\n        end\n        if conf.post_logout_redirect_uri and not discovery.end_session_endpoint then\n            -- If the end_session_endpoint field does not exist in the OpenID Provider Discovery\n            -- Metadata, the redirect_after_logout_uri field is used for redirection.\n            conf.redirect_after_logout_uri = conf.post_logout_redirect_uri\n        end\n    end\n\n    local response, err, session, _\n\n    if conf.bearer_only or conf.introspection_endpoint or conf.public_key or conf.use_jwks then\n        -- An introspection endpoint or a public key has been configured. Try to\n        -- validate the access token from the request, if it is present in a\n        -- request header. Otherwise, return a nil response. See below for\n        -- handling of the case where the access token is stored in a session cookie.\n        local access_token, userinfo\n        response, err, access_token, userinfo = introspect(ctx, conf)\n\n        if err then\n            -- Error while validating token or invalid token.\n            core.log.error(\"OIDC introspection failed: \", err)\n            return response\n        end\n\n        if response then\n            if conf.required_scopes then\n                local http_scopes = response.scope and split_scopes_by_space(response.scope) or {}\n                local is_authorized = required_scopes_present(conf.required_scopes, http_scopes)\n                if not is_authorized then\n                    core.log.error(\"OIDC introspection failed: \", \"required scopes not present\")\n                    local error_response = {\n                        error = \"required scopes \" .. concat(conf.required_scopes, \", \") ..\n                        \" not present\"\n                    }\n                    return 403, core.json.encode(error_response)\n                end\n            end\n\n            -- jwt audience claim validator\n            local audience_claim = core.table.try_read_attr(conf, \"claim_validator\",\n                                                             \"audience\", \"claim\") or \"aud\"\n            local audience_value = response[audience_claim]\n            if core.table.try_read_attr(conf, \"claim_validator\", \"audience\", \"required\")\n                and not audience_value then\n                core.log.error(\"OIDC introspection failed: required audience (\",\n                                audience_claim, \") not present\")\n                local error_response = { error = \"required audience claim not present\" }\n                return 403, core.json.encode(error_response)\n            end\n            if core.table.try_read_attr(conf, \"claim_validator\", \"audience\", \"match_with_client_id\")\n                and audience_value ~= nil then\n                local error_response = { error = \"mismatched audience\" }\n                local matched = false\n                if type(audience_value) == \"table\" then\n                    for _, v in ipairs(audience_value) do\n                        if conf.client_id == v then\n                            matched = true\n                        end\n                    end\n                    if not matched then\n                        core.log.error(\"OIDC introspection failed: \",\n                                        \"audience list does not contain the client id\")\n                        return 403, core.json.encode(error_response)\n                    end\n                elseif conf.client_id ~= audience_value then\n                    core.log.error(\"OIDC introspection failed: \",\n                                    \"audience does not match the client id\")\n                    return 403, core.json.encode(error_response)\n                end\n            end\n\n            -- Add configured access token header, maybe.\n            add_access_token_header(ctx, conf, access_token)\n\n            if userinfo and conf.set_userinfo_header then\n                -- Set X-Userinfo header to introspection endpoint response.\n                core.request.set_header(ctx, \"X-Userinfo\",\n                    ngx_encode_base64(core.json.encode(userinfo)))\n            end\n        end\n    end\n\n    if not response then\n        -- Either token validation via introspection endpoint or public key is\n        -- not configured, and/or token could not be extracted from the request.\n\n        local unauth_action = conf.unauth_action\n        if unauth_action ~= \"auth\" then\n            unauth_action = \"deny\"\n        end\n\n        -- Authenticate the request. This will validate the access token if it\n        -- is stored in a sessions cookie, and also renew the token if required.\n        -- If no token can be extracted, the response will redirect to the ID\n        -- provider's authorization endpoint to initiate the Relying Party flow.\n        -- This code path also handles when the ID provider then redirects to\n        -- the configured redirect URI after successful authentication.\n        response, err, _, session  = openidc.authenticate(conf, nil, unauth_action, conf.session)\n\n        if err then\n            if session then\n                session:close()\n            end\n            if err == \"unauthorized request\" then\n                if conf.unauth_action == \"pass\" then\n                    return nil\n                end\n                return 401\n            end\n            core.log.error(\"OIDC authentication failed: \", err)\n            return 500\n        end\n\n        if response then\n            local ok, err = validate_claims_in_oidcauth_response(response, conf)\n            if not ok then\n                core.log.error(\"OIDC claim validation failed: \", err)\n                ngx.header[\"WWW-Authenticate\"] = 'Bearer realm=\"' .. conf.realm ..\n                        '\", error=\"invalid_token\", error_description=\"' .. err .. '\"'\n                return ngx.HTTP_UNAUTHORIZED\n            end\n            -- If the openidc module has returned a response, it may contain,\n            -- respectively, the access token, the ID token, the refresh token,\n            -- and the userinfo.\n            -- Add respective headers to the request, if so configured.\n\n            -- Add configured access token header, maybe.\n            add_access_token_header(ctx, conf, response.access_token)\n\n            -- Add X-ID-Token header, maybe.\n            if response.id_token and conf.set_id_token_header then\n                local token = core.json.encode(response.id_token)\n                core.request.set_header(ctx, \"X-ID-Token\", ngx.encode_base64(token))\n            end\n\n            -- Add X-Userinfo header, maybe.\n            if response.user and conf.set_userinfo_header then\n                core.request.set_header(ctx, \"X-Userinfo\",\n                    ngx_encode_base64(core.json.encode(response.user)))\n            end\n\n            -- Add X-Refresh-Token header, maybe.\n            local refresh_token = session:get(\"refresh_token\")\n            if refresh_token and conf.set_refresh_token_header then\n                core.request.set_header(ctx, \"X-Refresh-Token\", refresh_token)\n            end\n        end\n    end\n    if session then\n        session:close()\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/opentelemetry.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal plugin_name = \"opentelemetry\"\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal process = require(\"ngx.process\")\n\nlocal always_off_sampler_new = require(\"opentelemetry.trace.sampling.always_off_sampler\").new\nlocal always_on_sampler_new = require(\"opentelemetry.trace.sampling.always_on_sampler\").new\nlocal parent_base_sampler_new = require(\"opentelemetry.trace.sampling.parent_base_sampler\").new\nlocal trace_id_ratio_sampler_new =\n                                require(\"opentelemetry.trace.sampling.trace_id_ratio_sampler\").new\n\nlocal exporter_client_new = require(\"opentelemetry.trace.exporter.http_client\").new\nlocal otlp_exporter_new = require(\"opentelemetry.trace.exporter.otlp\").new\nlocal batch_span_processor_new = require(\"opentelemetry.trace.batch_span_processor\").new\nlocal id_generator = require(\"opentelemetry.trace.id_generator\")\nlocal tracer_provider_new = require(\"opentelemetry.trace.tracer_provider\").new\n\nlocal span_kind = require(\"opentelemetry.trace.span_kind\")\nlocal span_status = require(\"opentelemetry.trace.span_status\")\nlocal resource_new = require(\"opentelemetry.resource\").new\nlocal attr = require(\"opentelemetry.attribute\")\n\nlocal context = require(\"opentelemetry.context\").new()\nlocal trace_context_propagator =\n                require(\"opentelemetry.trace.propagation.text_map.trace_context_propagator\").new()\n\nlocal ngx     = ngx\nlocal ngx_var = ngx.var\nlocal table   = table\nlocal type    = type\nlocal pairs   = pairs\nlocal ipairs  = ipairs\nlocal unpack  = unpack\nlocal string_format = string.format\nlocal update_time = ngx.update_time\n\nlocal lrucache = core.lrucache.new({\n    type = 'plugin', count = 128, ttl = 24 * 60 * 60,\n})\n\nlocal asterisk = string.byte(\"*\", 1)\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        trace_id_source = {\n            type = \"string\",\n            enum = {\"x-request-id\", \"random\"},\n            description = \"the source of trace id\",\n            default = \"random\",\n        },\n        resource = {\n            type = \"object\",\n            description = \"additional resource\",\n            additionalProperties = {{type = \"boolean\"}, {type = \"number\"}, {type = \"string\"}},\n        },\n        collector = {\n            type = \"object\",\n            description = \"opentelemetry collector\",\n            properties = {\n                address = {type = \"string\", description = \"host:port\", default = \"127.0.0.1:4318\"},\n                request_timeout = {type = \"integer\", description = \"second uint\", default = 3},\n                request_headers = {\n                    type = \"object\",\n                    description = \"http headers\",\n                    additionalProperties = {\n                        one_of = {{type = \"boolean\"},{type = \"number\"}, {type = \"string\"}},\n                   },\n                }\n            },\n            default = {address = \"127.0.0.1:4318\", request_timeout = 3}\n        },\n        batch_span_processor = {\n            type = \"object\",\n            description = \"batch span processor\",\n            properties = {\n                drop_on_queue_full = {\n                    type = \"boolean\",\n                    description = \"if true, drop span when queue is full,\"\n                            .. \" otherwise force process batches\",\n                },\n                max_queue_size = {\n                    type = \"integer\",\n                    description = \"maximum queue size to buffer spans for delayed processing\",\n                },\n                batch_timeout = {\n                    type = \"number\",\n                    description = \"maximum duration for constructing a batch\",\n                },\n                inactive_timeout = {\n                    type = \"number\",\n                    description = \"maximum duration for processing batches\",\n                },\n                max_export_batch_size = {\n                    type = \"integer\",\n                    description = \"maximum number of spans to process in a single batch\",\n                }\n            },\n            default = {},\n        },\n        set_ngx_var = {\n          type = \"boolean\",\n          description = \"set nginx variables\",\n          default = false,\n        },\n    },\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        sampler = {\n            type = \"object\",\n            properties = {\n                name = {\n                    type = \"string\",\n                    enum = {\"always_on\", \"always_off\", \"trace_id_ratio\", \"parent_base\"},\n                    title = \"sampling strategy\",\n                    default = \"always_off\"\n                },\n                options = {\n                    type = \"object\",\n                    properties = {\n                        fraction = {\n                            type = \"number\", title = \"trace_id_ratio fraction\", default = 0\n                        },\n                        root = {\n                            type = \"object\",\n                            title = \"parent_base root sampler\",\n                            properties = {\n                                name = {\n                                    type = \"string\",\n                                    enum = {\"always_on\", \"always_off\", \"trace_id_ratio\"},\n                                    title = \"sampling strategy\",\n                                    default = \"always_off\"\n                                },\n                                options = {\n                                    type = \"object\",\n                                    properties = {\n                                        fraction = {\n                                            type = \"number\",\n                                            title = \"trace_id_ratio fraction parameter\",\n                                            default = 0,\n                                        },\n                                    },\n                                    default = {fraction = 0}\n                                }\n                            },\n                            default = {name = \"always_off\", options = {fraction = 0}}\n                        },\n                    },\n                    default = {fraction = 0, root = {name = \"always_off\"}}\n                }\n            },\n            default = {name = \"always_off\", options = {fraction = 0, root = {name = \"always_off\"}}}\n        },\n        additional_attributes = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n            }\n        },\n        additional_header_prefix_attributes = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n            }\n        }\n    }\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 12009,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        local ok, err = core.schema.check(metadata_schema, conf)\n        if not ok then\n            return ok, err\n        end\n        local check = {\"collector.address\"}\n        core.utils.check_https(check, conf, plugin_name)\n        return true\n    end\n    return core.schema.check(schema, conf)\nend\n\n\nlocal hostname\nlocal sampler_factory\n\nfunction _M.init()\n    if process.type() ~= \"worker\" then\n        return\n    end\n\n    sampler_factory = {\n        always_off = always_off_sampler_new,\n        always_on = always_on_sampler_new,\n        parent_base = parent_base_sampler_new,\n        trace_id_ratio = trace_id_ratio_sampler_new,\n    }\n    hostname = core.utils.gethostname()\nend\n\n\nlocal function create_tracer_obj(conf, plugin_info)\n    if plugin_info.trace_id_source == \"x-request-id\" then\n        id_generator.new_ids = function()\n            local trace_id = core.request.headers()[\"x-request-id\"] or ngx_var.request_id\n            return trace_id, id_generator.new_span_id()\n        end\n    end\n    -- create exporter\n    local exporter = otlp_exporter_new(exporter_client_new(plugin_info.collector.address,\n                                                            plugin_info.collector.request_timeout,\n                                                            plugin_info.collector.request_headers))\n    -- create span processor\n    local batch_span_processor = batch_span_processor_new(exporter,\n                                                            plugin_info.batch_span_processor)\n    -- create sampler\n    local sampler\n    local sampler_name = conf.sampler.name\n    local sampler_options = conf.sampler.options\n    if sampler_name == \"parent_base\" then\n        local root_sampler\n        if sampler_options.root then\n            local name, fraction = sampler_options.root.name, sampler_options.root.options.fraction\n            root_sampler = sampler_factory[name](fraction)\n        else\n            root_sampler = always_off_sampler_new()\n        end\n        sampler = sampler_factory[sampler_name](root_sampler)\n    else\n        sampler = sampler_factory[sampler_name](sampler_options.fraction)\n    end\n    local resource_attrs = {attr.string(\"hostname\", hostname)}\n    if plugin_info.resource then\n        if not plugin_info.resource[\"service.name\"] then\n            table.insert(resource_attrs, attr.string(\"service.name\", \"APISIX\"))\n        end\n        for k, v in pairs(plugin_info.resource) do\n            if type(v) == \"string\" then\n                table.insert(resource_attrs, attr.string(k, v))\n            end\n            if type(v) == \"number\" then\n                table.insert(resource_attrs, attr.double(k, v))\n            end\n            if type(v) == \"boolean\" then\n                table.insert(resource_attrs, attr.bool(k, v))\n            end\n        end\n    end\n    -- create tracer provider\n    local tp = tracer_provider_new(batch_span_processor, {\n        resource = resource_new(unpack(resource_attrs)),\n        sampler = sampler,\n    })\n    -- create tracer\n    return tp:tracer(\"opentelemetry-lua\")\nend\n\n\nlocal function inject_attributes(attributes, wanted_attributes, source, with_prefix)\n    for _, key in ipairs(wanted_attributes) do\n        local is_key_a_match = #key >= 2 and key:byte(-1) == asterisk and with_prefix\n\n        if is_key_a_match then\n            local prefix = key:sub(0, -2)\n            for possible_key, value in pairs(source) do\n                if core.string.has_prefix(possible_key, prefix) then\n                    core.table.insert(attributes, attr.string(possible_key, value))\n                end\n            end\n        else\n            local val = source[key]\n            if val then\n                core.table.insert(attributes, attr.string(key, val))\n            end\n        end\n    end\nend\n\n\nfunction _M.rewrite(conf, api_ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    if metadata == nil then\n        core.log.warn(\"plugin_metadata is required for opentelemetry plugin to working properly\")\n        return\n    end\n    core.log.info(\"metadata: \", core.json.delay_encode(metadata))\n    local plugin_info = metadata.value\n    local vars = api_ctx.var\n\n    local tracer, err = core.lrucache.plugin_ctx(lrucache, api_ctx, nil,\n                                                create_tracer_obj, conf, plugin_info)\n    if not tracer then\n        core.log.error(\"failed to fetch tracer object: \", err)\n        return\n    end\n\n    local span_name = vars.method\n\n    local attributes = {\n        attr.string(\"net.host.name\", vars.host),\n        -- deprecated attributes\n        attr.string(\"http.method\", vars.method),\n        attr.string(\"http.scheme\", vars.scheme),\n        attr.string(\"http.target\", vars.request_uri),\n        attr.string(\"http.user_agent\", vars.http_user_agent),\n\n        -- new attributes\n        attr.string(\"http.request.method\", vars.method),\n        attr.string(\"url.scheme\", vars.scheme),\n        attr.string(\"url.path\", vars.uri),\n        attr.string(\"user_agent.original\", vars.http_user_agent),\n    }\n\n    if api_ctx.curr_req_matched then\n        table.insert(attributes, attr.string(\"apisix.route_id\", api_ctx.route_id))\n        table.insert(attributes, attr.string(\"apisix.route_name\", api_ctx.route_name))\n        table.insert(attributes, attr.string(\"http.route\", api_ctx.curr_req_matched._path))\n        span_name = span_name .. \" \" .. api_ctx.curr_req_matched._path\n    end\n\n    if api_ctx.service_id then\n        table.insert(attributes, attr.string(\"apisix.service_id\", api_ctx.service_id))\n        table.insert(attributes, attr.string(\"apisix.service_name\", api_ctx.service_name))\n    end\n\n    if conf.additional_attributes then\n        inject_attributes(attributes, conf.additional_attributes, api_ctx.var, false)\n    end\n\n    if conf.additional_header_prefix_attributes then\n        inject_attributes(\n            attributes,\n            conf.additional_header_prefix_attributes,\n            core.request.headers(api_ctx),\n            true\n        )\n    end\n\n    -- extract trace context from the headers of downstream HTTP request\n    local upstream_context = trace_context_propagator:extract(context, ngx.req)\n\n    local ctx = tracer:start(upstream_context, span_name, {\n        kind = span_kind.server,\n        attributes = attributes,\n    })\n\n    if plugin_info.set_ngx_var then\n      local span_context = ctx:span():context()\n      ngx_var.opentelemetry_context_traceparent = string_format(\"00-%s-%s-%02x\",\n                                                                 span_context.trace_id,\n                                                                 span_context.span_id,\n                                                                 span_context.trace_flags)\n      ngx_var.opentelemetry_trace_id = span_context.trace_id\n      ngx_var.opentelemetry_span_id = span_context.span_id\n    end\n\n    if not ctx:span():is_recording() and ngx.ctx.tracing then\n        ngx.ctx.tracing.skip = true\n    end\n\n    api_ctx.otel_context_token = ctx:attach()\n\n    -- inject trace context into the headers of upstream HTTP request\n    trace_context_propagator:inject(ctx, ngx.req)\nend\n\n\nlocal function create_child_span(tracer, parent_span_ctx, spans, span)\n    if not span or span.finished then\n        return\n    end\n    span.finished = true\n    local new_span_ctx, new_span = tracer:start(parent_span_ctx, span.name,\n                                    {\n                                        kind = span.kind,\n                                        attributes = span.attributes,\n                                    })\n    new_span.start_time = span.start_time\n\n    for _, idx in ipairs(span.child_ids or {}) do\n        create_child_span(tracer, new_span_ctx, spans, spans[idx])\n    end\n    if span.status then\n        new_span:set_status(span.status.code, span.status.message)\n    end\n    new_span:finish(span.end_time)\nend\n\n\nlocal function inject_core_spans(root_span_ctx, api_ctx, conf)\n    local tracing = api_ctx.ngx_ctx.tracing\n    if not tracing then\n        return\n    end\n\n    local span = root_span_ctx:span()\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local plugin_info = metadata.value\n    if span and not span:is_recording() then\n        return\n    end\n    local inject_conf = {\n        sampler = {\n            name = \"always_on\",\n            options = conf.sampler.options\n        },\n        additional_attributes = conf.additional_attributes,\n        additional_header_prefix_attributes = conf.additional_header_prefix_attributes\n    }\n    local tracer, err = core.lrucache.plugin_ctx(lrucache, api_ctx, nil,\n                                                create_tracer_obj, inject_conf, plugin_info)\n    if not tracer then\n        core.log.error(\"failed to fetch tracer object: \", err)\n        return\n    end\n\n    if #tracing.spans == 0 then\n        return\n    end\n    span.start_time = tracing.spans[1].start_time\n    local root_span = tracing.root_span\n    local spans = tracing.spans\n    for _, idx in ipairs(root_span.child_ids or {}) do\n        create_child_span(tracer, root_span_ctx, spans, spans[idx])\n    end\nend\n\n\nfunction _M.log(conf, api_ctx)\n    if api_ctx.otel_context_token then\n        -- ctx:detach() is not necessary, because of ctx is stored in ngx.ctx\n        local upstream_status = core.response.get_upstream_status(api_ctx)\n\n        -- get span from current context\n        local ctx = context:current()\n        local span = ctx:span()\n        if upstream_status and upstream_status >= 500 then\n            span:set_status(span_status.ERROR,\n                    \"upstream response status: \" .. upstream_status)\n        end\n\n        inject_core_spans(ctx, api_ctx, conf)\n        span:set_attributes(attr.int(\"http.status_code\", upstream_status),\n                            attr.int(\"http.response.status_code\", upstream_status))\n        update_time()\n        span:finish()\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/openwhisk.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core              = require(\"apisix.core\")\nlocal http              = require(\"resty.http\")\nlocal ngx_encode_base64 = ngx.encode_base64\nlocal tostring          = tostring\n\nlocal name_pattern = [[\\A([\\w]|[\\w][\\w@ .-]*[\\w@.-]+)\\z]]\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        api_host = {type = \"string\"},\n        ssl_verify = {\n            type = \"boolean\",\n            default = true,\n        },\n        service_token = {type = \"string\"},\n        namespace = {type = \"string\", maxLength = 256, pattern = name_pattern},\n        package = {type = \"string\", maxLength = 256, pattern = name_pattern},\n        action = {type = \"string\", maxLength = 256, pattern = name_pattern},\n        result = {\n            type = \"boolean\",\n            default = true,\n        },\n        timeout = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 60000,\n            default = 3000,\n            description = \"timeout in milliseconds\",\n        },\n        keepalive = {type = \"boolean\", default = true},\n        keepalive_timeout = {type = \"integer\", minimum = 1000, default = 60000},\n        keepalive_pool = {type = \"integer\", minimum = 1, default = 5}\n    },\n    required = {\"api_host\", \"service_token\", \"namespace\", \"action\"},\n    encrypt_fields = {\"service_token\"}\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = -1901,\n    name = \"openwhisk\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local check = {\"api_host\"}\n    core.utils.check_https(check, conf, _M.name)\n    core.utils.check_tls_bool({\"ssl_verify\"}, conf, _M.name)\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    local params = {\n        method = \"POST\",\n        body = core.request.get_body(),\n        query = {\n            blocking = \"true\",\n            result = tostring(conf.result),\n            timeout = conf.timeout\n        },\n        headers = {\n            [\"Authorization\"] = \"Basic \" .. ngx_encode_base64(conf.service_token),\n            [\"Content-Type\"] = \"application/json\",\n        },\n        keepalive = conf.keepalive,\n        ssl_verify = conf.ssl_verify\n    }\n\n    if conf.keepalive then\n        params.keepalive_timeout = conf.keepalive_timeout\n        params.keepalive_pool = conf.keepalive_pool\n    end\n\n    -- OpenWhisk action endpoint\n    local package = conf.package and conf.package .. \"/\" or \"\"\n    local endpoint = conf.api_host .. \"/api/v1/namespaces/\" .. conf.namespace ..\n        \"/actions/\" .. package .. conf.action\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout)\n\n    local res, err = httpc:request_uri(endpoint, params)\n\n    if not res then\n        core.log.error(\"failed to process openwhisk action, err: \", err)\n        return 503\n    end\n\n    -- check if res.body is nil\n    if res.body == nil then\n        return res.status, res.body\n    end\n\n    -- parse OpenWhisk JSON response\n    -- OpenWhisk supports two types of responses, the user can return only\n    -- the response body, or set the status code and header.\n    local result, err = core.json.decode(res.body)\n\n    if not result then\n        core.log.error(\"failed to parse openwhisk response data: \", err)\n        return 503\n    end\n\n    -- setting response headers\n    if result.headers ~= nil then\n        core.response.set_header(result.headers)\n    end\n\n    local code = result.statusCode or res.status\n    local body = result.body or res.body\n    return code, body\n\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/prometheus/exporter.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal base_prometheus = require(\"prometheus\")\nlocal tonumber        = tonumber\nlocal core      = require(\"apisix.core\")\nlocal plugin    = require(\"apisix.plugin\")\nlocal control   = require(\"apisix.control.v1\")\nlocal ipairs    = ipairs\nlocal pairs     = pairs\nlocal ngx       = ngx\nlocal ffi       = require(\"ffi\")\nlocal C         = ffi.C\nlocal pcall = pcall\nlocal select = select\nlocal type = type\nlocal prometheus\nlocal prometheus_bkp\nlocal router = require(\"apisix.router\")\nlocal get_routes = router.http_routes\nlocal get_ssls   = router.ssls\nlocal get_services = require(\"apisix.http.service\").services\nlocal get_consumers = require(\"apisix.consumer\").consumers\nlocal get_upstreams = require(\"apisix.upstream\").upstreams\nlocal get_global_rules = require(\"apisix.global_rules\").global_rules\nlocal get_global_rules_prev_index = require(\"apisix.global_rules\").get_pre_index\nlocal clear_tab = core.table.clear\nlocal get_stream_routes = router.stream_routes\nlocal get_protos = require(\"apisix.plugins.grpc-transcode.proto\").protos\nlocal service_fetch = require(\"apisix.http.service\").get\nlocal latency_details = require(\"apisix.utils.log-util\").latency_details_in_ms\nlocal xrpc = require(\"apisix.stream.xrpc\")\nlocal unpack = unpack\nlocal next = next\nlocal process = require(\"ngx.process\")\nlocal tonumber = tonumber\nlocal shdict_prometheus_cache = ngx.shared[\"prometheus-cache\"]\nlocal ngx_timer_every = ngx.timer.every\n\nif not shdict_prometheus_cache then\n    error(\"lua_shared_dict \\\"prometheus-cache\\\" not configured\")\nend\n\nlocal plugin_name = \"prometheus\"\nlocal default_export_uri = \"/apisix/prometheus/metrics\"\n-- Default set of latency buckets, 1ms to 60s:\nlocal DEFAULT_BUCKETS = {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 30000, 60000}\n-- Default refresh interval\nlocal DEFAULT_REFRESH_INTERVAL = 15\n\nlocal CACHED_METRICS_KEY = \"cached_metrics_text\"\n\nlocal metrics = {}\n\nlocal inner_tab_arr = {}\n\nlocal exporter_timer_running = false\n\nlocal exporter_timer_created = false\n\n\nlocal function gen_arr(...)\n    clear_tab(inner_tab_arr)\n    for i = 1, select('#', ...) do\n        inner_tab_arr[i] = select(i, ...)\n    end\n\n    return inner_tab_arr\nend\n\nlocal extra_labels_tbl = {}\n\nlocal function extra_labels(name, ctx)\n    clear_tab(extra_labels_tbl)\n\n    local attr = plugin.plugin_attr(\"prometheus\")\n    local metrics = attr.metrics\n\n    if metrics and metrics[name] and metrics[name].extra_labels then\n        local labels = metrics[name].extra_labels\n        for _, kv in ipairs(labels) do\n            local val, v = next(kv)\n            if ctx then\n                val = ctx.var[v:sub(2)]\n                if val == nil then\n                    val = \"\"\n                end\n            end\n            core.table.insert(extra_labels_tbl, val)\n        end\n    end\n\n    return extra_labels_tbl\nend\n\n\nlocal _M = {}\n\n\nlocal function init_stream_metrics()\n    metrics.stream_connection_total = prometheus:counter(\"stream_connection_total\",\n        \"Total number of connections handled per stream route in APISIX\",\n        {\"route\"})\n\n    xrpc.init_metrics(prometheus)\nend\n\n\nfunction _M.http_init(prometheus_enabled_in_stream)\n    -- todo: support hot reload, we may need to update the lua-prometheus\n    -- library\n    if ngx.get_phase() ~= \"init\" and ngx.get_phase() ~= \"init_worker\"  then\n        if prometheus_bkp then\n            prometheus = prometheus_bkp\n        end\n        return\n    end\n\n    clear_tab(metrics)\n    -- Newly added metrics should follow the naming best practices described in\n    -- https://prometheus.io/docs/practices/naming/#metric-names\n    -- For example,\n    -- 1. Add unit as the suffix\n    -- 2. Add `_total` as the suffix if the metric type is counter\n    -- 3. Use base unit\n    -- We keep the old metric names for the compatibility.\n\n    -- across all services\n    local metric_prefix = \"apisix_\"\n    local attr = plugin.plugin_attr(\"prometheus\")\n    if attr and attr.metric_prefix then\n        metric_prefix = attr.metric_prefix\n    end\n\n    local status_metrics_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                   \"http_status\", \"expire\")\n    local latency_metrics_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                   \"http_latency\", \"expire\")\n    local bandwidth_metrics_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                   \"bandwidth\", \"expire\")\n    local upstream_status_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                   \"upstream_status\", \"expire\")\n    local llm_latency_exptime = core.table.try_read_attr(attr, \"metrics\", \"llm_latency\", \"expire\")\n    local llm_prompt_tokens_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                                            \"llm_prompt_tokens\", \"expire\")\n    local llm_completion_tokens_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                                            \"llm_completion_tokens\", \"expire\")\n    local llm_active_connections_exptime = core.table.try_read_attr(attr, \"metrics\",\n                                                            \"llm_active_connections\", \"expire\")\n\n    prometheus = base_prometheus.init(\"prometheus-metrics\", metric_prefix)\n\n    metrics.connections = prometheus:gauge(\"nginx_http_current_connections\",\n            \"Number of HTTP connections\",\n            {\"state\"})\n\n    metrics.requests = prometheus:gauge(\"http_requests_total\",\n            \"The total number of client requests since APISIX started\")\n\n    metrics.etcd_reachable = prometheus:gauge(\"etcd_reachable\",\n            \"Config server etcd reachable from APISIX, 0 is unreachable\")\n\n    metrics.node_info = prometheus:gauge(\"node_info\",\n            \"Info of APISIX node\",\n            {\"hostname\", \"version\"})\n\n    metrics.etcd_modify_indexes = prometheus:gauge(\"etcd_modify_indexes\",\n            \"Etcd modify index for APISIX keys\",\n            {\"key\"})\n\n    metrics.shared_dict_capacity_bytes = prometheus:gauge(\"shared_dict_capacity_bytes\",\n            \"The capacity of each nginx shared DICT since APISIX start\",\n            {\"name\"})\n\n    metrics.shared_dict_free_space_bytes = prometheus:gauge(\"shared_dict_free_space_bytes\",\n            \"The free space of each nginx shared DICT since APISIX start\",\n            {\"name\"})\n\n    metrics.upstream_status = prometheus:gauge(\"upstream_status\",\n            \"Upstream status from health check\",\n            {\"name\", \"ip\", \"port\"},\n            upstream_status_exptime)\n\n    -- per service\n\n    -- The consumer label indicates the name of consumer corresponds to the\n    -- request to the route/service, it will be an empty string if there is\n    -- no consumer in request.\n    metrics.status = prometheus:counter(\"http_status\",\n            \"HTTP status codes per service in APISIX\",\n            {\"code\", \"route\", \"matched_uri\", \"matched_host\", \"service\", \"consumer\", \"node\",\n            \"request_type\", \"request_llm_model\", \"llm_model\",\n            unpack(extra_labels(\"http_status\"))},\n            status_metrics_exptime)\n\n    local buckets = DEFAULT_BUCKETS\n    if attr and attr.default_buckets then\n        buckets = attr.default_buckets\n    end\n\n    metrics.latency = prometheus:histogram(\"http_latency\",\n        \"HTTP request latency in milliseconds per service in APISIX\",\n        {\"type\", \"route\", \"service\", \"consumer\", \"node\",\n        \"request_type\", \"request_llm_model\", \"llm_model\",\n        unpack(extra_labels(\"http_latency\"))},\n        buckets, latency_metrics_exptime)\n\n    metrics.bandwidth = prometheus:counter(\"bandwidth\",\n            \"Total bandwidth in bytes consumed per service in APISIX\",\n            {\"type\", \"route\", \"service\", \"consumer\", \"node\",\n            \"request_type\", \"request_llm_model\", \"llm_model\",\n            unpack(extra_labels(\"bandwidth\"))},\n            bandwidth_metrics_exptime)\n\n    local llm_latency_buckets = DEFAULT_BUCKETS\n    if attr and attr.llm_latency_buckets then\n        llm_latency_buckets = attr.llm_latency_buckets\n    end\n    metrics.llm_latency = prometheus:histogram(\"llm_latency\",\n        \"LLM request latency in milliseconds\",\n        {\"route_id\", \"service_id\", \"consumer\", \"node\",\n        \"request_type\", \"request_llm_model\", \"llm_model\",\n        unpack(extra_labels(\"llm_latency\"))},\n        llm_latency_buckets,\n        llm_latency_exptime)\n\n    metrics.llm_prompt_tokens = prometheus:counter(\"llm_prompt_tokens\",\n            \"LLM service consumed prompt tokens\",\n            {\"route_id\", \"service_id\", \"consumer\", \"node\",\n            \"request_type\", \"request_llm_model\", \"llm_model\",\n            unpack(extra_labels(\"llm_prompt_tokens\"))},\n            llm_prompt_tokens_exptime)\n\n    metrics.llm_completion_tokens = prometheus:counter(\"llm_completion_tokens\",\n            \"LLM service consumed completion tokens\",\n            {\"route_id\", \"service_id\", \"consumer\", \"node\",\n            \"request_type\", \"request_llm_model\", \"llm_model\",\n            unpack(extra_labels(\"llm_completion_tokens\"))},\n            llm_completion_tokens_exptime)\n\n    metrics.llm_active_connections = prometheus:gauge(\"llm_active_connections\",\n            \"Number of active connections to LLM service\",\n            {\"route\", \"route_id\", \"matched_uri\", \"matched_host\",\n            \"service\", \"service_id\", \"consumer\", \"node\",\n            \"request_type\", \"request_llm_model\", \"llm_model\",\n            unpack(extra_labels(\"llm_active_connections\"))},\n            llm_active_connections_exptime)\n\n    if prometheus_enabled_in_stream then\n        init_stream_metrics()\n    end\nend\n\n\nfunction _M.stream_init()\n    if ngx.get_phase() ~= \"init\" and ngx.get_phase() ~= \"init_worker\"  then\n        return\n    end\n\n    if not pcall(function() return C.ngx_meta_lua_ffi_shdict_udata_to_zone end) then\n        core.log.error(\"need to build APISIX-Runtime to support L4 metrics\")\n        return\n    end\n\n    clear_tab(metrics)\n\n    local metric_prefix = \"apisix_\"\n    local attr = plugin.plugin_attr(\"prometheus\")\n    if attr and attr.metric_prefix then\n        metric_prefix = attr.metric_prefix\n    end\n\n    prometheus = base_prometheus.init(\"prometheus-metrics\", metric_prefix)\n\n    init_stream_metrics()\nend\n\n\nfunction _M.http_log(conf, ctx)\n    local vars = ctx.var\n\n    local route_id = \"\"\n    local balancer_ip = ctx.balancer_ip or \"\"\n    local service_id = \"\"\n    local consumer_name = ctx.consumer_name or \"\"\n\n    local matched_route = ctx.matched_route and ctx.matched_route.value\n    if matched_route then\n        route_id = matched_route.id\n        service_id = matched_route.service_id or \"\"\n        if conf.prefer_name == true then\n            route_id = matched_route.name or route_id\n            if service_id ~= \"\" then\n                local service = service_fetch(service_id)\n                service_id = service and service.value.name or service_id\n            end\n        end\n    end\n\n    local matched_uri = \"\"\n    local matched_host = \"\"\n    if ctx.curr_req_matched then\n        matched_uri = ctx.curr_req_matched._path or \"\"\n        matched_host = ctx.curr_req_matched._host or \"\"\n    end\n\n    metrics.status:inc(1,\n        gen_arr(vars.status, route_id, matched_uri, matched_host,\n                service_id, consumer_name, balancer_ip,\n                vars.request_type, vars.request_llm_model, vars.llm_model,\n                unpack(extra_labels(\"http_status\", ctx))))\n\n    local latency, upstream_latency, apisix_latency = latency_details(ctx)\n    local latency_extra_label_values = extra_labels(\"http_latency\", ctx)\n\n    metrics.latency:observe(latency,\n        gen_arr(\"request\", route_id, service_id, consumer_name, balancer_ip,\n        vars.request_type, vars.request_llm_model, vars.llm_model,\n        unpack(latency_extra_label_values)))\n\n    if upstream_latency then\n        metrics.latency:observe(upstream_latency,\n            gen_arr(\"upstream\", route_id, service_id, consumer_name, balancer_ip,\n            vars.request_type, vars.request_llm_model, vars.llm_model,\n            unpack(latency_extra_label_values)))\n    end\n\n    metrics.latency:observe(apisix_latency,\n        gen_arr(\"apisix\", route_id, service_id, consumer_name, balancer_ip,\n        vars.request_type, vars.request_llm_model, vars.llm_model,\n        unpack(latency_extra_label_values)))\n\n    local bandwidth_extra_label_values = extra_labels(\"bandwidth\", ctx)\n\n    metrics.bandwidth:inc(vars.request_length,\n        gen_arr(\"ingress\", route_id, service_id, consumer_name, balancer_ip,\n        vars.request_type, vars.request_llm_model, vars.llm_model,\n        unpack(bandwidth_extra_label_values)))\n\n    metrics.bandwidth:inc(vars.bytes_sent,\n        gen_arr(\"egress\", route_id, service_id, consumer_name, balancer_ip,\n        vars.request_type, vars.request_llm_model, vars.llm_model,\n        unpack(bandwidth_extra_label_values)))\n\n    local llm_time_to_first_token = vars.llm_time_to_first_token\n    if llm_time_to_first_token ~= \"\" then\n        metrics.llm_latency:observe(tonumber(llm_time_to_first_token),\n            gen_arr(route_id, service_id, consumer_name, balancer_ip,\n            vars.request_type, vars.request_llm_model, vars.llm_model,\n            unpack(extra_labels(\"llm_latency\", ctx))))\n    end\n    if vars.llm_prompt_tokens ~= \"\" then\n        metrics.llm_prompt_tokens:inc(tonumber(vars.llm_prompt_tokens),\n            gen_arr(route_id, service_id, consumer_name, balancer_ip,\n            vars.request_type, vars.request_llm_model, vars.llm_model,\n            unpack(extra_labels(\"llm_prompt_tokens\", ctx))))\n    end\n    if vars.llm_completion_tokens ~= \"\" then\n        metrics.llm_completion_tokens:inc(tonumber(vars.llm_completion_tokens),\n            gen_arr(route_id, service_id, consumer_name, balancer_ip,\n            vars.request_type, vars.request_llm_model, vars.llm_model,\n            unpack(extra_labels(\"llm_completion_tokens\", ctx))))\n    end\nend\n\n\nfunction _M.stream_log(conf, ctx)\n    local route_id = \"\"\n    local matched_route = ctx.matched_route and ctx.matched_route.value\n    if matched_route then\n        route_id = matched_route.id\n        if conf.prefer_name == true then\n            route_id = matched_route.name or route_id\n        end\n    end\n\n    metrics.stream_connection_total:inc(1, gen_arr(route_id))\nend\n\n\n-- FFI definitions for nginx connection status\n-- Based on https://github.com/nginx/nginx/blob/master/src/event/ngx_event.c#L61-L78\nffi.cdef[[\n    typedef uint64_t ngx_atomic_uint_t;\n    extern ngx_atomic_uint_t  *ngx_stat_accepted;\n    extern ngx_atomic_uint_t  *ngx_stat_handled;\n    extern ngx_atomic_uint_t  *ngx_stat_requests;\n    extern ngx_atomic_uint_t  *ngx_stat_active;\n    extern ngx_atomic_uint_t  *ngx_stat_reading;\n    extern ngx_atomic_uint_t  *ngx_stat_writing;\n    extern ngx_atomic_uint_t  *ngx_stat_waiting;\n]]\n\nlocal label_values = {}\n\n-- Mapping of status names to FFI global variables and metrics\nlocal status_mapping = {\n    {name = \"active\", var = \"ngx_stat_active\"},\n    {name = \"accepted\", var = \"ngx_stat_accepted\"},\n    {name = \"handled\", var = \"ngx_stat_handled\"},\n    {name = \"total\", var = \"ngx_stat_requests\"},\n    {name = \"reading\", var = \"ngx_stat_reading\"},\n    {name = \"writing\", var = \"ngx_stat_writing\"},\n    {name = \"waiting\", var = \"ngx_stat_waiting\"},\n}\n\n-- Use FFI to get nginx status directly from global variables\nlocal function nginx_status()\n    for _, item in ipairs(status_mapping) do\n        local ok, value = pcall(function()\n            local stat_ptr = C[item.var]\n            return stat_ptr and tonumber(stat_ptr[0]) or 0\n        end)\n\n        if not ok then\n            core.log.error(\"failed to read \", item.name, \" via FFI\")\n            return\n        end\n\n        if item.name == \"total\" then\n            metrics.requests:set(value)\n        else\n            label_values[1] = item.name\n            metrics.connections:set(value, label_values)\n        end\n    end\nend\n\n\nlocal key_values = {}\nlocal function set_modify_index(key, items, items_ver, global_max_index)\n    clear_tab(key_values)\n    local max_idx = 0\n    if items_ver and items then\n        for _, item in ipairs(items) do\n            if type(item) == \"table\" then\n                local modify_index = item.orig_modifiedIndex or item.modifiedIndex\n                if modify_index > max_idx then\n                    max_idx = modify_index\n                end\n            end\n        end\n    end\n\n    key_values[1] = key\n    metrics.etcd_modify_indexes:set(max_idx, key_values)\n\n\n    global_max_index = max_idx > global_max_index and max_idx or global_max_index\n\n    return global_max_index\nend\n\n\nlocal function etcd_modify_index()\n    clear_tab(key_values)\n    local global_max_idx = 0\n\n    -- routes\n    local routes, routes_ver = get_routes()\n    global_max_idx = set_modify_index(\"routes\", routes, routes_ver, global_max_idx)\n\n    -- services\n    local services, services_ver = get_services()\n    global_max_idx = set_modify_index(\"services\", services, services_ver, global_max_idx)\n\n    -- ssls\n    local ssls, ssls_ver = get_ssls()\n    global_max_idx = set_modify_index(\"ssls\", ssls, ssls_ver, global_max_idx)\n\n    -- consumers\n    local consumers, consumers_ver = get_consumers()\n    global_max_idx = set_modify_index(\"consumers\", consumers, consumers_ver, global_max_idx)\n\n    -- global_rules\n    local global_rules, global_rules_ver = get_global_rules()\n    if global_rules then\n        global_max_idx = set_modify_index(\"global_rules\", global_rules,\n            global_rules_ver, global_max_idx)\n\n        -- prev_index\n        key_values[1] = \"prev_index\"\n        local prev_index = get_global_rules_prev_index()\n        metrics.etcd_modify_indexes:set(prev_index, key_values)\n\n    else\n        global_max_idx = set_modify_index(\"global_rules\", nil, nil, global_max_idx)\n    end\n\n    -- upstreams\n    local upstreams, upstreams_ver = get_upstreams()\n    global_max_idx = set_modify_index(\"upstreams\", upstreams, upstreams_ver, global_max_idx)\n\n    -- stream_routes\n    local stream_routes, stream_routes_ver = get_stream_routes()\n    global_max_idx = set_modify_index(\"stream_routes\", stream_routes,\n        stream_routes_ver, global_max_idx)\n\n    -- proto\n    local protos, protos_ver = get_protos()\n    global_max_idx = set_modify_index(\"protos\", protos, protos_ver, global_max_idx)\n\n    -- global max\n    key_values[1] = \"max_modify_index\"\n    metrics.etcd_modify_indexes:set(global_max_idx, key_values)\n\nend\n\n\nlocal function shared_dict_status()\n    local name = {}\n    for shared_dict_name, shared_dict in pairs(ngx.shared) do\n        name[1] = shared_dict_name\n        metrics.shared_dict_capacity_bytes:set(shared_dict:capacity(), name)\n        metrics.shared_dict_free_space_bytes:set(shared_dict:free_space(), name)\n    end\nend\n\n\nlocal function collect(yieldable)\n    -- collect ngx.shared.DICT status\n    shared_dict_status()\n\n    -- across all services\n    nginx_status()\n\n    local config = core.config.new()\n\n    -- config server status\n    local hostname = core.utils.gethostname() or \"\"\n    local version = core.version.VERSION or \"\"\n\n    local local_conf = core.config.local_conf()\n    local stream_only = local_conf.apisix.proxy_mode == \"stream\"\n    -- we can't get etcd index in metric server if only stream subsystem is enabled\n    if config.type == \"etcd\" and not stream_only then\n        -- etcd modify index\n        etcd_modify_index()\n\n        if yieldable then\n            local version, err = config:server_version()\n            if version then\n                metrics.etcd_reachable:set(1)\n\n            else\n                metrics.etcd_reachable:set(0)\n                core.log.error(\"prometheus: failed to reach config server while \",\n                            \"processing metrics endpoint: \", err)\n            end\n\n            -- Because request any key from etcd will return the \"X-Etcd-Index\".\n            -- A non-existed key is preferred because it doesn't return too much data.\n            -- So use phantom key to get etcd index.\n            local res, _ = config:getkey(\"/phantomkey\")\n            if res and res.headers then\n                clear_tab(key_values)\n                -- global max\n                key_values[1] = \"x_etcd_index\"\n                metrics.etcd_modify_indexes:set(res.headers[\"X-Etcd-Index\"], key_values)\n            end\n        end\n    end\n\n    metrics.node_info:set(1, gen_arr(hostname, version))\n\n    -- update upstream_status metrics\n    local stats = control.get_health_checkers()\n    for _, stat in ipairs(stats) do\n        for _, node in ipairs(stat.nodes) do\n            metrics.upstream_status:set(\n                    (node.status == \"healthy\" or node.status == \"mostly_healthy\") and 1 or 0,\n                    gen_arr(stat.name, node.ip, node.port)\n            )\n        end\n    end\n\n    return core.table.concat(prometheus:metric_data())\nend\n\n\nlocal function exporter_timer(premature, yieldable, cache_exptime)\n    if premature then\n        return\n    end\n\n    if not prometheus then\n        return\n    end\n\n    if exporter_timer_running then\n        core.log.warn(\"Previous calculation still running, skipping\")\n        return\n    end\n\n    exporter_timer_running = true\n\n    local ok, res = pcall(collect, yieldable)\n    if not ok then\n        core.log.error(\"Failed to collect metrics: \", res)\n        exporter_timer_running = false\n        return\n    end\n\n    -- Clear the cached data after cache_exptime to prevent stale data in case of an error.\n    local _, err, forcible = shdict_prometheus_cache:set(CACHED_METRICS_KEY, res, cache_exptime)\n    if err then\n        core.log.error(\"Failed to save metrics to the `prometheus-cache` shared dict: \", err,\n                    \". The size of the value being attempted to be saved is: \", #res)\n    end\n\n    if forcible then\n        core.log.error(\"Shared dictionary used for prometheus cache \" ..\n  \"is full. REPORTED METRIC DATA MIGHT BE INCOMPLETE. Please increase the \" ..\n  \"size of the `prometheus-cache` shared dict or decrease metric cardinality.\")\n    end\n\n    exporter_timer_running = false\nend\n\n\nlocal function init_exporter_timer()\n    if process.type() ~= \"privileged agent\" then\n        return\n    end\n\n    local refresh_interval = DEFAULT_REFRESH_INTERVAL\n    local attr = plugin.plugin_attr(\"prometheus\")\n    if attr and attr.refresh_interval then\n        refresh_interval = attr.refresh_interval\n    end\n\n    local cache_exptime = refresh_interval * 2\n\n    exporter_timer(false, false, cache_exptime)\n\n    if exporter_timer_created then\n        core.log.info(\"Exporter timer already created, skipping\")\n        return\n    end\n\n    -- When the APISIX configuration `refresh_interval` is updated,\n    -- the timer will not restart, and the new `refresh_interval` will not be applied.\n    -- APISIX needs to be restarted to apply the changes.\n    local ok, err = ngx_timer_every(refresh_interval, exporter_timer, true, cache_exptime)\n\n    if ok then\n        exporter_timer_created = true\n    else\n        core.log.error(\"Failed to start the exporter timer: \", err)\n    end\nend\n_M.init_exporter_timer = init_exporter_timer\n\n\nlocal function get_cached_metrics()\n    if not prometheus or not metrics then\n        core.log.error(\"prometheus: plugin is not initialized, please make sure \",\n                     \" 'prometheus_metrics' shared dict is present in nginx template\")\n        return 500, {message = \"An unexpected error occurred\"}\n    end\n\n    core.response.set_header(\"content_type\", \"text/plain\")\n    local cached_metrics_text, err = shdict_prometheus_cache:get(CACHED_METRICS_KEY)\n    if err then\n        core.log.error(\"Failed to retrieve cached metrics: err: \", err)\n        return 500, {message = \"Failed to retrieve metrics. err: \" .. err}\n    end\n    if not cached_metrics_text then\n        core.log.error(\"Failed to retrieve cached metrics: data is nil\")\n        return 500, {message = \"Failed to retrieve metrics: no data available\"}\n    end\n\n    return 200, cached_metrics_text\nend\n\n\nlocal function get_api(called_by_api_router)\n    local export_uri = default_export_uri\n    local attr = plugin.plugin_attr(plugin_name)\n    if attr and attr.export_uri then\n        export_uri = attr.export_uri\n    end\n\n    local api = {\n        methods = {\"GET\"},\n        uri = export_uri,\n        handler = get_cached_metrics\n    }\n\n    if not called_by_api_router then\n        return api\n    end\n\n    if attr.enable_export_server then\n        return {}\n    end\n\n    return {api}\nend\n_M.get_api = get_api\n\n\nfunction _M.export_metrics()\n    if not prometheus then\n        return core.response.exit(200, \"{}\")\n    end\n    local api = get_api(false)\n    local uri = ngx.var.uri\n    local method = ngx.req.get_method()\n\n    if uri == api.uri and method == api.methods[1] then\n        local code, body = api.handler()\n        if code or body then\n            core.response.exit(code, body)\n        end\n    end\n\n    return core.response.exit(404)\nend\n\n\nfunction _M.metric_data()\n    return prometheus:metric_data()\nend\n\nlocal function inc_llm_active_connections(ctx, value)\n\n    local vars = ctx.var\n\n    local route_id = \"\"\n    local route_name = \"\"\n    local balancer_ip = ctx.balancer_ip or \"\"\n    local service_id = \"\"\n    local service_name = \"\"\n    local consumer_name = ctx.consumer_name or \"\"\n\n    local matched_route = ctx.matched_route and ctx.matched_route.value\n    if matched_route then\n        route_id = matched_route.id\n        route_name = matched_route.name or \"\"\n        service_id = matched_route.service_id or \"\"\n        if service_id ~= \"\" then\n            local fetched_service = service_fetch(service_id)\n            service_name = fetched_service and fetched_service.value.name or \"\"\n        end\n    end\n\n    local matched_uri = \"\"\n    local matched_host = \"\"\n    if ctx.curr_req_matched then\n        matched_uri = ctx.curr_req_matched._path or \"\"\n        matched_host = ctx.curr_req_matched._host or \"\"\n    end\n\n    metrics.llm_active_connections:inc(\n        value,\n        gen_arr(route_name, route_id, matched_uri,\n            matched_host, service_name, service_id, consumer_name, balancer_ip,\n            vars.request_type, vars.request_llm_model, vars.llm_model,\n        unpack(extra_labels(\"llm_active_connections\", ctx)))\n    )\nend\n\n\nfunction _M.inc_llm_active_connections(ctx)\n    inc_llm_active_connections(ctx, 1)\nend\n\n\nfunction _M.dec_llm_active_connections(ctx)\n    inc_llm_active_connections(ctx, -1)\nend\n\nfunction _M.get_prometheus()\n    return prometheus\nend\n\n\nfunction _M.destroy()\n    if prometheus ~= nil then\n        prometheus_bkp = prometheus\n        prometheus = nil\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/prometheus.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal exporter = require(\"apisix.plugins.prometheus.exporter\")\n\nlocal plugin_name = \"prometheus\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        prefer_name = {\n            type = \"boolean\",\n            default = false\n        }\n    },\n}\n\n\nlocal _M = {\n    version = 0.2,\n    priority = 500,\n    name = plugin_name,\n    log  = exporter.http_log,\n    destroy = exporter.destroy,\n    schema = schema,\n    run_policy = \"prefer_route\",\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.api()\n    return exporter.get_api(true)\nend\n\n\nfunction _M.init()\n    local local_conf = core.config.local_conf()\n    local enabled_in_stream = core.table.array_find(local_conf.stream_plugins, \"prometheus\")\n    exporter.http_init(enabled_in_stream)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-cache/disk_handler.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal os = os\nlocal ngx_re = require(\"ngx.re\")\nlocal core = require(\"apisix.core\")\nlocal util = require(\"apisix.plugins.proxy-cache.util\")\n\nlocal _M = {}\n\n\nlocal function disk_cache_purge(conf, ctx)\n    local cache_zone_info = ngx_re.split(ctx.var.upstream_cache_zone_info, \",\")\n\n    local filename = util.generate_cache_filename(cache_zone_info[1], cache_zone_info[2],\n        ctx.var.upstream_cache_key)\n\n    if util.file_exists(filename) then\n        os.remove(filename)\n        return nil\n    end\n\n    return \"Not found\"\nend\n\n\nfunction _M.access(conf, ctx)\n    ctx.var.upstream_cache_zone = conf.cache_zone\n\n    if ctx.var.request_method == \"PURGE\" then\n        local err = disk_cache_purge(conf, ctx)\n        if err ~= nil then\n            return 404\n        end\n\n        return 200\n    end\n\n    if conf.cache_bypass ~= nil then\n        local value = util.generate_complex_value(conf.cache_bypass, ctx)\n        ctx.var.upstream_cache_bypass = value\n        core.log.info(\"proxy-cache cache bypass value:\", value)\n    end\n\n    if not util.match_method(conf, ctx) then\n        ctx.var.upstream_cache_bypass = \"1\"\n        core.log.info(\"proxy-cache cache bypass method: \", ctx.var.request_method)\n    end\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    local no_cache = \"1\"\n\n    if util.match_method(conf, ctx) and util.match_status(conf, ctx) then\n        no_cache = \"0\"\n    end\n\n    if conf.no_cache ~= nil then\n        local value = util.generate_complex_value(conf.no_cache, ctx)\n        core.log.info(\"proxy-cache no-cache value:\", value)\n\n        if value ~= nil and value ~= \"\" and value ~= \"0\" then\n            no_cache = \"1\"\n        end\n    end\n\n    local upstream_hdr_cache_control\n    local upstream_hdr_expires\n\n    if conf.hide_cache_headers == true then\n        upstream_hdr_cache_control = \"\"\n        upstream_hdr_expires = \"\"\n    else\n        upstream_hdr_cache_control = ctx.var.upstream_http_cache_control\n        upstream_hdr_expires = ctx.var.upstream_http_expires\n    end\n\n    core.response.set_header(\"Cache-Control\", upstream_hdr_cache_control,\n        \"Expires\", upstream_hdr_expires,\n        \"Apisix-Cache-Status\", ctx.var.upstream_cache_status)\n\n    ctx.var.upstream_no_cache = no_cache\n    core.log.info(\"proxy-cache no cache:\", no_cache)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-cache/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal memory_handler = require(\"apisix.plugins.proxy-cache.memory_handler\")\nlocal disk_handler = require(\"apisix.plugins.proxy-cache.disk_handler\")\nlocal util = require(\"apisix.plugins.proxy-cache.util\")\nlocal core = require(\"apisix.core\")\nlocal ipairs = ipairs\n\nlocal plugin_name = \"proxy-cache\"\n\nlocal STRATEGY_DISK = \"disk\"\nlocal STRATEGY_MEMORY = \"memory\"\nlocal DEFAULT_CACHE_ZONE = \"disk_cache_one\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        cache_zone = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 100,\n            default = DEFAULT_CACHE_ZONE,\n        },\n        cache_strategy = {\n            type = \"string\",\n            enum = {STRATEGY_DISK, STRATEGY_MEMORY},\n            default = STRATEGY_DISK,\n        },\n        cache_key = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                description = \"a key for caching\",\n                type = \"string\",\n                pattern = [[(^[^\\$].+$|^\\$[0-9a-zA-Z_]+$)]],\n            },\n            default = {\"$host\", \"$request_uri\"}\n        },\n        cache_http_status = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                description = \"http response status\",\n                type = \"integer\",\n                minimum = 200,\n                maximum = 599,\n            },\n            uniqueItems = true,\n            default = {200, 301, 404},\n        },\n        cache_method = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                description = \"supported http method\",\n                type = \"string\",\n                enum = {\"GET\", \"POST\", \"HEAD\"},\n            },\n            uniqueItems = true,\n            default = {\"GET\", \"HEAD\"},\n        },\n        hide_cache_headers = {\n            type = \"boolean\",\n            default = false,\n        },\n        cache_control = {\n            type = \"boolean\",\n            default = false,\n        },\n        cache_bypass = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                pattern = [[(^[^\\$].+$|^\\$[0-9a-zA-Z_]+$)]]\n            },\n        },\n        no_cache = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                pattern = [[(^[^\\$].+$|^\\$[0-9a-zA-Z_]+$)]]\n            },\n        },\n        cache_ttl = {\n            type = \"integer\",\n            minimum = 1,\n            default = 300,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.2,\n    priority = 1085,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    for _, key in ipairs(conf.cache_key) do\n        if key == \"$request_method\" then\n            return false, \"cache_key variable \" .. key .. \" unsupported\"\n        end\n    end\n\n    local found = false\n    local local_conf = core.config.local_conf()\n    if local_conf.apisix.proxy_cache then\n        local err = \"cache_zone \" .. conf.cache_zone .. \" not found\"\n        for _, cache in ipairs(local_conf.apisix.proxy_cache.zones) do\n            -- cache_zone passed in plugin config matched one of the proxy_cache zones\n            if cache.name == conf.cache_zone then\n                -- check for the mismatch between cache_strategy and corresponding cache zone\n                if (conf.cache_strategy == STRATEGY_MEMORY and cache.disk_path) or\n                (conf.cache_strategy == STRATEGY_DISK and not cache.disk_path) then\n                    err =  \"invalid or empty cache_zone for cache_strategy: \"..conf.cache_strategy\n                else\n                    found = true\n                end\n                break\n            end\n        end\n\n        if found == false then\n            return false, err\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    core.log.info(\"proxy-cache plugin access phase, conf: \", core.json.delay_encode(conf))\n\n    local value = util.generate_complex_value(conf.cache_key, ctx)\n    ctx.var.upstream_cache_key = value\n    core.log.info(\"proxy-cache cache key value:\", value)\n\n    local handler\n    if conf.cache_strategy == STRATEGY_MEMORY then\n        handler = memory_handler\n    else\n        handler = disk_handler\n    end\n\n    return handler.access(conf, ctx)\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    core.log.info(\"proxy-cache plugin header filter phase, conf: \", core.json.delay_encode(conf))\n\n    local handler\n    if conf.cache_strategy == STRATEGY_MEMORY then\n        handler = memory_handler\n    else\n        handler = disk_handler\n    end\n\n    handler.header_filter(conf, ctx)\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    core.log.info(\"proxy-cache plugin body filter phase, conf: \", core.json.delay_encode(conf))\n\n    if conf.cache_strategy == STRATEGY_MEMORY then\n        memory_handler.body_filter(conf, ctx)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-cache/memory.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = ngx\nlocal ngx_shared = ngx.shared\nlocal setmetatable = setmetatable\nlocal core = require(\"apisix.core\")\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\nfunction _M.new(opts)\n    return setmetatable({\n        dict = ngx_shared[opts.shdict_name],\n    }, mt)\nend\n\n\nfunction _M:set(key, obj, ttl)\n    if self.dict == nil then\n        return nil, \"invalid cache_zone provided\"\n    end\n\n    local obj_json = core.json.encode(obj)\n    if not obj_json then\n        return nil, \"could not encode object\"\n    end\n\n    local succ, err = self.dict:set(key, obj_json, ttl)\n    return succ and obj_json or nil, err\nend\n\n\nfunction _M:get(key)\n    if self.dict == nil then\n        return nil, \"invalid cache_zone provided\"\n    end\n\n    -- If the key does not exist or has expired, then res_json will be nil.\n    local res_json, err, stale = self.dict:get_stale(key)\n    if not res_json then\n        if not err then\n            return nil, \"not found\"\n        else\n            return nil, err\n        end\n    end\n    if stale then\n        return nil, \"expired\"\n    end\n\n    local res_obj, err = core.json.decode(res_json)\n    if not res_obj then\n        return nil, err\n    end\n\n    return res_obj, nil\nend\n\n\nfunction _M:purge(key)\n    if self.dict == nil then\n        return nil, \"invalid cache_zone provided\"\n    end\n    self.dict:delete(key)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-cache/memory_handler.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal memory_strategy = require(\"apisix.plugins.proxy-cache.memory\").new\nlocal util = require(\"apisix.plugins.proxy-cache.util\")\nlocal core = require(\"apisix.core\")\nlocal tab_new = require(\"table.new\")\nlocal ngx_re_gmatch = ngx.re.gmatch\nlocal ngx_re_match = ngx.re.match\nlocal parse_http_time = ngx.parse_http_time\nlocal concat = table.concat\nlocal lower = string.lower\nlocal floor = math.floor\nlocal tostring = tostring\nlocal tonumber = tonumber\nlocal ngx = ngx\nlocal type = type\nlocal pairs = pairs\nlocal time = ngx.now\nlocal max = math.max\n\nlocal CACHE_VERSION = 1\n\nlocal _M = {}\n\n-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1\n-- note content-length & apisix-cache-status are not strictly\n-- hop-by-hop but we will be adjusting it here anyhow\nlocal hop_by_hop_headers = {\n    [\"connection\"]          = true,\n    [\"keep-alive\"]          = true,\n    [\"proxy-authenticate\"]  = true,\n    [\"proxy-authorization\"] = true,\n    [\"te\"]                  = true,\n    [\"trailers\"]            = true,\n    [\"transfer-encoding\"]   = true,\n    [\"upgrade\"]             = true,\n    [\"content-length\"]      = true,\n    [\"apisix-cache-status\"] = true,\n}\n\n\nlocal function include_cache_header(header)\n    local n_header = lower(header)\n    if n_header == \"expires\" or n_header == \"cache-control\" then\n        return true\n    end\n\n    return false\nend\n\n\nlocal function overwritable_header(header)\n    local n_header = lower(header)\n\n    return not hop_by_hop_headers[n_header]\n            and not ngx_re_match(n_header, \"ratelimit-remaining\")\nend\n\n\n-- The following format can accept:\n--      Cache-Control: no-cache\n--      Cache-Control: no-store\n--      Cache-Control: max-age=3600\n--      Cache-Control: max-stale=3600\n--      Cache-Control: min-fresh=3600\n--      Cache-Control: private, max-age=600\n--      Cache-Control: public, max-age=31536000\n-- Refer to: https://www.holisticseo.digital/pagespeed/cache-control/\nlocal function parse_directive_header(h)\n    if not h then\n        return {}\n    end\n\n    if type(h) == \"table\" then\n        h = concat(h, \", \")\n    end\n\n    local t    = {}\n    local res  = tab_new(3, 0)\n    local iter = ngx_re_gmatch(h, \"([^,]+)\", \"oj\")\n\n    local m = iter()\n    while m do\n        local _, err = ngx_re_match(m[0], [[^\\s*([^=]+)(?:=(.+))?]],\n            \"oj\", nil, res)\n        if err then\n            core.log.error(err)\n        end\n\n        -- store the directive token as a numeric value if it looks like a number;\n        -- otherwise, store the string value. for directives without token, we just\n        -- set the key to true\n        t[lower(res[1])] = tonumber(res[2]) or res[2] or true\n\n        m = iter()\n    end\n\n    return t\nend\n\n\nlocal function parse_resource_ttl(ctx, cc)\n    local max_age = cc[\"s-maxage\"] or cc[\"max-age\"]\n\n    if not max_age then\n        local expires = ctx.var.upstream_http_expires\n\n        -- if multiple Expires headers are present, last one wins\n        if type(expires) == \"table\" then\n            expires = expires[#expires]\n        end\n\n        local exp_time = parse_http_time(tostring(expires))\n        if exp_time then\n            max_age = exp_time - time()\n        end\n    end\n\n    return max_age and max(max_age, 0) or 0\nend\n\n\nlocal function cacheable_request(conf, ctx, cc)\n    if not util.match_method(conf, ctx) then\n        return false, \"MISS\"\n    end\n\n    if conf.cache_bypass ~= nil then\n        local value = util.generate_complex_value(conf.cache_bypass, ctx)\n        core.log.info(\"proxy-cache cache bypass value:\", value)\n        if value ~= nil and value ~= \"\" and value ~= \"0\" then\n            return false, \"BYPASS\"\n        end\n    end\n\n    if conf.cache_control and (cc[\"no-store\"] or cc[\"no-cache\"]) then\n        return false, \"BYPASS\"\n    end\n\n    return true, \"\"\nend\n\n\nlocal function cacheable_response(conf, ctx, cc)\n    if not util.match_status(conf, ctx) then\n        return false\n    end\n\n    if conf.no_cache ~= nil then\n        local value = util.generate_complex_value(conf.no_cache, ctx)\n        core.log.info(\"proxy-cache no-cache value:\", value)\n\n        if value ~= nil and value ~= \"\" and value ~= \"0\" then\n            return false\n        end\n    end\n\n    if conf.cache_control and (cc[\"private\"] or cc[\"no-store\"] or cc[\"no-cache\"]) then\n        return false\n    end\n\n    if conf.cache_control and parse_resource_ttl(ctx, cc) <= 0 then\n        return false\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    local cc = parse_directive_header(ctx.var.http_cache_control)\n\n    if ctx.var.request_method ~= \"PURGE\" then\n        local ret, msg = cacheable_request(conf, ctx, cc)\n        if not ret then\n            core.response.set_header(\"Apisix-Cache-Status\", msg)\n            return\n        end\n    end\n\n    if not ctx.cache then\n        ctx.cache = {\n            memory = memory_strategy({shdict_name = conf.cache_zone}),\n            hit = false,\n            ttl = 0,\n        }\n    end\n\n    local res, err = ctx.cache.memory:get(ctx.var.upstream_cache_key)\n\n    if ctx.var.request_method == \"PURGE\" then\n        if err == \"not found\" then\n            return 404\n        end\n        ctx.cache.memory:purge(ctx.var.upstream_cache_key)\n        ctx.cache = nil\n        return 200\n    end\n\n    if err then\n        if err == \"expired\" then\n            core.response.set_header(\"Apisix-Cache-Status\", \"EXPIRED\")\n\n        elseif err ~= \"not found\" then\n            core.response.set_header(\"Apisix-Cache-Status\", \"MISS\")\n            core.log.error(\"failed to get from cache, err: \", err)\n\n        elseif conf.cache_control and cc[\"only-if-cached\"] then\n            core.response.set_header(\"Apisix-Cache-Status\", \"MISS\")\n            return 504\n\n        else\n            core.response.set_header(\"Apisix-Cache-Status\", \"MISS\")\n        end\n        return\n    end\n\n    if res.version ~= CACHE_VERSION then\n        core.log.warn(\"cache format mismatch, purging \", ctx.var.upstream_cache_key)\n        core.response.set_header(\"Apisix-Cache-Status\", \"BYPASS\")\n        ctx.cache.memory:purge(ctx.var.upstream_cache_key)\n        return\n    end\n\n    if conf.cache_control then\n        if cc[\"max-age\"] and time() - res.timestamp > cc[\"max-age\"] then\n            core.response.set_header(\"Apisix-Cache-Status\", \"STALE\")\n            return\n        end\n\n        if cc[\"max-stale\"] and time() - res.timestamp - res.ttl > cc[\"max-stale\"] then\n            core.response.set_header(\"Apisix-Cache-Status\", \"STALE\")\n            return\n        end\n\n        if cc[\"min-fresh\"] and res.ttl - (time() - res.timestamp) < cc[\"min-fresh\"] then\n            core.response.set_header(\"Apisix-Cache-Status\", \"STALE\")\n            return\n        end\n    else\n        if time() - res.timestamp > res.ttl then\n            core.response.set_header(\"Apisix-Cache-Status\", \"STALE\")\n            return\n        end\n    end\n\n    ctx.cache.hit = true\n\n    for key, value in pairs(res.headers) do\n        if conf.hide_cache_headers == true and include_cache_header(key) then\n            core.response.set_header(key, \"\")\n        elseif overwritable_header(key) then\n            core.response.set_header(key, value)\n        end\n    end\n\n    core.response.set_header(\"Age\", floor(time() - res.timestamp))\n    core.response.set_header(\"Apisix-Cache-Status\", \"HIT\")\n\n    return res.status, res.body\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    local cache = ctx.cache\n    if not cache or cache.hit then\n        return\n    end\n\n    local res_headers = ngx.resp.get_headers(0, true)\n\n    for key in pairs(res_headers) do\n        if conf.hide_cache_headers == true and include_cache_header(key) then\n            core.response.set_header(key, \"\")\n        end\n    end\n\n    local cc = parse_directive_header(ctx.var.upstream_http_cache_control)\n\n    if cacheable_response(conf, ctx, cc) then\n        cache.res_headers = res_headers\n        cache.ttl = conf.cache_control and parse_resource_ttl(ctx, cc) or conf.cache_ttl\n    else\n        ctx.cache = nil\n    end\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    local cache = ctx.cache\n    if not cache or cache.hit then\n        return\n    end\n\n    local res_body = core.response.hold_body_chunk(ctx, true)\n    if not res_body then\n        return\n    end\n\n    local res = {\n        status    = ngx.status,\n        body      = res_body,\n        body_len  = #res_body,\n        headers   = cache.res_headers,\n        ttl       = cache.ttl,\n        timestamp = time(),\n        version   = CACHE_VERSION,\n    }\n\n    local res, err = cache.memory:set(ctx.var.upstream_cache_key, res, cache.ttl)\n    if not res then\n        core.log.error(\"failed to set cache, err: \", err)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-cache/util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal ngx_re = require(\"ngx.re\")\nlocal tab_concat = table.concat\nlocal string = string\nlocal io_open = io.open\nlocal io_close = io.close\nlocal ngx = ngx\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal tonumber = tonumber\n\nlocal _M = {}\n\nlocal tmp = {}\nfunction _M.generate_complex_value(data, ctx)\n    core.table.clear(tmp)\n\n    core.log.info(\"proxy-cache complex value: \", core.json.delay_encode(data))\n    for i, value in ipairs(data) do\n        core.log.info(\"proxy-cache complex value index-\", i, \": \", value)\n\n        if string.byte(value, 1, 1) == string.byte('$') then\n            tmp[i] = ctx.var[string.sub(value, 2)] or \"\"\n        else\n            tmp[i] = value\n        end\n    end\n\n    return tab_concat(tmp, \"\")\nend\n\n\n-- check whether the request method match the user defined.\nfunction _M.match_method(conf, ctx)\n    for _, method in ipairs(conf.cache_method) do\n        if method == ctx.var.request_method then\n            return true\n        end\n    end\n\n    return false\nend\n\n\n-- check whether the response status match the user defined.\nfunction _M.match_status(conf, ctx)\n    for _, status in ipairs(conf.cache_http_status) do\n        if status == ngx.status then\n            return true\n        end\n    end\n\n    return false\nend\n\n\nfunction _M.file_exists(name)\n    local f = io_open(name, \"r\")\n    if f ~= nil then\n        io_close(f)\n        return true\n    end\n    return false\nend\n\n\nfunction _M.generate_cache_filename(cache_path, cache_levels, cache_key)\n    local md5sum = ngx.md5(cache_key)\n    local levels = ngx_re.split(cache_levels, \":\")\n    local filename = \"\"\n\n    local index = #md5sum\n    for k, v in pairs(levels) do\n        local length = tonumber(v)\n        index = index - length\n        filename = filename .. md5sum:sub(index+1, index+length) .. \"/\"\n    end\n    if cache_path:sub(-1) ~= \"/\" then\n        cache_path = cache_path .. \"/\"\n    end\n    filename = cache_path .. filename .. md5sum\n    return filename\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-control.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal ok, apisix_ngx_client = pcall(require, \"resty.apisix.client\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        request_buffering = {\n            type = \"boolean\",\n            default = true,\n        },\n    },\n}\n\n\nlocal plugin_name = \"proxy-control\"\nlocal _M = {\n    version = 0.1,\n    priority = 21990,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\n-- we want to control proxy behavior before auth, so put the code under rewrite method\nfunction _M.rewrite(conf, ctx)\n    if not ok then\n        core.log.error(\"need to build APISIX-Runtime to support proxy control\")\n        return 501\n    end\n\n    local request_buffering = conf.request_buffering\n    if request_buffering  ~= nil then\n        local ok, err = apisix_ngx_client.set_proxy_request_buffering(request_buffering)\n        if not ok then\n            core.log.error(\"failed to set request_buffering: \", err)\n            return 503\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-mirror.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core          = require(\"apisix.core\")\nlocal url           = require(\"net.url\")\n\nlocal math_random = math.random\nlocal has_mod, apisix_ngx_client = pcall(require, \"resty.apisix.client\")\n\n\nlocal plugin_name   = \"proxy-mirror\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        host = {\n            type = \"string\",\n            pattern = [=[^(http(s)?|grpc(s)?):\\/\\/([\\da-zA-Z.-]+|\\[[\\da-fA-F:]+\\])(:\\d+)?$]=],\n        },\n        path = {\n            type = \"string\",\n            pattern = [[^/[^?&]+$]],\n        },\n        path_concat_mode = {\n            type = \"string\",\n            default = \"replace\",\n            enum = {\"replace\", \"prefix\"},\n            description = \"the concatenation mode for custom path\"\n        },\n        sample_ratio = {\n            type = \"number\",\n            minimum = 0.00001,\n            maximum = 1,\n            default = 1,\n        },\n    },\n    required = {\"host\"},\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1010,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nlocal function resolver_host(prop_host)\n    local url_decoded = url.parse(prop_host)\n    local decoded_host = url_decoded.host\n    if not core.utils.parse_ipv4(decoded_host) and not core.utils.parse_ipv6(decoded_host) then\n        local ip, err = core.resolver.parse_domain(decoded_host)\n\n        if not ip then\n            core.log.error(\"dns resolver resolves domain: \", decoded_host,\" error: \", err,\n                            \" will continue to use the host: \", decoded_host)\n            return url_decoded.scheme, prop_host\n        end\n\n        local host = url_decoded.scheme .. '://' .. ip ..\n            (url_decoded.port and ':' .. url_decoded.port or '')\n        core.log.info(prop_host, \" is resolved to: \", host)\n        return url_decoded.scheme, host\n    end\n    return url_decoded.scheme, prop_host\nend\n\n\nlocal function enable_mirror(ctx, conf)\n    local uri = (ctx.var.upstream_uri and ctx.var.upstream_uri ~= \"\") and\n                ctx.var.upstream_uri or\n                ctx.var.uri .. ctx.var.is_args .. (ctx.var.args or '')\n\n    if conf.path then\n        if conf.path_concat_mode == \"prefix\" then\n            uri = conf.path .. uri\n        else\n            uri = conf.path .. ctx.var.is_args .. (ctx.var.args or '')\n        end\n    end\n\n    local _, mirror_host = resolver_host(conf.host)\n    ctx.var.upstream_mirror_host = mirror_host\n    ctx.var.upstream_mirror_uri = mirror_host .. uri\n\n    if has_mod then\n        apisix_ngx_client.enable_mirror()\n    end\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    core.log.info(\"proxy mirror plugin rewrite phase, conf: \", core.json.delay_encode(conf))\n\n    if conf.sample_ratio == 1 then\n        enable_mirror(ctx, conf)\n        ctx.enable_mirror = true\n    else\n        local val = math_random()\n        core.log.info(\"mirror request sample_ratio conf: \", conf.sample_ratio,\n                                \", random value: \", val)\n        if val < conf.sample_ratio then\n            enable_mirror(ctx, conf)\n            ctx.enable_mirror = true\n        end\n    end\n\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/proxy-rewrite.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core        = require(\"apisix.core\")\nlocal plugin_name = \"proxy-rewrite\"\nlocal pairs       = pairs\nlocal ipairs      = ipairs\nlocal ngx         = ngx\nlocal type        = type\nlocal re_sub      = ngx.re.sub\nlocal re_match    = ngx.re.match\nlocal req_set_uri = ngx.req.set_uri\nlocal sub_str     = string.sub\nlocal str_find    = core.string.find\n\nlocal switch_map = {GET = ngx.HTTP_GET, POST = ngx.HTTP_POST, PUT = ngx.HTTP_PUT,\n                    HEAD = ngx.HTTP_HEAD, DELETE = ngx.HTTP_DELETE,\n                    OPTIONS = ngx.HTTP_OPTIONS, MKCOL = ngx.HTTP_MKCOL,\n                    COPY = ngx.HTTP_COPY, MOVE = ngx.HTTP_MOVE,\n                    PROPFIND = ngx.HTTP_PROPFIND, LOCK = ngx.HTTP_LOCK,\n                    UNLOCK = ngx.HTTP_UNLOCK, PATCH = ngx.HTTP_PATCH,\n                    TRACE = ngx.HTTP_TRACE,\n                }\nlocal schema_method_enum = {}\nfor key in pairs(switch_map) do\n    core.table.insert(schema_method_enum, key)\nend\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\ncore.ctx.register_var(\"proxy_rewrite_regex_uri_captures\", function(ctx)\n    return ctx.proxy_rewrite_regex_uri_captures\nend)\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        uri = {\n            description = \"new uri for upstream\",\n            type        = \"string\",\n            minLength   = 1,\n            maxLength   = 4096,\n            pattern     = [[^\\/.*]],\n        },\n        method = {\n            description = \"proxy route method\",\n            type        = \"string\",\n            enum        = schema_method_enum\n        },\n        regex_uri = {\n            description = \"new uri that substitute from client uri \" ..\n                          \"for upstream, lower priority than uri property\",\n            type        = \"array\",\n            minItems    = 2,\n            items       = {\n                description = \"regex uri\",\n                type = \"string\",\n            }\n        },\n        host = {\n            description = \"new host for upstream\",\n            type        = \"string\",\n            pattern     = [[^[0-9a-zA-Z-.]+(:\\d{1,5})?$]],\n        },\n        headers = {\n            description = \"new headers for request\",\n            oneOf = {\n                {\n                    type = \"object\",\n                    minProperties = 1,\n                    additionalProperties = false,\n                    properties = {\n                        add = {\n                            type = \"object\",\n                            minProperties = 1,\n                            patternProperties = {\n                                [\"^[^:]+$\"] = {\n                                    oneOf = {\n                                        { type = \"string\" },\n                                        { type = \"number\" }\n                                    }\n                                }\n                            },\n                        },\n                        set = {\n                            type = \"object\",\n                            minProperties = 1,\n                            patternProperties = {\n                                [\"^[^:]+$\"] = {\n                                    oneOf = {\n                                        { type = \"string\" },\n                                        { type = \"number\" },\n                                    }\n                                }\n                            },\n                        },\n                        remove = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                type = \"string\",\n                                -- \"Referer\"\n                                pattern = \"^[^:]+$\"\n                            }\n                        },\n                    },\n                },\n                {\n                    type = \"object\",\n                    minProperties = 1,\n                    patternProperties = {\n                        [\"^[^:]+$\"] = {\n                            oneOf = {\n                                { type = \"string\" },\n                                { type = \"number\" }\n                            }\n                        }\n                    },\n                }\n            },\n\n        },\n        use_real_request_uri_unsafe = {\n            description = \"use real_request_uri instead, THIS IS VERY UNSAFE.\",\n            type        = \"boolean\",\n            default     = false,\n        },\n    },\n    minProperties = 1,\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 1008,\n    name     = plugin_name,\n    schema   = schema,\n}\n\nlocal function is_new_headers_conf(headers)\n    return (headers.add and type(headers.add) == \"table\") or\n        (headers.set and type(headers.set) == \"table\") or\n        (headers.remove and type(headers.remove) == \"table\")\nend\n\nlocal function check_set_headers(headers)\n    for field, value in pairs(headers) do\n        if type(field) ~= 'string' then\n            return false, 'invalid type as header field'\n        end\n\n        if type(value) ~= 'string' and type(value) ~= 'number' then\n            return false, 'invalid type as header value'\n        end\n\n        if #field == 0 then\n            return false, 'invalid field length in header'\n        end\n\n        core.log.info(\"header field: \", field)\n        if not core.utils.validate_header_field(field) then\n            return false, 'invalid field character in header'\n        end\n        if not core.utils.validate_header_value(value) then\n            return false, 'invalid value character in header'\n        end\n    end\n\n    return true\nend\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.regex_uri and #conf.regex_uri > 0 then\n        if (#conf.regex_uri % 2 ~= 0) then\n            return false, \"The length of regex_uri should be an even number\"\n        end\n        for i = 1, #conf.regex_uri, 2 do\n            local _, _, err = re_sub(\"/fake_uri\", conf.regex_uri[i],\n                conf.regex_uri[i + 1], \"jo\")\n            if err then\n                return false, \"invalid regex_uri(\" .. conf.regex_uri[i] ..\n                    \", \" .. conf.regex_uri[i + 1] .. \"): \" .. err\n            end\n        end\n    end\n\n    -- check headers\n    if not conf.headers then\n        return true\n    end\n\n    if conf.headers then\n        if not is_new_headers_conf(conf.headers) then\n            ok, err = check_set_headers(conf.headers)\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\ndo\n    local upstream_vars = {\n        host       = \"upstream_host\",\n        upgrade    = \"upstream_upgrade\",\n        connection = \"upstream_connection\",\n    }\n    local upstream_names = {}\n    for name, _ in pairs(upstream_vars) do\n        core.table.insert(upstream_names, name)\n    end\n\n    local function create_header_operation(hdr_conf)\n        local set = {}\n        local add = {}\n\n        if is_new_headers_conf(hdr_conf) then\n            if hdr_conf.add then\n                for field, value in pairs(hdr_conf.add) do\n                    core.table.insert_tail(add, field, value)\n                end\n            end\n            if hdr_conf.set then\n                for field, value in pairs(hdr_conf.set) do\n                    core.table.insert_tail(set, field, value)\n                end\n            end\n\n        else\n            for field, value in pairs(hdr_conf) do\n                core.table.insert_tail(set, field, value)\n            end\n        end\n\n        return {\n            add = add,\n            set = set,\n            remove = hdr_conf.remove or {},\n        }\n    end\n\n\n    local function escape_separator(s)\n        return re_sub(s, [[\\?]], \"%3F\", \"jo\")\n    end\n\n\nfunction _M.rewrite(conf, ctx)\n    for _, name in ipairs(upstream_names) do\n        if conf[name] then\n            ctx.var[upstream_vars[name]] = conf[name]\n        end\n    end\n\n    local upstream_uri = ctx.var.uri\n    local separator_escaped = false\n    if conf.use_real_request_uri_unsafe then\n        upstream_uri = ctx.var.real_request_uri\n    end\n\n    if conf.uri ~= nil then\n        separator_escaped = true\n        upstream_uri = core.utils.resolve_var(conf.uri, ctx.var, escape_separator)\n\n    elseif conf.regex_uri ~= nil then\n        if not str_find(upstream_uri, \"?\") then\n            separator_escaped = true\n        end\n\n        local error_msg\n        for i = 1, #conf.regex_uri, 2 do\n            local captures, err = re_match(upstream_uri, conf.regex_uri[i], \"jo\")\n            if err then\n                error_msg = \"failed to match the uri \" .. ctx.var.uri ..\n                    \" (\" .. conf.regex_uri[i] .. \") \" .. \" : \" .. err\n                break\n            end\n\n            if captures then\n                ctx.proxy_rewrite_regex_uri_captures = captures\n\n                local uri, _, err = re_sub(upstream_uri,\n                    conf.regex_uri[i], conf.regex_uri[i + 1], \"jo\")\n                if uri then\n                    upstream_uri = uri\n                else\n                    error_msg = \"failed to substitute the uri \" .. ngx.var.uri ..\n                        \" (\" .. conf.regex_uri[i] .. \") with \" ..\n                        conf.regex_uri[i + 1] .. \" : \" .. err\n                end\n\n                break\n            end\n        end\n\n        if error_msg ~= nil then\n            core.log.error(error_msg)\n            return 500, { error_msg = error_msg }\n        end\n    end\n\n    if not conf.use_real_request_uri_unsafe then\n        local index\n        if separator_escaped then\n            index = str_find(upstream_uri, \"?\")\n        end\n\n        if index then\n            upstream_uri = core.utils.uri_safe_encode(sub_str(upstream_uri, 1, index - 1)) ..\n                sub_str(upstream_uri, index)\n        else\n            -- The '?' may come from client request '%3f' when we use ngx.var.uri directly or\n            -- via regex_uri\n            upstream_uri = core.utils.uri_safe_encode(upstream_uri)\n        end\n\n        req_set_uri(upstream_uri)\n\n        if ctx.var.is_args == \"?\" then\n            if index then\n                ctx.var.upstream_uri = upstream_uri .. \"&\" .. (ctx.var.args or \"\")\n            else\n                ctx.var.upstream_uri = upstream_uri .. \"?\" .. (ctx.var.args or \"\")\n            end\n        else\n            ctx.var.upstream_uri = upstream_uri\n        end\n    else\n        ctx.var.upstream_uri = upstream_uri\n    end\n\n    if conf.headers then\n        local hdr_op, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,\n                                    create_header_operation, conf.headers)\n        if not hdr_op then\n            core.log.error(\"failed to create header operation: \", err)\n            return\n        end\n\n        local field_cnt = #hdr_op.add\n        for i = 1, field_cnt, 2 do\n            local val = core.utils.resolve_var_with_captures(hdr_op.add[i + 1],\n                                            ctx.proxy_rewrite_regex_uri_captures)\n            val = core.utils.resolve_var(val, ctx.var)\n            -- A nil or empty table value will cause add_header function to throw an error.\n            if val then\n                local header = hdr_op.add[i]\n                core.request.add_header(ctx, header, val)\n            end\n        end\n\n        local field_cnt = #hdr_op.set\n        for i = 1, field_cnt, 2 do\n            local val = core.utils.resolve_var_with_captures(hdr_op.set[i + 1],\n                                            ctx.proxy_rewrite_regex_uri_captures)\n            val = core.utils.resolve_var(val, ctx.var)\n            core.request.set_header(ctx, hdr_op.set[i], val)\n        end\n\n        local field_cnt = #hdr_op.remove\n        for i = 1, field_cnt do\n            core.request.set_header(ctx, hdr_op.remove[i], nil)\n        end\n\n    end\n\n    if conf.method then\n        ngx.req.set_method(switch_map[conf.method])\n    end\nend\n\nend  -- do\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/public-api.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core   = require(\"apisix.core\")\nlocal router = require(\"apisix.router\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        uri = {type = \"string\"},\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 501,\n    name = \"public-api\",\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    -- overwrite the uri in the ctx when the user has set the target uri\n    ctx.var.uri = conf.uri or ctx.var.uri\n\n    -- perform route matching\n    if router.api.match(ctx) then\n        return\n    end\n\n    return 404\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/real-ip.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ngx_re_split = require(\"ngx.re\").split\nlocal is_apisix_or, client = pcall(require, \"resty.apisix.client\")\nlocal str_byte = string.byte\nlocal str_sub = string.sub\nlocal ipairs = ipairs\nlocal type = type\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        trusted_addresses = {\n            type = \"array\",\n            items = {anyOf = core.schema.ip_def},\n            minItems = 1\n        },\n        source = {\n            type = \"string\",\n            minLength = 1\n        },\n        recursive = {\n            type = \"boolean\",\n            default = false\n        }\n    },\n    required = {\"source\"},\n}\n\n\nlocal plugin_name = \"real-ip\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 23000,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.trusted_addresses then\n        for _, cidr in ipairs(conf.trusted_addresses) do\n            if not core.ip.validate_cidr_or_ip(cidr) then\n                return false, \"invalid ip address: \" .. cidr\n            end\n        end\n    end\n    return true\nend\n\n\nlocal function addr_match(conf, ctx, addr)\n    local matcher, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,\n                                                  core.ip.create_ip_matcher, conf.trusted_addresses)\n    if not matcher then\n        core.log.error(\"failed to create ip matcher: \", err)\n        return false\n    end\n\n    return matcher:match(addr)\nend\n\n\nlocal function get_addr(conf, ctx)\n    if conf.source == \"http_x_forwarded_for\" then\n        -- use the last address from X-Forwarded-For header\n        -- after core.request.header function changed\n        -- we need to get original header value by using core.request.headers\n        local addrs = core.request.headers(ctx)[\"X-Forwarded-For\"]\n        if not addrs then\n            return nil\n        end\n\n        if type(addrs) == \"table\" then\n            addrs = addrs[#addrs]\n        end\n\n        local idx = core.string.rfind_char(addrs, \",\")\n        if not idx then\n            return addrs\n        end\n\n        if conf.recursive and conf.trusted_addresses then\n            local split_addrs = ngx_re_split(addrs, \",\\\\s*\", \"jo\")\n            for i = #split_addrs, 2, -1 do\n                if not addr_match(conf, ctx, split_addrs[i]) then\n                    return split_addrs[i]\n                end\n            end\n\n            return split_addrs[1]\n        end\n\n        for i = idx + 1, #addrs do\n            if str_byte(addrs, i) == str_byte(\" \") then\n                idx = idx + 1\n            else\n                break\n            end\n        end\n\n        return str_sub(addrs, idx + 1)\n    end\n    return ctx.var[conf.source]\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    if not is_apisix_or then\n        core.log.error(\"need to build APISIX-Runtime to support setting real ip\")\n        return 501\n    end\n\n    if conf.trusted_addresses then\n        local remote_addr = ctx.var.remote_addr\n        if not addr_match(conf, ctx, remote_addr) then\n            return\n        end\n    end\n\n    local addr = get_addr(conf, ctx)\n    if not addr then\n        core.log.warn(\"missing real address\")\n        return\n    end\n\n    local ip, port = core.utils.parse_addr(addr)\n    if not ip or (not core.utils.parse_ipv4(ip) and not core.utils.parse_ipv6(ip)) then\n        core.log.warn(\"bad address: \", addr)\n        return\n    end\n\n    if str_byte(ip, 1, 1) == str_byte(\"[\") then\n        -- For IPv6, the `set_real_ip` accepts '::1' but not '[::1]'\n        ip = str_sub(ip, 2, #ip - 1)\n    end\n\n    if port ~= nil and (port < 1 or port > 65535) then\n        core.log.warn(\"bad port: \", port)\n        return\n    end\n\n    core.log.info(\"set real ip: \", ip, \", port: \", port)\n\n    local ok, err = client.set_real_ip(ip, port)\n    if not ok then\n        core.log.error(\"failed to set real ip: \", err)\n        return\n    end\n\n    -- flush cached vars in APISIX\n    ctx.var.remote_addr = nil\n    ctx.var.remote_port = nil\n    ctx.var.realip_remote_addr = nil\n    ctx.var.realip_remote_port = nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/redirect.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal tab_insert = table.insert\nlocal tab_concat = table.concat\nlocal string_format = string.format\nlocal re_gmatch = ngx.re.gmatch\nlocal re_sub = ngx.re.sub\nlocal ipairs = ipairs\nlocal ngx = ngx\nlocal str_find = core.string.find\nlocal str_sub  = string.sub\nlocal type = type\nlocal math_random = math.random\n\nlocal lrucache = core.lrucache.new({\n    ttl = 300, count = 100\n})\n\n\nlocal reg = [[(\\\\\\$[0-9a-zA-Z_]+)|]]         -- \\$host\n            .. [[\\$\\{([0-9a-zA-Z_]+)\\}|]]    -- ${host}\n            .. [[\\$([0-9a-zA-Z_]+)|]]        -- $host\n            .. [[(\\$|[^$\\\\]+)]]              -- $ or others\nlocal schema = {\n    type = \"object\",\n    properties = {\n        ret_code = {type = \"integer\", minimum = 200, default = 302},\n        uri = {type = \"string\", minLength = 2, pattern = reg},\n        regex_uri = {\n            description = \"params for generating new uri that substitute from client uri, \" ..\n                          \"first param is regular expression, the second one is uri template\",\n            type        = \"array\",\n            maxItems    = 2,\n            minItems    = 2,\n            items       = {\n                description = \"regex uri\",\n                type = \"string\",\n            }\n        },\n        http_to_https = {type = \"boolean\"},\n        encode_uri = {type = \"boolean\", default = false},\n        append_query_string = {type = \"boolean\", default = false},\n    },\n    oneOf = {\n        {required = {\"uri\"}},\n        {required = {\"regex_uri\"}},\n        {required = {\"http_to_https\"}}\n    }\n}\n\n\nlocal plugin_name = \"redirect\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 900,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nlocal function parse_uri(uri)\n    local iterator, err = re_gmatch(uri, reg, \"jiox\")\n    if not iterator then\n        return nil, err\n    end\n\n    local t = {}\n    while true do\n        local m, err = iterator()\n        if err then\n            return nil, err\n        end\n\n        if not m then\n            break\n        end\n\n        tab_insert(t, m)\n    end\n\n    return t\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n\n    if not ok then\n        return false, err\n    end\n\n    if conf.regex_uri and #conf.regex_uri > 0 then\n        local _, _, err = re_sub(\"/fake_uri\", conf.regex_uri[1],\n                                 conf.regex_uri[2], \"jo\")\n        if err then\n            local msg = string_format(\"invalid regex_uri (%s, %s), err:%s\",\n                                      conf.regex_uri[1], conf.regex_uri[2], err)\n            return false, msg\n        end\n    end\n\n    if conf.http_to_https and conf.append_query_string then\n        return false, \"only one of `http_to_https` and `append_query_string` can be configured.\"\n    end\n\n    return true\nend\n\n\n    local tmp = {}\nlocal function concat_new_uri(uri, ctx)\n    local passed_uri_segs, err = lrucache(uri, nil, parse_uri, uri)\n    if not passed_uri_segs then\n        return nil, err\n    end\n\n    core.table.clear(tmp)\n\n    for _, uri_segs in ipairs(passed_uri_segs) do\n        local pat1 = uri_segs[1]    -- \\$host\n        local pat2 = uri_segs[2]    -- ${host}\n        local pat3 = uri_segs[3]    -- $host\n        local pat4 = uri_segs[4]    -- $ or others\n        core.log.info(\"parsed uri segs: \", core.json.delay_encode(uri_segs))\n\n        if pat2 or pat3 then\n            tab_insert(tmp, ctx.var[pat2 or pat3])\n        else\n            tab_insert(tmp, pat1 or pat4)\n        end\n    end\n\n    return tab_concat(tmp, \"\")\nend\n\nlocal function get_port(attr)\n    local port\n    if attr then\n        port = attr.https_port\n    end\n\n    if port then\n        return port\n    end\n\n    local local_conf = core.config.local_conf()\n    local ssl = core.table.try_read_attr(local_conf, \"apisix\", \"ssl\")\n    if not ssl or not ssl[\"enable\"] then\n        return port\n    end\n\n    local ports = ssl[\"listen\"]\n    if ports and #ports > 0 then\n        local idx = math_random(1, #ports)\n        port = ports[idx]\n        if type(port) == \"table\" then\n            port = port.port\n        end\n    end\n\n    return port\nend\n\nfunction _M.rewrite(conf, ctx)\n    core.log.info(\"plugin rewrite phase, conf: \", core.json.delay_encode(conf))\n\n    local ret_code = conf.ret_code\n\n    local attr = plugin.plugin_attr(plugin_name)\n    local ret_port = get_port(attr)\n\n    local uri = conf.uri\n    local regex_uri = conf.regex_uri\n\n    local proxy_proto = core.request.header(ctx, \"X-Forwarded-Proto\")\n    local _scheme = proxy_proto or core.request.get_scheme(ctx)\n    if conf.http_to_https and _scheme ~= \"https\" then\n        if ret_port == nil or ret_port == 443 or ret_port <= 0 or ret_port > 65535  then\n            uri = \"https://$host$request_uri\"\n        else\n            uri = \"https://$host:\" .. ret_port .. \"$request_uri\"\n        end\n\n        local method_name = ngx.req.get_method()\n        if method_name == \"GET\" or method_name == \"HEAD\" then\n            ret_code = 301\n        else\n         -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308\n            ret_code = 308\n        end\n    end\n\n    if ret_code then\n        local new_uri\n        if uri then\n            local err\n            new_uri, err = concat_new_uri(uri, ctx)\n            if not new_uri then\n                core.log.error(\"failed to generate new uri by: \" .. uri .. err)\n                return 500\n            end\n        elseif regex_uri then\n            local n, err\n            new_uri, n, err = re_sub(ctx.var.uri, regex_uri[1],\n                                     regex_uri[2], \"jo\")\n            if not new_uri then\n                local msg = string_format(\"failed to substitute the uri:%s (%s) with %s, error:%s\",\n                                          ctx.var.uri, regex_uri[1], regex_uri[2], err)\n                core.log.error(msg)\n                return 500\n            end\n\n            if n < 1 then\n                return\n            end\n        end\n\n        if not new_uri then\n            return\n        end\n\n        local index = str_find(new_uri, \"?\")\n        if conf.encode_uri then\n            if index then\n                new_uri = core.utils.uri_safe_encode(str_sub(new_uri, 1, index-1)) ..\n                          str_sub(new_uri, index)\n            else\n                new_uri = core.utils.uri_safe_encode(new_uri)\n            end\n        end\n\n        if conf.append_query_string and ctx.var.is_args == \"?\" then\n            if index then\n                new_uri = new_uri .. \"&\" .. (ctx.var.args or \"\")\n            else\n                new_uri = new_uri .. \"?\" .. (ctx.var.args or \"\")\n            end\n        end\n\n        core.response.set_header(\"Location\", new_uri)\n        return ret_code\n    end\n\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/referer-restriction.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ipairs    = ipairs\nlocal core      = require(\"apisix.core\")\nlocal http      = require \"resty.http\"\nlocal lrucache  = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        bypass_missing = {\n            type = \"boolean\",\n            default = false,\n        },\n        whitelist = {\n            type = \"array\",\n            items = core.schema.host_def,\n            minItems = 1,\n        },\n        blacklist = {\n            type = \"array\",\n            items = core.schema.host_def,\n            minItems = 1,\n        },\n        message = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 1024,\n            default = \"Your referer host is not allowed\",\n        },\n    },\n    oneOf = {\n        {required = {\"whitelist\"}},\n        {required = {\"blacklist\"}},\n    },\n}\n\n\nlocal plugin_name = \"referer-restriction\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2990,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function match_host(matcher, host)\n     if matcher.map[host] then\n        return true\n    end\n    for _, h in ipairs(matcher.suffixes) do\n        if core.string.has_suffix(host, h) then\n            return true\n        end\n    end\n    return false\nend\n\n\nlocal function create_host_matcher(hosts)\n    local hosts_suffix = {}\n    local hosts_map = {}\n\n    for _, h in ipairs(hosts) do\n        if h:byte(1) == 42 then -- start with '*'\n            core.table.insert(hosts_suffix, h:sub(2))\n        else\n            hosts_map[h] = true\n        end\n    end\n\n    return {\n        suffixes = hosts_suffix,\n        map = hosts_map,\n    }\nend\n\n\nfunction _M.access(conf, ctx)\n    local block = false\n    local referer = ctx.var.http_referer\n    if referer then\n        -- parse_uri doesn't support IPv6 literal, it is OK since we only\n        -- expect hostname in the whitelist.\n        -- See https://github.com/ledgetech/lua-resty-http/pull/104\n        local uri = http.parse_uri(nil, referer)\n        if not uri then\n            -- malformed Referer\n            referer = nil\n        else\n            -- take host part only\n            referer = uri[2]\n        end\n    end\n\n\n    if not referer then\n        block = not conf.bypass_missing\n\n    elseif conf.whitelist then\n        local matcher = lrucache(conf.whitelist, nil,\n            create_host_matcher, conf.whitelist)\n        block = not match_host(matcher, referer)\n    elseif conf.blacklist then\n        local matcher = lrucache(conf.blacklist, nil,\n            create_host_matcher, conf.blacklist)\n        block = match_host(matcher, referer)\n    end\n\n    if block then\n        return 403, { message = conf.message }\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/request-id.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = ngx\nlocal core = require(\"apisix.core\")\nlocal uuid = require(\"resty.jit-uuid\")\nlocal nanoid = require(\"nanoid\")\nlocal ksuid = require(\"resty.ksuid\")\nlocal math_random = math.random\nlocal str_byte = string.byte\nlocal ffi = require \"ffi\"\n\nlocal plugin_name = \"request-id\"\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        header_name = {type = \"string\", default = \"X-Request-Id\"},\n        include_in_response = {type = \"boolean\", default = true},\n        algorithm = {\n            type = \"string\",\n            enum = {\"uuid\", \"nanoid\", \"range_id\", \"ksuid\"},\n            default = \"uuid\"\n        },\n        range_id = {\n            type = \"object\",\n            properties = {\n                length = {\n                    type = \"integer\",\n                    minimum = 6,\n                    default = 16\n                },\n                char_set = {\n                    type = \"string\",\n                    -- The Length is set to 6 just avoid too short length, it may repeat\n                    minLength = 6,\n                    default = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789\"\n                }\n            },\n            default = {\n                length = 16,\n                char_set = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789\"\n            }\n        }\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 12015,\n    name = plugin_name,\n    schema = schema\n}\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n-- generate range_id\nlocal function get_range_id(range_id)\n    local res = ffi.new(\"unsigned char[?]\", range_id.length)\n    for i = 0, range_id.length - 1 do\n        res[i] = str_byte(range_id.char_set, math_random(#range_id.char_set))\n    end\n    return ffi.string(res, range_id.length)\nend\n\nlocal function get_request_id(conf)\n    if conf.algorithm == \"uuid\" then\n        return uuid()\n    end\n    if conf.algorithm == \"nanoid\" then\n        return nanoid.safe_simple()\n    end\n\n    if conf.algorithm == \"range_id\" then\n        return get_range_id(conf.range_id)\n    end\n\n    if conf.algorithm == \"ksuid\" then\n        return ksuid.generate()\n    end\n\n    return uuid()\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local headers = ngx.req.get_headers()\n    local uuid_val\n    local header_req_id = headers[conf.header_name]\n    if not header_req_id or header_req_id == \"\" then\n        uuid_val = get_request_id(conf)\n        core.request.set_header(ctx, conf.header_name, uuid_val)\n    else\n        uuid_val = headers[conf.header_name]\n    end\n\n    if conf.include_in_response then\n        ctx[\"request-id-\" .. conf.header_name] = uuid_val\n    end\n    if ctx.var.apisix_request_id then\n        ctx.var.apisix_request_id = uuid_val\n    end\nend\n\nfunction _M.header_filter(conf, ctx)\n    if not conf.include_in_response then\n        return\n    end\n\n    local headers = ngx.resp.get_headers()\n    local header_req_id = headers[conf.header_name]\n    if not header_req_id or header_req_id == \"\" then\n        core.response.set_header(conf.header_name, ctx[\"request-id-\" .. conf.header_name])\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/request-validation.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core          = require(\"apisix.core\")\nlocal plugin_name   = \"request-validation\"\nlocal ngx           = ngx\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        header_schema = {type = \"object\"},\n        body_schema = {type = \"object\"},\n        rejected_code = {type = \"integer\", minimum = 200, maximum = 599, default = 400},\n        rejected_msg = {type = \"string\", minLength = 1, maxLength = 256}\n    },\n    anyOf = {\n        {required = {\"header_schema\"}},\n        {required = {\"body_schema\"}}\n    }\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 2800,\n    type = 'validation',\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.body_schema then\n        ok, err = core.schema.valid(conf.body_schema)\n        if not ok then\n            return false, err\n        end\n    end\n\n    if conf.header_schema then\n        ok, err = core.schema.valid(conf.header_schema)\n        if not ok then\n            return false, err\n        end\n    end\n\n    return true, nil\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local headers = core.request.headers(ctx)\n\n    if conf.header_schema then\n        local ok, err = core.schema.check(conf.header_schema, headers)\n        if not ok then\n            core.log.error(\"req schema validation failed\", err)\n            return conf.rejected_code, conf.rejected_msg or err\n        end\n    end\n\n    if conf.body_schema then\n        local req_body\n        local body, err = core.request.get_body()\n        if not body then\n            if err then\n                core.log.error(\"failed to get body: \", err)\n            end\n            return conf.rejected_code, conf.rejected_msg\n        end\n\n        local body_is_json = true\n        if headers[\"content-type\"]\n            and core.string.has_prefix(\n                headers[\"content-type\"]:lower(),\n                \"application/x-www-form-urlencoded\"\n            )\n        then\n            -- use 0 to avoid truncated result and keep the behavior as the\n            -- same as other platforms\n            req_body, err = ngx.decode_args(body, 0)\n            body_is_json = false\n        else -- JSON as default\n            req_body, err = core.json.decode(body)\n        end\n\n        if not req_body then\n            core.log.error('failed to decode the req body: ', err)\n            return conf.rejected_code, conf.rejected_msg or err\n        end\n\n        local ok, err = core.schema.check(conf.body_schema, req_body)\n        if not ok then\n            core.log.error(\"req schema validation failed: \", err)\n            return conf.rejected_code, conf.rejected_msg or err\n        end\n\n        if body_is_json then\n            -- ensure the JSON we check is the JSON we pass to the upstream,\n            -- see https://bishopfox.com/blog/json-interoperability-vulnerabilities\n            req_body = core.json.encode(req_body)\n            ngx.req.set_body_data(req_body)\n        end\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/response-rewrite.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core        = require(\"apisix.core\")\nlocal expr        = require(\"resty.expr.v1\")\nlocal re_compile  = require(\"resty.core.regex\").re_match_compile\nlocal plugin_name = \"response-rewrite\"\nlocal ngx         = ngx\nlocal ngx_header  = ngx.header\nlocal re_match    = ngx.re.match\nlocal re_sub      = ngx.re.sub\nlocal re_gsub     = ngx.re.gsub\nlocal pairs       = pairs\nlocal ipairs      = ipairs\nlocal type        = type\nlocal pcall       = pcall\nlocal content_decode = require(\"apisix.utils.content-decode\")\n\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        headers = {\n            description = \"new headers for response\",\n            anyOf = {\n                {\n                    type = \"object\",\n                    minProperties = 1,\n                    patternProperties = {\n                        [\"^[^:]+$\"] = {\n                            oneOf = {\n                                {type = \"string\"},\n                                {type = \"number\"},\n                            }\n                        }\n                    },\n                },\n                {\n                    properties = {\n                        add = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                type = \"string\",\n                                -- \"Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>\"\n                                pattern = \"^[^:]+:[^:]*[^/]$\"\n                            }\n                        },\n                        set = {\n                            type = \"object\",\n                            minProperties = 1,\n                            patternProperties = {\n                                [\"^[^:]+$\"] = {\n                                    oneOf = {\n                                        {type = \"string\"},\n                                        {type = \"number\"},\n                                    }\n                                }\n                            },\n                        },\n                        remove = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                type = \"string\",\n                                -- \"Set-Cookie\"\n                                pattern = \"^[^:]+$\"\n                            }\n                        },\n                    },\n                }\n            }\n        },\n        body = {\n            description = \"new body for response\",\n            type = \"string\",\n        },\n        body_base64 = {\n            description = \"whether new body for response need base64 decode before return\",\n            type = \"boolean\",\n            default = false,\n        },\n        status_code = {\n            description = \"new status code for response\",\n            type = \"integer\",\n            minimum = 200,\n            maximum = 598,\n        },\n        vars = {\n            type = \"array\",\n        },\n        filters = {\n            description = \"a group of filters that modify response body\" ..\n                          \"by replacing one specified string by another\",\n            type = \"array\",\n            minItems = 1,\n            items = {\n                description = \"filter that modifies response body\",\n                type = \"object\",\n                required = {\"regex\", \"replace\"},\n                properties = {\n                    regex = {\n                        description = \"match pattern on response body\",\n                        type = \"string\",\n                        minLength = 1,\n                    },\n                    scope = {\n                        description = \"regex substitution range\",\n                        type = \"string\",\n                        enum = {\"once\", \"global\"},\n                        default = \"once\",\n                    },\n                    replace = {\n                        description = \"regex substitution content\",\n                        type = \"string\",\n                    },\n                    options = {\n                        description = \"regex options\",\n                        type = \"string\",\n                        default = \"jo\",\n                    }\n                },\n            },\n        },\n    },\n    dependencies = {\n        body = {\n            [\"not\"] = {required = {\"filters\"}}\n        },\n        filters = {\n            [\"not\"] = {required = {\"body\"}}\n        }\n    }\n}\n\n\nlocal _M = {\n    version  = 0.1,\n    priority = 899,\n    name     = plugin_name,\n    schema   = schema,\n}\n\nlocal function vars_matched(conf, ctx)\n    if not conf.vars then\n        return true\n    end\n\n    if not conf.response_expr then\n        local response_expr, _ = expr.new(conf.vars)\n        conf.response_expr = response_expr\n    end\n\n    local match_result = conf.response_expr:eval(ctx.var)\n\n    return match_result\nend\n\n\nlocal function is_new_headers_conf(headers)\n    return\n        (headers.add and type(headers.add) == \"table\") or\n        (headers.set and type(headers.set) == \"table\") or\n        (headers.remove and type(headers.remove) == \"table\")\nend\n\n\nlocal function check_set_headers(headers)\n    for field, value in pairs(headers) do\n        if type(field) ~= 'string' then\n            return false, 'invalid type as header field'\n        end\n\n        if type(value) ~= 'string' and type(value) ~= 'number' then\n            return false, 'invalid type as header value'\n        end\n\n        if #field == 0 then\n            return false, 'invalid field length in header'\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    if conf.headers then\n        if not is_new_headers_conf(conf.headers) then\n            ok, err = check_set_headers(conf.headers)\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    if conf.body_base64 then\n        if not conf.body or #conf.body == 0 then\n            return false, 'invalid base64 content'\n        end\n        local body = ngx.decode_base64(conf.body)\n        if not body then\n            return  false, 'invalid base64 content'\n        end\n    end\n\n    if conf.vars then\n        local ok, err = expr.new(conf.vars)\n        if not ok then\n            return false, \"failed to validate the 'vars' expression: \" .. err\n        end\n    end\n\n    if conf.filters then\n        for _, filter in ipairs(conf.filters) do\n            local ok, err = pcall(re_compile, filter.regex, filter.options)\n            if not ok then\n                return false, \"regex \\\"\" .. filter.regex ..\n                        \"\\\" validation failed: \"  .. err\n            end\n        end\n    end\n\n    return true\nend\n\n\ndo\n\nfunction _M.body_filter(conf, ctx)\n    if not ctx.response_rewrite_matched then\n        return\n    end\n\n    if conf.filters then\n\n        local body = core.response.hold_body_chunk(ctx)\n        if not body then\n            return\n        end\n\n        local err\n        if ctx.response_encoding ~= nil then\n            local decoder = content_decode.dispatch_decoder(ctx.response_encoding)\n            if not decoder then\n                core.log.error(\"filters may not work as expected \",\n                               \"due to unsupported compression encoding type: \",\n                               ctx.response_encoding)\n                return\n            end\n            body, err = decoder(body)\n            if err ~= nil then\n                core.log.error(\"filters may not work as expected: \", err)\n                return\n            end\n        end\n\n        for _, filter in ipairs(conf.filters) do\n            if filter.scope == \"once\" then\n                body, _, err = re_sub(body, filter.regex, filter.replace, filter.options)\n            else\n                body, _, err = re_gsub(body, filter.regex, filter.replace, filter.options)\n            end\n            if err ~= nil then\n                core.log.error(\"regex \\\"\" .. filter.regex .. \"\\\" substitutes failed:\" .. err)\n            end\n        end\n\n        ngx.arg[1] = body\n        return\n    end\n\n    if conf.body then\n        ngx.arg[2] = true\n        if conf.body_base64 then\n            ngx.arg[1] = ngx.decode_base64(conf.body)\n        else\n            ngx.arg[1] = conf.body\n        end\n    end\nend\n\n\nlocal function create_header_operation(hdr_conf)\n    local set = {}\n    local add = {}\n    if is_new_headers_conf(hdr_conf) then\n        if hdr_conf.add then\n            for _, value in ipairs(hdr_conf.add) do\n                local m, err = re_match(value, [[^([^:\\s]+)\\s*:\\s*([^:]+)$]], \"jo\")\n                if not m then\n                    return nil, err\n                end\n                core.table.insert_tail(add, m[1], m[2])\n            end\n        end\n\n        if hdr_conf.set then\n            for field, value in pairs(hdr_conf.set) do\n                --reform header from object into array, so can avoid use pairs, which is NYI\n                core.table.insert_tail(set, field, value)\n            end\n        end\n\n    else\n        for field, value in pairs(hdr_conf) do\n            core.table.insert_tail(set, field, value)\n        end\n    end\n\n    return {\n        add = add,\n        set = set,\n        remove = hdr_conf.remove or {},\n    }\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    ctx.response_rewrite_matched = vars_matched(conf, ctx)\n    if not ctx.response_rewrite_matched then\n        return\n    end\n\n    if conf.status_code then\n        ngx.status = conf.status_code\n    end\n\n    -- if filters have no any match, response body won't be modified.\n    if conf.filters or conf.body then\n        local response_encoding = ngx_header[\"Content-Encoding\"]\n        core.response.clear_header_as_body_modified()\n        ctx.response_encoding = response_encoding\n    end\n\n    if not conf.headers then\n        return\n    end\n\n    local hdr_op, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,\n                                                 create_header_operation, conf.headers)\n    if not hdr_op then\n        core.log.error(\"failed to create header operation: \", err)\n        return\n    end\n\n    local field_cnt = #hdr_op.add\n    for i = 1, field_cnt, 2 do\n        local val = core.utils.resolve_var(hdr_op.add[i+1], ctx.var)\n        core.response.add_header(hdr_op.add[i], val)\n    end\n\n    local field_cnt = #hdr_op.set\n    for i = 1, field_cnt, 2 do\n        local val = core.utils.resolve_var(hdr_op.set[i+1], ctx.var)\n        core.response.set_header(hdr_op.set[i], val)\n    end\n\n    local field_cnt = #hdr_op.remove\n    for i = 1, field_cnt do\n        core.response.set_header(hdr_op.remove[i], nil)\n    end\nend\n\nend  -- do\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/rocketmq-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core     = require(\"apisix.core\")\nlocal plugin   = require(\"apisix.plugin\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal producer = require (\"resty.rocketmq.producer\")\nlocal acl_rpchook = require(\"resty.rocketmq.acl_rpchook\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\n\nlocal type     = type\nlocal plugin_name = \"rocketmq-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(\"rocketmq logger\")\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        meta_format = {\n            type = \"string\",\n            default = \"default\",\n            enum = {\"default\", \"origin\"},\n        },\n        nameserver_list = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\"\n            }\n        },\n        topic = {type = \"string\"},\n        key = {type = \"string\"},\n        tag = {type = \"string\"},\n        log_format = {type = \"object\"},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        use_tls = {type = \"boolean\", default = false},\n        access_key = {type = \"string\", default = \"\"},\n        secret_key = {type = \"string\", default = \"\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = {type = \"boolean\", default = false},\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    encrypt_fields = {\"secret_key\"},\n    required = {\"nameserver_list\", \"topic\"}\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 402,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, err\n    end\n    core.utils.check_tls_bool({\"use_tls\"}, conf, plugin_name)\n    return log_util.check_log_schema(conf)\nend\n\n\nlocal function create_producer(nameserver_list, producer_config)\n    core.log.info(\"create new rocketmq producer instance\")\n    local prod = producer.new(nameserver_list, \"apisixLogProducer\")\n    if producer_config.use_tls then\n        prod:setUseTLS(true)\n    end\n    if producer_config.access_key ~= '' then\n        local aclHook = acl_rpchook.new(producer_config.access_key, producer_config.secret_key)\n        prod:addRPCHook(aclHook)\n    end\n    prod:setTimeout(producer_config.timeout)\n    return prod\nend\n\n\nlocal function send_rocketmq_data(conf, log_message, prod)\n    local result, err = prod:send(conf.topic, log_message, conf.tag, conf.key)\n    if not result then\n        return false, \"failed to send data to rocketmq topic: \" .. err ..\n                \", nameserver_list: \" .. core.json.encode(conf.nameserver_list)\n    end\n\n    core.log.info(\"queue: \", result.sendResult.messageQueue.queueId)\n\n    return true\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry\n    if conf.meta_format == \"origin\" then\n        entry = log_util.get_req_original(ctx, conf)\n    else\n        entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    end\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- reuse producer via lrucache to avoid unbalanced partitions of messages in rocketmq\n    local producer_config = {\n        timeout = conf.timeout * 1000,\n        use_tls = conf.use_tls,\n        access_key = conf.access_key,\n        secret_key = conf.secret_key,\n    }\n\n    local prod, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_producer,\n            conf.nameserver_list, producer_config)\n    if err then\n        return nil, \"failed to create the rocketmq producer: \" .. err\n    end\n    core.log.info(\"rocketmq nameserver_list[1]: \",\n            prod.client.nameservers[1])\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err\n        if batch_max_size == 1 then\n            data = entries[1]\n            if type(data) ~= \"string\" then\n                data, err = core.json.encode(data) -- encode as single {}\n            end\n        else\n            data, err = core.json.encode(entries) -- encode as array [{}]\n        end\n\n        if not data then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        core.log.info(\"send data to rocketmq: \", data)\n        return send_rocketmq_data(conf, data, prod)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/server-info.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal timers = require(\"apisix.timers\")\nlocal plugin = require(\"apisix.plugin\")\n\nlocal ngx_time = ngx.time\nlocal ngx_timer_at = ngx.timer.at\nlocal ngx_worker_id = ngx.worker.id\nlocal type = type\n\nlocal load_time = os.time()\nlocal plugin_name = \"server-info\"\nlocal default_report_ttl = 60\nlocal lease_id\n\nlocal schema = {\n    type = \"object\",\n}\nlocal attr_schema = {\n    type = \"object\",\n    properties = {\n        report_ttl = {\n            type = \"integer\",\n            description = \"live time for server info in etcd\",\n            default = default_report_ttl,\n            minimum = 3,\n            maximum = 86400,\n        }\n    }\n}\n\nlocal internal_status = ngx.shared[\"internal-status\"]\nif not internal_status then\n    error(\"lua_shared_dict \\\"internal-status\\\" not configured\")\nend\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 990,\n    name = plugin_name,\n    schema = schema,\n    scope = \"global\",\n}\n\n\nlocal function get_boot_time()\n    local time, err = internal_status:get(\"server_info:boot_time\")\n    if err ~= nil then\n        core.log.error(\"failed to get boot_time from shdict: \", err)\n        return load_time\n    end\n\n    if time ~= nil then\n        return time\n    end\n\n    local _, err = internal_status:set(\"server_info:boot_time\", load_time)\n    if err ~= nil then\n        core.log.error(\"failed to save boot_time to shdict: \", err)\n    end\n\n    return load_time\nend\n\n\nlocal function uninitialized_server_info()\n    local boot_time = get_boot_time()\n    return {\n        etcd_version     = \"unknown\",\n        hostname         = core.utils.gethostname(),\n        id               = core.id.get(),\n        version          = core.version.VERSION,\n        boot_time        = boot_time,\n    }\nend\n\n\nlocal function get()\n    local data, err = internal_status:get(\"server_info\")\n    if err ~= nil then\n        core.log.error(\"get error: \", err)\n        return nil, err\n    end\n\n    if not data then\n        return uninitialized_server_info()\n    end\n\n    local server_info, err = core.json.decode(data)\n    if not server_info then\n        core.log.error(\"failed to decode server_info: \", err)\n        return nil, err\n    end\n\n    return server_info\nend\n\n\nlocal function get_server_info()\n    local info, err = get()\n    if not info then\n        core.log.error(\"failed to get server_info: \", err)\n        return 500\n    end\n\n    return 200, info\nend\n\n\nlocal function set(key, value, ttl)\n    local res_new, err = core.etcd.set(key, value, ttl)\n    if not res_new then\n        core.log.error(\"failed to set server_info: \", err)\n        return nil, err\n    end\n\n    if not res_new.body.lease_id then\n        core.log.error(\"failed to get lease_id: \", err)\n        return nil, err\n    end\n\n    lease_id = res_new.body.lease_id\n\n    -- set or update lease_id\n    local ok, err = internal_status:set(\"lease_id\", lease_id)\n    if not ok then\n        core.log.error(\"failed to set lease_id to shdict: \", err)\n        return nil, err\n    end\n\n    return true\nend\n\n\nlocal function report(premature, report_ttl)\n    if premature then\n        return\n    end\n\n    -- get apisix node info\n    local server_info, err = get()\n    if not server_info then\n        core.log.error(\"failed to get server_info: \", err)\n        return\n    end\n\n    if server_info.etcd_version == \"unknown\" then\n        local res, err = core.etcd.server_version()\n        if not res then\n            core.log.error(\"failed to fetch etcd version: \", err)\n            return\n\n        elseif type(res.body) ~= \"table\" then\n            core.log.error(\"failed to fetch etcd version: bad version info\")\n            return\n\n        else\n            if res.body.etcdcluster == \"\" then\n                server_info.etcd_version = res.body.etcdserver\n            else\n                server_info.etcd_version = res.body.etcdcluster\n            end\n        end\n    end\n\n    -- get inside etcd data, if not exist, create it\n    local key = \"/data_plane/server_info/\" .. server_info.id\n    local res, err = core.etcd.get(key)\n    if not res or (res.status ~= 200 and res.status ~= 404) then\n        core.log.error(\"failed to get server_info from etcd: \", err)\n        return\n    end\n\n    if not res.body.node then\n        local ok, err = set(key, server_info, report_ttl)\n        if not ok then\n            core.log.error(\"failed to set server_info to etcd: \", err)\n            return\n        end\n\n        return\n    end\n\n    local ok = core.table.deep_eq(server_info, res.body.node.value)\n    -- not equal, update it\n    if not ok then\n        local ok, err = set(key, server_info, report_ttl)\n        if not ok then\n            core.log.error(\"failed to set server_info to etcd: \", err)\n            return\n        end\n\n        return\n    end\n\n    -- get lease_id from ngx dict\n    lease_id, err = internal_status:get(\"lease_id\")\n    if not lease_id then\n        core.log.error(\"failed to get lease_id from shdict: \", err)\n        return\n    end\n\n    -- call keepalive\n    local res, err = core.etcd.keepalive(lease_id)\n    if not res then\n        core.log.error(\"send heartbeat failed: \", err)\n        return\n    end\n\n    local data, err = core.json.encode(server_info)\n    if not data then\n        core.log.error(\"failed to encode server_info: \", err)\n        return\n    end\n\n    local ok, err = internal_status:set(\"server_info\", data)\n    if not ok then\n        core.log.error(\"failed to encode and save server info: \", err)\n        return\n    end\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.control_api()\n    return {\n        {\n            methods = {\"GET\"},\n            uris ={\"/v1/server_info\"},\n            handler = get_server_info,\n        }\n    }\nend\n\n\nfunction _M.init()\n    core.log.warn(\"The server-info plugin is deprecated and will be removed in a future release.\")\n    if core.config ~= require(\"apisix.core.config_etcd\") then\n        -- we don't need to report server info if etcd is not in use.\n        return\n    end\n\n\n    local local_conf = core.config.local_conf()\n    local deployment_role = core.table.try_read_attr(\n                       local_conf, \"deployment\", \"role\")\n    if deployment_role == \"data_plane\" then\n        -- data_plane should not write to etcd\n        return\n    end\n\n    local attr = plugin.plugin_attr(plugin_name)\n    local ok, err = core.schema.check(attr_schema, attr)\n    if not ok then\n        core.log.error(\"failed to check plugin_attr: \", err)\n        return\n    end\n\n    local report_ttl = attr and attr.report_ttl or default_report_ttl\n    local start_at = ngx_time()\n\n    local fn = function()\n        local now = ngx_time()\n        -- If ttl remaining time is less than half, then flush the ttl\n        if now - start_at >= (report_ttl / 2) then\n            start_at = now\n            report(nil, report_ttl)\n        end\n    end\n\n    if ngx_worker_id() == 0 then\n        local ok, err = ngx_timer_at(0, report, report_ttl)\n        if not ok then\n            core.log.error(\"failed to create initial timer to report server info: \", err)\n            return\n        end\n    end\n\n    timers.register_timer(\"plugin#server-info\", fn, true)\n\n    core.log.info(\"timer update the server info ttl, current ttl: \", report_ttl)\nend\n\n\nfunction _M.destroy()\n    timers.unregister_timer(\"plugin#server-info\", true)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/serverless/generic-upstream.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n\nlocal ngx  = ngx\nlocal require = require\nlocal type = type\nlocal string = string\n\nreturn function(plugin_name, version, priority, request_processor, authz_schema, metadata_schema)\n    local core = require(\"apisix.core\")\n    local http = require(\"resty.http\")\n    local url = require(\"net.url\")\n\n    if request_processor and type(request_processor) ~= \"function\" then\n        return \"Failed to generate plugin due to invalid header processor type, \" ..\n                    \"expected: function, received: \" .. type(request_processor)\n    end\n\n    local schema = {\n        type = \"object\",\n        properties = {\n            function_uri = {type = \"string\"},\n            authorization = authz_schema,\n            timeout = {type = \"integer\", minimum = 100, default = 3000},\n            ssl_verify = {type = \"boolean\", default = true},\n            keepalive = {type = \"boolean\", default = true},\n            keepalive_timeout = {type = \"integer\", minimum = 1000, default = 60000},\n            keepalive_pool = {type = \"integer\", minimum = 1, default = 5}\n        },\n        required = {\"function_uri\"}\n    }\n\n    local _M = {\n        version = version,\n        priority = priority,\n        name = plugin_name,\n        schema = schema,\n        metadata_schema = metadata_schema\n    }\n\n    function _M.check_schema(conf, schema_type)\n        if schema_type == core.schema.TYPE_METADATA then\n            return core.schema.check(metadata_schema, conf)\n        end\n        return core.schema.check(schema, conf)\n    end\n\n    function _M.access(conf, ctx)\n        local uri_args = core.request.get_uri_args(ctx)\n        local headers = core.request.headers(ctx) or {}\n\n        local req_body, err = core.request.get_body()\n\n        if err then\n            core.log.error(\"error while reading request body: \", err)\n            return 400\n        end\n\n        -- forward the url path came through the matched uri\n        local url_decoded = url.parse(conf.function_uri)\n        local path = url_decoded.path or \"/\"\n\n        if ctx.curr_req_matched and ctx.curr_req_matched[\":ext\"] then\n            local end_path = ctx.curr_req_matched[\":ext\"]\n\n            if path:byte(-1) == string.byte(\"/\") or end_path:byte(1) == string.byte(\"/\") then\n                path = path .. end_path\n            else\n                path = path .. \"/\" .. end_path\n            end\n        end\n\n\n        headers[\"host\"] = url_decoded.host\n        local params = {\n            method = ngx.req.get_method(),\n            body = req_body,\n            query = uri_args,\n            headers = headers,\n            path = path,\n            keepalive = conf.keepalive,\n            ssl_verify = conf.ssl_verify\n        }\n\n        -- Keepalive options\n        if conf.keepalive then\n            params.keepalive_timeout = conf.keepalive_timeout\n            params.keepalive_pool = conf.keepalive_pool\n        end\n\n        -- modify request info (if required)\n        request_processor(conf, ctx, params)\n\n        local httpc = http.new()\n        httpc:set_timeout(conf.timeout)\n\n        local res\n        res, err = httpc:request_uri(conf.function_uri, params)\n\n        if not res then\n            core.log.error(\"failed to process \", plugin_name, \", err: \", err)\n            return 503\n        end\n\n        -- According to RFC7540 https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2,\n        -- endpoint must not generate any connection specific headers for HTTP/2 requests.\n        local response_headers = res.headers\n        if ngx.var.http2 then\n            response_headers[\"Connection\"] = nil\n            response_headers[\"Keep-Alive\"] = nil\n            response_headers[\"Proxy-Connection\"] = nil\n            response_headers[\"Upgrade\"] = nil\n            response_headers[\"Transfer-Encoding\"] = nil\n        end\n\n        -- setting response headers\n        core.response.set_header(response_headers)\n\n        return res.status, res.body\n    end\n\n    return _M\nend\n"
  },
  {
    "path": "apisix/plugins/serverless/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ipairs = ipairs\nlocal pcall = pcall\nlocal loadstring = loadstring\nlocal require = require\nlocal type = type\n\n\nlocal phases = {\n    \"rewrite\", \"access\", \"header_filter\", \"body_filter\",\n    \"log\", \"before_proxy\"\n}\n\n\nreturn function(plugin_name, priority)\n    local core = require(\"apisix.core\")\n\n\n    local lrucache = core.lrucache.new({\n        type = \"plugin\",\n    })\n\n    local schema = {\n        type = \"object\",\n        properties = {\n            phase = {\n                type = \"string\",\n                default = \"access\",\n                enum = phases,\n            },\n            functions = {\n                type = \"array\",\n                items = {type = \"string\"},\n                minItems = 1\n            },\n        },\n        required = {\"functions\"}\n    }\n\n    local _M = {\n        version = 0.1,\n        priority = priority,\n        name = plugin_name,\n        schema = schema,\n    }\n\n    local function load_funcs(functions)\n        local funcs = core.table.new(#functions, 0)\n\n        local index = 1\n        for _, func_str in ipairs(functions) do\n            local _, func = pcall(loadstring(func_str))\n            funcs[index] = func\n            index = index + 1\n        end\n\n        return funcs\n    end\n\n    local function call_funcs(phase, conf, ctx)\n        if phase ~= conf.phase then\n            return\n        end\n\n        local functions = core.lrucache.plugin_ctx(lrucache, ctx, nil,\n                                                   load_funcs, conf.functions)\n\n        for _, func in ipairs(functions) do\n            local code, body = func(conf, ctx)\n            if code or body then\n                return code, body\n            end\n        end\n    end\n\n    function _M.check_schema(conf)\n        local ok, err = core.schema.check(schema, conf)\n        if not ok then\n            return false, err\n        end\n\n        local functions = conf.functions\n        for _, func_str in ipairs(functions) do\n            local func, err = loadstring(func_str)\n            if err then\n                return false, 'failed to loadstring: ' .. err\n            end\n\n            local ok, ret = pcall(func)\n            if not ok then\n                return false, 'pcall error: ' .. ret\n            end\n            if type(ret) ~= 'function' then\n                return false, 'only accept Lua function,'\n                               .. ' the input code type is ' .. type(ret)\n            end\n        end\n\n        return true\n    end\n\n    for _, phase in ipairs(phases) do\n        _M[phase] = function (conf, ctx)\n            return call_funcs(phase, conf, ctx)\n        end\n    end\n\n    return _M\nend\n"
  },
  {
    "path": "apisix/plugins/serverless-post-function.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn require(\"apisix.plugins.serverless.init\")(\"serverless-post-function\", -2000)\n"
  },
  {
    "path": "apisix/plugins/serverless-pre-function.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn require(\"apisix.plugins.serverless.init\")(\"serverless-pre-function\", 10000)\n"
  },
  {
    "path": "apisix/plugins/skywalking-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal core            = require(\"apisix.core\")\nlocal http            = require(\"resty.http\")\nlocal url             = require(\"net.url\")\n\nlocal base64          = require(\"ngx.base64\")\nlocal ngx_re          = require(\"ngx.re\")\n\nlocal ngx      = ngx\nlocal tostring = tostring\nlocal tonumber = tonumber\n\nlocal plugin_name = \"skywalking-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(\"skywalking logger\")\nlocal schema = {\n    type = \"object\",\n    properties = {\n        endpoint_addr = core.schema.uri_def,\n        service_name = {type = \"string\", default = \"APISIX\"},\n        service_instance_name = {type = \"string\", default = \"APISIX Instance Name\"},\n        log_format = {type = \"object\"},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    required = {\"endpoint_addr\"},\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 408,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    local check = {\"endpoint_addr\"}\n    core.utils.check_https(check, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function send_http_data(conf, log_message)\n    local err_msg\n    local res = true\n    local url_decoded = url.parse(conf.endpoint_addr)\n    local host = url_decoded.host\n    local port = url_decoded.port\n\n    core.log.info(\"sending a batch logs to \", conf.endpoint_addr)\n\n    local httpc = http.new()\n    httpc:set_timeout(conf.timeout * 1000)\n    local ok, err = httpc:connect(host, port)\n\n    if not ok then\n        return false, \"failed to connect to host[\" .. host .. \"] port[\"\n            .. tostring(port) .. \"] \" .. err\n    end\n\n    local httpc_res, httpc_err = httpc:request({\n        method = \"POST\",\n        path = \"/v3/logs\",\n        body = log_message,\n        headers = {\n            [\"Host\"] = url_decoded.host,\n            [\"Content-Type\"] = \"application/json\",\n        }\n    })\n\n    if not httpc_res then\n        return false, \"error while sending data to [\" .. host .. \"] port[\"\n            .. tostring(port) .. \"] \" .. httpc_err\n    end\n\n    -- some error occurred in the server\n    if httpc_res.status >= 400 then\n        res =  false\n        err_msg = \"server returned status code[\" .. httpc_res.status .. \"] host[\"\n            .. host .. \"] port[\" .. tostring(port) .. \"] \"\n            .. \"body[\" .. httpc_res:read_body() .. \"]\"\n    end\n\n    return res, err_msg\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local log_body = log_util.get_log_entry(plugin_name, conf, ctx)\n    local trace_context\n    local sw_header = ngx.req.get_headers()[\"sw8\"]\n    if sw_header then\n        -- 1-TRACEID-SEGMENTID-SPANID-PARENT_SERVICE-PARENT_INSTANCE-PARENT_ENDPOINT-IPPORT\n        local ids = ngx_re.split(sw_header, '-')\n        if #ids == 8 then\n            trace_context = {\n                traceId = base64.decode_base64url(ids[2]),\n                traceSegmentId = base64.decode_base64url(ids[3]),\n                spanId = tonumber(ids[4])\n            }\n        else\n            core.log.warn(\"failed to parse trace_context header: \", sw_header)\n        end\n    end\n\n    local service_instance_name = conf.service_instance_name\n    if service_instance_name == \"$hostname\" then\n        service_instance_name = core.utils.gethostname()\n    end\n\n    local entry = {\n        traceContext = trace_context,\n        body = {\n            json = {\n                json = core.json.encode(log_body, true)\n            }\n        },\n        service = conf.service_name,\n        serviceInstance = service_instance_name,\n        endpoint = ctx.var.uri,\n    }\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err = core.json.encode(entries)\n        if not data then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        return send_http_data(conf, data)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/skywalking.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal process = require(\"ngx.process\")\nlocal sw_tracer = require(\"skywalking.tracer\")\nlocal Span = require(\"skywalking.span\")\nlocal ngx = ngx\nlocal math = math\n\nlocal plugin_name = \"skywalking\"\nlocal attr_schema = {\n    type = \"object\",\n    properties = {\n        service_name = {\n            type = \"string\",\n            description = \"service name for skywalking\",\n            default = \"APISIX\",\n        },\n        service_instance_name = {\n            type = \"string\",\n            description = \"User Service Instance Name\",\n            default = \"APISIX Instance Name\",\n        },\n        endpoint_addr = {\n            type = \"string\",\n            default = \"http://127.0.0.1:12800\",\n        },\n        report_interval = {\n            type = \"integer\",\n        },\n    },\n}\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        sample_ratio = {\n            type = \"number\",\n            minimum = 0.00001,\n            maximum = 1,\n            default = 1\n        }\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 12010,\n    name = plugin_name,\n    schema = schema,\n    attr_schema = attr_schema,\n    run_policy = \"prefer_route\",\n}\n\n\nfunction _M.check_schema(conf)\n    local check = {\"endpoint_addr\"}\n    core.utils.check_https(check, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    core.log.debug(\"rewrite phase of skywalking plugin\")\n    ctx.skywalking_sample = false\n    if conf.sample_ratio == 1 or math.random() < conf.sample_ratio then\n        ctx.skywalking_sample = true\n        sw_tracer:start(\"upstream service\")\n        core.log.info(\"tracer start\")\n        return\n    end\n\n    core.log.info(\"miss sampling, ignore\")\nend\n\n\nfunction _M.delayed_body_filter(conf, ctx)\n    if ctx.skywalking_sample and ngx.arg[2] then\n        Span.setComponentId(ngx.ctx.exitSpan, 6002)\n        Span.setComponentId(ngx.ctx.entrySpan, 6002)\n        sw_tracer:finish()\n        core.log.info(\"tracer finish\")\n    end\nend\n\n\nlocal started_skywalking_reporter = false\nfunction _M.log(conf, ctx)\n    if not ctx.skywalking_sample then\n        return\n    end\n\n    if not started_skywalking_reporter then\n        started_skywalking_reporter = true\n\n        local sk_cli = require(\"skywalking.client\")\n\n        local plugin_info = _M.plugin_info\n        if plugin_info.report_interval then\n            sk_cli.backendTimerDelay = plugin_info.report_interval\n        end\n        sk_cli:startBackendTimer(plugin_info.endpoint_addr)\n    end\n\n    sw_tracer:prepareForReport()\n    core.log.info(\"tracer prepare for report\")\nend\n\n\nfunction _M.init()\n    if process.type() ~= \"worker\" then\n        return\n    end\n    local local_plugin_info = plugin.plugin_attr(plugin_name)\n    local_plugin_info = local_plugin_info and core.table.clone(local_plugin_info) or {}\n    local ok, err = core.schema.check(attr_schema, local_plugin_info)\n    if not ok then\n        core.log.error(\"failed to check the plugin_attr[\", plugin_name, \"]\",\n                       \": \", err)\n        return\n    end\n\n    core.log.info(\"plugin attribute: \",\n                  core.json.delay_encode(local_plugin_info))\n\n    -- TODO: maybe need to fetch them from plugin-metadata\n    if local_plugin_info.service_instance_name == \"$hostname\" then\n        local_plugin_info.service_instance_name = core.utils.gethostname()\n    end\n\n    _M.plugin_info = local_plugin_info\n\n    local metadata_shdict = ngx.shared.tracing_buffer\n\n    metadata_shdict:set('serviceName', local_plugin_info.service_name)\n    metadata_shdict:set('serviceInstanceName', local_plugin_info.service_instance_name)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/sls-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\n\n\nlocal plugin_name = \"sls-logger\"\nlocal ngx = ngx\nlocal rf5424 = require(\"apisix.utils.rfc5424\")\nlocal tcp = ngx.socket.tcp\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal table = table\n\n\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\nlocal schema = {\n    type = \"object\",\n    properties = {\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        timeout = {type = \"integer\", minimum = 1, default= 5000},\n        log_format = {type = \"object\"},\n        host = {type = \"string\"},\n        port = {type = \"integer\"},\n        project = {type = \"string\"},\n        logstore = {type = \"string\"},\n        access_key_id = {type = \"string\"},\n        access_key_secret = {type =\"string\"}\n    },\n    encrypt_fields = {\"access_key_secret\"},\n    required = {\"host\", \"port\", \"project\", \"logstore\", \"access_key_id\", \"access_key_secret\"}\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        }\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 406,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\nfunction _M.check_schema(conf,schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n      return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n\nlocal function send_tcp_data(route_conf, log_message)\n    local err_msg\n    local res = true\n    local sock, soc_err = tcp()\n    local can_close\n\n    if not sock then\n        return false, \"failed to init the socket\" .. soc_err\n    end\n\n    sock:settimeout(route_conf.timeout)\n    local ok, err = sock:connect(route_conf.host, route_conf.port)\n    if not ok then\n        return false, \"failed to connect to TCP server: host[\" .. route_conf.host\n                      .. \"] port[\" .. tostring(route_conf.port) .. \"] err: \" .. err\n    end\n\n    ok, err = sock:sslhandshake(true, nil, false)\n    if not ok then\n        return false, \"failed to perform TLS handshake to TCP server: host[\"\n                      .. route_conf.host .. \"] port[\" .. tostring(route_conf.port)\n                      .. \"] err: \" .. err\n    end\n\n    core.log.debug(\"sls logger send data \", log_message)\n    ok, err = sock:send(log_message)\n    if not ok then\n        res = false\n        can_close = true\n        err_msg = \"failed to send data to TCP server: host[\" .. route_conf.host\n                  .. \"] port[\" .. tostring(route_conf.port) .. \"] err: \" .. err\n    else\n        ok, err = sock:setkeepalive(120 * 1000, 20)\n        if not ok then\n            can_close = true\n            core.log.warn(\"failed to set socket keepalive: host[\", route_conf.host,\n                          \"] port[\", tostring(route_conf.port), \"] err: \", err)\n        end\n    end\n\n    if  can_close then\n        ok, err = sock:close()\n        if not ok then\n            core.log.warn(\"failed to close the TCP connection, host[\",\n                          route_conf.host, \"] port[\", route_conf.port, \"] \", err)\n        end\n    end\n\n    return res, err_msg\nend\n\nlocal function combine_syslog(entries)\n    local items = {}\n    for _, entry in ipairs(entries) do\n        table.insert(items, entry.data)\n        core.log.info(\"buffered logs:\", entry.data)\n    end\n\n    return table.concat(items)\nend\n\n_M.combine_syslog = combine_syslog\n\nlocal function handle_log(entries)\n    local data = combine_syslog(entries)\n    if not data then\n        return true\n    end\n\n    return send_tcp_data(entries[1].route_conf, data)\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\n-- log phase in APISIX\nfunction _M.log(conf, ctx)\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    local json_str, err = core.json.encode(entry)\n    if not json_str then\n        core.log.error('error occurred while encoding the data: ', err)\n        return\n    end\n\n    local structured_data = {\n        {name = \"project\", value = conf.project},\n        {name = \"logstore\", value = conf.logstore},\n        {name = \"access-key-id\", value = conf.access_key_id},\n        {name = \"access-key-secret\", value = conf.access_key_secret},\n    }\n    local rf5424_data = rf5424.encode(\"SYSLOG\", \"INFO\", ctx.var.host, \"apisix\",\n                                      ctx.var.pid, json_str, structured_data)\n    core.log.info(\"collect_data:\" .. rf5424_data)\n    local process_context = {\n        data = rf5424_data,\n        route_conf = conf\n    }\n\n    if batch_processor_manager:add_entry(conf, process_context) then\n        return\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, process_context, ctx, handle_log)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/splunk-hec-logging.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core            = require(\"apisix.core\")\nlocal ngx             = ngx\nlocal ngx_now         = ngx.now\nlocal http            = require(\"resty.http\")\nlocal log_util        = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod  = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin          = require(\"apisix.plugin\")\nlocal table_insert    = core.table.insert\nlocal table_concat    = core.table.concat\nlocal ipairs          = ipairs\n\n\nlocal DEFAULT_SPLUNK_HEC_ENTRY_SOURCE = \"apache-apisix-splunk-hec-logging\"\nlocal DEFAULT_SPLUNK_HEC_ENTRY_TYPE = \"_json\"\n\n\nlocal plugin_name = \"splunk-hec-logging\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        endpoint = {\n            type = \"object\",\n            properties = {\n                uri = core.schema.uri_def,\n                token = {\n                    type = \"string\",\n                },\n                channel = {\n                    type = \"string\",\n                },\n                timeout = {\n                    type = \"integer\",\n                    minimum = 1,\n                    default = 10\n                },\n                keepalive_timeout = {\n                    type = \"integer\",\n                    minimum = 1000,\n                    default = 60000,\n                    description = \"keepalive timeout in milliseconds\",\n                }\n            },\n            required = { \"uri\", \"token\" }\n        },\n        ssl_verify = {\n            type = \"boolean\",\n            default = true\n        },\n        log_format = {type = \"object\"},\n    },\n    required = { \"endpoint\" },\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        }\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 409,\n    name = plugin_name,\n    metadata_schema = metadata_schema,\n    schema = batch_processor_manager:wrap_schema(schema),\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function get_logger_entry(conf, ctx)\n    local entry, customized = log_util.get_log_entry(plugin_name, conf, ctx)\n    local splunk_entry = {\n        time = ngx_now(),\n        source = DEFAULT_SPLUNK_HEC_ENTRY_SOURCE,\n        sourcetype = DEFAULT_SPLUNK_HEC_ENTRY_TYPE,\n    }\n\n    if not customized then\n        splunk_entry.host = entry.server.hostname\n        splunk_entry.event = {\n            request_url = entry.request.url,\n            request_method = entry.request.method,\n            request_headers = entry.request.headers,\n            request_query = entry.request.querystring,\n            request_size = entry.request.size,\n            response_headers = entry.response.headers,\n            response_status = entry.response.status,\n            response_size = entry.response.size,\n            latency = entry.latency,\n            upstream = entry.upstream,\n        }\n    else\n        splunk_entry.host = core.utils.gethostname()\n        splunk_entry.event = entry\n    end\n\n    return splunk_entry\nend\n\n\nlocal function send_to_splunk(conf, entries)\n    local request_headers = {}\n    request_headers[\"Content-Type\"] = \"application/json\"\n    request_headers[\"Authorization\"] = \"Splunk \" .. conf.endpoint.token\n    if conf.endpoint.channel then\n        request_headers[\"X-Splunk-Request-Channel\"] = conf.endpoint.channel\n    end\n\n    local http_new = http.new()\n    http_new:set_timeout(conf.endpoint.timeout * 1000)\n    local t = {}\n    for _, e in ipairs(entries) do\n        table_insert(t, core.json.encode(e))\n    end\n\n    local res, err = http_new:request_uri(conf.endpoint.uri, {\n        ssl_verify = conf.ssl_verify,\n        method = \"POST\",\n        body = table_concat(t),\n        headers = request_headers,\n        keepalive_timeout = conf.endpoint.keepalive_timeout\n    })\n\n    if not res then\n        return false, \"failed to write log to splunk, \" .. err\n    end\n\n    if res.status ~= 200 then\n        local body = core.json.decode(res.body)\n        if not body then\n            return false, \"failed to send splunk, http status code: \" .. res.status\n        else\n            return false, \"failed to send splunk, \" .. body.text\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = get_logger_entry(conf, ctx)\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    local process = function(entries)\n        return send_to_splunk(conf, entries)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx,\n                                                        process, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/syslog/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal logger_socket = require(\"resty.logger.socket\")\nlocal rfc5424 = require(\"apisix.utils.rfc5424\")\nlocal ipairs = ipairs\nlocal table_insert = core.table.insert\nlocal table_concat = core.table.concat\n\nlocal batch_processor_manager = bp_manager_mod.new(\"sys logger\")\n\nlocal lrucache = core.lrucache.new({\n    ttl = 300, count = 512, serial_creating = true,\n})\n\nlocal _M = {}\n\nfunction _M.flush_syslog(logger)\n    local ok, err = logger:flush(logger)\n    if not ok then\n        core.log.error(\"failed to flush message:\", err)\n    end\n\n    return ok\nend\n\n\nlocal function send_syslog_data(conf, log_message, api_ctx)\n    local err_msg\n    local res = true\n\n    core.log.info(\"sending a batch logs to \", conf.host, \":\", conf.port)\n\n    -- fetch it from lrucache\n    local logger, err = core.lrucache.plugin_ctx(\n        lrucache, api_ctx, nil, logger_socket.new, logger_socket, {\n            host = conf.host,\n            port = conf.port,\n            flush_limit = conf.flush_limit,\n            drop_limit = conf.drop_limit,\n            timeout = conf.timeout,\n            sock_type = conf.sock_type,\n            pool_size = conf.pool_size,\n            tls = conf.tls,\n        }\n    )\n\n    if not logger then\n        res = false\n        err_msg = \"failed when initiating the sys logger processor\".. err\n    end\n\n    -- reuse the logger object\n    local ok, err = logger:log(log_message)\n\n    if not ok then\n        res = false\n        err_msg = \"failed to log message\" .. err\n    end\n\n    return res, err_msg\nend\n\n\n-- called in log phase of APISIX\nfunction _M.push_entry(conf, ctx, entry)\n    local json_str, err = core.json.encode(entry)\n    if not json_str then\n        core.log.error('error occurred while encoding the data: ', err)\n        return\n    end\n\n    local rfc5424_data = rfc5424.encode(\"SYSLOG\", \"INFO\", ctx.var.host,\n                                \"apisix\", ctx.var.pid, json_str)\n    core.log.info(\"collect_data:\" .. rfc5424_data)\n    if batch_processor_manager:add_entry(conf, rfc5424_data) then\n        return\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local cp_ctx = core.table.clone(ctx)\n    local func = function(entries)\n        local items = {}\n        for _, e in ipairs(entries) do\n            table_insert(items, e)\n            core.log.debug(\"buffered logs:\", e)\n        end\n\n        return send_syslog_data(conf, table_concat(items), cp_ctx)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, rfc5424_data, ctx, func)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/syslog.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal syslog = require(\"apisix.plugins.syslog.init\")\nlocal plugin_name = \"syslog\"\n\nlocal batch_processor_manager = bp_manager_mod.new(\"sys logger\")\nlocal schema = {\n    type = \"object\",\n    properties = {\n        host = {type = \"string\"},\n        port = {type = \"integer\"},\n        flush_limit = {type = \"integer\", minimum = 1, default = 4096},\n        drop_limit = {type = \"integer\", default = 1048576},\n        timeout = {type = \"integer\", minimum = 1, default = 3000},\n        sock_type = {type = \"string\", default = \"tcp\", enum = {\"tcp\", \"udp\"}},\n        pool_size = {type = \"integer\", minimum = 5, default = 5},\n        tls = {type = \"boolean\", default = false},\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    required = {\"host\", \"port\"}\n}\n\n\nlocal schema = batch_processor_manager:wrap_schema(schema)\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        }\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 401,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n    flush_syslog = syslog.flush_syslog,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    core.utils.check_tls_bool({\"tls\"}, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    syslog.push_entry(conf, ctx, entry)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/tcp-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core     = require(\"apisix.core\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal plugin   = require(\"apisix.plugin\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin_name = \"tcp-logger\"\nlocal tostring = tostring\nlocal ngx = ngx\nlocal tcp = ngx.socket.tcp\n\n\nlocal batch_processor_manager = bp_manager_mod.new(\"tcp logger\")\nlocal schema = {\n    type = \"object\",\n    properties = {\n        host = {type = \"string\"},\n        port = {type = \"integer\", minimum = 0},\n        tls = {type = \"boolean\", default = false},\n        tls_options = {type = \"string\"},\n        timeout = {type = \"integer\", minimum = 1, default= 1000},\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    required = {\"host\", \"port\"}\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 405,\n    name = plugin_name,\n    metadata_schema = metadata_schema,\n    schema = batch_processor_manager:wrap_schema(schema),\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    core.utils.check_tls_bool({\"tls\"}, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function send_tcp_data(conf, log_message)\n    local err_msg\n    local res = true\n    local sock, soc_err = tcp()\n\n    if not sock then\n        return false, \"failed to init the socket\" .. soc_err\n    end\n\n    sock:settimeout(conf.timeout)\n\n    core.log.info(\"sending a batch logs to \", conf.host, \":\", conf.port)\n    core.log.info(\"sending log_message: \", log_message)\n\n    local ok, err = sock:connect(conf.host, conf.port)\n    if not ok then\n        return false, \"failed to connect to TCP server: host[\" .. conf.host\n                      .. \"] port[\" .. tostring(conf.port) .. \"] err: \" .. err\n    end\n\n    if conf.tls then\n        ok, err = sock:sslhandshake(true, conf.tls_options, false)\n        if not ok then\n            return false, \"failed to perform TLS handshake to TCP server: host[\"\n                          .. conf.host .. \"] port[\" .. tostring(conf.port) .. \"] err: \" .. err\n        end\n    end\n\n    ok, err = sock:send(log_message)\n    if not ok then\n        res = false\n        err_msg = \"failed to send data to TCP server: host[\" .. conf.host\n                  .. \"] port[\" .. tostring(conf.port) .. \"] err: \" .. err\n    end\n\n    ok, err = sock:close()\n    if not ok then\n        core.log.error(\"failed to close the TCP connection, host[\",\n                        conf.host, \"] port[\", conf.port, \"] \", err)\n    end\n\n    return res, err_msg\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err\n        if batch_max_size == 1 then\n            data, err = core.json.encode(entries[1]) -- encode as single {}\n        else\n            data, err = core.json.encode(entries) -- encode as array [{}]\n        end\n\n        if not data then\n            core.log.error('error occurred while encoding the data: ', err)\n        end\n\n        return send_tcp_data(conf, data)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/tencent-cloud-cls/cls-sdk.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal pb = require \"pb\"\nlocal protoc = require(\"protoc\").new()\nlocal http = require(\"resty.http\")\nlocal socket = require(\"socket\")\nlocal str_util = require(\"resty.string\")\nlocal core = require(\"apisix.core\")\nlocal core_gethostname = require(\"apisix.core.utils\").gethostname\nlocal json = core.json\nlocal json_encode = json.encode\nlocal ngx = ngx\nlocal ngx_time = ngx.time\nlocal ngx_now = ngx.now\nlocal ngx_sha1_bin = ngx.sha1_bin\nlocal ngx_hmac_sha1 = ngx.hmac_sha1\nlocal fmt = string.format\nlocal table = table\nlocal concat_tab = table.concat\nlocal clear_tab = table.clear\nlocal new_tab = table.new\nlocal insert_tab = table.insert\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal type = type\nlocal tostring = tostring\nlocal setmetatable = setmetatable\nlocal pcall = pcall\nlocal unpack = unpack\n\n-- api doc https://www.tencentcloud.com/document/product/614/16873\nlocal MAX_SINGLE_VALUE_SIZE = 1 * 1024 * 1024\nlocal MAX_LOG_GROUP_VALUE_SIZE = 5 * 1024 * 1024 -- 5MB\n\nlocal cls_api_path = \"/structuredlog\"\nlocal auth_expire_time = 60\nlocal cls_conn_timeout = 1000\nlocal cls_read_timeout = 10000\nlocal cls_send_timeout = 10000\n\nlocal headers_cache = {}\nlocal params_cache = {\n    ssl_verify = false,\n    headers = headers_cache,\n}\n\n\nlocal function get_ip(hostname)\n    local _, resolved = socket.dns.toip(hostname)\n    local ip_list = {}\n    if not resolved.ip then\n        -- DNS parsing failure\n        local err = resolved\n        core.log.error(\"resolve ip failed, hostname: \" .. hostname .. \", error: \" .. err)\n        return nil, err\n    else\n        for _, v in ipairs(resolved.ip) do\n            insert_tab(ip_list, v)\n        end\n    end\n    return ip_list\nend\n\nlocal host_ip\nlocal log_group_list = {}\nlocal log_group_list_pb = {\n    logGroupList = log_group_list,\n}\n\n\nlocal function sha1(msg)\n    return str_util.to_hex(ngx_sha1_bin(msg))\nend\n\n\nlocal function sha1_hmac(key, msg)\n    return str_util.to_hex(ngx_hmac_sha1(key, msg))\nend\n\n\n-- sign algorithm https://cloud.tencent.com/document/product/614/12445\nlocal function sign(secret_id, secret_key)\n    local method = \"post\"\n    local format_params = \"\"\n    local format_headers = \"\"\n    local sign_algorithm = \"sha1\"\n    local http_request_info = fmt(\"%s\\n%s\\n%s\\n%s\\n\",\n                                  method, cls_api_path, format_params, format_headers)\n    local cur_time = ngx_time()\n    local sign_time = fmt(\"%d;%d\", cur_time, cur_time + auth_expire_time)\n    local string_to_sign = fmt(\"%s\\n%s\\n%s\\n\", sign_algorithm, sign_time, sha1(http_request_info))\n\n    local sign_key = sha1_hmac(secret_key, sign_time)\n    local signature = sha1_hmac(sign_key, string_to_sign)\n\n    local arr = {\n        \"q-sign-algorithm=sha1\",\n        \"q-ak=\" .. secret_id,\n        \"q-sign-time=\" .. sign_time,\n        \"q-key-time=\" .. sign_time,\n        \"q-header-list=\",\n        \"q-url-param-list=\",\n        \"q-signature=\" .. signature,\n    }\n\n    return concat_tab(arr, '&')\nend\n\n\n-- normalized log data for CLS API\nlocal function normalize_log(log)\n    local normalized_log = {}\n    local log_size = 4 -- empty obj alignment\n    for k, v in pairs(log) do\n        local v_type = type(v)\n        local field = { key = k, value = \"\" }\n        if v_type == \"string\" then\n            field[\"value\"] = v\n        elseif v_type == \"number\" then\n            field[\"value\"] = tostring(v)\n        elseif v_type == \"table\" then\n            field[\"value\"] = json_encode(v)\n        else\n            field[\"value\"] = tostring(v)\n            core.log.warn(\"unexpected type \" .. v_type .. \" for field \" .. k)\n        end\n        if #field.value > MAX_SINGLE_VALUE_SIZE then\n            core.log.warn(field.key, \" value size over \", MAX_SINGLE_VALUE_SIZE, \" , truncated\")\n            field.value = field.value:sub(1, MAX_SINGLE_VALUE_SIZE)\n        end\n        insert_tab(normalized_log, field)\n        log_size = log_size + #field.key + #field.value\n    end\n    return normalized_log, log_size\nend\n\n\nlocal _M = { version = 0.1 }\nlocal mt = { __index = _M }\n\nlocal pb_state\nlocal function init_pb_state()\n    local old_pb_state = pb.state(nil)\n    protoc.reload()\n    local cls_sdk_protoc = protoc.new()\n    -- proto file in https://www.tencentcloud.com/document/product/614/42787\n    local ok, err = pcall(cls_sdk_protoc.load, cls_sdk_protoc, [[\npackage cls;\n\nmessage Log\n{\n  message Content\n  {\n    required string key   = 1; // Key of each field group\n    required string value = 2; // Value of each field group\n  }\n  required int64   time     = 1; // Unix timestamp\n  repeated Content contents = 2; // Multiple key-value pairs in one log\n}\n\nmessage LogTag\n{\n  required string key       = 1;\n  required string value     = 2;\n}\n\nmessage LogGroup\n{\n  repeated Log    logs        = 1; // Log array consisting of multiple logs\n  optional string contextFlow = 2; // This parameter does not take effect currently\n  optional string filename    = 3; // Log filename\n  optional string source      = 4; // Log source, which is generally the machine IP\n  repeated LogTag logTags     = 5;\n}\n\nmessage LogGroupList\n{\n  repeated LogGroup logGroupList = 1; // Log group list\n}\n        ]], \"tencent-cloud-cls/cls.proto\")\n    if not ok then\n        cls_sdk_protoc:reset()\n        pb.state(old_pb_state)\n        return \"failed to load cls.proto: \".. err\n    end\n    pb_state = pb.state(old_pb_state)\nend\n\n\nfunction _M.new(scheme, host, topic, secret_id, secret_key)\n    if not pb_state then\n        local err = init_pb_state()\n        if err then\n            return nil, err\n        end\n    end\n    local self = {\n        scheme = scheme,\n        host = host,\n        topic = topic,\n        secret_id = secret_id,\n        secret_key = secret_key,\n    }\n    return setmetatable(self, mt)\nend\n\n\nlocal function do_request_uri(uri, params)\n    local client = http:new()\n    client:set_timeouts(cls_conn_timeout, cls_send_timeout, cls_read_timeout)\n    local res, err = client:request_uri(uri, params)\n    client:close()\n    return res, err\nend\n\n\nfunction _M.send_cls_request(self, pb_obj)\n    -- recovery of stored pb_store\n    local old_pb_state = pb.state(pb_state)\n    local ok, pb_data = pcall(pb.encode, \"cls.LogGroupList\", pb_obj)\n    pb_state = pb.state(old_pb_state)\n    if not ok or not pb_data then\n        core.log.error(\"failed to encode LogGroupList, err: \", pb_data)\n        return false, pb_data\n    end\n\n    clear_tab(headers_cache)\n    headers_cache[\"Host\"] = self.host\n    headers_cache[\"Content-Type\"] = \"application/x-protobuf\"\n    headers_cache[\"Authorization\"] = sign(self.secret_id, self.secret_key, cls_api_path)\n\n    -- TODO: support lz4/zstd compress\n    params_cache.method = \"POST\"\n    params_cache.body = pb_data\n\n    local cls_url = self.scheme .. \"://\" .. self.host .. cls_api_path .. \"?topic_id=\" .. self.topic\n    core.log.debug(\"CLS request URL: \", cls_url)\n\n    local res, err = do_request_uri(cls_url, params_cache)\n    if not res then\n        return false, err\n    end\n\n    if res.status ~= 200 then\n        err = fmt(\"got wrong status: %s, headers: %s, body, %s\",\n                res.status, json.encode(res.headers), res.body)\n        -- 413, 404, 401, 403 are not retryable\n        if res.status == 413 or res.status == 404 or res.status == 401 or res.status == 403 then\n            core.log.error(err, \", not retryable\")\n            return true\n        end\n\n        return false, err\n    end\n\n    core.log.debug(\"CLS report success\")\n    return true\nend\n\n\nfunction _M.send_to_cls(self, logs)\n    clear_tab(log_group_list)\n    local now = ngx_now() * 1000\n\n    local total_size = 0\n    local format_logs = new_tab(#logs, 0)\n    -- sums of all value in all LogGroup should be no more than 5MB\n    -- so send whenever size exceed max size\n    local group_list_start = 1\n\n    if not host_ip then\n        local host_ip_list, err = get_ip(core_gethostname())\n        if not host_ip_list then\n            return false, err\n        end\n        host_ip = tostring(unpack(host_ip_list))\n    end\n\n    for i = 1, #logs, 1 do\n        local contents, log_size = normalize_log(logs[i])\n        if log_size > MAX_LOG_GROUP_VALUE_SIZE then\n            core.log.error(\"size of log is over 5MB, dropped\")\n            goto continue\n        end\n        total_size = total_size + log_size\n        if total_size > MAX_LOG_GROUP_VALUE_SIZE then\n            insert_tab(log_group_list, {\n                logs = format_logs,\n                source = host_ip,\n            })\n            local ok, err = self:send_cls_request(log_group_list_pb)\n            if not ok then\n                return false, err, group_list_start\n            end\n            group_list_start = i\n            format_logs = new_tab(#logs - i, 0)\n            total_size = 0\n            clear_tab(log_group_list)\n        end\n        insert_tab(format_logs, {\n            time = now,\n            contents = contents,\n        })\n        :: continue ::\n    end\n\n    insert_tab(log_group_list, {\n        logs = format_logs,\n        source = host_ip,\n    })\n    local ok, err = self:send_cls_request(log_group_list_pb)\n    return ok, err, group_list_start\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/tencent-cloud-cls.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal cls_sdk = require(\"apisix.plugins.tencent-cloud-cls.cls-sdk\")\nlocal math = math\nlocal pairs = pairs\n\nlocal plugin_name = \"tencent-cloud-cls\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\nlocal schema = {\n    type = \"object\",\n    properties = {\n        cls_host = { type = \"string\" },\n        cls_topic = { type = \"string\" },\n        scheme = { type = \"string\", default = \"https\"},\n        secret_id = { type = \"string\" },\n        secret_key = { type = \"string\" },\n        sample_ratio = {\n            type = \"number\",\n            minimum = 0.00001,\n            maximum = 1,\n            default = 1\n        },\n        include_req_body = { type = \"boolean\", default = false },\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = { type = \"integer\", minimum = 1, default = 524288 },\n        max_resp_body_bytes = { type = \"integer\", minimum = 1, default = 524288 },\n        global_tag = { type = \"object\" },\n        log_format = {type = \"object\"},\n    },\n    encrypt_fields = {\"secret_key\"},\n    required = { \"cls_host\", \"cls_topic\", \"secret_id\", \"secret_key\" }\n}\n\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 397,\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n    metadata_schema = metadata_schema,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return nil, err\n    end\n    return log_util.check_log_schema(conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    ctx.cls_sample = false\n    if conf.sample_ratio == 1 or math.random() < conf.sample_ratio then\n        core.log.debug(\"cls sampled\")\n        ctx.cls_sample = true\n    else\n        return\n    end\n\n    log_util.check_and_read_req_body(conf, ctx)\nend\n\n\nfunction _M.body_filter(conf, ctx)\n    if ctx.cls_sample then\n        log_util.collect_body(conf, ctx)\n    end\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    -- sample if set\n    if not ctx.cls_sample then\n        core.log.debug(\"cls not sampled, skip log\")\n        return\n    end\n\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n\n    if conf.global_tag then\n        for k, v in pairs(conf.global_tag) do\n            entry[k] = v\n        end\n    end\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    local process = function(entries)\n        local sdk, err = cls_sdk.new(\n                            conf.scheme, conf.cls_host,\n                            conf.cls_topic, conf.secret_id,\n                            conf.secret_key)\n        if err then\n            core.log.error(\"init sdk failed err:\", err)\n            return false, err\n        end\n        return sdk:send_to_cls(entries)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx,\n                                                        process, max_pending_entries)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/traffic-split.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core       = require(\"apisix.core\")\nlocal upstream   = require(\"apisix.upstream\")\nlocal schema_def = require(\"apisix.schema_def\")\nlocal roundrobin = require(\"resty.roundrobin\")\nlocal ipmatcher  = require(\"resty.ipmatcher\")\nlocal expr       = require(\"resty.expr.v1\")\nlocal pairs      = pairs\nlocal ipairs     = ipairs\nlocal type       = type\nlocal table_insert = table.insert\nlocal tostring   = tostring\n\nlocal lrucache = core.lrucache.new({\n    ttl = 0, count = 512\n})\n\n\nlocal vars_schema = {\n    type = \"array\",\n}\n\n\nlocal match_schema = {\n    type = \"array\",\n    items = {\n        type = \"object\",\n        properties = {\n            vars = vars_schema\n        }\n    },\n}\n\n\nlocal upstreams_schema = {\n    type = \"array\",\n    items = {\n        type = \"object\",\n        properties = {\n            upstream_id = schema_def.id_schema,\n            upstream = schema_def.upstream,\n            weight = {\n                description = \"used to split traffic between different\" ..\n                              \"upstreams for plugin configuration\",\n                type = \"integer\",\n                default = 1,\n                minimum = 0\n            }\n        }\n    },\n    -- When the upstream configuration of the plugin is missing,\n    -- the upstream of `route` is used by default.\n    default = {\n        {\n            weight = 1\n        }\n    },\n    minItems = 1,\n    maxItems = 20\n}\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        rules = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    match = match_schema,\n                    weighted_upstreams = upstreams_schema\n                },\n            }\n        }\n    },\n}\n\nlocal plugin_name = \"traffic-split\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 966,\n    name = plugin_name,\n    schema = schema\n}\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n\n    if not ok then\n        return false, err\n    end\n\n    if conf.rules then\n        for _, rule in ipairs(conf.rules) do\n            if rule.match then\n                for _, m in ipairs(rule.match) do\n                    local ok, err = expr.new(m.vars)\n                    if not ok then\n                        return false, \"failed to validate the 'vars' expression: \" .. err\n                    end\n                end\n            end\n        end\n    end\n\n    return true\nend\n\n\nlocal function parse_domain_for_node(node)\n    local host = node.domain or node.host\n    if not ipmatcher.parse_ipv4(host)\n       and not ipmatcher.parse_ipv6(host)\n    then\n        node.domain = host\n\n        local ip, err = core.resolver.parse_domain(host)\n        if ip then\n            node.host = ip\n        end\n\n        if err then\n            core.log.error(\"dns resolver domain: \", host, \" error: \", err)\n        end\n    end\nend\n\n\nlocal function set_upstream(upstream_info, ctx)\n    local nodes = upstream_info.nodes\n    local new_nodes = {}\n    if core.table.isarray(nodes) then\n        for _, node in ipairs(nodes) do\n            parse_domain_for_node(node)\n            table_insert(new_nodes, node)\n        end\n    else\n        for addr, weight in pairs(nodes) do\n            local node = {}\n            local port, host\n            host, port = core.utils.parse_addr(addr)\n            node.host = host\n            parse_domain_for_node(node)\n            node.port = port\n            node.weight = weight\n            table_insert(new_nodes, node)\n        end\n    end\n\n    local up_conf = {\n        name = upstream_info.name,\n        type = upstream_info.type,\n        hash_on = upstream_info.hash_on,\n        pass_host = upstream_info.pass_host,\n        upstream_host = upstream_info.upstream_host,\n        key = upstream_info.key,\n        nodes = new_nodes,\n        timeout = upstream_info.timeout,\n        scheme = upstream_info.scheme\n    }\n\n    local ok, err = upstream.check_schema(up_conf)\n    if not ok then\n        core.log.error(\"failed to validate generated upstream: \", err)\n        return 500, err\n    end\n\n    local matched_route = ctx.matched_route\n    up_conf.parent = matched_route\n    local upstream_key = up_conf.type .. \"#route_\" ..\n                         matched_route.value.id .. \"_\" .. upstream_info.vid\n    if upstream_info.node_tid then\n        upstream_key = upstream_key .. \"_\" .. upstream_info.node_tid\n    end\n    core.log.info(\"upstream_key: \", upstream_key)\n    upstream.set(ctx, upstream_key, ctx.conf_version, up_conf)\n    if upstream_info.scheme == \"https\" then\n        upstream.set_scheme(ctx, up_conf)\n    end\n    return\nend\n\n\nlocal function new_rr_obj(weighted_upstreams)\n    local server_list = {}\n    for i, upstream_obj in ipairs(weighted_upstreams) do\n        if upstream_obj.upstream_id then\n            server_list[upstream_obj.upstream_id] = upstream_obj.weight\n        elseif upstream_obj.upstream then\n            -- Add a virtual id field to uniquely identify the upstream key.\n            upstream_obj.upstream.vid = i\n            -- Get the table id of the nodes as part of the upstream_key,\n            -- avoid upstream_key duplicate because vid is the same in the loop\n            -- when multiple rules with multiple weighted_upstreams under each rule.\n            -- see https://github.com/apache/apisix/issues/5276\n            local node_tid = tostring(upstream_obj.upstream.nodes):sub(#\"table: \" + 1)\n            upstream_obj.upstream.node_tid = node_tid\n            server_list[upstream_obj.upstream] = upstream_obj.weight\n        else\n            -- If the upstream object has only the weight value, it means\n            -- that the upstream weight value on the default route has been reached.\n            -- Mark empty upstream services in the plugin.\n            server_list[\"plugin#upstream#is#empty\"] = upstream_obj.weight\n\n        end\n    end\n\n    return roundrobin:new(server_list)\nend\n\n\nfunction _M.access(conf, ctx)\n    if not conf or not conf.rules then\n        return\n    end\n\n    local weighted_upstreams\n    local match_passed = true\n\n    for _, rule in ipairs(conf.rules) do\n        -- check if all upstream_ids are valid\n        if rule.weighted_upstreams then\n            for _, wupstream in ipairs(rule.weighted_upstreams) do\n                local ups_id = wupstream.upstream_id\n                if ups_id then\n                    local ups = upstream.get_by_id(ups_id)\n                    if not ups then\n                        return 500, \"failed to fetch upstream info by \"\n                                    .. \"upstream id: \" .. ups_id\n                    end\n                end\n            end\n        end\n\n        if not rule.match then\n            match_passed = true\n            weighted_upstreams = rule.weighted_upstreams\n            break\n        end\n\n        for _, single_match in ipairs(rule.match) do\n            local expr, err = expr.new(single_match.vars)\n            if err then\n                core.log.error(\"vars expression does not match: \", err)\n                return 500, err\n            end\n\n            match_passed = expr:eval(ctx.var)\n            if match_passed then\n                break\n            end\n        end\n\n        if match_passed then\n            weighted_upstreams = rule.weighted_upstreams\n            break\n        end\n    end\n\n    core.log.info(\"match_passed: \", match_passed)\n\n    if not match_passed then\n        return\n    end\n\n    local rr_up, err = lrucache(weighted_upstreams, nil, new_rr_obj, weighted_upstreams)\n    if not rr_up then\n        core.log.error(\"lrucache roundrobin failed: \", err)\n        return 500\n    end\n\n    local upstream = rr_up:find()\n    if upstream and type(upstream) == \"table\" then\n        core.log.info(\"upstream: \", core.json.encode(upstream))\n        return set_upstream(upstream, ctx)\n    elseif upstream and upstream ~= \"plugin#upstream#is#empty\" then\n        ctx.upstream_id = upstream\n        core.log.info(\"upstream_id: \", upstream)\n        return\n    end\n\n    ctx.upstream_id = nil\n    core.log.info(\"route_up: \", upstream)\n    return\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/ua-restriction.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ipairs = ipairs\nlocal core = require(\"apisix.core\")\nlocal re_compile = require(\"resty.core.regex\").re_match_compile\nlocal stringx = require('pl.stringx')\nlocal type = type\nlocal str_strip = stringx.strip\nlocal re_find = ngx.re.find\n\nlocal lrucache_allow = core.lrucache.new({ ttl = 300, count = 4096 })\nlocal lrucache_deny = core.lrucache.new({ ttl = 300, count = 4096 })\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        bypass_missing = {\n            type = \"boolean\",\n            default = false,\n        },\n        allowlist = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                minLength = 1,\n            }\n        },\n        denylist = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                minLength = 1,\n            }\n        },\n        message = {\n            type = \"string\",\n            minLength = 1,\n            maxLength = 1024,\n            default = \"Not allowed\"\n        },\n    },\n    oneOf = {\n        {required = {\"allowlist\"}},\n        {required = {\"denylist\"}}\n    }\n}\n\nlocal plugin_name = \"ua-restriction\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 2999,\n    name = plugin_name,\n    schema = schema,\n}\n\nlocal function check_with_allow_list(user_agents, allowlist)\n    local check = function (user_agent)\n        user_agent = str_strip(user_agent)\n\n        for _, rule in ipairs(allowlist) do\n            if re_find(user_agent, rule, \"jo\") then\n                return true\n            end\n        end\n        return false\n    end\n\n    if type(user_agents) == \"table\" then\n        for _, v in ipairs(user_agents) do\n            if lrucache_allow(v, allowlist, check, v) then\n                return true\n            end\n        end\n        return false\n    else\n        return lrucache_allow(user_agents, allowlist, check, user_agents)\n    end\nend\n\n\nlocal function check_with_deny_list(user_agents, denylist)\n    local check = function (user_agent)\n        user_agent = str_strip(user_agent)\n\n        for _, rule in ipairs(denylist) do\n            if re_find(user_agent, rule, \"jo\") then\n                return false\n            end\n        end\n        return true\n    end\n\n    if type(user_agents) == \"table\" then\n        for _, v in ipairs(user_agents) do\n            if lrucache_deny(v, denylist, check, v) then\n                return false\n            end\n        end\n        return true\n    else\n        return lrucache_deny(user_agents, denylist, check, user_agents)\n    end\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n\n    if not ok then\n        return false, err\n    end\n\n    if conf.allowlist then\n        for _, re_rule in ipairs(conf.allowlist) do\n            ok, err = re_compile(re_rule, \"j\")\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    if conf.denylist then\n        for _, re_rule in ipairs(conf.denylist) do\n            ok, err = re_compile(re_rule, \"j\")\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    -- after core.request.header function changed\n    -- we need to get original header value by using core.request.headers\n    local user_agent = core.request.headers(ctx)[\"User-Agent\"]\n\n    if not user_agent then\n        if conf.bypass_missing then\n            return\n        else\n            return 403, { message = conf.message }\n        end\n    end\n\n    local is_passed\n\n    if conf.allowlist then\n        is_passed = check_with_allow_list(user_agent, conf.allowlist)\n    else\n        is_passed = check_with_deny_list(user_agent, conf.denylist)\n    end\n\n    if not is_passed then\n        return 403, { message = conf.message }\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/udp-logger.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal plugin = require(\"apisix.plugin\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal plugin_name = \"udp-logger\"\nlocal tostring = tostring\nlocal ngx = ngx\nlocal udp = ngx.socket.udp\n\n\nlocal batch_processor_manager = bp_manager_mod.new(\"udp logger\")\nlocal schema = {\n    type = \"object\",\n    properties = {\n        host = {type = \"string\"},\n        port = {type = \"integer\", minimum = 0},\n        timeout = {type = \"integer\", minimum = 1, default = 3},\n        log_format = {type = \"object\"},\n        include_req_body = {type = \"boolean\", default = false},\n        include_req_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        include_resp_body = { type = \"boolean\", default = false },\n        include_resp_body_expr = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"array\"\n            }\n        },\n        max_req_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n        max_resp_body_bytes = {type = \"integer\", minimum = 1, default = 524288},\n    },\n    required = {\"host\", \"port\"}\n}\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        },\n        max_pending_entries = {\n            type = \"integer\",\n            description = \"maximum number of pending entries in the batch processor\",\n            minimum = 1,\n        },\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 400,\n    name = plugin_name,\n    metadata_schema = metadata_schema,\n    schema = batch_processor_manager:wrap_schema(schema),\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function send_udp_data(conf, log_message)\n    local err_msg\n    local res = true\n    local sock = udp()\n    sock:settimeout(conf.timeout * 1000)\n\n    core.log.info(\"sending a batch logs to \", conf.host, \":\", conf.port)\n    core.log.info(\"sending log_message: \", log_message)\n\n    local ok, err = sock:setpeername(conf.host, conf.port)\n\n    if not ok then\n        return false, \"failed to connect to UDP server: host[\" .. conf.host\n                    .. \"] port[\" .. tostring(conf.port) .. \"] err: \" .. err\n    end\n\n    ok, err = sock:send(log_message)\n    if not ok then\n        res = false\n        err_msg = \"failed to send data to UDP server: host[\" .. conf.host\n                  .. \"] port[\" .. tostring(conf.port) .. \"] err:\" .. err\n    end\n\n    ok, err = sock:close()\n    if not ok then\n        core.log.error(\"failed to close the UDP connection, host[\",\n                        conf.host, \"] port[\", conf.port, \"] \", err)\n    end\n\n    return res, err_msg\nend\n\n\n_M.access = log_util.check_and_read_req_body\n\n\nfunction _M.body_filter(conf, ctx)\n    log_util.collect_body(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    local metadata = plugin.plugin_metadata(plugin_name)\n    local max_pending_entries = metadata and metadata.value and\n                                metadata.value.max_pending_entries or nil\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n\n    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then\n        return\n    end\n\n    -- Generate a function to be executed by the batch processor\n    local func = function(entries, batch_max_size)\n        local data, err\n        if batch_max_size == 1 then\n            data, err = core.json.encode(entries[1]) -- encode as single {}\n        else\n            data, err = core.json.encode(entries) -- encode as array [{}]\n        end\n\n        if not data then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        return send_udp_data(conf, data)\n    end\n\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/uri-blocker.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal re_compile = require(\"resty.core.regex\").re_match_compile\nlocal re_find = ngx.re.find\nlocal ipairs = ipairs\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        block_rules = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                minLength = 1,\n                maxLength = 4096,\n            },\n            uniqueItems = true\n        },\n        rejected_code = {\n            type = \"integer\",\n            minimum = 200,\n            default = 403\n        },\n        rejected_msg = {\n            type = \"string\",\n            minLength = 1\n        },\n        case_insensitive = {\n            type = \"boolean\",\n            default = false\n        },\n    },\n    required = {\"block_rules\"},\n}\n\n\nlocal plugin_name = \"uri-blocker\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 2900,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    for i, re_rule in ipairs(conf.block_rules) do\n        local ok, err = re_compile(re_rule, \"j\")\n        -- core.log.warn(\"ok: \", tostring(ok), \" err: \", tostring(err),\n        --               \" re_rule: \", re_rule)\n        if not ok then\n            return false, err\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    core.log.info(\"uri: \", ctx.var.request_uri)\n    core.log.info(\"block uri rules: \", conf.block_rules_concat)\n\n    if not conf.block_rules_concat then\n        local block_rules = {}\n        for i, re_rule in ipairs(conf.block_rules) do\n            block_rules[i] = re_rule\n        end\n\n        conf.block_rules_concat = core.table.concat(block_rules, \"|\")\n        if conf.case_insensitive then\n            conf.block_rules_concat = \"(?i)\" .. conf.block_rules_concat\n        end\n        core.log.info(\"concat block_rules: \", conf.block_rules_concat)\n    end\n\n    local from = re_find(ctx.var.request_uri, conf.block_rules_concat, \"jo\")\n    if from then\n        if conf.rejected_msg then\n            return conf.rejected_code, { error_msg = conf.rejected_msg }\n        end\n        return conf.rejected_code\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/wolf-rbac.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core     = require(\"apisix.core\")\nlocal consumer = require(\"apisix.consumer\")\nlocal json     = require(\"apisix.core.json\")\nlocal sleep    = core.sleep\nlocal ngx_re = require(\"ngx.re\")\nlocal http     = require(\"resty.http\")\nlocal ngx      = ngx\nlocal rawget   = rawget\nlocal rawset   = rawset\nlocal setmetatable = setmetatable\nlocal type     = type\nlocal string   = string\nlocal req_read_body = ngx.req.read_body\nlocal req_get_body_data = ngx.req.get_body_data\n\nlocal plugin_name = \"wolf-rbac\"\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        appid = {\n            type = \"string\",\n            default = \"unset\"\n        },\n        server = {\n            type = \"string\",\n            default = \"http://127.0.0.1:12180\"\n        },\n        header_prefix = {\n            type = \"string\",\n            default = \"X-\"\n        },\n    }\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 2555,\n    type = 'auth',\n    name = plugin_name,\n    schema = schema,\n}\n\n\nlocal token_version = 'V1'\nlocal function create_rbac_token(appid, wolf_token)\n    return token_version .. \"#\" .. appid .. \"#\" .. wolf_token\nend\n\nlocal function fail_response(message, init_values)\n    local response = init_values or {}\n    response.message = message\n    return response\nend\n\nlocal function success_response(message, init_values)\n    local response = init_values or {}\n    response.message = message\n    return response\nend\n\nlocal function parse_rbac_token(rbac_token)\n    local res, err = ngx_re.split(rbac_token, \"#\", nil, nil, 3)\n    if not res then\n        return nil, err\n    end\n\n    if #res ~= 3 or res[1] ~= token_version then\n        return nil, 'invalid rbac token: version'\n    end\n    local appid = res[2]\n    local wolf_token = res[3]\n\n    return {appid = appid, wolf_token = wolf_token}\nend\n\nlocal function new_headers()\n    local t = {}\n    local lt = {}\n    local _mt = {\n        __index = function(t, k)\n            return rawget(lt, string.lower(k))\n        end,\n        __newindex = function(t, k, v)\n            rawset(t, k, v)\n            rawset(lt, string.lower(k), v)\n        end,\n     }\n    return setmetatable(t, _mt)\nend\n\n-- timeout in ms\nlocal function http_req(method, uri, body, myheaders, timeout)\n    if not myheaders then\n        myheaders = new_headers()\n    end\n\n    local httpc = http.new()\n    if timeout then\n        httpc:set_timeout(timeout)\n    end\n\n    local res, err = httpc:request_uri(uri, {\n        method = method,\n        headers = myheaders,\n        body = body,\n        ssl_verify = false\n    })\n\n    if not res then\n        core.log.error(\"FAIL REQUEST [ \",core.json.delay_encode(\n            {method = method, uri = uri, body = body, headers = myheaders}),\n            \" ] failed! res is nil, err:\", err)\n        return nil, err\n    end\n\n    return res\nend\n\nlocal function http_get(uri, myheaders, timeout)\n    return http_req(\"GET\", uri, nil, myheaders, timeout)\nend\n\n\nfunction _M.check_schema(conf)\n    local check = {\"server\"}\n    core.utils.check_https(check, conf, plugin_name)\n    core.log.info(\"input conf: \", core.json.delay_encode(conf))\n\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nlocal function fetch_rbac_token(ctx)\n    if ctx.var.arg_rbac_token then\n        return ngx.unescape_uri(ctx.var.arg_rbac_token)\n    end\n\n    if ctx.var.http_authorization then\n        return ctx.var.http_authorization\n    end\n\n    if ctx.var.http_x_rbac_token then\n        return ctx.var.http_x_rbac_token\n    end\n\n    return ctx.var['cookie_x-rbac-token']\nend\n\n\nlocal function check_url_permission(server, appid, action, resName, client_ip, wolf_token)\n    local retry_max = 3\n    local errmsg\n    local userInfo\n    local res\n    local err\n    local access_check_url = server .. \"/wolf/rbac/access_check\"\n    local headers = new_headers()\n    headers[\"x-rbac-token\"] = wolf_token\n    headers[\"Content-Type\"] = \"application/json; charset=utf-8\"\n    local args = { appID = appid, resName = resName, action = action, clientIP = client_ip}\n    local url = access_check_url .. \"?\" .. ngx.encode_args(args)\n    local timeout = 1000 * 10\n\n    for i = 1, retry_max do\n        -- TODO: read apisix info.\n        res, err = http_get(url, headers, timeout)\n        if err then\n            break\n        else\n            core.log.info(\"check permission request:\", url, \", status:\", res.status,\n                            \",body:\", core.json.delay_encode(res.body))\n            if res.status < 500 then\n                break\n            else\n                core.log.info(\"request [curl -v \", url, \"] failed! status:\", res.status)\n                if i < retry_max then\n                    sleep(0.1)\n                end\n            end\n        end\n    end\n\n    if err then\n        core.log.error(\"fail request: \", url, \", err:\", err)\n        return {\n            status = 500,\n            err = \"request to wolf-server failed, err:\" .. err\n        }\n    end\n\n    if res.status ~= 200 and res.status >= 500 then\n        return {\n            status = 500,\n            err = 'request to wolf-server failed, status:' .. res.status\n        }\n    end\n\n    local body, err = json.decode(res.body)\n    if not body then\n        errmsg = 'check permission failed! parse response json failed!'\n        core.log.error( \"json.decode(\", res.body, \") failed! err:\", err)\n        return {status = res.status, err = errmsg}\n    else\n        if body.data then\n            userInfo = body.data.userInfo\n        end\n        errmsg = body.reason\n        return {status = res.status, err = errmsg, userInfo = userInfo}\n    end\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    local url = ctx.var.uri\n    local action = ctx.var.request_method\n    local client_ip = ctx.var.http_x_real_ip or core.request.get_ip(ctx)\n    local perm_item = {action = action, url = url, clientIP = client_ip}\n    core.log.info(\"hit wolf-rbac rewrite\")\n\n    local rbac_token = fetch_rbac_token(ctx)\n    if rbac_token == nil then\n        core.log.info(\"no permission to access \",\n                      core.json.delay_encode(perm_item), \", need login!\")\n        return 401, fail_response(\"Missing rbac token in request\")\n    end\n\n    local tokenInfo, err = parse_rbac_token(rbac_token)\n    core.log.info(\"token info: \", core.json.delay_encode(tokenInfo),\n                  \", err: \", err)\n    if err then\n        return 401, fail_response('invalid rbac token: parse failed')\n    end\n\n    local appid = tokenInfo.appid\n    local wolf_token = tokenInfo.wolf_token\n    perm_item.appid = appid\n    perm_item.wolf_token = wolf_token\n\n    local consumer_conf = consumer.plugin(plugin_name)\n    if not consumer_conf then\n        return 401, fail_response(\"Missing related consumer\")\n    end\n\n    local consumers = consumer.consumers_kv(plugin_name, consumer_conf, \"appid\")\n\n    core.log.info(\"------ consumers: \", core.json.delay_encode(consumers))\n    local cur_consumer = consumers[appid]\n    if not cur_consumer then\n        core.log.error(\"consumer [\", appid, \"] not found\")\n        return 401, fail_response(\"Invalid appid in rbac token\")\n    end\n    core.log.info(\"consumer: \", core.json.delay_encode(cur_consumer))\n    local server = cur_consumer.auth_conf.server\n\n    local res = check_url_permission(server, appid, action, url,\n                    client_ip, wolf_token)\n    core.log.info(\" check_url_permission(\", core.json.delay_encode(perm_item),\n                  \") res: \",core.json.delay_encode(res))\n\n    local username = nil\n    local nickname = nil\n    if type(res.userInfo) == 'table' then\n        local userInfo = res.userInfo\n        ctx.userInfo = userInfo\n        local userId = userInfo.id\n        username = userInfo.username\n        nickname = userInfo.nickname or userInfo.username\n        local prefix = cur_consumer.auth_conf.header_prefix or ''\n        core.response.set_header(prefix .. \"UserId\", userId)\n        core.response.set_header(prefix .. \"Username\", username)\n        core.response.set_header(prefix .. \"Nickname\", ngx.escape_uri(nickname))\n        core.request.set_header(ctx, prefix .. \"UserId\", userId)\n        core.request.set_header(ctx, prefix .. \"Username\", username)\n        core.request.set_header(ctx, prefix .. \"Nickname\", ngx.escape_uri(nickname))\n    end\n\n    if res.status ~= 200 then\n        -- no permission.\n        core.log.error(\" check_url_permission(\",\n            core.json.delay_encode(perm_item),\n            \") failed, res: \",core.json.delay_encode(res))\n        return res.status, fail_response(res.err, { username = username, nickname = nickname })\n    end\n    consumer.attach_consumer(ctx, cur_consumer, consumer_conf)\n    core.log.info(\"wolf-rbac check permission passed\")\nend\n\nlocal function get_args()\n    local ctx = ngx.ctx.api_ctx\n    local args, err\n    req_read_body()\n    if string.find(ctx.var.http_content_type or \"\",\"application/json\",\n                   1, true) then\n        local req_body = req_get_body_data()\n        args, err = json.decode(req_body)\n        if not args then\n            core.log.error(\"json.decode(\", req_body, \") failed! \", err)\n        end\n    else\n        args = core.request.get_post_args(ctx)\n    end\n\n    return args\nend\n\nlocal function get_consumer(appid)\n    local consumer_conf = consumer.plugin(plugin_name)\n    if not consumer_conf then\n        core.response.exit(500)\n    end\n\n    local consumers = consumer.consumers_kv(plugin_name, consumer_conf, \"appid\")\n\n    core.log.info(\"------ consumers: \", core.json.delay_encode(consumers))\n    local consumer = consumers[appid]\n    if not consumer then\n        core.log.info(\"request appid [\", appid, \"] not found\")\n        core.response.exit(400,\n                fail_response(\"appid not found\")\n            )\n    end\n    return consumer\nend\n\nlocal function request_to_wolf_server(method, uri, headers, body)\n    headers[\"Content-Type\"] = \"application/json; charset=utf-8\"\n    local timeout = 1000 * 5\n    local request_debug = core.json.delay_encode(\n        {\n            method = method, uri = uri, body = body,\n            headers = headers,timeout = timeout\n        }\n    )\n\n    core.log.info(\"request [\", request_debug, \"] ....\")\n    local res, err = http_req(method, uri, core.json.encode(body), headers, timeout)\n    if not res then\n        core.log.error(\"request [\", request_debug, \"] failed! err: \", err)\n        return core.response.exit(500,\n            fail_response(\"request to wolf-server failed!\")\n        )\n    end\n    core.log.info(\"request [\", request_debug, \"] status: \", res.status,\n                  \", body: \", res.body)\n\n    if res.status ~= 200 then\n        core.log.error(\"request [\", request_debug, \"] failed! status: \",\n                        res.status)\n        return core.response.exit(500,\n        fail_response(\"request to wolf-server failed!\")\n        )\n    end\n    local body, err = json.decode(res.body)\n    if not body then\n        core.log.error(\"request [\", request_debug, \"] failed! err:\", err)\n        return core.response.exit(500, fail_response(\"request to wolf-server failed!\"))\n    end\n    if not body.ok then\n        core.log.error(\"request [\", request_debug, \"] failed! response body:\",\n                       core.json.delay_encode(body))\n        return core.response.exit(200, fail_response(\"request to wolf-server failed!\"))\n    end\n\n    core.log.info(\"request [\", request_debug, \"] success! response body:\",\n                  core.json.delay_encode(body))\n    return body\nend\n\nlocal function wolf_rbac_login()\n    local args = get_args()\n    if not args then\n        return core.response.exit(400, fail_response(\"invalid request\"))\n    end\n    if not args.appid then\n        return core.response.exit(400, fail_response(\"appid is missing\"))\n    end\n\n    local appid = args.appid\n    local consumer = get_consumer(appid)\n    core.log.info(\"consumer: \", core.json.delay_encode(consumer))\n\n    local uri = consumer.auth_conf.server .. '/wolf/rbac/login.rest'\n    local headers = new_headers()\n    local body = request_to_wolf_server('POST', uri, headers, args)\n\n    local userInfo = body.data.userInfo\n    local wolf_token = body.data.token\n\n    local rbac_token = create_rbac_token(appid, wolf_token)\n    core.response.exit(200, success_response(nil, {rbac_token = rbac_token, user_info = userInfo}))\nend\n\nlocal function get_wolf_token(ctx)\n    core.log.info(\"hit wolf-rbac change_password api\")\n    local rbac_token = fetch_rbac_token(ctx)\n    if rbac_token == nil then\n        local url = ctx.var.uri\n        local action = ctx.var.request_method\n        local client_ip = core.request.get_ip(ctx)\n        local perm_item = {action = action, url = url, clientIP = client_ip}\n        core.log.info(\"no permission to access \",\n                      core.json.delay_encode(perm_item), \", need login!\")\n        return core.response.exit(401, fail_response(\"Missing rbac token in request\"))\n    end\n\n    local tokenInfo, err = parse_rbac_token(rbac_token)\n    core.log.info(\"token info: \", core.json.delay_encode(tokenInfo),\n                  \", err: \", err)\n    if err then\n        return core.response.exit(401, fail_response('invalid rbac token: parse failed'))\n    end\n    return tokenInfo\nend\n\nlocal function wolf_rbac_change_pwd()\n    local args = get_args()\n\n    local ctx = ngx.ctx.api_ctx\n    local tokenInfo = get_wolf_token(ctx)\n    local appid = tokenInfo.appid\n    local wolf_token = tokenInfo.wolf_token\n    local consumer = get_consumer(appid)\n    core.log.info(\"consumer: \", core.json.delay_encode(consumer))\n\n    local uri = consumer.auth_conf.server .. '/wolf/rbac/change_pwd'\n    local headers = new_headers()\n    headers['x-rbac-token'] = wolf_token\n    request_to_wolf_server('POST', uri, headers, args)\n    core.response.exit(200, success_response('success to change password', { }))\nend\n\nlocal function wolf_rbac_user_info()\n    local ctx = ngx.ctx.api_ctx\n    local tokenInfo = get_wolf_token(ctx)\n    local appid = tokenInfo.appid\n    local wolf_token = tokenInfo.wolf_token\n    local consumer = get_consumer(appid)\n    core.log.info(\"consumer: \", core.json.delay_encode(consumer))\n\n    local uri = consumer.auth_conf.server .. '/wolf/rbac/user_info'\n    local headers = new_headers()\n    headers['x-rbac-token'] = wolf_token\n    local body = request_to_wolf_server('GET', uri, headers, {})\n    local userInfo = body.data.userInfo\n    core.response.exit(200, success_response(nil, {user_info = userInfo}))\nend\n\nfunction _M.api()\n    return {\n        {\n            methods = {\"POST\"},\n            uri = \"/apisix/plugin/wolf-rbac/login\",\n            handler = wolf_rbac_login,\n        },\n        {\n            methods = {\"PUT\"},\n            uri = \"/apisix/plugin/wolf-rbac/change_pwd\",\n            handler = wolf_rbac_change_pwd,\n        },\n        {\n            methods = {\"GET\"},\n            uri = \"/apisix/plugin/wolf-rbac/user_info\",\n            handler = wolf_rbac_user_info,\n        },\n    }\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/workflow.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core         = require(\"apisix.core\")\nlocal expr         = require(\"resty.expr.v1\")\nlocal ipairs       = ipairs\nlocal setmetatable = setmetatable\nlocal getmetatable = getmetatable\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        rules = {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    case = {\n                        type = \"array\",\n                        items = {\n                            anyOf = {\n                                {\n                                    type = \"array\",\n                                },\n                                {\n                                    type = \"string\",\n                                },\n                            }\n                        },\n                        minItems = 1,\n                    },\n                    actions = {\n                        type = \"array\",\n                        items = {\n                            type = \"array\",\n                            minItems = 1\n                        }\n                    }\n                },\n                required = {\"actions\"}\n            }\n        }\n    },\n    required = {\"rules\"}\n}\n\nlocal plugin_name = \"workflow\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 1006,\n    name = plugin_name,\n    schema = schema\n}\n\n\nlocal return_schema = {\n    type = \"object\",\n    properties = {\n        code = {\n            type = \"integer\",\n            minimum = 100,\n            maximum = 599\n        }\n    },\n    required = {\"code\"}\n}\n\n\nlocal function check_return_schema(conf)\n    local ok, err = core.schema.check(return_schema, conf)\n    if not ok then\n        return false, err\n    end\n    return true\nend\n\n\nlocal function exit(conf)\n    return conf.code, {error_msg = \"rejected by workflow\"}\nend\n\n\n\nlocal support_action = {\n    [\"return\"] = {\n        handler        = exit,\n        check_schema   = check_return_schema,\n    }\n}\n\n\nfunction _M.register(plugin_name, check_schema, access_handler, log_handler)\n    support_action[plugin_name] = {\n        check_schema   = check_schema,\n        handler        = access_handler,\n        log_handler    = log_handler\n    }\nend\n\n\nfunction _M.check_schema(conf)\n    local ok, err = core.schema.check(schema, conf)\n    if not ok then\n        return false, err\n    end\n\n    for idx, rule in ipairs(conf.rules) do\n         if rule.case then\n            local expr, err = expr.new(rule.case)\n            if not ok then\n                return false, \"failed to validate the 'case' expression: \" .. err\n            end\n            local mt = getmetatable(rule)\n            if not mt then\n                mt = {}\n                mt.__index = mt\n                setmetatable(rule, mt)\n            end\n            mt.__expr = expr\n        end\n\n        local actions = rule.actions\n        for _, action in ipairs(actions) do\n\n            if not support_action[action[1]] then\n                return false, \"unsupported action: \" .. action[1]\n            end\n\n            -- use the action's idx as an identifier to isolate between confs\n            action[2][\"_vid\"] = idx\n            local ok, err = support_action[action[1]].check_schema(action[2], plugin_name)\n            if not ok then\n                return false, \"failed to validate the '\" .. action[1] .. \"' action: \" .. err\n            end\n       end\n    end\n\n    return true\nend\n\n\nfunction _M.access(conf, ctx)\n    ctx._workflow_cache = ctx._workflow_cache or {}\n    for idx, rule in ipairs(conf.rules) do\n        local match_result = true\n        if rule.case then\n            local expr = rule.__expr\n            if expr then\n                match_result = expr:eval(ctx.var)\n            end\n        end\n        ctx._workflow_cache[idx] = match_result\n        if match_result then\n            -- only one action is currently supported\n            local action = rule.actions[1]\n\n            local action_name = action[1]\n            local action_conf = action[2]\n            action_conf._meta = conf._meta\n\n            return support_action[action_name].handler(action_conf, ctx)\n        end\n    end\nend\n\nfunction _M.log(conf, ctx)\n    for idx, rule in ipairs(conf.rules) do\n        local match_result = ctx._workflow_cache[idx]\n        if match_result then\n            -- only one action is currently supported\n            local action = rule.actions[1]\n            local log_handler = support_action[action[1]].log_handler\n            if log_handler then\n                return log_handler(action[2], ctx)\n            end\n        end\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/zipkin/codec.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal to_hex = require \"resty.string\".to_hex\nlocal new_span_context = require(\"opentracing.span_context\").new\nlocal ngx    = ngx\nlocal string = string\nlocal pairs = pairs\nlocal tonumber = tonumber\n\nlocal function hex_to_char(c)\n    return string.char(tonumber(c, 16))\nend\n\nlocal function from_hex(str)\n    if str ~= nil then -- allow nil to pass through\n        str = str:gsub(\"%x%x\", hex_to_char)\n    end\n    return str\nend\n\nlocal function new_extractor()\n    return function(ctx)\n        local had_invalid_id = false\n\n        local zipkin_ctx = ctx.zipkin\n        local trace_id = zipkin_ctx.trace_id\n        local parent_span_id = zipkin_ctx.parent_span_id\n        local request_span_id = zipkin_ctx.request_span_id\n\n        -- Validate trace id\n        if trace_id and\n            ((#trace_id ~= 16 and #trace_id ~= 32) or trace_id:match(\"%X\")) then\n            core.log.warn(\"x-b3-traceid header invalid; ignoring.\")\n            had_invalid_id = true\n        end\n\n        -- Validate parent_span_id\n        if parent_span_id and\n            (#parent_span_id ~= 16 or parent_span_id:match(\"%X\")) then\n            core.log.warn(\"x-b3-parentspanid header invalid; ignoring.\")\n            had_invalid_id = true\n        end\n\n        -- Validate request_span_id\n        if request_span_id and\n            (#request_span_id ~= 16 or request_span_id:match(\"%X\")) then\n            core.log.warn(\"x-b3-spanid header invalid; ignoring.\")\n            had_invalid_id = true\n        end\n\n        if trace_id == nil or had_invalid_id then\n            return nil\n        end\n\n        -- Process jaegar baggage header\n        local baggage = {}\n        local headers = core.request.headers(ctx)\n        for k, v in pairs(headers) do\n            local baggage_key = k:match(\"^uberctx%-(.*)$\")\n            if baggage_key then\n                baggage[baggage_key] = ngx.unescape_uri(v)\n            end\n        end\n\n        core.log.info(\"new span context: trace id: \", trace_id,\n                      \", span id: \", request_span_id,\n                      \", parent span id: \", parent_span_id)\n\n        trace_id = from_hex(trace_id)\n        parent_span_id = from_hex(parent_span_id)\n        request_span_id = from_hex(request_span_id)\n\n        return new_span_context(trace_id, request_span_id, parent_span_id,\n                                baggage)\n    end\nend\n\nlocal function new_injector()\n    return function(span_context, headers)\n        -- We want to remove headers if already present\n        headers[\"x-b3-traceid\"] = to_hex(span_context.trace_id)\n        headers[\"x-b3-parentspanid\"] = span_context.parent_id\n                                    and to_hex(span_context.parent_id) or nil\n        headers[\"x-b3-spanid\"] = to_hex(span_context.span_id)\n        headers[\"x-b3-sampled\"] = span_context:get_baggage_item(\"x-b3-sampled\")\n        for key, value in span_context:each_baggage_item() do\n            -- skip x-b3-sampled baggage\n            if key ~= \"x-b3-sampled\" then\n                -- XXX: https://github.com/opentracing/specification/issues/117\n                headers[\"uberctx-\"..key] = ngx.escape_uri(value)\n            end\n        end\n    end\nend\n\nreturn {\n    new_extractor = new_extractor,\n    new_injector = new_injector,\n}\n"
  },
  {
    "path": "apisix/plugins/zipkin/random_sampler.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal assert = assert\nlocal type = type\nlocal setmetatable = setmetatable\nlocal math = math\n\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\nfunction _M.new(conf)\n    return setmetatable({}, mt)\nend\n\nfunction _M.sample(self, sample_ratio)\n    assert(type(sample_ratio) == \"number\" and\n           sample_ratio >= 0 and sample_ratio <= 1, \"invalid sample_ratio\")\n    return math.random() < sample_ratio\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/zipkin/reporter.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal resty_http = require \"resty.http\"\nlocal to_hex = require \"resty.string\".to_hex\nlocal cjson = require \"cjson.safe\".new()\ncjson.encode_number_precision(16)\nlocal assert = assert\nlocal type = type\nlocal setmetatable = setmetatable\nlocal math = math\nlocal tostring = tostring\nlocal batch_processor = require(\"apisix.utils.batch-processor\")\nlocal core = require(\"apisix.core\")\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\nlocal span_kind_map = {\n    client = \"CLIENT\",\n    server = \"SERVER\",\n    producer = \"PRODUCER\",\n    consumer = \"CONSUMER\",\n}\n\n\nfunction _M.new(conf)\n    local endpoint = conf.endpoint\n    local service_name = conf.service_name\n    local server_port = conf.server_port\n    local server_addr = conf.server_addr\n    assert(type(endpoint) == \"string\", \"invalid http endpoint\")\n    return setmetatable({\n        endpoint = endpoint,\n        service_name = service_name,\n        server_addr = server_addr,\n        server_port = server_port,\n        pending_spans_n = 0,\n        route_id = conf.route_id\n    }, mt)\nend\n\n\nfunction _M.report(self, span)\n    if span:get_baggage_item(\"x-b3-sampled\") == \"0\" then\n        return\n    end\n    local span_context = span:context()\n\n    local zipkin_tags = {}\n    for k, v in span:each_tag() do\n        -- Zipkin tag values should be strings\n        zipkin_tags[k] = tostring(v)\n    end\n\n    local span_kind = zipkin_tags[\"span.kind\"]\n    zipkin_tags[\"span.kind\"] = nil\n\n    local localEndpoint = {\n        serviceName = self.service_name,\n        ipv4 = self.server_addr,\n        port = self.server_port,\n        -- TODO: ip/port from ngx.var.server_name/ngx.var.server_port?\n    }\n\n    local remoteEndpoint do\n        local peer_port = span:get_tag \"peer.port\" -- get as number\n        if peer_port then\n            zipkin_tags[\"peer.port\"] = nil\n            remoteEndpoint = {\n                ipv4 = zipkin_tags[\"peer.ipv4\"],\n                -- ipv6 = zipkin_tags[\"peer.ipv6\"],\n                port = peer_port, -- port is *not* optional\n            }\n            zipkin_tags[\"peer.ipv4\"] = nil\n            zipkin_tags[\"peer.ipv6\"] = nil\n        else\n            remoteEndpoint = cjson.null\n        end\n    end\n\n    local zipkin_span = {\n        traceId = to_hex(span_context.trace_id),\n        name = span.name,\n        parentId = span_context.parent_id and\n                    to_hex(span_context.parent_id) or nil,\n        id = to_hex(span_context.span_id),\n        kind = span_kind_map[span_kind],\n        timestamp = span.timestamp * 1000000,\n        duration = math.floor(span.duration * 1000000), -- zipkin wants integer\n        -- TODO: debug?\n        localEndpoint = localEndpoint,\n        remoteEndpoint = remoteEndpoint,\n        tags = zipkin_tags,\n        annotations = span.logs\n    }\n\n    self.pending_spans_n = self.pending_spans_n + 1\n    if self.processor then\n        self.processor:push(zipkin_span)\n    end\nend\n\n\nlocal function send_span(pending_spans, report)\n    local httpc = resty_http.new()\n    local res, err = httpc:request_uri(report.endpoint, {\n        method = \"POST\",\n        headers = {\n            [\"content-type\"] = \"application/json\",\n        },\n        body = pending_spans,\n        keepalive = 5000,\n        keepalive_pool = 5\n    })\n\n    if not res then\n        -- for zipkin test\n        core.log.error(\"report zipkin span failed\")\n        return nil, \"failed: \" .. err .. \", url: \" .. report.endpoint\n    elseif res.status < 200 or res.status >= 300 then\n        return nil, \"failed: \" .. report.endpoint .. \" \"\n               .. res.status .. \" \" .. res.reason\n    end\n\n    return true\nend\n\n\nfunction _M.init_processor(self)\n    local process_conf = {\n        name = \"zipkin_report\",\n        retry_delay = 1,\n        batch_max_size = 1000,\n        max_retry_count = 0,\n        buffer_duration = 60,\n        inactive_timeout = 5,\n        route_id = self.route_id,\n        server_addr = self.server_addr,\n    }\n\n    local flush = function (entries, batch_max_size)\n        if not entries then\n            return true\n        end\n\n        local pending_spans, err\n        if batch_max_size == 1 then\n            pending_spans, err = cjson.encode(entries[1])\n        else\n            pending_spans, err = cjson.encode(entries)\n        end\n\n        if not pending_spans then\n            return false, 'error occurred while encoding the data: ' .. err\n        end\n\n        return send_span(pending_spans, self)\n    end\n\n    local processor, err = batch_processor:new(flush, process_conf)\n    if not processor then\n        return false, \"create processor error: \" .. err\n    end\n\n    self.processor = processor\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/plugins/zipkin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal new_tracer = require(\"opentracing.tracer\").new\nlocal zipkin_codec = require(\"apisix.plugins.zipkin.codec\")\nlocal new_random_sampler = require(\"apisix.plugins.zipkin.random_sampler\").new\nlocal new_reporter = require(\"apisix.plugins.zipkin.reporter\").new\nlocal ngx = ngx\nlocal ngx_var = ngx.var\nlocal ngx_re = require(\"ngx.re\")\nlocal pairs = pairs\nlocal tonumber = tonumber\nlocal to_hex = require \"resty.string\".to_hex\n\nlocal plugin_name = \"zipkin\"\nlocal ZIPKIN_SPAN_VER_1 = 1\nlocal ZIPKIN_SPAN_VER_2 = 2\nlocal plugin = require(\"apisix.plugin\")\nlocal string_format = string.format\n\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        endpoint = {type = \"string\"},\n        sample_ratio = {type = \"number\", minimum = 0.00001, maximum = 1},\n        service_name = {\n            type = \"string\",\n            description = \"service name for zipkin reporter\",\n            default = \"APISIX\",\n        },\n        server_addr = {\n            type = \"string\",\n            description = \"default is $server_addr, you can specify your external ip address\",\n            pattern = \"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$\"\n        },\n        span_version = {\n            enum = {ZIPKIN_SPAN_VER_1, ZIPKIN_SPAN_VER_2},\n            default = ZIPKIN_SPAN_VER_2,\n        },\n    },\n    required = {\"endpoint\", \"sample_ratio\"}\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 12011,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    local check = {\"endpoint\"}\n    core.utils.check_https(check, conf, plugin_name)\n    return core.schema.check(schema, conf)\nend\n\nlocal plugin_info  = plugin.plugin_attr(plugin_name) or {}\n\n\nlocal function create_tracer(conf,ctx)\n    conf.route_id = ctx.route_id\n    local reporter = new_reporter(conf)\n    reporter:init_processor()\n    local tracer = new_tracer(reporter, new_random_sampler(conf))\n    tracer:register_injector(\"http_headers\", zipkin_codec.new_injector())\n    tracer:register_extractor(\"http_headers\", zipkin_codec.new_extractor())\n    return tracer\nend\n\n\nlocal function parse_b3(b3)\n    -- See https://github.com/openzipkin/b3-propagation#single-header\n    if b3 == \"0\" then\n        return nil, nil, nil, \"0\", nil\n    end\n\n    local pieces, err = ngx_re.split(b3, \"-\", nil, nil, 4)\n    if not pieces then\n        return err\n    end\n    if not pieces[1] then\n        return \"missing trace_id\"\n    end\n    if not pieces[2] then\n        return \"missing span_id\"\n    end\n    return nil, pieces[1], pieces[2], pieces[3], pieces[4]\nend\n\nlocal function inject_header(ctx)\n    local opentracing = ctx.opentracing\n    local tracer = opentracing.tracer\n    local outgoing_headers = {}\n\n    local span = opentracing.request_span\n    if ctx.opentracing_sample then\n        span = opentracing.proxy_span\n    end\n    tracer:inject(span, \"http_headers\", outgoing_headers)\n\n    for k, v in pairs(outgoing_headers) do\n        core.request.set_header(ctx, k, v)\n    end\nend\n\nfunction _M.rewrite(plugin_conf, ctx)\n    local conf = core.table.clone(plugin_conf)\n    -- once the server started, server_addr and server_port won't change, so we can cache it.\n    conf.server_port = tonumber(ctx.var['server_port'])\n\n    if not conf.server_addr or conf.server_addr == '' then\n        conf.server_addr = ctx.var[\"server_addr\"]\n    end\n\n    local tracer = core.lrucache.plugin_ctx(lrucache, ctx, conf.server_addr .. conf.server_port,\n                                            create_tracer, conf, ctx)\n\n    local headers = core.request.headers(ctx)\n    local per_req_sample_ratio\n\n    -- X-B3-Flags: if it equals '1' then it overrides sampling policy\n    -- We still want to warn on invalid sampled header, so do this after the above\n    local debug = headers[\"x-b3-flags\"]\n    if debug == \"1\" then\n        per_req_sample_ratio = 1\n    end\n\n    local trace_id, request_span_id, sampled, parent_span_id\n    local b3 = headers[\"b3\"]\n    if b3 then\n        -- don't pass b3 header by default\n        -- TODO: add an option like 'single_b3_header' so we can adapt to the upstream\n        -- which doesn't support b3 header without always breaking down the header\n        core.request.set_header(ctx, \"b3\", nil)\n\n        local err\n        err, trace_id, request_span_id, sampled, parent_span_id = parse_b3(b3)\n\n        if err then\n            core.log.error(\"invalid b3 header: \", b3, \", ignored: \", err)\n            return 400\n        end\n\n        if sampled == \"d\" then\n            core.request.set_header(ctx, \"x-b3-flags\", \"1\")\n            sampled = \"1\"\n        end\n    else\n        -- X-B3-Sampled: if the client decided to sample this request, we do too.\n        sampled = headers[\"x-b3-sampled\"]\n        trace_id = headers[\"x-b3-traceid\"]\n        parent_span_id = headers[\"x-b3-parentspanid\"]\n        request_span_id = headers[\"x-b3-spanid\"]\n    end\n\n    local zipkin_ctx = core.tablepool.fetch(\"zipkin_ctx\", 0, 3)\n    zipkin_ctx.trace_id = trace_id\n    zipkin_ctx.parent_span_id = parent_span_id\n    zipkin_ctx.request_span_id = request_span_id\n    ctx.zipkin = zipkin_ctx\n\n    local wire_context = tracer:extract(\"http_headers\", ctx)\n\n    local start_timestamp = ngx.req.start_time()\n    local request_span = tracer:start_span(\"apisix.request\", {\n        child_of = wire_context,\n        start_timestamp = start_timestamp,\n        tags = {\n            component = \"apisix\",\n            [\"span.kind\"] = \"server\",\n            [\"http.method\"] = ctx.var.request_method,\n            [\"http.url\"] = ctx.var.request_uri,\n             -- TODO: support ipv6\n            [\"peer.ipv4\"] = core.request.get_remote_client_ip(ctx),\n            [\"peer.port\"] = core.request.get_remote_client_port(ctx),\n        }\n    })\n\n    ctx.opentracing = {\n        tracer = tracer,\n        wire_context = wire_context,\n        request_span = request_span,\n    }\n\n    -- Process sampled\n    if sampled == \"1\" or sampled == \"true\" then\n        per_req_sample_ratio = 1\n    elseif sampled == \"0\" or sampled == \"false\" then\n        per_req_sample_ratio = 0\n    end\n\n    ctx.opentracing_sample = tracer.sampler:sample(per_req_sample_ratio or conf.sample_ratio)\n    if not ctx.opentracing_sample then\n        request_span:set_baggage_item(\"x-b3-sampled\",\"0\")\n    else\n       request_span:set_baggage_item(\"x-b3-sampled\",\"1\")\n    end\n\n    if plugin_info.set_ngx_var then\n        local span_context = request_span:context()\n        ngx_var.zipkin_context_traceparent = string_format(\"00-%s-%s-%02x\",\n                                             to_hex(span_context.trace_id),\n                                             to_hex(span_context.span_id),\n                                             span_context:get_baggage_item(\"x-b3-sampled\"))\n        ngx_var.zipkin_trace_id = to_hex(span_context.trace_id)\n        ngx_var.zipkin_span_id = to_hex(span_context.span_id)\n    end\n\n    if not ctx.opentracing_sample then\n        return\n    end\n\n    local request_span = ctx.opentracing.request_span\n    if conf.span_version == ZIPKIN_SPAN_VER_1 then\n        ctx.opentracing.rewrite_span = request_span:start_child_span(\"apisix.rewrite\",\n                                                                     start_timestamp)\n        ctx.REWRITE_END_TIME = tracer:time()\n        ctx.opentracing.rewrite_span:finish(ctx.REWRITE_END_TIME)\n    else\n        ctx.opentracing.proxy_span = request_span:start_child_span(\"apisix.proxy\",\n                                                                   start_timestamp)\n    end\nend\n\nfunction _M.access(conf, ctx)\n    local opentracing = ctx.opentracing\n    local tracer = opentracing.tracer\n\n    if conf.span_version == ZIPKIN_SPAN_VER_1 then\n        opentracing.access_span = opentracing.request_span:start_child_span(\n            \"apisix.access\", ctx.REWRITE_END_TIME)\n\n        ctx.ACCESS_END_TIME = tracer:time()\n        opentracing.access_span:finish(ctx.ACCESS_END_TIME)\n\n        opentracing.proxy_span = opentracing.request_span:start_child_span(\n                \"apisix.proxy\", ctx.ACCESS_END_TIME)\n    end\n\n    -- send headers to upstream\n    inject_header(ctx)\nend\n\n\nfunction _M.header_filter(conf, ctx)\n    if not ctx.opentracing_sample then\n        return\n    end\n\n    local opentracing = ctx.opentracing\n    local end_time = opentracing.tracer:time()\n\n    if conf.span_version == ZIPKIN_SPAN_VER_1 then\n        if  opentracing.proxy_span then\n            opentracing.body_filter_span = opentracing.proxy_span:start_child_span(\n                \"apisix.body_filter\", end_time)\n        end\n    else\n        opentracing.proxy_span:finish(end_time)\n        opentracing.response_span = opentracing.request_span:start_child_span(\n            \"apisix.response_span\", end_time)\n    end\nend\n\n\nfunction _M.log(conf, ctx)\n    if ctx.zipkin then\n        core.tablepool.release(\"zipkin_ctx\", ctx.zipkin)\n        ctx.zipkin = nil\n    end\n\n    if not ctx.opentracing_sample then\n        return\n    end\n\n    local opentracing = ctx.opentracing\n\n    local log_end_time = opentracing.tracer:time()\n\n    if conf.span_version == ZIPKIN_SPAN_VER_1 then\n        if opentracing.body_filter_span then\n            opentracing.body_filter_span:finish(log_end_time)\n        end\n        if opentracing.proxy_span then\n            opentracing.proxy_span:finish(log_end_time)\n        end\n    elseif opentracing.response_span then\n        opentracing.response_span:finish(log_end_time)\n    end\n\n    local upstream_status = core.response.get_upstream_status(ctx)\n    opentracing.request_span:set_tag(\"http.status_code\", upstream_status)\n\n    opentracing.request_span:finish(log_end_time)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/pubsub/kafka.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core      = require(\"apisix.core\")\nlocal bconsumer = require(\"resty.kafka.basic-consumer\")\nlocal ffi       = require(\"ffi\")\nlocal C         = ffi.C\nlocal tostring  = tostring\nlocal type      = type\nlocal ipairs    = ipairs\nlocal str_sub   = string.sub\n\nffi.cdef[[\n    int64_t atoll(const char *num);\n]]\n\n\nlocal _M = {}\n\n\n-- Handles the conversion of 64-bit integers in the lua-protobuf.\n--\n-- Because of the limitations of luajit, we cannot use native 64-bit\n-- numbers, so pb decode converts int64 to a string in #xxx format\n-- to avoid loss of precision, by this function, we convert this\n-- string to int64 cdata numbers.\nlocal function pb_convert_to_int64(src)\n    if type(src) == \"string\" then\n        -- the format is #1234, so there is a small minimum length of 2\n        if #src < 2 then\n            return 0\n        end\n        return C.atoll(ffi.cast(\"char *\", src) + 1)\n    else\n        return src\n    end\nend\n\n\n-- Takes over requests of type kafka upstream in the http_access phase.\nfunction _M.access(api_ctx)\n    local pubsub, err = core.pubsub.new()\n    if not pubsub then\n        core.log.error(\"failed to initialize pubsub module, err: \", err)\n        core.response.exit(400)\n        return\n    end\n\n    local up_nodes = api_ctx.matched_upstream.nodes\n\n    -- kafka client broker-related configuration\n    local broker_list = {}\n    for i, node in ipairs(up_nodes) do\n        broker_list[i] = {\n            host = node.host,\n            port = node.port,\n        }\n\n        if api_ctx.kafka_consumer_enable_sasl then\n            broker_list[i].sasl_config = {\n                mechanism = \"PLAIN\",\n                user = api_ctx.kafka_consumer_sasl_username,\n                password = api_ctx.kafka_consumer_sasl_password,\n            }\n        end\n    end\n\n    local client_config = {refresh_interval = 30 * 60 * 1000}\n    if api_ctx.matched_upstream.tls then\n        client_config.ssl = true\n        client_config.ssl_verify = api_ctx.matched_upstream.tls.verify\n    end\n\n    -- load and create the consumer instance when it is determined\n    -- that the websocket connection was created successfully\n    local consumer = bconsumer:new(broker_list, client_config)\n\n    pubsub:on(\"cmd_kafka_list_offset\", function (params)\n        -- The timestamp parameter uses a 64-bit integer, which is difficult\n        -- for luajit to handle well, so the int64_as_string option in\n        -- lua-protobuf is used here. Smaller numbers will be decoded as\n        -- lua number, while overly larger numbers will be decoded as strings\n        -- in the format #number, where the # symbol at the beginning of the\n        -- string will be removed and converted to int64_t with the atoll function.\n        local timestamp = pb_convert_to_int64(params.timestamp)\n\n        local offset, err = consumer:list_offset(params.topic, params.partition, timestamp)\n\n        if not offset then\n            return nil, \"failed to list offset, topic: \" .. params.topic ..\n                \", partition: \" .. params.partition .. \", err: \" .. err\n        end\n\n        offset = tostring(offset)\n        return {\n            kafka_list_offset_resp = {\n                offset = str_sub(offset, 1, #offset - 2)\n            }\n        }\n    end)\n\n    pubsub:on(\"cmd_kafka_fetch\", function (params)\n        local offset = pb_convert_to_int64(params.offset)\n\n        local ret, err = consumer:fetch(params.topic, params.partition, offset)\n        if not ret then\n            return nil, \"failed to fetch message, topic: \" .. params.topic ..\n                \", partition: \" .. params.partition .. \", err: \" .. err\n        end\n\n        -- split into multiple messages when the amount of data in\n        -- a single batch is too large\n        local messages = ret.records\n\n        -- special handling of int64 for luajit compatibility\n        for _, message in ipairs(messages) do\n            local timestamp = tostring(message.timestamp)\n            message.timestamp = str_sub(timestamp, 1, #timestamp - 2)\n            local offset = tostring(message.offset)\n            message.offset = str_sub(offset, 1, #offset - 2)\n        end\n\n        return {\n            kafka_fetch_resp = {\n                messages = messages,\n            },\n        }\n    end)\n\n    -- start processing client commands\n    pubsub:wait()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/resource.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal log = require(\"apisix.core.log\")\nlocal table = require(\"apisix.core.table\")\nlocal core = require(\"apisix.core\")\nlocal string_sub = string.sub\nlocal config_util = require(\"apisix.core.config_util\")\nlocal require = require\n\nlocal _M = {}\nlocal function remove_etcd_prefix(key)\n    local prefix = \"\"\n    local local_conf =  require(\"apisix.core.config_local\").local_conf()\n    local role = table.try_read_attr(local_conf, \"deployment\", \"role\")\n    local provider = table.try_read_attr(local_conf, \"deployment\", \"role_\" ..\n    role, \"config_provider\")\n    if provider == \"etcd\" and local_conf.etcd and local_conf.etcd.prefix then\n        prefix = local_conf.etcd.prefix\n    end\n    return string_sub(key, #prefix + 1)\nend\n\n\nlocal function fetch_latest_conf(resource_path)\n    -- if resource path contains json path, extract out the prefix\n    -- for eg: extracts /routes/1 from /routes/1#plugins.abc\n    resource_path = config_util.parse_path(resource_path)\n    local resource_type, id\n    -- Handle both formats:\n    -- 1. /<etcd-prefix>/<resource_type>/<id>\n    -- 2. /<resource_type>/<id>\n    resource_path = remove_etcd_prefix(resource_path)\n    resource_type, id = resource_path:match(\"^/([^/]+)/([^/]+)$\")\n    if not resource_type or not id then\n        log.error(\"invalid resource path: \", resource_path)\n        return nil\n    end\n\n    local key\n    if resource_type == \"upstreams\" then\n        key = \"/upstreams\"\n    elseif resource_type == \"routes\" then\n        key = \"/routes\"\n    elseif resource_type == \"services\" then\n        key = \"/services\"\n    elseif resource_type == \"stream_routes\" then\n        key = \"/stream_routes\"\n    else\n        log.error(\"unsupported resource type: \", resource_type)\n        return nil\n    end\n\n    local data = core.config.fetch_created_obj(key)\n    if not data then\n        log.error(\"failed to fetch configuration for type: \", key)\n        return nil\n    end\n    local resource = data:get(id)\n    if not resource then\n        -- this can happen if the resource was deleted\n        -- after the this function was called so we don't throw error\n        log.warn(\"resource not found: \", id, \" in \", key,\n                      \"this can happen if the resource was deleted\")\n        return nil\n    end\n\n    return resource\nend\n\nfunction _M.get_nodes_ver(resource_path)\n    local res_conf = fetch_latest_conf(resource_path)\n    local upstream = res_conf.value.upstream or res_conf.value\n    return upstream._nodes_ver\nend\n\n\nfunction _M.set_nodes_ver_and_nodes(resource_path, nodes_ver, nodes)\n    local res_conf = fetch_latest_conf(resource_path)\n    local upstream = res_conf.value.upstream or res_conf.value\n    upstream._nodes_ver = nodes_ver\n    upstream.nodes = nodes\nend\n\n_M.fetch_latest_conf = fetch_latest_conf\nreturn _M\n"
  },
  {
    "path": "apisix/router.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal http_route = require(\"apisix.http.route\")\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal core    = require(\"apisix.core\")\nlocal set_plugins_meta_parent = require(\"apisix.plugin\").set_plugins_meta_parent\nlocal str_lower = string.lower\nlocal ipairs  = ipairs\n\nlocal _M = {version = 0.3}\n\n\nlocal function filter(route)\n    route.orig_modifiedIndex = route.modifiedIndex\n\n    route.has_domain = false\n    if not route.value then\n        return\n    end\n\n    set_plugins_meta_parent(route.value.plugins, route)\n\n    if route.value.host then\n        route.value.host = str_lower(route.value.host)\n    elseif route.value.hosts then\n        for i, v in ipairs(route.value.hosts) do\n            route.value.hosts[i] = str_lower(v)\n        end\n    end\n\n    apisix_upstream.filter_upstream(route.value.upstream, route)\nend\n\n\n-- attach common methods if the router doesn't provide its custom implementation\nlocal function attach_http_router_common_methods(http_router)\n    if http_router.routes == nil then\n        http_router.routes = function ()\n            if not http_router.user_routes then\n                return nil, nil\n            end\n\n            local user_routes = http_router.user_routes\n            return user_routes.values, user_routes.conf_version\n        end\n    end\n\n    if http_router.init_worker == nil then\n        http_router.init_worker = function (filter)\n            http_router.user_routes = http_route.init_worker(filter)\n        end\n    end\nend\n\n\nfunction _M.http_init_worker()\n    local conf = core.config.local_conf()\n    local router_http_name = \"radixtree_uri\"\n    local router_ssl_name = \"radixtree_sni\"\n\n    if conf and conf.apisix and conf.apisix.router then\n        router_http_name = conf.apisix.router.http or router_http_name\n        router_ssl_name = conf.apisix.router.ssl or router_ssl_name\n    end\n\n    local router_http = require(\"apisix.http.router.\" .. router_http_name)\n    attach_http_router_common_methods(router_http)\n    router_http.init_worker(filter)\n    _M.router_http = router_http\n\n    local router_ssl = require(\"apisix.ssl.router.\" .. router_ssl_name)\n    router_ssl.init_worker()\n    _M.router_ssl = router_ssl\n\n    -- Initialize stream router in HTTP workers only if stream mode is enabled\n    -- This allows the Control API (which runs in HTTP workers) to access stream routes\n    if conf and conf.apisix and conf.apisix.stream_proxy then\n        local router_stream = require(\"apisix.stream.router.ip_port\")\n        router_stream.stream_init_worker(filter)\n        _M.router_stream = router_stream\n    end\n\n    _M.api = require(\"apisix.api_router\")\nend\n\n\nfunction _M.stream_init_worker()\n    local router_ssl_name = \"radixtree_sni\"\n\n    local router_stream = require(\"apisix.stream.router.ip_port\")\n    router_stream.stream_init_worker(filter)\n    _M.router_stream = router_stream\n\n    local router_ssl = require(\"apisix.ssl.router.\" .. router_ssl_name)\n    router_ssl.init_worker()\n    _M.router_ssl = router_ssl\nend\n\n\nfunction _M.ssls()\n    return _M.router_ssl.ssls()\nend\n\nfunction _M.http_routes()\n    if not _M.router_http then\n        return nil, nil\n    end\n    return _M.router_http.routes()\nend\n\nfunction _M.stream_routes()\n    -- maybe it's not inited.\n    if not _M.router_stream then\n        return nil, nil\n    end\n    return _M.router_stream.routes()\nend\n\n\n-- for test\n_M.filter_test = filter\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/schema_def.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal table_insert = table.insert\nlocal table_concat = table.concat\n\nlocal _M = {version = 0.5}\n\n\nlocal plugins_schema = {\n    type = \"object\"\n}\n\n_M.anonymous_consumer_schema = {\n    type = \"string\",\n    minLength = 1\n}\n\nfunction _M.get_realm_schema(default_val)\n    return {\n        type = \"string\",\n        -- Pattern: Only allow printable ASCII, but EXCLUDE \" and \\\n        -- \\x20-\\x21 (Space and !)\n        -- \\x23-\\x5B (# through [)\n        -- \\x5D-\\x7E (] through ~)\n        -- Escaped closing bracket (\\x5D) assertion for PCRE compatibility\n        pattern = \"^[\\x20-\\x21\\x23-\\x5B\\\\]-\\x7E]+$\",\n        default = default_val,\n        minLength = 1,\n        maxLength = 128,\n    }\nend\n\nlocal id_schema = {\n    anyOf = {\n        {\n            type = \"string\", minLength = 1, maxLength = 64,\n            pattern = [[^[a-zA-Z0-9-_.]+$]]\n        },\n        {type = \"integer\", minimum = 1}\n    }\n}\n\nlocal host_def_pat = \"^\\\\*$|^\\\\*?[0-9a-zA-Z-._\\\\[\\\\]:]+$\"\nlocal host_def = {\n    type = \"string\",\n    pattern = host_def_pat,\n}\n_M.host_def = host_def\n\n\nlocal ipv4_seg = \"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\"\nlocal ipv4_def_buf = {}\nfor i = 1, 4 do\n    table_insert(ipv4_def_buf, ipv4_seg)\nend\nlocal ipv4_def = table_concat(ipv4_def_buf, [[\\.]])\n-- There is false negative for ipv6/cidr. For instance, `:/8` will be valid.\n-- It is fine as the correct regex will be too complex.\nlocal ipv6_def = \"([a-fA-F0-9]{0,4}:){1,8}(:[a-fA-F0-9]{0,4}){0,8}\"\n                 .. \"([a-fA-F0-9]{0,4})?\"\nlocal ip_def = {\n    {title = \"IPv4\", type = \"string\", format = \"ipv4\"},\n    {title = \"IPv4/CIDR\", type = \"string\", pattern = \"^\" .. ipv4_def .. \"/([12]?[0-9]|3[0-2])$\"},\n    {title = \"IPv6\", type = \"string\", format = \"ipv6\"},\n    {title = \"IPv6/CIDR\", type = \"string\", pattern = \"^\" .. ipv6_def .. \"/[0-9]{1,3}$\"},\n}\n_M.ip_def = ip_def\n\n\n_M.uri_def = {type = \"string\", pattern = [=[^[^\\/]+:\\/\\/([\\da-zA-Z.-]+|\\[[\\da-fA-F:]+\\])(:\\d+)?]=]}\n\n\nlocal timestamp_def = {\n    type = \"integer\",\n}\n\nlocal remote_addr_def = {\n    description = \"client IP\",\n    type = \"string\",\n    anyOf = ip_def,\n}\n\n\nlocal label_value_def = {\n    description = \"value of label\",\n    type = \"string\",\n    pattern = [[^\\S+$]],\n    maxLength = 256,\n    minLength = 1\n}\n_M.label_value_def = label_value_def\n\n\nlocal labels_def = {\n    description = \"key/value pairs to specify attributes\",\n    type = \"object\",\n    patternProperties = {\n        [\".*\"] = label_value_def\n    },\n}\n\n\nlocal rule_name_def = {\n    type = \"string\",\n    maxLength = 256,\n    minLength = 1,\n}\n\n\nlocal desc_def = {\n    type = \"string\",\n    maxLength = 256,\n}\n\n\nlocal timeout_def = {\n    type = \"object\",\n    properties = {\n        connect = {type = \"number\", exclusiveMinimum = 0},\n        send = {type = \"number\", exclusiveMinimum = 0},\n        read = {type = \"number\", exclusiveMinimum = 0},\n    },\n    required = {\"connect\", \"send\", \"read\"},\n}\n\n\nlocal health_checker_active = {\n    type = \"object\",\n    properties = {\n        type = {\n            type = \"string\",\n            enum = {\"http\", \"https\", \"tcp\"},\n            default = \"http\"\n        },\n        timeout = {type = \"number\", default = 1},\n        concurrency = {type = \"integer\", default = 10},\n        host = host_def,\n        port = {\n            type = \"integer\",\n            minimum = 1,\n            maximum = 65535\n        },\n        http_path = {type = \"string\", default = \"/\"},\n        https_verify_certificate = {type = \"boolean\", default = true},\n        healthy = {\n            type = \"object\",\n            properties = {\n                interval = {type = \"integer\", minimum = 1, default = 1},\n                http_statuses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"integer\",\n                        minimum = 200,\n                        maximum = 599\n                    },\n                    uniqueItems = true,\n                    default = {200, 302}\n                },\n                successes = {\n                    type = \"integer\",\n                    minimum = 1,\n                    maximum = 254,\n                    default = 2\n                }\n            }\n        },\n        unhealthy = {\n            type = \"object\",\n            properties = {\n                interval = {type = \"integer\", minimum = 1, default = 1},\n                http_statuses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"integer\",\n                        minimum = 200,\n                        maximum = 599\n                    },\n                    uniqueItems = true,\n                    default = {429, 404, 500, 501, 502, 503, 504, 505}\n                },\n                http_failures = {\n                    type = \"integer\",\n                    minimum = 1,\n                    maximum = 254,\n                    default = 5\n                },\n                tcp_failures = {\n                    type = \"integer\",\n                    minimum = 1,\n                    maximum = 254,\n                    default = 2\n                },\n                timeouts = {\n                    type = \"integer\",\n                    minimum = 1,\n                    maximum = 254,\n                    default = 3\n                }\n            }\n        },\n        req_headers = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"string\",\n                uniqueItems = true,\n            },\n        }\n    }\n}\n_M.health_checker_active = health_checker_active\n\n\nlocal health_checker_passive = {\n    type = \"object\",\n    properties = {\n        type = {\n            type = \"string\",\n            enum = {\"http\", \"https\", \"tcp\"},\n            default = \"http\"\n        },\n        healthy = {\n            type = \"object\",\n            properties = {\n                http_statuses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"integer\",\n                        minimum = 200,\n                        maximum = 599,\n                    },\n                    uniqueItems = true,\n                    default = {200, 201, 202, 203, 204, 205, 206, 207,\n                               208, 226, 300, 301, 302, 303, 304, 305,\n                               306, 307, 308}\n                },\n                successes = {\n                    type = \"integer\",\n                    minimum = 0,\n                    maximum = 254,\n                    default = 5\n                }\n            }\n        },\n        unhealthy = {\n            type = \"object\",\n            properties = {\n                http_statuses = {\n                    type = \"array\",\n                    minItems = 1,\n                    items = {\n                        type = \"integer\",\n                        minimum = 200,\n                        maximum = 599,\n                    },\n                    uniqueItems = true,\n                    default = {429, 500, 503}\n                },\n                tcp_failures = {\n                    type = \"integer\",\n                    minimum = 0,\n                    maximum = 254,\n                    default = 2\n                },\n                timeouts = {\n                    type = \"integer\",\n                    minimum = 0,\n                    maximum = 254,\n                    default = 7\n                },\n                http_failures = {\n                    type = \"integer\",\n                    minimum = 0,\n                    maximum = 254,\n                    default = 5\n                },\n            }\n        }\n    },\n}\n_M.health_checker_passive = health_checker_passive\n\n\nlocal health_checker = {\n    type = \"object\",\n    properties = {\n        active = health_checker_active,\n        passive = health_checker_passive,\n    },\n    anyOf = {\n        {required = {\"active\"}},\n        {required = {\"active\", \"passive\"}},\n    },\n}\n_M.health_checker = health_checker\n\n\nlocal nodes_schema = {\n    anyOf = {\n        {\n            type = \"object\",\n            patternProperties = {\n                [\".*\"] = {\n                    description = \"weight of node\",\n                    type = \"integer\",\n                    minimum = 0,\n                }\n            },\n        },\n        {\n            type = \"array\",\n            items = {\n                type = \"object\",\n                properties = {\n                    host = host_def,\n                    port = {\n                        description = \"port of node\",\n                        type = \"integer\",\n                        minimum = 1,\n                        maximum = 65535\n                    },\n                    weight = {\n                        description = \"weight of node\",\n                        type = \"integer\",\n                        minimum = 0,\n                    },\n                    priority = {\n                        description = \"priority of node\",\n                        type = \"integer\",\n                        default = 0,\n                    },\n                    metadata = {\n                        description = \"metadata of node\",\n                        type = \"object\",\n                    }\n                },\n                required = {\"host\", \"weight\"},\n            },\n        }\n    }\n}\n_M.discovery_nodes = {\n    type = \"array\",\n    items = {\n        type = \"object\",\n        properties = {\n            host = {\n                description = \"domain or ip\",\n            },\n            port = {\n                description = \"port of node\",\n                type = \"integer\",\n                minimum = 1,\n                maximum = 65535\n            },\n            weight = {\n                description = \"weight of node\",\n                type = \"integer\",\n                minimum = 0,\n            },\n            priority = {\n                description = \"priority of node\",\n                type = \"integer\",\n            },\n            metadata = {\n                description = \"metadata of node\",\n                type = \"object\",\n            }\n        },\n        -- nodes from DNS discovery may not contain port\n        required = {\"host\", \"weight\"},\n    },\n}\n\n\nlocal certificate_scheme = {\n    type = \"string\", minLength = 128, maxLength = 64*1024\n}\n\n\nlocal private_key_schema = {\n    type = \"string\", minLength = 128, maxLength = 64*1024\n}\n\n\nlocal upstream_schema = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        nodes = nodes_schema,\n        retries = {\n            type = \"integer\",\n            minimum = 0,\n        },\n        retry_timeout = {\n            type = \"number\",\n            minimum = 0,\n        },\n        timeout = timeout_def,\n        tls = {\n            type = \"object\",\n            properties = {\n                client_cert_id = id_schema,\n                client_cert = certificate_scheme,\n                client_key = private_key_schema,\n                verify = {\n                    type = \"boolean\",\n                    description = \"Turn on server certificate verification, \"..\n                        \"currently only kafka upstream is supported\",\n                    default = false,\n                },\n            },\n            dependencies = {\n                client_cert = {required = {\"client_key\"}},\n                client_key = {required = {\"client_cert\"}},\n                client_cert_id = {\n                    [\"not\"] = {required = {\"client_cert\", \"client_key\"}}\n                }\n            }\n        },\n        keepalive_pool = {\n            type = \"object\",\n            properties = {\n                size = {\n                    type = \"integer\",\n                    default = 320,\n                    minimum = 1,\n                },\n                idle_timeout = {\n                    type = \"number\",\n                    default = 60,\n                    minimum = 0,\n                },\n                requests = {\n                    type = \"integer\",\n                    default = 1000,\n                    minimum = 1,\n                },\n            },\n        },\n        type = {\n            description = \"algorithms of load balancing\",\n            type = \"string\",\n            default = \"roundrobin\",\n        },\n        checks = health_checker,\n        hash_on = {\n            type = \"string\",\n            default = \"vars\",\n            enum = {\n              \"vars\",\n              \"header\",\n              \"cookie\",\n              \"consumer\",\n              \"vars_combinations\",\n            },\n        },\n        key = {\n            description = \"the key of chash for dynamic load balancing\",\n            type = \"string\",\n        },\n        scheme = {\n            default = \"http\",\n            enum = {\"grpc\", \"grpcs\", \"http\", \"https\", \"tcp\", \"tls\", \"udp\",\n                \"kafka\"},\n            description = \"The scheme of the upstream.\" ..\n                \" For L7 proxy, it can be one of grpc/grpcs/http/https.\" ..\n                \" For L4 proxy, it can be one of tcp/tls/udp.\" ..\n                \" For specific protocols, it can be kafka.\"\n        },\n        discovery_type = {\n            description = \"discovery type\",\n            type = \"string\",\n        },\n        discovery_args = {\n            type = \"object\",\n            properties = {\n                namespace_id = {\n                    description = \"namespace id\",\n                    type = \"string\",\n                },\n                group_name = {\n                    description = \"group name\",\n                    type = \"string\",\n                },\n            }\n        },\n        pass_host = {\n            description = \"mod of host passing\",\n            type = \"string\",\n            enum = {\"pass\", \"node\", \"rewrite\"},\n            default = \"pass\"\n        },\n        upstream_host = host_def,\n        service_name = {\n            type = \"string\",\n            maxLength = 256,\n            minLength = 1\n        },\n    },\n    oneOf = {\n        {required = {\"nodes\"}},\n        {required = {\"service_name\", \"discovery_type\"}},\n    },\n    additionalProperties = false\n}\n\n-- TODO: add more nginx variable support\n_M.upstream_hash_vars_schema = {\n    type = \"string\",\n    pattern = [[^((uri|server_name|server_addr|request_uri|remote_port]]\n               .. [[|remote_addr|query_string|host|hostname|mqtt_client_id)]]\n               .. [[|arg_[0-9a-zA-z_-]+)$]],\n}\n\n-- validates header name, cookie name.\n-- a-z, A-Z, 0-9, '_' and '-' are allowed.\n-- when \"underscores_in_headers on\", header name allow '_'.\n-- http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers\n_M.upstream_hash_header_schema = {\n    type = \"string\",\n    pattern = [[^[a-zA-Z0-9-_]+$]]\n}\n\n-- validates string only\n_M.upstream_hash_vars_combinations_schema = {\n    type = \"string\"\n}\n\n\nlocal method_schema = {\n    description = \"HTTP method\",\n    type = \"string\",\n    enum = {\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\",\n        \"OPTIONS\", \"CONNECT\", \"TRACE\", \"PURGE\"},\n}\n_M.method_schema = method_schema\n\n\n_M.route = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        uri = {type = \"string\", minLength = 1, maxLength = 4096},\n        uris = {\n            type = \"array\",\n            items = {\n                description = \"HTTP uri\",\n                type = \"string\",\n            },\n            minItems = 1,\n            uniqueItems = true,\n        },\n        priority = {type = \"integer\", default = 0},\n\n        methods = {\n            type = \"array\",\n            items = method_schema,\n            uniqueItems = true,\n        },\n        host = host_def,\n        hosts = {\n            type = \"array\",\n            items = host_def,\n            minItems = 1,\n            uniqueItems = true,\n        },\n        remote_addr = remote_addr_def,\n        remote_addrs = {\n            type = \"array\",\n            items = remote_addr_def,\n            minItems = 1,\n            uniqueItems = true,\n        },\n        timeout = timeout_def,\n        vars = {\n            type = \"array\",\n        },\n        filter_func = {\n            type = \"string\",\n            minLength = 10,\n            pattern = [[^function]],\n        },\n\n        -- The 'script' fields below are used by dashboard for plugin orchestration\n        script = {type = \"string\", minLength = 10, maxLength = 102400},\n        script_id = id_schema,\n\n        plugins = plugins_schema,\n        plugin_config_id = id_schema,\n\n        upstream = upstream_schema,\n\n        service_id = id_schema,\n        upstream_id = id_schema,\n\n        enable_websocket = {\n            description = \"enable websocket for request\",\n            type        = \"boolean\",\n        },\n\n        status = {\n            description = \"route status, 1 to enable, 0 to disable\",\n            type = \"integer\",\n            enum = {1, 0},\n            default = 1\n        },\n    },\n    allOf = {\n        {\n            oneOf = {\n                {required = {\"uri\"}},\n                {required = {\"uris\"}},\n            },\n        },\n        {\n            oneOf = {\n                {[\"not\"] = {\n                    anyOf = {\n                        {required = {\"host\"}},\n                        {required = {\"hosts\"}},\n                    }\n                }},\n                {required = {\"host\"}},\n                {required = {\"hosts\"}}\n            },\n        },\n        {\n            oneOf = {\n                {[\"not\"] = {\n                    anyOf = {\n                        {required = {\"remote_addr\"}},\n                        {required = {\"remote_addrs\"}},\n                    }\n                }},\n                {required = {\"remote_addr\"}},\n                {required = {\"remote_addrs\"}}\n            },\n        },\n    },\n    anyOf = {\n        {required = {\"plugins\", \"uri\"}},\n        {required = {\"upstream\", \"uri\"}},\n        {required = {\"upstream_id\", \"uri\"}},\n        {required = {\"service_id\", \"uri\"}},\n        {required = {\"plugins\", \"uris\"}},\n        {required = {\"upstream\", \"uris\"}},\n        {required = {\"upstream_id\", \"uris\"}},\n        {required = {\"service_id\", \"uris\"}},\n        {required = {\"script\", \"uri\"}},\n        {required = {\"script\", \"uris\"}},\n    },\n    [\"not\"] = {\n        anyOf = {\n            {required = {\"script\", \"plugins\"}},\n            {required = {\"script\", \"plugin_config_id\"}},\n        }\n    },\n    additionalProperties = false,\n}\n\n\n_M.service = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        plugins = plugins_schema,\n        upstream = upstream_schema,\n        upstream_id = id_schema,\n        script = {type = \"string\", minLength = 10, maxLength = 102400},\n        enable_websocket = {\n            description = \"enable websocket for request\",\n            type        = \"boolean\",\n        },\n        hosts = {\n            type = \"array\",\n            items = host_def,\n            minItems = 1,\n            uniqueItems = true,\n        },\n    },\n    additionalProperties = false,\n}\n\n\n_M.consumer = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        username = {\n            type = \"string\", minLength = 1, maxLength = rule_name_def.maxLength,\n            pattern = [[^[a-zA-Z0-9_\\-]+$]]\n        },\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        group_id = id_schema,\n        plugins = plugins_schema,\n    },\n    required = {\"username\"},\n    additionalProperties = false,\n}\n\n_M.credential = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        plugins = {\n            type = \"object\",\n            maxProperties = 1,\n        },\n    },\n    additionalProperties = false,\n}\n\n_M.upstream = upstream_schema\n\n\nlocal secret_uri_schema = {\n    type = \"string\",\n    pattern = \"^\\\\$(secret|env|ENV)://\"\n}\n\n\n_M.ssl = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        type = {\n            description = \"ssl certificate type, \" ..\n                            \"server to server certificate, \" ..\n                            \"client to client certificate for upstream\",\n            type = \"string\",\n            default = \"server\",\n            enum = {\"server\", \"client\"}\n        },\n        cert = {\n            oneOf = {\n                certificate_scheme,\n                secret_uri_schema\n            }\n        },\n        key = {\n            oneOf = {\n                private_key_schema,\n                secret_uri_schema\n            }\n        },\n        sni = {\n            type = \"string\",\n            pattern = host_def_pat,\n        },\n        snis = {\n            type = \"array\",\n            items = {\n                type = \"string\",\n                pattern = host_def_pat,\n            },\n            minItems = 1,\n        },\n        certs = {\n            type = \"array\",\n            items = {\n                oneOf = {\n                    certificate_scheme,\n                    secret_uri_schema\n                }\n            }\n        },\n        keys = {\n            type = \"array\",\n            items = {\n                oneOf = {\n                    private_key_schema,\n                    secret_uri_schema\n                }\n            }\n        },\n        client = {\n            type = \"object\",\n            properties = {\n                ca = certificate_scheme,\n                depth = {\n                    type = \"integer\",\n                    minimum = 0,\n                    default = 1,\n                },\n                skip_mtls_uri_regex = {\n                    type = \"array\",\n                    minItems = 1,\n                    uniqueItems = true,\n                    items = {\n                        description = \"uri regular expression to skip mtls\",\n                        type = \"string\",\n                    }\n                },\n            },\n            required = {\"ca\"},\n        },\n        status = {\n            description = \"ssl status, 1 to enable, 0 to disable\",\n            type = \"integer\",\n            enum = {1, 0},\n            default = 1\n        },\n        ssl_protocols = {\n            description = \"set ssl protocols\",\n            type = \"array\",\n            maxItems = 3,\n            uniqueItems = true,\n            items = {\n                enum = {\"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\"}\n            },\n        },\n    },\n    [\"if\"] = {\n        properties = {\n            type = {\n                enum = {\"server\"},\n            },\n        },\n    },\n    [\"then\"] = {\n        oneOf = {\n            {required = {\"sni\", \"key\", \"cert\"}},\n            {required = {\"snis\", \"key\", \"cert\"}}\n        }\n    },\n    [\"else\"] = {required = {\"key\", \"cert\"}},\n    additionalProperties = false,\n}\n\n\n\n-- TODO: Design a plugin resource registration framework used by plugins and move the proto\n--       resource to grpc-transcode plugin, which should not be an APISIX core resource\n_M.proto = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        content = {\n            type = \"string\", minLength = 1, maxLength = 1024*1024\n        }\n    },\n    required = {\"content\"},\n    additionalProperties = false,\n}\n\n\n_M.global_rule = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        plugins = plugins_schema,\n    },\n    required = {\"id\", \"plugins\"},\n    additionalProperties = false,\n}\n\n\nlocal xrpc_protocol_schema = {\n    type = \"object\",\n    properties = {\n        name = {\n            type = \"string\",\n        },\n        superior_id = id_schema,\n        conf = {\n            description = \"protocol-specific configuration\",\n            type = \"object\",\n        },\n        logger = {\n            type = \"array\",\n            items = {\n                properties = {\n                    name = {\n                        type = \"string\",\n                    },\n                    filter = {\n                        description = \"logger filter rules\",\n                        type = \"array\",\n                    },\n                    conf = {\n                        description = \"logger plugin configuration\",\n                        type = \"object\",\n                    },\n                },\n                dependencies = {\n                    name = {\"conf\"},\n                },\n                additionalProperties = false,\n            },\n        },\n\n    },\n    required = {\"name\"}\n}\n\n\n_M.stream_route = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        remote_addr = remote_addr_def,\n        server_addr = {\n            description = \"server IP\",\n            type = \"string\",\n            anyOf = ip_def,\n        },\n        server_port = {\n            description = \"server port\",\n            type = \"integer\",\n            minimum = 1,\n            maximum = 65535\n        },\n        sni = {\n            description = \"server name indication\",\n            type = \"string\",\n            pattern = host_def_pat,\n        },\n        upstream = upstream_schema,\n        upstream_id = id_schema,\n        service_id = id_schema,\n        plugins = plugins_schema,\n        protocol = xrpc_protocol_schema,\n    },\n    additionalProperties = false,\n}\n\n\n_M.plugins = {\n    type = \"array\",\n    items = {\n        type = \"object\",\n        properties = {\n            name = {\n                type = \"string\",\n                minLength = 1,\n            },\n            stream = {\n                type = \"boolean\"\n            },\n            additionalProperties = false,\n        },\n        required = {\"name\"}\n    }\n}\n\n\n_M.plugin_config = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = {\n            type = \"string\",\n        },\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        plugins = plugins_schema,\n    },\n    required = {\"id\", \"plugins\"},\n    additionalProperties = false,\n}\n\n\n_M.consumer_group = {\n    type = \"object\",\n    properties = {\n        -- metadata\n        id = id_schema,\n        name = rule_name_def,\n        desc = desc_def,\n        labels = labels_def,\n        create_time = timestamp_def,\n        update_time = timestamp_def,\n\n        -- properties\n        plugins = plugins_schema,\n    },\n    required = {\"id\", \"plugins\"},\n    additionalProperties = false,\n}\n\n\n_M.id_schema = id_schema\n\n\n_M.plugin_injected_schema = {\n    [\"$comment\"] = \"this is a mark for our injected plugin schema\",\n    _meta = {\n        type = \"object\",\n        properties = {\n            disable = {\n                type = \"boolean\",\n            },\n            error_response = {\n                oneOf = {\n                    { type = \"string\" },\n                    { type = \"object\" },\n                }\n            },\n            priority = {\n                description = \"priority of plugins by customized order\",\n                type = \"integer\",\n            },\n            filter = {\n                description = \"filter determines whether the plugin \"..\n                                \"needs to be executed at runtime\",\n                type  = \"array\",\n            },\n            pre_function = {\n                description = \"function to be executed in each phase \" ..\n                              \"before execution of plugins. The pre_function will have access \" ..\n                              \"to two arguments: `conf` and `ctx`.\",\n                type = \"string\",\n            },\n        },\n        additionalProperties = false,\n    }\n}\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/script.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require    = require\nlocal core       = require(\"apisix.core\")\nlocal loadstring = loadstring\nlocal error      = error\n\n\nlocal _M = {}\n\n\nfunction _M.load(route, api_ctx)\n    local script = route.value.script\n    if script == nil or script == \"\" then\n        error(\"missing valid script\")\n    end\n\n    local loadfun, err = loadstring(script, \"route#\" .. route.value.id)\n    if not loadfun then\n        error(\"failed to load script: \" .. err .. \" script: \" .. script)\n        return nil\n    end\n    api_ctx.script_obj = loadfun()\nend\n\n\nfunction _M.run(phase, api_ctx)\n    local obj = api_ctx and api_ctx.script_obj\n    if not obj then\n        core.log.error(\"missing loaded script object\")\n        return api_ctx\n    end\n\n    core.log.info(\"loaded script_obj: \", core.json.delay_encode(obj, true))\n\n    local phase_func = obj[phase]\n    if phase_func then\n        phase_func(api_ctx)\n    end\n\n    return api_ctx\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/secret/aws.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- AWS Tools.\nrequire(\"resty.aws.config\") -- to read env vars before initing aws module\n\nlocal core = require(\"apisix.core\")\nlocal http = require(\"resty.http\")\nlocal aws = require(\"resty.aws\")\nlocal aws_instance\n\nlocal sub = core.string.sub\nlocal find = core.string.find\nlocal env = core.env\nlocal unpack = unpack\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        access_key_id = {\n            type = \"string\",\n        },\n        secret_access_key = {\n            type = \"string\",\n        },\n        session_token = {\n            type = \"string\",\n        },\n        region = {\n            type = \"string\",\n            default = \"us-east-1\",\n        },\n        endpoint_url = core.schema.uri_def,\n    },\n    required = {\"access_key_id\", \"secret_access_key\"},\n}\n\nlocal _M = {\n    schema = schema\n}\n\nlocal function make_request_to_aws(conf, key)\n    if not aws_instance then\n        aws_instance = aws()\n    end\n\n    local region = conf.region\n\n    local access_key_id = env.fetch_by_uri(conf.access_key_id) or conf.access_key_id\n\n    local secret_access_key = env.fetch_by_uri(conf.secret_access_key) or conf.secret_access_key\n\n    local session_token = env.fetch_by_uri(conf.session_token) or conf.session_token\n\n    local credentials = aws_instance:Credentials({\n        accessKeyId = access_key_id,\n        secretAccessKey = secret_access_key,\n        sessionToken = session_token,\n    })\n\n    local default_endpoint = \"https://secretsmanager.\" .. region .. \".amazonaws.com\"\n    local scheme, host, port, _, _ = unpack(http:parse_uri(conf.endpoint_url or default_endpoint))\n    local endpoint = scheme .. \"://\" .. host\n\n    local sm = aws_instance:SecretsManager({\n        credentials = credentials,\n        endpoint = endpoint,\n        region = region,\n        port = port,\n    })\n\n    local res, err = sm:getSecretValue({\n        SecretId = key,\n        VersionStage = \"AWSCURRENT\",\n    })\n\n    if not res then\n        return nil, err\n    end\n\n    if res.status ~= 200 then\n        local data = core.json.encode(res.body)\n        if data then\n            return nil, \"invalid status code \" .. res.status .. \", \" .. data\n        end\n\n        return nil, \"invalid status code \" .. res.status\n    end\n\n    return res.body.SecretString\nend\n\n-- key is the aws secretId\nfunction _M.get(conf, key)\n    core.log.info(\"fetching data from aws for key: \", key)\n\n    local idx = find(key, '/')\n\n    local main_key = idx and sub(key, 1, idx - 1) or key\n    if main_key == \"\" then\n        return nil, \"can't find main key, key: \" .. key\n    end\n\n    local sub_key = idx and sub(key, idx + 1) or nil\n\n    core.log.info(\"main: \", main_key, sub_key and \", sub: \" .. sub_key or \"\")\n\n    local res, err = make_request_to_aws(conf, main_key)\n    if not res then\n        return nil, \"failed to retrtive data from aws secret manager: \" .. err\n    end\n\n    if not sub_key then\n        return res\n    end\n\n    local data, err = core.json.decode(res)\n    if not data then\n        return nil, \"failed to decode result, res: \" .. res .. \", err: \" .. err\n    end\n\n    return data[sub_key]\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/secret/gcp.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- GCP Tools.\nlocal core         = require(\"apisix.core\")\nlocal http         = require(\"resty.http\")\nlocal google_oauth = require(\"apisix.utils.google-cloud-oauth\")\n\nlocal str_sub       = core.string.sub\nlocal str_find      = core.string.find\nlocal decode_base64 = ngx.decode_base64\n\nlocal lrucache = core.lrucache.new({ ttl = 300, count = 8 })\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        auth_config = {\n            type = \"object\",\n            properties = {\n                client_email = { type = \"string\" },\n                private_key = { type = \"string\" },\n                project_id = { type = \"string\" },\n                token_uri = {\n                    type = \"string\",\n                    default = \"https://oauth2.googleapis.com/token\"\n                },\n                scope = {\n                    type = \"array\",\n                    items = {\n                        type = \"string\"\n                    },\n                    default = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    }\n                },\n                entries_uri = {\n                    type = \"string\",\n                    default = \"https://secretmanager.googleapis.com/v1\"\n                },\n            },\n            required = { \"client_email\", \"private_key\", \"project_id\" }\n        },\n        ssl_verify = {\n            type = \"boolean\",\n            default = true\n        },\n        auth_file = { type = \"string\" },\n    },\n    oneOf = {\n        { required = { \"auth_config\" } },\n        { required = { \"auth_file\" } },\n    },\n}\n\nlocal _M = {\n    schema = schema\n}\n\nlocal function fetch_oauth_conf(conf)\n    if conf.auth_config then\n        return conf.auth_config\n    end\n\n    local file_content, err = core.io.get_file(conf.auth_file)\n    if not file_content then\n        return nil, \"failed to read configuration, file: \" .. conf.auth_file .. \", err: \" .. err\n    end\n\n    local config_tab, err = core.json.decode(file_content)\n    if not config_tab then\n        return nil, \"config parse failure, data: \" .. file_content .. \", err: \" .. err\n    end\n\n    local config = {\n        auth_config = {\n            client_email = config_tab.client_email,\n            private_key = config_tab.private_key,\n            project_id = config_tab.project_id\n        }\n    }\n\n    local ok, err = core.schema.check(schema, config)\n    if not ok then\n        return nil, \"config parse failure, file: \" .. conf.auth_file .. \", err: \" .. err\n    end\n\n    return config_tab\nend\n\n\nlocal function get_secret(oauth, secrets_id)\n    local httpc = http.new()\n\n    local access_token = oauth:generate_access_token()\n    if not access_token then\n        return nil, \"failed to get google oauth token\"\n    end\n\n    local entries_uri = oauth.entries_uri .. \"/projects/\" .. oauth.project_id\n                            .. \"/secrets/\" .. secrets_id .. \"/versions/latest:access\"\n\n    local res, err = httpc:request_uri(entries_uri, {\n        ssl_verify = oauth.ssl_verify,\n        method = \"GET\",\n        headers = {\n            [\"Content-Type\"] = \"application/json\",\n            [\"Authorization\"] = (oauth.access_token_type or \"Bearer\") .. \" \" .. access_token,\n        },\n    })\n\n    if not res then\n        return nil, err\n    end\n\n    if res.status ~= 200 then\n        return nil, res.body\n    end\n\n    local body, err = core.json.decode(res.body)\n    if not body then\n        return nil, \"failed to parse response data, \" .. err\n    end\n\n    local payload = body.payload\n    if not payload then\n        return nil, \"invalid payload\"\n    end\n\n    return decode_base64(payload.data)\nend\n\n\nlocal function make_request_to_gcp(conf, secrets_id)\n    local auth_config, err = fetch_oauth_conf(conf)\n    if not auth_config then\n        return nil, err\n    end\n\n    local lru_key =  auth_config.client_email .. \"#\" .. auth_config.project_id\n\n    local oauth, err = lrucache(lru_key, \"gcp\", google_oauth.new, auth_config, conf.ssl_verify)\n    if not oauth then\n        return nil, \"failed to create oauth object, \" .. err\n    end\n\n    local secret, err = get_secret(oauth, secrets_id)\n    if not secret then\n        return nil, err\n    end\n\n    return secret\nend\n\n\nfunction _M.get(conf, key)\n    core.log.info(\"fetching data from gcp for key: \", key)\n\n    local idx = str_find(key, '/')\n\n    local main_key = idx and str_sub(key, 1, idx - 1) or key\n    if main_key == \"\" then\n        return nil, \"can't find main key, key: \" .. key\n    end\n\n    local sub_key = idx and str_sub(key, idx + 1)\n\n    core.log.info(\"main: \", main_key, sub_key and \", sub: \" .. sub_key or \"\")\n\n    local res, err = make_request_to_gcp(conf, main_key)\n    if not res then\n        return nil, \"failed to retrtive data from gcp secret manager: \" .. err\n    end\n\n    if not sub_key then\n        return res\n    end\n\n    local data, err = core.json.decode(res)\n    if not data then\n        return nil, \"failed to decode result, err: \" .. err\n    end\n\n    return data[sub_key]\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/secret/vault.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Vault Tools.\n--  Vault is an identity-based secrets and encryption management system.\n\nlocal core       = require(\"apisix.core\")\nlocal http       = require(\"resty.http\")\n\nlocal norm_path = require(\"pl.path\").normpath\n\nlocal sub        = core.string.sub\nlocal rfind_char = core.string.rfind_char\nlocal env        = core.env\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        uri = core.schema.uri_def,\n        prefix = {\n            type = \"string\",\n        },\n        token = {\n            type = \"string\",\n        },\n        namespace = {\n            type = \"string\",\n        },\n    },\n    required = {\"uri\", \"prefix\", \"token\"},\n}\n\nlocal _M = {\n    schema = schema\n}\n\nlocal function make_request_to_vault(conf, method, key, data)\n    local httpc = http.new()\n    -- config timeout or default to 5000 ms\n    httpc:set_timeout((conf.timeout or 5)*1000)\n\n    local req_addr = conf.uri .. norm_path(\"/v1/\"\n                .. conf.prefix .. \"/\" .. key)\n\n    local token, _ = env.fetch_by_uri(conf.token)\n    if not token then\n        token = conf.token\n    end\n\n    local headers = {\n        [\"X-Vault-Token\"] = token\n    }\n    if conf.namespace then\n        -- The namespace rule is referenced in\n        -- https://developer.hashicorp.com/vault/docs/enterprise/namespaces#vault-api-and-namespaces\n        headers[\"X-Vault-Namespace\"] = conf.namespace\n    end\n\n    local res, err = httpc:request_uri(req_addr, {\n        method = method,\n        headers = headers,\n        body = core.json.encode(data or {}, true)\n    })\n\n    if not res then\n        return nil, err\n    end\n\n    return res.body\nend\n\n-- key is the vault kv engine path\nlocal function get(conf, key)\n    core.log.info(\"fetching data from vault for key: \", key)\n\n    local idx = rfind_char(key, '/')\n    if not idx then\n        return nil, \"error key format, key: \" .. key\n    end\n\n    local main_key = sub(key, 1, idx - 1)\n    if main_key == \"\" then\n        return nil, \"can't find main key, key: \" .. key\n    end\n    local sub_key = sub(key, idx + 1)\n    if sub_key == \"\" then\n        return nil, \"can't find sub key, key: \" .. key\n    end\n\n    core.log.info(\"main: \", main_key, \" sub: \", sub_key)\n\n    local res, err = make_request_to_vault(conf, \"GET\", main_key)\n    if not res then\n        return nil, \"failed to retrtive data from vault kv engine: \" .. err\n    end\n\n    local ret = core.json.decode(res)\n    if not ret or not ret.data then\n        return nil, \"failed to decode result, res: \" .. res\n    end\n\n    return ret.data[sub_key]\nend\n\n_M.get = get\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/secret.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal require   = require\nlocal core      = require(\"apisix.core\")\nlocal string    = require(\"apisix.core.string\")\nlocal tracer    = require(\"apisix.tracer\")\n\nlocal local_conf = require(\"apisix.core.config_local\").local_conf()\n\nlocal find      = string.find\nlocal sub       = string.sub\nlocal upper     = string.upper\nlocal byte      = string.byte\nlocal type      = type\nlocal pcall     = pcall\nlocal pairs     = pairs\nlocal ngx       = ngx\n\nlocal _M = {}\n\n\nlocal PREFIX = \"$secret://\"\nlocal secrets\n\nlocal function check_secret(conf)\n    local idx = find(conf.id or \"\", \"/\")\n    if not idx then\n        return false, \"no secret id\"\n    end\n    local manager = sub(conf.id, 1, idx - 1)\n\n    local ok, secret_manager = pcall(require, \"apisix.secret.\" .. manager)\n    if not ok then\n        return false, \"secret manager not exits, manager: \" .. manager\n    end\n\n    return core.schema.check(secret_manager.schema, conf)\nend\n\n\nlocal function secret_kv(manager, confid)\n    local secret_values\n    secret_values = core.config.fetch_created_obj(\"/secrets\")\n    if not secret_values or not secret_values.values then\n       return nil\n    end\n\n    local secret = secret_values:get(manager .. \"/\" .. confid)\n    if not secret then\n        return nil\n    end\n\n    return secret.value\nend\n\n\nfunction _M.secrets()\n    if not secrets then\n        return nil, nil\n    end\n\n    return secrets.values, secrets.conf_version\nend\n\n\nfunction _M.init_worker()\n    local cfg = {\n        automatic = true,\n        checker = check_secret,\n    }\n\n    secrets = core.config.new(\"/secrets\", cfg)\nend\n\n\nlocal function check_secret_uri(secret_uri)\n    -- Avoid the error caused by has_prefix to cause a crash.\n    if type(secret_uri) ~= \"string\" then\n        return false, \"error secret_uri type: \" .. type(secret_uri)\n    end\n\n    if not string.has_prefix(secret_uri, PREFIX) and\n        not string.has_prefix(upper(secret_uri), core.env.PREFIX) then\n        return false, \"error secret_uri prefix: \" .. secret_uri\n    end\n\n    return true\nend\n\n_M.check_secret_uri = check_secret_uri\n\n\nlocal function parse_secret_uri(secret_uri)\n    local is_secret_uri, err = check_secret_uri(secret_uri)\n    if not is_secret_uri then\n        return is_secret_uri, err\n    end\n\n    local path = sub(secret_uri, #PREFIX + 1)\n    local idx1 = find(path, \"/\")\n    if not idx1 then\n        return nil, \"error format: no secret manager\"\n    end\n    local manager = sub(path, 1, idx1 - 1)\n\n    local idx2 = find(path, \"/\", idx1 + 1)\n    if not idx2 then\n        return nil, \"error format: no secret conf id\"\n    end\n    local confid = sub(path, idx1 + 1, idx2 - 1)\n\n    local key = sub(path, idx2 + 1)\n    if key == \"\" then\n        return nil, \"error format: no secret key id\"\n    end\n\n    local opts = {\n        manager = manager,\n        confid = confid,\n        key = key\n    }\n    return opts\nend\n\n\nlocal function fetch_by_uri_secret(secret_uri)\n    core.log.info(\"fetching data from secret uri: \", secret_uri)\n    local opts, err = parse_secret_uri(secret_uri)\n    if not opts then\n        return nil, err\n    end\n\n    local conf = secret_kv(opts.manager, opts.confid)\n    if not conf then\n        return nil, \"no secret conf, secret_uri: \" .. secret_uri\n    end\n\n    local ok, sm = pcall(require, \"apisix.secret.\" .. opts.manager)\n    if not ok then\n        return nil, \"no secret manager: \" .. opts.manager\n    end\n\n    local span = tracer.start(ngx.ctx, \"fetch_secret\", tracer.kind.client)\n    local value, err = sm.get(conf, opts.key)\n    if err then\n        span:set_status(tracer.status.ERROR, err)\n        span:finish(ngx.ctx)\n        return nil, err\n    end\n\n    span:finish(ngx.ctx)\n    return value\nend\n\n-- for test\n_M.fetch_by_uri = fetch_by_uri_secret\n\n\nlocal function new_lrucache()\n    local ttl = core.table.try_read_attr(local_conf, \"apisix\", \"lru\", \"secret\", \"ttl\")\n    if not ttl then\n        ttl = 300\n    end\n\n    local count = core.table.try_read_attr(local_conf, \"apisix\", \"lru\", \"secret\", \"count\")\n    if not count then\n        count = 512\n    end\n\n    local neg_ttl = core.table.try_read_attr(local_conf, \"apisix\", \"lru\", \"secret\", \"neg_ttl\")\n    if not neg_ttl then\n        neg_ttl = 60  -- 1 minute default for failures\n    end\n\n    local neg_count = core.table.try_read_attr(local_conf, \"apisix\", \"lru\", \"secret\", \"neg_count\")\n    if not neg_count then\n        neg_count = 512\n    end\n\n    core.log.info(\"secret lrucache ttl: \", ttl, \", count: \", count,\n                  \", neg_ttl: \", neg_ttl, \", neg_count: \", neg_count)\n\n    return core.lrucache.new({\n        ttl = ttl,\n        count = count,\n        neg_ttl = neg_ttl,\n        neg_count = neg_count,\n        invalid_stale = true,\n        refresh_stale = true\n    })\nend\n\nlocal secrets_cache = new_lrucache()\n\n\n\nlocal function fetch(uri, use_cache)\n    -- do a quick filter to improve retrieval speed\n    if byte(uri, 1, 1) ~= byte('$') then\n        return nil\n    end\n\n    local fetch_by_uri\n    if string.has_prefix(upper(uri), core.env.PREFIX) then\n        fetch_by_uri = core.env.fetch_by_uri\n    elseif string.has_prefix(uri, PREFIX) then\n        fetch_by_uri = fetch_by_uri_secret\n    else\n        return nil\n    end\n\n    if not use_cache then\n        local val, err = fetch_by_uri(uri)\n        if err then\n            core.log.error(\"failed to fetch secret value: \", err)\n            return nil\n        end\n        return val\n    end\n\n    return secrets_cache(uri, \"\", fetch_by_uri, uri)\nend\n\nlocal function retrieve_refs(refs, use_cache)\n    for k, v in pairs(refs) do\n        local typ = type(v)\n        if typ == \"string\" then\n            refs[k] = fetch(v, use_cache) or v\n        elseif typ == \"table\" then\n            retrieve_refs(v, use_cache)\n        end\n    end\n    return refs\nend\n\nfunction _M.fetch_secrets(refs, use_cache)\n    if not refs or type(refs) ~= \"table\" then\n        return nil\n    end\n\n    local new_refs = core.table.deepcopy(refs)\n    return retrieve_refs(new_refs, use_cache)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/ssl/router/radixtree_sni.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal get_request      = require(\"resty.core.base\").get_request\nlocal router_new       = require(\"apisix.utils.router\").new\nlocal core             = require(\"apisix.core\")\nlocal apisix_ssl       = require(\"apisix.ssl\")\nlocal secret           = require(\"apisix.secret\")\nlocal ngx_ssl          = require(\"ngx.ssl\")\nlocal config_util      = require(\"apisix.core.config_util\")\nlocal tracer           = require(\"apisix.tracer\")\nlocal ngx              = ngx\nlocal ipairs           = ipairs\nlocal type             = type\nlocal error            = error\nlocal str_find         = core.string.find\nlocal str_gsub         = string.gsub\nlocal str_lower        = string.lower\nlocal tostring         = tostring\nlocal ssl_certificates\nlocal radixtree_router\nlocal radixtree_router_ver\n\n\nlocal _M = {\n    version = 0.1,\n    server_name = ngx_ssl.server_name,\n}\n\n\nlocal function create_router(ssl_items)\n    local ssl_items = ssl_items or {}\n\n    local route_items = core.table.new(#ssl_items, 0)\n    local idx = 0\n\n    for _, ssl in config_util.iterate_values(ssl_items) do\n        if ssl.value ~= nil and ssl.value.type == \"server\" and\n            (ssl.value.status == nil or ssl.value.status == 1) then  -- compatible with old version\n\n            local j = 0\n            local sni\n            if type(ssl.value.snis) == \"table\" and #ssl.value.snis > 0 then\n                sni = core.table.new(0, #ssl.value.snis)\n                for _, s in ipairs(ssl.value.snis) do\n                    j = j + 1\n                    sni[j] = s:reverse()\n                end\n            else\n                sni = ssl.value.sni:reverse()\n            end\n\n            idx = idx + 1\n            route_items[idx] = {\n                paths = sni,\n                handler = function (api_ctx)\n                    if not api_ctx then\n                        return\n                    end\n                    api_ctx.matched_ssl = ssl\n                    api_ctx.matched_sni = sni\n                end\n            }\n        end\n    end\n\n    core.log.info(\"route items: \", core.json.delay_encode(route_items, true))\n    -- for testing\n    if idx > 1 then\n        core.log.info(\"we have more than 1 ssl certs now\")\n    end\n    local router, err = router_new(route_items)\n    if not router then\n        return nil, err\n    end\n\n    return router\nend\n\n\nlocal function set_pem_ssl_key(sni, cert, pkey)\n    local r = get_request()\n    if r == nil then\n        return false, \"no request found\"\n    end\n\n    local parsed_cert, err = apisix_ssl.fetch_cert(sni, cert)\n    if not parsed_cert then\n        return false, \"failed to parse PEM cert: \" .. err\n    end\n\n    local ok, err = ngx_ssl.set_cert(parsed_cert)\n    if not ok then\n        return false, \"failed to set PEM cert: \" .. err\n    end\n\n    local parsed_pkey, err = apisix_ssl.fetch_pkey(sni, pkey)\n    if not parsed_pkey then\n        return false, \"failed to parse PEM priv key: \" .. err\n    end\n\n    ok, err = ngx_ssl.set_priv_key(parsed_pkey)\n    if not ok then\n        return false, \"failed to set PEM priv key: \" .. err\n    end\n\n    return true\nend\n_M.set_pem_ssl_key = set_pem_ssl_key\n\n\n-- export the set cert/key process so we can hook it in the other plugins\nfunction _M.set_cert_and_key(sni, value)\n    local ok, err = set_pem_ssl_key(sni, value.cert, value.key)\n    if not ok then\n        return false, err\n    end\n\n    -- multiple certificates support.\n    if value.certs then\n        for i = 1, #value.certs do\n            local cert = value.certs[i]\n            local key = value.keys[i]\n\n            ok, err = set_pem_ssl_key(sni, cert, key)\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.match_and_set(api_ctx, match_only, alt_sni)\n    local err\n    if not radixtree_router or\n       radixtree_router_ver ~= ssl_certificates.conf_version then\n        radixtree_router, err = create_router(ssl_certificates.values)\n        if not radixtree_router then\n            return false, \"failed to create radixtree router: \" .. err\n        end\n        radixtree_router_ver = ssl_certificates.conf_version\n    end\n\n    local sni = alt_sni\n    if not sni then\n        sni, err = apisix_ssl.server_name()\n        if type(sni) ~= \"string\" then\n            local advise = \"please check if the client requests via IP or uses an outdated \" ..\n                           \"protocol. If you need to report an issue, \" ..\n                           \"provide a packet capture file of the TLS handshake.\"\n            return false, \"failed to find SNI: \" .. (err or advise)\n        end\n    end\n\n    core.log.debug(\"sni: \", sni)\n\n    local span = tracer.start(api_ctx.ngx_ctx, \"sni_radixtree_match\", tracer.kind.internal)\n    local sni_rev = sni:reverse()\n    local ok = radixtree_router:dispatch(sni_rev, nil, api_ctx)\n    if not ok then\n        if not alt_sni then\n            -- it is expected that alternative SNI doesn't have a SSL certificate associated\n            -- with it sometimes\n            core.log.error(\"failed to find any SSL certificate by SNI: \", sni)\n        end\n        span:set_status(tracer.status.ERROR, \"failed match SNI\")\n        span:finish(api_ctx.ngx_ctx)\n        return false\n    end\n    span:finish(api_ctx.ngx_ctx)\n\n    if api_ctx.matched_sni == \"*\" then\n        -- wildcard matches everything, no need for further validation\n        core.log.info(\"matched wildcard SSL for SNI: \", sni)\n    elseif type(api_ctx.matched_sni) == \"table\" then\n        local matched = false\n        for _, msni in ipairs(api_ctx.matched_sni) do\n            if sni_rev == msni or not str_find(sni_rev, \".\", #msni) then\n                matched = true\n                break\n            end\n        end\n        if not matched then\n            local log_snis = core.json.encode(api_ctx.matched_sni, true)\n            if log_snis ~= nil then\n                log_snis = str_gsub(log_snis:reverse(), \"%[\", \"%]\")\n                log_snis = str_gsub(log_snis, \"%]\", \"%[\", 1)\n            end\n            core.log.warn(\"failed to find any SSL certificate by SNI: \",\n                          sni, \" matched SNIs: \", log_snis)\n            return false\n        end\n    else\n        if str_find(sni_rev, \".\", #api_ctx.matched_sni) then\n            core.log.warn(\"failed to find any SSL certificate by SNI: \",\n                          sni, \" matched SNI: \", api_ctx.matched_sni:reverse())\n            return false\n        end\n    end\n\n    core.log.info(\"debug - matched: \", core.json.delay_encode(api_ctx.matched_ssl, true))\n\n    if match_only then\n        return true\n    end\n\n    ok, err = _M.set(api_ctx.matched_ssl, sni)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nfunction _M.set(matched_ssl, sni)\n    if not matched_ssl then\n        return false, \"failed to match ssl certificate\"\n    end\n    local ok, err\n    if not sni then\n        sni, err = apisix_ssl.server_name()\n        if type(sni) ~= \"string\" then\n            local advise = \"please check if the client requests via IP or uses an outdated \" ..\n                           \"protocol. If you need to report an issue, \" ..\n                           \"provide a packet capture file of the TLS handshake.\"\n            return false, \"failed to find SNI: \" .. (err or advise)\n        end\n    end\n    ngx_ssl.clear_certs()\n\n    local new_ssl_value = secret.fetch_secrets(matched_ssl.value, true)\n                            or matched_ssl.value\n\n    ok, err = _M.set_cert_and_key(sni, new_ssl_value)\n    if not ok then\n        return false, err\n    end\n\n    if matched_ssl.value.client then\n        local ca_cert = matched_ssl.value.client.ca\n        local depth = matched_ssl.value.client.depth\n        if apisix_ssl.support_client_verification() then\n            local parsed_cert, err = apisix_ssl.fetch_cert(sni, ca_cert)\n            if not parsed_cert then\n                return false, \"failed to parse client cert: \" .. err\n            end\n\n            local reject_in_handshake =\n                (ngx.config.subsystem == \"stream\") or\n                (matched_ssl.value.client.skip_mtls_uri_regex == nil)\n            -- TODO: support passing `trusted_certs` (3rd arg, keep it nil for now)\n            local ok, err = ngx_ssl.verify_client(parsed_cert, depth, nil,\n                reject_in_handshake)\n            if not ok then\n                return false, err\n            end\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.ssls()\n    if not ssl_certificates then\n        return nil, nil\n    end\n\n    return ssl_certificates.values, ssl_certificates.conf_version\nend\n\n\nlocal function ssl_filter(ssl)\n    if not ssl.value then\n        return\n    end\n\n    if ssl.value.sni then\n        ssl.value.sni = ngx.re.sub(ssl.value.sni, \"\\\\.$\", \"\", \"jo\")\n        ssl.value.sni = str_lower(ssl.value.sni)\n    elseif ssl.value.snis then\n        for i, v in ipairs(ssl.value.snis) do\n            v = ngx.re.sub(v, \"\\\\.$\", \"\", \"jo\")\n            ssl.value.snis[i] = str_lower(v)\n        end\n    end\nend\n\n\nfunction _M.init_worker()\n    local err\n    ssl_certificates, err = core.config.new(\"/ssls\", {\n        automatic = true,\n        item_schema = core.schema.ssl,\n        checker = function (item, schema_type)\n            return apisix_ssl.check_ssl_conf(true, item)\n        end,\n        filter = ssl_filter,\n    })\n    if not ssl_certificates then\n        error(\"failed to create etcd instance for fetching ssl certificates: \"\n              .. err)\n    end\nend\n\n\nfunction _M.get_by_id(ssl_id)\n    local ssl\n    local ssls = core.config.fetch_created_obj(\"/ssls\")\n    if ssls then\n        ssl = ssls:get(tostring(ssl_id))\n    end\n\n    if not ssl then\n        return nil\n    end\n\n    return ssl.value\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/ssl.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core           = require(\"apisix.core\")\nlocal secret         = require(\"apisix.secret\")\nlocal ngx_ssl        = require(\"ngx.ssl\")\nlocal ngx_ssl_client = require(\"ngx.ssl.clienthello\")\nlocal ffi            = require(\"ffi\")\n\nlocal C = ffi.C\nlocal ngx_encode_base64 = ngx.encode_base64\nlocal ngx_decode_base64 = ngx.decode_base64\nlocal aes = require(\"resty.aes\")\nlocal str_lower = string.lower\nlocal str_byte = string.byte\nlocal assert = assert\nlocal type = type\nlocal ipairs = ipairs\nlocal ngx_sub = ngx.re.sub\n\nffi.cdef[[\nunsigned long ERR_peek_error(void);\nvoid ERR_clear_error(void);\n]]\n\nlocal cert_cache = core.lrucache.new {\n    ttl = 3600, count = 1024,\n}\n\nlocal pkey_cache = core.lrucache.new {\n    ttl = 3600, count = 1024,\n}\n\n\nlocal _M = {}\n\n\nfunction _M.server_name(clienthello)\n    local sni, err\n    if clienthello then\n        sni, err = ngx_ssl_client.get_client_hello_server_name()\n    else\n        sni, err = ngx_ssl.server_name()\n    end\n    if err then\n        return nil, err\n    end\n\n    if not sni then\n        local local_conf = core.config.local_conf()\n        sni = core.table.try_read_attr(local_conf, \"apisix\", \"ssl\", \"fallback_sni\")\n        if not sni then\n            return nil\n        end\n    end\n\n    sni = ngx_sub(sni, \"\\\\.$\", \"\", \"jo\")\n    sni = str_lower(sni)\n    return sni\nend\n\n\nfunction _M.session_hostname()\n    return ngx_ssl.session_hostname()\nend\n\n\nfunction _M.set_protocols_by_clienthello(ssl_protocols)\n    if ssl_protocols then\n       return ngx_ssl_client.set_protocols(ssl_protocols)\n    end\n    return true\nend\n\n\nlocal function init_iv_tbl(ivs)\n    local _aes_128_cbc_with_iv_tbl = core.table.new(2, 0)\n    local type_ivs = type(ivs)\n\n    if type_ivs == \"table\" then\n        for _, iv in ipairs(ivs) do\n            local aes_with_iv = assert(aes:new(iv, nil, aes.cipher(128, \"cbc\"), {iv = iv}))\n            core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)\n        end\n    elseif type_ivs == \"string\" then\n        local aes_with_iv = assert(aes:new(ivs, nil, aes.cipher(128, \"cbc\"), {iv = ivs}))\n        core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)\n    end\n\n    return _aes_128_cbc_with_iv_tbl\nend\n\n\nlocal _aes_128_cbc_with_iv_tbl_gde\nlocal function get_aes_128_cbc_with_iv_gde(local_conf)\n    if _aes_128_cbc_with_iv_tbl_gde == nil then\n        local ivs = core.table.try_read_attr(local_conf, \"apisix\", \"data_encryption\", \"keyring\")\n        _aes_128_cbc_with_iv_tbl_gde = init_iv_tbl(ivs)\n    end\n\n    return _aes_128_cbc_with_iv_tbl_gde\nend\n\n\n\nlocal function encrypt(aes_128_cbc_with_iv, origin)\n    local encrypted = aes_128_cbc_with_iv:encrypt(origin)\n    if encrypted == nil then\n        core.log.error(\"failed to encrypt key[\", origin, \"] \")\n        return origin\n    end\n\n    return ngx_encode_base64(encrypted)\nend\n\nfunction _M.aes_encrypt_pkey(origin, field)\n    local local_conf = core.config.local_conf()\n    local aes_128_cbc_with_iv_tbl_gde = get_aes_128_cbc_with_iv_gde(local_conf)\n    local aes_128_cbc_with_iv_gde = aes_128_cbc_with_iv_tbl_gde[1]\n\n    if not field then\n        if aes_128_cbc_with_iv_gde ~= nil and core.string.has_prefix(origin, \"---\") then\n            return encrypt(aes_128_cbc_with_iv_gde, origin)\n        end\n    else\n        if field == \"data_encrypt\" then\n            if aes_128_cbc_with_iv_gde ~= nil then\n                return encrypt(aes_128_cbc_with_iv_gde, origin)\n            end\n        end\n    end\n    return origin\nend\n\n\nlocal function aes_decrypt_pkey(origin, field)\n    if not field and core.string.has_prefix(origin, \"---\") then\n        return origin\n    end\n\n    local local_conf = core.config.local_conf()\n    local aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv_gde(local_conf)\n    if #aes_128_cbc_with_iv_tbl == 0 then\n        return origin\n    end\n\n    local decoded_key = ngx_decode_base64(origin)\n    if not decoded_key then\n        core.log.error(\"base64 decode ssl key failed. key[\", origin, \"] \")\n        return nil\n    end\n\n    for _, aes_128_cbc_with_iv in ipairs(aes_128_cbc_with_iv_tbl) do\n        local decrypted = aes_128_cbc_with_iv:decrypt(decoded_key)\n        if decrypted then\n            return decrypted\n        end\n\n        if C.ERR_peek_error() then\n            -- clean up the error queue of OpenSSL to prevent\n            -- normal requests from being interfered with.\n            C.ERR_clear_error()\n        end\n    end\n\n    return nil, \"decrypt ssl key failed\"\nend\n_M.aes_decrypt_pkey = aes_decrypt_pkey\n\n\nlocal function validate(cert, key)\n    local parsed_cert, err = ngx_ssl.parse_pem_cert(cert)\n    if not parsed_cert then\n        return nil, \"failed to parse cert: \" .. err\n    end\n\n    if key == nil then\n        -- sometimes we only need to validate the cert\n        return true\n    end\n\n    local err\n    key, err = aes_decrypt_pkey(key)\n    if not key then\n        core.log.error(err)\n        return nil, \"failed to decrypt previous encrypted key\"\n    end\n\n    local parsed_key, err = ngx_ssl.parse_pem_priv_key(key)\n    if not parsed_key then\n        return nil, \"failed to parse key: \" .. err\n    end\n\n    -- TODO: check if key & cert match\n    return true\nend\n_M.validate = validate\n\n\nlocal function parse_pem_cert(sni, cert)\n    core.log.debug(\"parsing cert for sni: \", sni)\n\n    local parsed, err = ngx_ssl.parse_pem_cert(cert)\n    return parsed, err\nend\n\n\nfunction _M.fetch_cert(sni, cert)\n    local parsed_cert, err = cert_cache(cert, nil, parse_pem_cert, sni, cert)\n    if not parsed_cert then\n        return false, err\n    end\n\n    return parsed_cert\nend\n\n\nlocal function parse_pem_priv_key(sni, pkey)\n    core.log.debug(\"parsing priv key for sni: \", sni)\n\n    local key, err = aes_decrypt_pkey(pkey)\n    if not key then\n        core.log.error(err)\n        return nil, err\n    end\n    local parsed, err = ngx_ssl.parse_pem_priv_key(key)\n    return parsed, err\nend\n\n\nfunction _M.fetch_pkey(sni, pkey)\n    local parsed_pkey, err = pkey_cache(pkey, nil, parse_pem_priv_key, sni, pkey)\n    if not parsed_pkey then\n        return false, err\n    end\n\n    return parsed_pkey\nend\n\n\nlocal function support_client_verification()\n    return ngx_ssl.verify_client ~= nil\nend\n_M.support_client_verification = support_client_verification\n\n\nfunction _M.check_ssl_conf(in_dp, conf)\n    if not in_dp then\n        local ok, err = core.schema.check(core.schema.ssl, conf)\n        if not ok then\n            return nil, \"invalid configuration: \" .. err\n        end\n    end\n\n    if not secret.check_secret_uri(conf.cert) and\n        not secret.check_secret_uri(conf.key) then\n\n        local ok, err = validate(conf.cert, conf.key)\n        if not ok then\n            return nil, err\n        end\n    end\n\n    if conf.type == \"client\" then\n        return true\n    end\n\n    local numcerts = conf.certs and #conf.certs or 0\n    local numkeys = conf.keys and #conf.keys or 0\n    if numcerts ~= numkeys then\n        return nil, \"mismatched number of certs and keys\"\n    end\n\n    for i = 1, numcerts do\n        if not secret.check_secret_uri(conf.certs[i]) and\n            not secret.check_secret_uri(conf.keys[i]) then\n\n            local ok, err = validate(conf.certs[i], conf.keys[i])\n            if not ok then\n                return nil, \"failed to handle cert-key pair[\" .. i .. \"]: \" .. err\n            end\n        end\n    end\n\n    if conf.client then\n        if not support_client_verification() then\n            return nil, \"client tls verify unsupported\"\n        end\n\n        local ok, err = validate(conf.client.ca, nil)\n        if not ok then\n            return nil, \"failed to validate client_cert: \" .. err\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.get_status_request_ext()\n    core.log.debug(\"parsing status request extension ... \")\n    local ext = ngx_ssl_client.get_client_hello_ext(5)\n    if not ext then\n        core.log.debug(\"no contains status request extension\")\n        return false\n    end\n    local total_len = #ext\n    -- 1-byte for CertificateStatusType\n    -- 2-byte for zero-length \"responder_id_list\"\n    -- 2-byte for zero-length \"request_extensions\"\n    if total_len < 5 then\n        core.log.error(\"bad ssl client hello extension: \",\n                       \"extension data error\")\n        return false\n    end\n\n    -- CertificateStatusType\n    local status_type = str_byte(ext, 1)\n    if status_type == 1 then\n        core.log.debug(\"parsing status request extension ok: \",\n                       \"status_type is ocsp(1)\")\n        return true\n    end\n\n    return false\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/plugins/ip-restriction.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal base = require(\"apisix.plugins.ip-restriction.init\")\n\n\n-- avoid unexpected data sharing\nlocal ip_restriction = core.table.clone(base)\nip_restriction.preread = base.restrict\n\n\nreturn ip_restriction\n"
  },
  {
    "path": "apisix/stream/plugins/limit-conn.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal limit_conn = require(\"apisix.plugins.limit-conn.init\")\n\n\nlocal plugin_name = \"limit-conn\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        conn = {type = \"integer\", exclusiveMinimum = 0},\n        burst = {type = \"integer\",  minimum = 0},\n        default_conn_delay = {type = \"number\", exclusiveMinimum = 0},\n        only_use_default_delay = {type = \"boolean\", default = false},\n        key = {type = \"string\"},\n        key_type = {type = \"string\",\n            enum = {\"var\", \"var_combination\"},\n            default = \"var\",\n        },\n    },\n    required = {\"conn\", \"burst\", \"default_conn_delay\", \"key\"}\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 1003,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.preread(conf, ctx)\n    return limit_conn.increase(conf, ctx)\nend\n\n\nfunction _M.log(conf, ctx)\n    return limit_conn.decrease(conf, ctx)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/plugins/mqtt-proxy.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core      = require(\"apisix.core\")\nlocal bit       = require(\"bit\")\nlocal ngx       = ngx\nlocal str_byte  = string.byte\nlocal str_sub   = string.sub\n\n\ncore.ctx.register_var(\"mqtt_client_id\", function(ctx)\n    return ctx.mqtt_client_id\nend)\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        protocol_name = {type = \"string\", default = \"MQTT\"},\n        protocol_level = {type = \"integer\"}\n    },\n    required = {\"protocol_level\"},\n}\n\n\nlocal plugin_name = \"mqtt-proxy\"\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 1000,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function decode_variable_byte_int(data, offset)\n    local multiplier = 1\n    local len = 0\n    local pos\n    for i = offset, offset + 3 do\n        pos = i\n        local byte = str_byte(data, i, i)\n        len = len + bit.band(byte, 127) * multiplier\n        multiplier = multiplier * 128\n        if bit.band(byte, 128) == 0 then\n            break\n        end\n    end\n\n    return len, pos\nend\n\n\nlocal function parse_msg_hdr(data)\n    local packet_type_flags_byte = str_byte(data, 1, 1)\n    if packet_type_flags_byte < 16 or packet_type_flags_byte > 32 then\n        return nil, nil,\n            \"Received unexpected MQTT packet type+flags: \" .. packet_type_flags_byte\n    end\n\n    local len, pos = decode_variable_byte_int(data, 2)\n    return len, pos\nend\n\n\nlocal function parse_mqtt(data, parsed_pos)\n    local res = {}\n\n    local protocol_len = str_byte(data, parsed_pos + 1, parsed_pos + 1) * 256\n                         + str_byte(data, parsed_pos + 2, parsed_pos + 2)\n    parsed_pos = parsed_pos + 2\n    res.protocol = str_sub(data, parsed_pos + 1, parsed_pos + protocol_len)\n    parsed_pos = parsed_pos + protocol_len\n\n    res.protocol_ver = str_byte(data, parsed_pos + 1, parsed_pos + 1)\n    parsed_pos = parsed_pos + 1\n\n    -- skip control flags & keepalive\n    parsed_pos = parsed_pos + 3\n\n    if res.protocol_ver == 5 then\n        -- skip properties\n        local property_len\n        property_len, parsed_pos = decode_variable_byte_int(data, parsed_pos + 1)\n        parsed_pos = parsed_pos + property_len\n    end\n\n    local client_id_len = str_byte(data, parsed_pos + 1, parsed_pos + 1) * 256\n                          + str_byte(data, parsed_pos + 2, parsed_pos + 2)\n    parsed_pos = parsed_pos + 2\n\n    if parsed_pos + client_id_len > #data then\n        res.expect_len = parsed_pos + client_id_len\n        return res\n    end\n\n    if client_id_len == 0 then\n        -- A Server MAY allow a Client to supply a ClientID that has a length of zero bytes\n        res.client_id = \"\"\n    else\n        res.client_id = str_sub(data, parsed_pos + 1, parsed_pos + client_id_len)\n    end\n\n    parsed_pos = parsed_pos + client_id_len\n\n    res.expect_len = parsed_pos\n    return res\nend\n\n\nfunction _M.preread(conf, ctx)\n    local sock = ngx.req.socket()\n    -- the header format of MQTT CONNECT can be found in\n    -- https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901033\n    local data, err = sock:peek(5)\n    if not data then\n        core.log.error(\"failed to read the msg header: \", err)\n        return 503\n    end\n\n    local remain_len, pos, err = parse_msg_hdr(data)\n    if not remain_len then\n        core.log.error(\"failed to parse the msg header: \", err)\n        return 503\n    end\n\n    local data, err = sock:peek(pos + remain_len)\n    if not data then\n        core.log.error(\"failed to read the Connect Command: \", err)\n        return 503\n    end\n\n    local res = parse_mqtt(data, pos)\n    if res.expect_len > #data then\n        core.log.error(\"failed to parse mqtt request, expect len: \",\n                        res.expect_len, \" but got \", #data)\n        return 503\n    end\n\n    if res.protocol and res.protocol ~= conf.protocol_name then\n        core.log.error(\"expect protocol name: \", conf.protocol_name,\n                       \", but got \", res.protocol)\n        return 503\n    end\n\n    if res.protocol_ver and res.protocol_ver ~= conf.protocol_level then\n        core.log.error(\"expect protocol level: \", conf.protocol_level,\n                       \", but got \", res.protocol_ver)\n        return 503\n    end\n\n    core.log.info(\"mqtt client id: \", res.client_id)\n\n    -- when client id is missing, fallback to balance by client IP\n    if res.client_id ~= \"\" then\n        ctx.mqtt_client_id = res.client_id\n    end\n    return\nend\n\n\nfunction _M.log(conf, ctx)\n    core.log.info(\"plugin log phase, conf: \", core.json.encode(conf))\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/plugins/prometheus.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal exporter = require(\"apisix.plugins.prometheus.exporter\")\n\n\nlocal plugin_name = \"prometheus\"\nlocal schema = {\n    type = \"object\",\n    properties = {\n        prefer_name = {\n            type = \"boolean\",\n            default = false -- stream route doesn't have name yet\n        }\n    },\n}\n\n\nlocal _M = {\n    version = 0.1,\n    priority = 500,\n    name = plugin_name,\n    log  = exporter.stream_log,\n    destroy = exporter.destroy,\n    init = exporter.stream_init,\n    schema = schema,\n    run_policy = \"prefer_route\",\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/plugins/syslog.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\nlocal syslog = require(\"apisix.plugins.syslog.init\")\nlocal plugin_name = \"syslog\"\n\nlocal batch_processor_manager = bp_manager_mod.new(\"stream sys logger\")\nlocal schema = {\n    type = \"object\",\n    properties = {\n        host = {type = \"string\"},\n        port = {type = \"integer\"},\n        flush_limit = {type = \"integer\", minimum = 1, default = 4096},\n        drop_limit = {type = \"integer\", default = 1048576},\n        timeout = {type = \"integer\", minimum = 1, default = 3000},\n        log_format = {type = \"object\"},\n        sock_type = {type = \"string\", default = \"tcp\", enum = {\"tcp\", \"udp\"}},\n        pool_size = {type = \"integer\", minimum = 5, default = 5},\n        tls = {type = \"boolean\", default = false}\n    },\n    required = {\"host\", \"port\"}\n}\n\nlocal schema = batch_processor_manager:wrap_schema(schema)\n\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        log_format = {\n            type = \"object\"\n        }\n    },\n}\n\nlocal _M = {\n    version = 0.1,\n    priority = 401,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n    flush_syslog = syslog.flush_syslog,\n}\n\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.log(conf, ctx)\n    local entry = log_util.get_log_entry(plugin_name, conf, ctx)\n    if not entry then\n        return\n    end\n\n    syslog.push_entry(conf, ctx, entry)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/plugins/traffic-split.lua",
    "content": "-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal traffic_split = require(\"apisix.plugins.traffic-split\")\nlocal log          = require(\"apisix.core.log\")\n\n\nlocal plugin_name = \"traffic-split\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 966,\n    name = plugin_name,\n    schema = traffic_split.schema,\n}\n_M.check_schema = traffic_split.check_schema\n\nfunction _M.preread(conf, ctx)\n    local status, err = traffic_split.access(conf, ctx)\n    if err then\n        log.error(err)\n    end\n    return status\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/router/ip_port.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core      = require(\"apisix.core\")\nlocal core_ip  = require(\"apisix.core.ip\")\nlocal config_util = require(\"apisix.core.config_util\")\nlocal stream_plugin_checker = require(\"apisix.plugin\").stream_plugin_checker\nlocal router_new = require(\"apisix.utils.router\").new\nlocal apisix_ssl = require(\"apisix.ssl\")\nlocal xrpc = require(\"apisix.stream.xrpc\")\nlocal error     = error\nlocal tonumber  = tonumber\nlocal ipairs = ipairs\n\nlocal user_routes\nlocal router_ver\nlocal tls_router\nlocal other_routes = {}\nlocal _M = {version = 0.1}\n\n\n\nlocal function match_addrs(route, vars)\n    -- todo: use resty-ipmatcher to support multiple ip address\n    if route.value.remote_addr then\n        local ok, _ = route.value.remote_addr_matcher:match(vars.remote_addr)\n        if not ok then\n            return false\n        end\n    end\n\n    if route.value.server_addr then\n        local ok, _ = route.value.server_addr_matcher:match(vars.server_addr)\n        if not ok then\n            return false\n        end\n    end\n\n    -- todo: use resty-ipmatcher to support multiple ip address\n    if route.value.server_port and\n       route.value.server_port ~= tonumber(vars.server_port) then\n        return false\n    end\n\n    return true\nend\n\n\nlocal create_router\ndo\n    local sni_to_items = {}\n    local tls_routes = {}\n\n    function create_router(items)\n        local tls_routes_idx = 1\n        local other_routes_idx = 1\n        core.table.clear(tls_routes)\n        core.table.clear(other_routes)\n        core.table.clear(sni_to_items)\n\n        for _, item in config_util.iterate_values(items) do\n            if item.value == nil then\n                goto CONTINUE\n            end\n\n            local route = item.value\n            if route.protocol and route.protocol.superior_id then\n                -- subordinate route won't be matched in the entry\n                -- TODO: check the subordinate relationship in the Admin API\n                goto CONTINUE\n            end\n\n            if item.value.remote_addr then\n                item.value.remote_addr_matcher = core_ip.create_ip_matcher({item.value.remote_addr})\n            end\n            if item.value.server_addr then\n                item.value.server_addr_matcher = core_ip.create_ip_matcher({item.value.server_addr})\n            end\n            if not route.sni then\n                other_routes[other_routes_idx] = item\n                other_routes_idx = other_routes_idx + 1\n                goto CONTINUE\n            end\n\n            local sni_rev = route.sni:reverse()\n            local stored = sni_to_items[sni_rev]\n            if stored then\n                core.table.insert(stored, item)\n                goto CONTINUE\n            end\n\n            sni_to_items[sni_rev] = {item}\n            tls_routes[tls_routes_idx] = {\n                paths = sni_rev,\n                filter_fun = function (vars, opts, ctx)\n                    local items = sni_to_items[sni_rev]\n                    for _, route in ipairs(items) do\n                        local hit = match_addrs(route, vars)\n                        if hit then\n                            ctx.matched_route = route\n                            return true\n                        end\n                    end\n                    return false\n                end,\n                handler = function (ctx, sni_rev)\n                    -- done in the filter_fun\n                end\n            }\n            tls_routes_idx = tls_routes_idx + 1\n\n            ::CONTINUE::\n        end\n\n        if #tls_routes > 0 then\n            local router, err = router_new(tls_routes)\n            if not router then\n                return err\n            end\n\n            tls_router = router\n        end\n\n        return nil\n    end\nend\n\n\ndo\n    local match_opts = {}\n\n    function _M.match(api_ctx)\n        if router_ver ~= user_routes.conf_version then\n            local err = create_router(user_routes.values)\n            if err then\n                return false, \"failed to create router: \" .. err\n            end\n\n            router_ver = user_routes.conf_version\n        end\n\n        local sni = apisix_ssl.server_name()\n        if sni and tls_router then\n            local sni_rev = sni:reverse()\n\n            core.table.clear(match_opts)\n            match_opts.vars = api_ctx.var\n\n            local _, err = tls_router:dispatch(sni_rev, match_opts, api_ctx)\n            if err then\n                return false, \"failed to match TLS router: \" .. err\n            end\n        end\n\n        if api_ctx.matched_route then\n            -- unlike the matcher for the SSL, it is fine to let\n            -- '*.x.com' to match 'a.b.x.com' as we don't care about\n            -- the certificate\n            return true\n        end\n\n        for _, route in ipairs(other_routes) do\n            local hit = match_addrs(route, api_ctx.var)\n            if hit then\n                api_ctx.matched_route = route\n                return true\n            end\n        end\n\n        core.log.info(\"not hit any route\")\n        return true\n    end\nend\n\n\nfunction _M.routes()\n    if not user_routes then\n        return nil, nil\n    end\n\n    return user_routes.values, user_routes.conf_version\nend\n\nlocal function stream_route_checker(item, in_cp)\n    if item.plugins then\n        local ok, message = stream_plugin_checker(item, in_cp)\n        if not ok then\n            return false, message\n        end\n    end\n    -- validate the address format when remote_address or server_address is not nil\n    if item.remote_addr then\n        if not core_ip.validate_cidr_or_ip(item.remote_addr) then\n            return false, \"invalid remote_addr: \" .. item.remote_addr\n        end\n    end\n    if item.server_addr then\n        if not core_ip.validate_cidr_or_ip(item.server_addr) then\n            return false, \"invalid server_addr: \" .. item.server_addr\n        end\n    end\n\n    if item.protocol then\n        local prot_conf = item.protocol\n        if prot_conf then\n            local ok, message = xrpc.check_schema(prot_conf, false)\n            if not ok then\n                return false, message\n            end\n        end\n    end\n\n    return true\nend\n_M.stream_route_checker = stream_route_checker\n\n\nfunction _M.stream_init_worker(filter)\n    local err\n    user_routes, err = core.config.new(\"/stream_routes\", {\n            automatic = true,\n            item_schema = core.schema.stream_route,\n            checker = function(item)\n                return stream_route_checker(item)\n            end,\n            filter = filter,\n        })\n\n    if not user_routes then\n        error(\"failed to create etcd instance for fetching /stream_routes : \"\n              .. err)\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/metrics.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal pairs = pairs\nlocal pcall = pcall\n\n\nlocal _M = {}\nlocal hubs = {}\n\n\nfunction _M.store(prometheus, name)\n    local ok, m = pcall(require, \"apisix.stream.xrpc.protocols.\" .. name .. \".metrics\")\n    if not ok then\n        core.log.notice(\"no metric for protocol \", name)\n        return\n    end\n\n    local hub = {}\n    for metric, conf in pairs(m) do\n        core.log.notice(\"register metric \", metric, \" for protocol \", name)\n        hub[metric] = prometheus[conf.type](prometheus, name .. '_' .. metric,\n                                            conf.help, conf.labels, conf.buckets)\n    end\n\n    hubs[name] = hub\nend\n\n\nfunction _M.load(name)\n    return hubs[name]\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/protocols/dubbo/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal sdk = require(\"apisix.stream.xrpc.sdk\")\nlocal xrpc_socket = require(\"resty.apisix.stream.xrpc.socket\")\nlocal math_random = math.random\nlocal ngx = ngx\nlocal OK = ngx.OK\nlocal str_format = string.format\nlocal DECLINED = ngx.DECLINED\nlocal DONE = ngx.DONE\nlocal bit = require(\"bit\")\nlocal ffi = require(\"ffi\")\nlocal ffi_str = ffi.string\n\n\n-- dubbo protocol spec: https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/tcp/\nlocal header_len = 16\nlocal _M = {}\n\n\nfunction _M.init_downstream(session)\n    session.req_id_seq = 0\n    session.resp_id_seq = 0\n    session.cmd_labels = { session.route.id, \"\" }\n    return xrpc_socket.downstream.socket()\nend\n\n\nlocal function parse_dubbo_header(header)\n    for i = 1, header_len do\n        local currentByte = header:byte(i)\n        if not currentByte then\n            return nil\n        end\n    end\n\n    local magic_number = str_format(\"%04x\", header:byte(1) * 256 + header:byte(2))\n    local message_flag = header:byte(3)\n    local status = header:byte(4)\n    local request_id = 0\n    for i = 5, 12 do\n        request_id = request_id * 256 + header:byte(i)\n    end\n\n    local byte13Val = header:byte(13) * 256 * 256 * 256\n    local byte14Val = header:byte(14) * 256 * 256\n    local data_length = byte13Val + byte14Val + header:byte(15) * 256 + header:byte(16)\n\n    local is_request = bit.band(bit.rshift(message_flag, 7), 0x01) == 1 and 1 or 0\n    local is_two_way = bit.band(bit.rshift(message_flag, 6), 0x01) == 1 and 1 or 0\n    local is_event = bit.band(bit.rshift(message_flag, 5), 0x01) == 1 and 1 or 0\n\n    return {\n        magic_number = magic_number,\n        message_flag = message_flag,\n        is_request = is_request,\n        is_two_way = is_two_way,\n        is_event = is_event,\n        status = status,\n        request_id = request_id,\n        data_length = data_length\n    }\nend\n\n\nlocal function read_data(sk, is_req)\n    local header_data, err = sk:read(header_len)\n    if not header_data then\n        return nil, err, false\n    end\n\n    local header_str = ffi_str(header_data, header_len)\n    local header_info = parse_dubbo_header(header_str)\n    if not header_info then\n        return nil, \"header insufficient\", false\n    end\n\n    local is_valid_magic_number = header_info.magic_number == \"dabb\"\n    if not is_valid_magic_number then\n        return nil, str_format(\"unknown magic number: \\\"%s\\\"\", header_info.magic_number), false\n    end\n\n    local body_data, err = sk:read(header_info.data_length)\n    if not body_data then\n        core.log.error(\"failed to read dubbo request body\")\n        return nil, err, false\n    end\n\n    local ctx = ngx.ctx\n    ctx.dubbo_serialization_id = bit.band(header_info.message_flag, 0x1F)\n\n    if is_req then\n        ctx.dubbo_req_body_data = body_data\n    else\n        ctx.dubbo_rsp_body_data = body_data\n    end\n\n    return true, nil, false\nend\n\n\nlocal function read_req(sk)\n    return read_data(sk, true)\nend\n\n\nlocal function read_reply(sk)\n    return read_data(sk, false)\nend\n\n\nlocal function handle_reply(session, sk)\n    local ok, err = read_reply(sk)\n    if not ok then\n        return nil, err\n    end\n\n    local ctx = sdk.get_req_ctx(session, 10)\n\n    return ctx\nend\n\n\nfunction _M.from_downstream(session, downstream)\n    local read_pipeline = false\n    session.req_id_seq = session.req_id_seq + 1\n    local ctx = sdk.get_req_ctx(session, session.req_id_seq)\n    session._downstream_ctx = ctx\n    while true do\n        local ok, err, pipelined = read_req(downstream)\n        if not ok then\n            if err ~= \"timeout\" and err ~= \"closed\" then\n                core.log.error(\"failed to read request: \", err)\n            end\n\n            if read_pipeline and err == \"timeout\" then\n                break\n            end\n\n            return DECLINED\n        end\n\n        if not pipelined then\n            break\n        end\n\n        if not read_pipeline then\n            read_pipeline = true\n            -- set minimal read timeout to read pipelined data\n            downstream:settimeouts(0, 0, 1)\n        end\n    end\n\n    if read_pipeline then\n        -- set timeout back\n        downstream:settimeouts(0, 0, 0)\n    end\n\n    return OK, ctx\nend\n\n\nfunction _M.connect_upstream(session, ctx)\n    local conf = session.upstream_conf\n    local nodes = conf.nodes\n    if #nodes == 0 then\n        core.log.error(\"failed to connect: no nodes\")\n        return DECLINED\n    end\n\n    local node = nodes[math_random(#nodes)]\n    local sk = sdk.connect_upstream(node, conf)\n    if not sk then\n        return DECLINED\n    end\n\n    core.log.debug(\"dubbo_connect_upstream end\")\n\n    return OK, sk\nend\n\nfunction _M.disconnect_upstream(session, upstream)\n    sdk.disconnect_upstream(upstream, session.upstream_conf)\nend\n\n\nfunction _M.to_upstream(session, ctx, downstream, upstream)\n    local ok, _ = upstream:move(downstream)\n    if not ok then\n        return DECLINED\n    end\n\n    return OK\nend\n\n\nfunction _M.from_upstream(session, downstream, upstream)\n    local ctx,err = handle_reply(session, upstream)\n    if err then\n        return DECLINED\n    end\n\n    local ok, _ = downstream:move(upstream)\n    if not ok then\n        return DECLINED\n    end\n\n    return DONE, ctx\nend\n\n\nfunction _M.log(_, _)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/protocols/dubbo/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\n\nlocal schema = {\n    type = \"object\",\n}\n\nlocal _M = {}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/protocols/redis/commands.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal ipairs = ipairs\nlocal pairs = pairs\n\n\nlocal cmd_to_key_finder = {}\n--[[\n-- the data is generated from the script below\nlocal redis = require \"resty.redis\"\nlocal red = redis:new()\n\nlocal ok, err = red:connect(\"127.0.0.1\", 6379)\nif not ok then\n    ngx.say(\"failed to connect: \", err)\n    return\nend\n\nlocal res = red:command(\"info\")\nlocal map = {}\nfor _, r in ipairs(res) do\n    local first_key = r[4]\n    local last_key = r[5]\n    local step = r[6]\n    local idx = first_key .. ':' .. last_key .. ':' .. step\n\n    if idx ~= \"1:1:1\" then\n        -- \"1:1:1\" is the default\n        if map[idx] then\n            table.insert(map[idx], r[1])\n        else\n            map[idx] = {r[1]}\n        end\n    end\nend\nfor _, r in pairs(map) do\n    table.sort(r)\nend\nlocal dump = require('pl.pretty').dump; dump(map)\n--]]\nlocal key_to_cmd = {\n    [\"0:0:0\"] = {\n        \"acl\",\n        \"asking\",\n        \"auth\",\n        \"bgrewriteaof\",\n        \"bgsave\",\n        \"blmpop\",\n        \"bzmpop\",\n        \"client\",\n        \"cluster\",\n        \"command\",\n        \"config\",\n        \"dbsize\",\n        \"debug\",\n        \"discard\",\n        \"echo\",\n        \"eval\",\n        \"eval_ro\",\n        \"evalsha\",\n        \"evalsha_ro\",\n        \"exec\",\n        \"failover\",\n        \"fcall\",\n        \"fcall_ro\",\n        \"flushall\",\n        \"flushdb\",\n        \"function\",\n        \"hello\",\n        \"info\",\n        \"keys\",\n        \"lastsave\",\n        \"latency\",\n        \"lmpop\",\n        \"lolwut\",\n        \"memory\",\n        \"module\",\n        \"monitor\",\n        \"multi\",\n        \"object\",\n        \"pfselftest\",\n        \"ping\",\n        \"psubscribe\",\n        \"psync\",\n        \"publish\",\n        \"pubsub\",\n        \"punsubscribe\",\n        \"quit\",\n        \"randomkey\",\n        \"readonly\",\n        \"readwrite\",\n        \"replconf\",\n        \"replicaof\",\n        \"reset\",\n        \"role\",\n        \"save\",\n        \"scan\",\n        \"script\",\n        \"select\",\n        \"shutdown\",\n        \"sintercard\",\n        \"slaveof\",\n        \"slowlog\",\n        \"subscribe\",\n        \"swapdb\",\n        \"sync\",\n        \"time\",\n        \"unsubscribe\",\n        \"unwatch\",\n        \"wait\",\n        \"xgroup\",\n        \"xinfo\",\n        \"xread\",\n        \"xreadgroup\",\n        \"zdiff\",\n        \"zinter\",\n        \"zintercard\",\n        \"zmpop\",\n        \"zunion\"\n    },\n    [\"1:-1:1\"] = {\n        \"del\",\n        \"exists\",\n        \"mget\",\n        \"pfcount\",\n        \"pfmerge\",\n        \"sdiff\",\n        \"sdiffstore\",\n        \"sinter\",\n        \"sinterstore\",\n        \"ssubscribe\",\n        \"sunion\",\n        \"sunionstore\",\n        \"sunsubscribe\",\n        \"touch\",\n        \"unlink\",\n        \"watch\"\n    },\n    [\"1:-1:2\"] = {\n        \"mset\",\n        \"msetnx\"\n    },\n    [\"1:-2:1\"] = {\n        \"blpop\",\n        \"brpop\",\n        \"bzpopmax\",\n        \"bzpopmin\"\n    },\n    [\"1:2:1\"] = {\n        \"blmove\",\n        \"brpoplpush\",\n        \"copy\",\n        \"geosearchstore\",\n        \"lcs\",\n        \"lmove\",\n        \"rename\",\n        \"renamenx\",\n        \"rpoplpush\",\n        \"smove\",\n        \"zrangestore\"\n    },\n    [\"2:-1:1\"] = {\n        \"bitop\"\n    },\n    [\"2:2:1\"] = {\n        \"pfdebug\"\n    },\n    [\"3:3:1\"] = {\n        \"migrate\"\n    }\n}\nlocal key_finders = {\n    [\"0:0:0\"] = false,\n    [\"1:-1:1\"] = function (idx, narg)\n        return 1 < idx\n    end,\n    [\"1:-1:2\"] = function (idx, narg)\n        return 1 < idx and idx % 2 == 0\n    end,\n    [\"1:-2:1\"] = function (idx, narg)\n        return 1 < idx and idx < narg - 1\n    end,\n    [\"1:2:1\"] = function (idx, narg)\n        return idx == 2 or idx == 3\n    end,\n    [\"2:-1:1\"] = function (idx, narg)\n        return 2 < idx\n    end,\n    [\"2:2:1\"] = function (idx, narg)\n        return idx == 3\n    end,\n    [\"3:3:1\"] = function (idx, narg)\n        return idx == 4\n    end\n}\nfor k, cmds in pairs(key_to_cmd) do\n    for _, cmd in ipairs(cmds) do\n        cmd_to_key_finder[cmd] = key_finders[k]\n    end\nend\n\n\nreturn {\n    cmd_to_key_finder = cmd_to_key_finder,\n    default_key_finder = function (idx, narg)\n        return idx == 2\n    end,\n}\n"
  },
  {
    "path": "apisix/stream/xrpc/protocols/redis/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal sdk = require(\"apisix.stream.xrpc.sdk\")\nlocal commands = require(\"apisix.stream.xrpc.protocols.redis.commands\")\nlocal xrpc_socket = require(\"resty.apisix.stream.xrpc.socket\")\nlocal ffi = require(\"ffi\")\nlocal ffi_str = ffi.string\nlocal math_random = math.random\nlocal OK = ngx.OK\nlocal DECLINED = ngx.DECLINED\nlocal DONE = ngx.DONE\nlocal sleep = ngx.sleep\nlocal str_byte = string.byte\nlocal str_fmt = string.format\nlocal ipairs = ipairs\nlocal tonumber = tonumber\n\n\n-- this variable is only used to log the redis command line in log_format\n-- and is not used for filter in the logger phase.\ncore.ctx.register_var(\"redis_cmd_line\", function(ctx)\n    return core.table.concat(ctx.cmd_line, \" \")\nend)\n\n-- redis protocol spec: https://redis.io/docs/reference/protocol-spec/\n-- There is no plan to support inline command format\nlocal protocol_name = \"redis\"\nlocal _M = {}\nlocal MAX_LINE_LEN = 128\nlocal MAX_VALUE_LEN = 128\nlocal PREFIX_ARR = str_byte(\"*\")\nlocal PREFIX_STR = str_byte(\"$\")\nlocal PREFIX_STA = str_byte(\"+\")\nlocal PREFIX_INT = str_byte(\":\")\nlocal PREFIX_ERR = str_byte(\"-\")\n\n\nlocal lrucache = core.lrucache.new({\n    type = \"plugin\",\n})\n\n\nlocal function create_matcher(conf)\n    local matcher = {}\n    --[[\n        {\"delay\": 5, \"key\":\"x\", \"commands\":[\"GET\", \"MGET\"]}\n        {\"delay\": 5, \"commands\":[\"GET\"]}\n        => {\n            get = {keys = {x = {delay = 5}, * = {delay = 5}}}\n            mget = {keys = {x = {delay = 5}}}\n        }\n    ]]--\n    for _, rule in ipairs(conf.faults) do\n        for _, cmd in ipairs(rule.commands) do\n            cmd = cmd:lower()\n            local key = rule.key\n            local kf = commands.cmd_to_key_finder[cmd]\n            local key_matcher = matcher[cmd]\n            if not key_matcher then\n                key_matcher = {\n                    keys = {}\n                }\n                matcher[cmd] = key_matcher\n            end\n\n            if not key or kf == false then\n                key = \"*\"\n            end\n\n            if key_matcher.keys[key] then\n                core.log.warn(\"override existent fault rule of cmd: \", cmd, \", key: \", key)\n            end\n\n            key_matcher.keys[key] = rule\n        end\n    end\n\n    return matcher\nend\n\n\nlocal function get_matcher(conf, ctx)\n    return core.lrucache.plugin_ctx(lrucache, ctx, nil, create_matcher, conf)\nend\n\n\nfunction _M.init_downstream(session)\n    local conf = session.route.protocol.conf\n    if conf and conf.faults then\n        local matcher = get_matcher(conf, session.conn_ctx)\n        session.matcher = matcher\n    end\n\n    session.req_id_seq = 0\n    session.resp_id_seq = 0\n    session.cmd_labels = {session.route.id, \"\"}\n    return xrpc_socket.downstream.socket()\nend\n\n\nlocal function read_line(sk)\n    local p, err, len = sk:read_line(MAX_LINE_LEN)\n    if not p then\n        return nil, err\n    end\n\n    if len < 2 then\n        return nil, \"line too short\"\n    end\n\n    return p, nil, len\nend\n\n\nlocal function read_len(sk)\n    local p, err, len = read_line(sk)\n    if not p then\n        return nil, err\n    end\n\n    local s = ffi_str(p + 1, len - 1)\n    local n = tonumber(s)\n    if not n then\n        return nil, str_fmt(\"invalid len string: \\\"%s\\\"\", s)\n    end\n    return n\nend\n\n\nlocal function read_req(session, sk)\n    local narg, err = read_len(sk)\n    if not narg then\n        return nil, err\n    end\n\n    local cmd_line = core.tablepool.fetch(\"xrpc_redis_cmd_line\", narg, 0)\n\n    local n, err = read_len(sk)\n    if not n then\n        return nil, err\n    end\n\n    local p, err = sk:read(n + 2)\n    if not p then\n        return nil, err\n    end\n\n    local s = ffi_str(p, n)\n    local cmd = s:lower()\n    cmd_line[1] = cmd\n\n    if cmd == \"subscribe\" or cmd == \"psubscribe\" then\n        session.in_pub_sub = true\n    end\n\n    local key_finder\n    local matcher = session.matcher\n    if matcher then\n        matcher = matcher[s:lower()]\n        if matcher then\n            key_finder = commands.cmd_to_key_finder[s] or commands.default_key_finder\n        end\n    end\n\n    for i = 2, narg do\n        local is_key = false\n        if key_finder then\n            is_key = key_finder(i, narg)\n        end\n\n        local n, err = read_len(sk)\n        if not n then\n            return nil, err\n        end\n\n        local s\n        if not is_key and n > MAX_VALUE_LEN then\n            -- avoid recording big value\n            local p, err = sk:read(MAX_VALUE_LEN)\n            if not p then\n                return nil, err\n            end\n\n            local ok, err = sk:drain(n - MAX_VALUE_LEN + 2)\n            if not ok then\n                return nil, err\n            end\n\n            s = ffi_str(p, MAX_VALUE_LEN) .. \"...(\" .. n .. \" bytes)\"\n        else\n            local p, err = sk:read(n + 2)\n            if not p then\n                return nil, err\n            end\n\n            s = ffi_str(p, n)\n\n            if is_key and matcher.keys[s] then\n                matcher = matcher.keys[s]\n                key_finder = nil\n            end\n        end\n\n        cmd_line[i] = s\n    end\n\n    session.req_id_seq = session.req_id_seq + 1\n    local ctx = sdk.get_req_ctx(session, session.req_id_seq)\n    ctx.cmd_line = cmd_line\n    ctx.cmd = cmd\n\n    local pipelined = sk:has_pending_data()\n\n    if matcher then\n        if matcher.keys then\n            -- try to match any key of this command\n            matcher = matcher.keys[\"*\"]\n        end\n\n        if matcher then\n            sleep(matcher.delay)\n        end\n    end\n\n    return true, nil, pipelined\nend\n\n\nlocal function read_subscribe_reply(sk)\n    local line, err, n = read_line(sk)\n    if not line then\n        return nil, err\n    end\n\n    local prefix = line[0]\n\n    if prefix == PREFIX_STR then    -- char '$'\n        local size = tonumber(ffi_str(line + 1, n - 1))\n        if size < 0 then\n            return true\n        end\n\n        local p, err = sk:read(size + 2)\n        if not p then\n            return nil, err\n        end\n\n        return ffi_str(p, size)\n\n    elseif prefix == PREFIX_INT then    -- char ':'\n        return tonumber(ffi_str(line + 1, n - 1))\n\n    else\n        return nil, str_fmt(\"unknown prefix: \\\"%s\\\"\", prefix)\n    end\nend\n\n\nlocal function read_reply(sk, session)\n    local line, err, n = read_line(sk)\n    if not line then\n        return nil, err\n    end\n\n    local prefix = line[0]\n\n    if prefix == PREFIX_STR then    -- char '$'\n        -- print(\"bulk reply\")\n\n        local size = tonumber(ffi_str(line + 1, n - 1))\n        if size < 0 then\n            return true\n        end\n\n        local ok, err = sk:drain(size + 2)\n        if not ok then\n            return nil, err\n        end\n\n        return true\n\n    elseif prefix == PREFIX_STA then    -- char '+'\n        -- print(\"status reply\")\n        return true\n\n    elseif prefix == PREFIX_ARR then -- char '*'\n        local narr = tonumber(ffi_str(line + 1, n - 1))\n\n        -- print(\"multi-bulk reply: \", narr)\n        if narr < 0 then\n            return true\n        end\n\n        if session and session.in_pub_sub and (narr == 3 or narr == 4) then\n            local msg_type, err = read_subscribe_reply(sk)\n            if msg_type == nil then\n                return nil, err\n            end\n\n            session.pub_sub_msg_type = msg_type\n\n            local res, err = read_reply(sk)\n            if res == nil then\n                return nil, err\n            end\n\n            if msg_type == \"unsubscribe\" or msg_type == \"punsubscribe\" then\n                local n_ch, err = read_subscribe_reply(sk)\n                if n_ch == nil then\n                    return nil, err\n                end\n\n                if n_ch == 0 then\n                    session.in_pub_sub = -1\n                    -- clear this flag later at the end of `handle_reply`\n                end\n\n            else\n                local n = msg_type == \"pmessage\" and 2 or 1\n                for i = 1, n do\n                    local res, err = read_reply(sk)\n                    if res == nil then\n                        return nil, err\n                    end\n                end\n            end\n\n        else\n            for i = 1, narr do\n                local res, err = read_reply(sk)\n                if res == nil then\n                    return nil, err\n                end\n            end\n        end\n\n        return true\n\n    elseif prefix == PREFIX_INT then    -- char ':'\n        -- print(\"integer reply\")\n        return true\n\n    elseif prefix == PREFIX_ERR then    -- char '-'\n        -- print(\"error reply: \", n)\n        return true\n\n    else\n        return nil, str_fmt(\"unknown prefix: \\\"%s\\\"\", prefix)\n    end\nend\n\n\nlocal function handle_reply(session, sk)\n    local ok, err = read_reply(sk, session)\n    if not ok then\n        return nil, err\n    end\n\n    local ctx\n    if session.in_pub_sub and session.pub_sub_msg_type then\n        local msg_type = session.pub_sub_msg_type\n        session.pub_sub_msg_type = nil\n        if session.resp_id_seq < session.req_id_seq then\n            local cur_ctx = sdk.get_req_ctx(session, session.resp_id_seq + 1)\n            local cmd = cur_ctx.cmd\n            if cmd == msg_type then\n                ctx = cur_ctx\n                session.resp_id_seq = session.resp_id_seq + 1\n            end\n        end\n\n        if session.in_pub_sub == -1 then\n            session.in_pub_sub = nil\n        end\n    else\n        session.resp_id_seq = session.resp_id_seq + 1\n        ctx = sdk.get_req_ctx(session, session.resp_id_seq)\n    end\n\n    return ctx\nend\n\n\nfunction _M.from_downstream(session, downstream)\n    local read_pipeline = false\n    while true do\n        local ok, err, pipelined = read_req(session, downstream)\n        if not ok then\n            if err ~= \"timeout\" and err ~= \"closed\" then\n                core.log.error(\"failed to read request: \", err)\n            end\n\n            if read_pipeline and err == \"timeout\" then\n                break\n            end\n\n            return DECLINED\n        end\n\n        if not pipelined then\n            break\n        end\n\n        if not read_pipeline then\n            read_pipeline = true\n            -- set minimal read timeout to read pipelined data\n            downstream:settimeouts(0, 0, 1)\n        end\n    end\n\n    if read_pipeline then\n        -- set timeout back\n        downstream:settimeouts(0, 0, 0)\n    end\n\n    return OK\nend\n\n\nfunction _M.connect_upstream(session, ctx)\n    local conf = session.upstream_conf\n    local nodes = conf.nodes\n    if #nodes == 0 then\n        core.log.error(\"failed to connect: no nodes\")\n        return DECLINED\n    end\n\n    local node = nodes[math_random(#nodes)]\n    local sk = sdk.connect_upstream(node, conf)\n    if not sk then\n        return DECLINED\n    end\n\n    return OK, sk\nend\n\n\nfunction _M.disconnect_upstream(session, upstream)\n    sdk.disconnect_upstream(upstream, session.upstream_conf)\nend\n\n\nfunction _M.to_upstream(session, ctx, downstream, upstream)\n    local ok, err = upstream:move(downstream)\n    if not ok then\n        core.log.error(\"failed to send to upstream: \", err)\n        return DECLINED\n    end\n\n    return OK\nend\n\n\nfunction _M.from_upstream(session, downstream, upstream)\n    local ctx, err = handle_reply(session, upstream)\n    if err then\n        core.log.error(\"failed to handle upstream: \", err)\n        return DECLINED\n    end\n\n    local ok, err = downstream:move(upstream)\n    if not ok then\n        core.log.error(\"failed to handle upstream: \", err)\n        return DECLINED\n    end\n\n    return DONE, ctx\nend\n\n\nfunction _M.log(session, ctx)\n    local metrics = sdk.get_metrics(session, protocol_name)\n    if metrics then\n        session.cmd_labels[2] = ctx.cmd\n        metrics.commands_total:inc(1, session.cmd_labels)\n        metrics.commands_latency_seconds:observe(ctx.var.rpc_time, session.cmd_labels)\n    end\n\n    core.tablepool.release(\"xrpc_redis_cmd_line\", ctx.cmd_line)\n    ctx.cmd_line = nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/protocols/redis/metrics.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal _M = {\n    commands_total = {\n        type = \"counter\",\n        help = \"Total number of requests for a specific Redis command\",\n        labels = {\"route\", \"command\"},\n    },\n    commands_latency_seconds = {\n        type = \"histogram\",\n        help = \"Latency of requests for a specific Redis command\",\n        labels = {\"route\", \"command\"},\n        -- latency buckets, 1ms to 1s:\n        buckets = {0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1}\n    },\n}\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/protocols/redis/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        faults = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"object\",\n                properties = {\n                    commands = {\n                        type = \"array\",\n                        minItems = 1,\n                        items = {\n                            type = \"string\"\n                        },\n                    },\n                    key = {\n                        type = \"string\",\n                        minLength = 1,\n                    },\n                    delay = {\n                        type = \"number\",\n                        description = \"additional delay in seconds\",\n                    }\n                },\n                required = {\"commands\", \"delay\"}\n            },\n        },\n    },\n}\n\nlocal _M = {}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/runner.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal expr = require(\"resty.expr.v1\")\nlocal pairs = pairs\nlocal ngx = ngx\nlocal ngx_now = ngx.now\nlocal OK = ngx.OK\nlocal DECLINED = ngx.DECLINED\nlocal DONE = ngx.DONE\nlocal pcall = pcall\nlocal ipairs = ipairs\nlocal tostring = tostring\n\n\ncore.ctx.register_var(\"rpc_time\", function(ctx)\n    return ctx._rpc_end_time - ctx._rpc_start_time\nend)\n\nlocal logger_expr_cache = core.lrucache.new({\n    ttl = 300, count = 1024\n})\n\nlocal _M = {}\n\n\nlocal function open_session(conn_ctx)\n    conn_ctx.xrpc_session = {\n        conn_ctx = conn_ctx,\n        route = conn_ctx.matched_route.value,\n        -- fields start with '_' should not be accessed by the protocol implementation\n        _upstream_conf = conn_ctx.matched_upstream,\n        _ctxs = {},\n    }\n    return conn_ctx.xrpc_session\nend\n\n\nlocal function put_req_ctx(session, ctx)\n    local id = ctx._id\n    session._ctxs[id] = nil\n\n    core.ctx.release_vars(ctx)\n\n    core.tablepool.release(\"xrpc_ctxs\", ctx)\nend\n\n\nlocal function filter_logger(ctx, logger)\n    if not logger then\n       return false\n    end\n\n    if not logger.filter or #logger.filter == 0 then\n        -- no valid filter, default execution plugin\n        return true\n    end\n\n    local version = tostring(logger.filter)\n    local filter_expr, err = logger_expr_cache(ctx.conf_id, version, expr.new, logger.filter)\n    if not filter_expr or err then\n        core.log.error(\"failed to validate the 'filter' expression: \", err)\n        return false\n    end\n    return filter_expr:eval(ctx.var)\nend\n\n\nlocal function run_log_plugin(ctx, logger)\n    local pkg_name = \"apisix.stream.plugins.\" .. logger.name\n    local ok, plugin = pcall(require, pkg_name)\n    if not ok then\n        core.log.error(\"failed to load plugin [\", logger.name, \"] err: \", plugin)\n        return\n    end\n\n    local log_func = plugin.log\n    if log_func then\n        log_func(logger.conf, ctx)\n    end\nend\n\n\nlocal function finialize_req(protocol, session, ctx)\n    ctx._rpc_end_time = ngx_now()\n    local loggers = session.route.protocol.logger\n    if loggers and #loggers > 0 then\n        for _, logger in ipairs(loggers) do\n            ctx.conf_id = tostring(logger.conf)\n            local matched = filter_logger(ctx, logger)\n            core.log.info(\"log filter: \", logger.name, \" filter result: \", matched)\n            if matched then\n                run_log_plugin(ctx, logger)\n            end\n        end\n    end\n\n    protocol.log(session, ctx)\n    put_req_ctx(session, ctx)\nend\n\n\nlocal function close_session(session, protocol)\n    local upstream_ctx = session._upstream_ctx\n    if upstream_ctx then\n        upstream_ctx.closed = true\n\n        local up = upstream_ctx.upstream\n        protocol.disconnect_upstream(session, up)\n    end\n\n    local upstream_ctxs = session._upstream_ctxs\n    if upstream_ctxs then\n        for _, upstream_ctx in pairs(upstream_ctxs) do\n            upstream_ctx.closed = true\n\n            local up = upstream_ctx.upstream\n            protocol.disconnect_upstream(session, up)\n        end\n    end\n\n    for id, ctx in pairs(session._ctxs) do\n        core.log.notice(\"RPC is not finished, id: \", id)\n        ctx.unfinished = true\n        finialize_req(protocol, session, ctx)\n    end\nend\n\n\nlocal function open_upstream(protocol, session, ctx)\n    local key = session._upstream_key\n    session._upstream_key = nil\n\n    if key then\n        if not session._upstream_ctxs then\n            session._upstream_ctxs = {}\n        end\n\n        local up_ctx = session._upstream_ctxs[key]\n        if up_ctx then\n            return OK, up_ctx\n        end\n    else\n        if session._upstream_ctx then\n            return OK, session._upstream_ctx\n        end\n\n        session.upstream_conf = session._upstream_conf\n    end\n\n    local state, upstream = protocol.connect_upstream(session, session)\n    if state ~= OK then\n        return state, nil\n    end\n\n    local up_ctx = {\n        upstream = upstream,\n        closed = false,\n    }\n    if key then\n        session._upstream_ctxs[key] = up_ctx\n    else\n        session._upstream_ctx = up_ctx\n    end\n\n    return OK, up_ctx\nend\n\n\nlocal function start_upstream_coroutine(session, protocol, downstream, up_ctx)\n    local upstream = up_ctx.upstream\n    while not up_ctx.closed do\n        local status, ctx = protocol.from_upstream(session, downstream, upstream)\n        if status ~= OK then\n            if ctx ~= nil then\n                finialize_req(protocol, session, ctx)\n            end\n\n            if status == DECLINED then\n                -- fail to read\n                break\n            end\n\n            if status == DONE then\n                -- a rpc is finished\n                goto continue\n            end\n        end\n\n        ::continue::\n    end\nend\n\n\nfunction _M.run(protocol, conn_ctx)\n    local session = open_session(conn_ctx)\n    local downstream = protocol.init_downstream(session)\n\n    while true do\n        local status, ctx = protocol.from_downstream(session, downstream)\n        if status ~= OK then\n            if ctx ~= nil then\n                finialize_req(protocol, session, ctx)\n            end\n\n            if status == DECLINED then\n                -- fail to read or can't be authorized\n                break\n            end\n\n            if status == DONE then\n                -- heartbeat or fault injection, already reply to downstream\n                goto continue\n            end\n        end\n\n        -- need to do some auth/routing jobs before reaching upstream\n        local status, up_ctx = open_upstream(protocol, session, ctx)\n        if status ~= OK then\n            if ctx ~= nil then\n                finialize_req(protocol, session, ctx)\n            end\n\n            break\n        end\n\n        status = protocol.to_upstream(session, ctx, downstream, up_ctx.upstream)\n        if status ~= OK then\n            if ctx ~= nil then\n                finialize_req(protocol, session, ctx)\n            end\n\n            if status == DECLINED then\n                break\n            end\n\n            if status == DONE then\n                -- for Unary request we can directly reply here\n                goto continue\n            end\n        end\n\n        if not up_ctx.coroutine then\n            local co, err = ngx.thread.spawn(\n                start_upstream_coroutine, session, protocol, downstream, up_ctx)\n            if not co then\n                core.log.error(\"failed to start upstream coroutine: \", err)\n                break\n            end\n\n            up_ctx.coroutine = co\n        end\n\n        ::continue::\n    end\n\n    close_session(session, protocol)\n\n    -- return non-zero code to terminal the session\n    return 200\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc/sdk.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--- Upstream helper functions which can be used in xRPC\n--\n-- @module xrpc.sdk\nlocal core = require(\"apisix.core\")\nlocal config_util = require(\"apisix.core.config_util\")\nlocal router = require(\"apisix.stream.router.ip_port\")\nlocal metrics = require(\"apisix.stream.xrpc.metrics\")\nlocal apisix_upstream = require(\"apisix.upstream\")\nlocal xrpc_socket = require(\"resty.apisix.stream.xrpc.socket\")\nlocal ngx_now = ngx.now\nlocal str_fmt = string.format\nlocal tab_insert = table.insert\nlocal error = error\nlocal tostring = tostring\n\n\nlocal _M = {}\n\n\n---\n-- Returns the connected xRPC upstream socket according to the configuration\n--\n-- @function xrpc.sdk.connect_upstream\n-- @tparam table node selected upstream node\n-- @tparam table up_conf upstream configuration\n-- @treturn table|nil the xRPC upstream socket, or nil if failed\nfunction _M.connect_upstream(node, up_conf)\n    local sk = xrpc_socket.upstream.socket()\n\n    local timeout = up_conf.timeout\n    if not timeout then\n        -- use the default timeout of Nginx proxy\n        sk:settimeouts(60 * 1000, 600 * 1000, 600 * 1000)\n    else\n        -- the timeout unit for balancer is second while the unit for cosocket is millisecond\n        sk:settimeouts(timeout.connect * 1000, timeout.send * 1000, timeout.read * 1000)\n    end\n\n    local ok, err = sk:connect(node.host, node.port)\n    if not ok then\n        core.log.error(\"failed to connect: \", err)\n        return nil\n    end\n\n    if up_conf.scheme == \"tls\" then\n        -- TODO: support mTLS\n        local ok, err = sk:sslhandshake(nil, node.host)\n        if not ok then\n            core.log.error(\"failed to handshake: \", err)\n            return nil\n        end\n    end\n\n    return sk\nend\n\n\n---\n-- Disconnect xRPC upstream socket according to the configuration\n--\n-- @function xrpc.sdk.disconnect_upstream\n-- @tparam table upstream xRPC upstream socket\n-- @tparam table up_conf upstream configuration\nfunction _M.disconnect_upstream(upstream, up_conf)\n    return upstream:close()\nend\n\n\n---\n-- Returns the request level ctx with an id\n--\n-- @function xrpc.sdk.get_req_ctx\n-- @tparam table session xrpc session\n-- @tparam string id ctx id\n-- @treturn table the request level ctx\nfunction _M.get_req_ctx(session, id)\n    if not id then\n        error(\"id is required\")\n    end\n\n    local ctx = session._ctxs[id]\n    if ctx then\n        return ctx\n    end\n\n    local ctx = core.tablepool.fetch(\"xrpc_ctxs\", 4, 4)\n    -- fields start with '_' should not be accessed by the protocol implementation\n    ctx._id = id\n    core.ctx.set_vars_meta(ctx)\n    ctx.conf_type = \"xrpc-\" .. session.route.protocol.name .. \"-logger\"\n\n    session._ctxs[id] = ctx\n\n    ctx._rpc_start_time = ngx_now()\n    return ctx\nend\n\n\n---\n-- Returns the new router if the stream routes are changed\n--\n-- @function xrpc.sdk.get_router\n-- @tparam table session xrpc session\n-- @tparam string version the current router version, should come from the last call\n-- @treturn boolean whether there is a change\n-- @treturn table the new router under the specific protocol\n-- @treturn string the new router version\nfunction _M.get_router(session, version)\n    local protocol_name = session.route.protocol.name\n    local id = session.route.id\n\n    local items, conf_version = router.routes()\n    if version == conf_version then\n        return false\n    end\n\n    local proto_router = {}\n    for _, item in config_util.iterate_values(items) do\n        if item.value == nil then\n            goto CONTINUE\n        end\n\n        local route = item.value\n        if route.protocol.name ~= protocol_name then\n            goto CONTINUE\n        end\n\n        if tostring(route.protocol.superior_id) ~= id then\n            goto CONTINUE\n        end\n\n        tab_insert(proto_router, route)\n\n        ::CONTINUE::\n    end\n\n    return true, proto_router, conf_version\nend\n\n\n---\n-- Set the session's current upstream according to the route's configuration\n--\n-- @function xrpc.sdk.set_upstream\n-- @tparam table session xrpc session\n-- @tparam table conf the route configuration\n-- @treturn nil|string error message if present\nfunction _M.set_upstream(session, conf)\n    local up\n    if conf.upstream then\n        up = conf.upstream\n    else\n        local id = conf.upstream_id\n        up = apisix_upstream.get_by_id(id)\n        if not up then\n            return str_fmt(\"upstream %s can't be got\", id)\n        end\n    end\n\n    local key = tostring(up)\n    core.log.info(\"set upstream to: \", key, \" conf: \", core.json.delay_encode(up, true))\n\n    session._upstream_key = key\n    session.upstream_conf = up\n    return nil\nend\n\n\n---\n-- Returns the protocol specific metrics object\n--\n-- @function xrpc.sdk.get_metrics\n-- @tparam table session xrpc session\n-- @tparam string protocol_name protocol name\n-- @treturn nil|table the metrics under the specific protocol if available\nfunction _M.get_metrics(session, protocol_name)\n    local metric_conf = session.route.protocol.metric\n    if not (metric_conf and metric_conf.enable) then\n        return nil\n    end\n    return metrics.load(protocol_name)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/stream/xrpc.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal metrics = require(\"apisix.stream.xrpc.metrics\")\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal ngx_exit = ngx.exit\n\n\nlocal is_http = true\nlocal runner\nif ngx.config.subsystem ~= \"http\" then\n    is_http = false\n    runner = require(\"apisix.stream.xrpc.runner\")\nend\n\nlocal _M = {}\nlocal registered_protocols = {}\nlocal registered_protocol_schemas = {}\n\n\n-- only need to load schema module when it is used in Admin API\nlocal function register_protocol(name, is_http)\n    if not is_http then\n        registered_protocols[name] = require(\"apisix.stream.xrpc.protocols.\" .. name)\n    end\n\n    registered_protocol_schemas[name] =\n        require(\"apisix.stream.xrpc.protocols.\" .. name .. \".schema\")\nend\n\n\nfunction _M.init()\n    local local_conf = core.config.local_conf()\n    if not local_conf.xrpc then\n        return\n    end\n\n    local prot_conf = local_conf.xrpc.protocols\n    if not prot_conf then\n        return\n    end\n\n    if is_http and not local_conf.apisix.enable_admin then\n        -- we need to register xRPC protocols in HTTP only when Admin API is enabled\n        return\n    end\n\n    for _, prot in ipairs(prot_conf) do\n        core.log.info(\"register xprc protocol \", prot.name)\n        register_protocol(prot.name, is_http)\n    end\nend\n\n\nfunction _M.init_metrics(collector)\n    local local_conf = core.config.local_conf()\n    if not local_conf.xrpc then\n        return\n    end\n\n    local prot_conf = local_conf.xrpc.protocols\n    if not prot_conf then\n        return\n    end\n\n    for _, prot in ipairs(prot_conf) do\n        metrics.store(collector, prot.name)\n    end\nend\n\n\nfunction _M.init_worker()\n    for name, prot in pairs(registered_protocols) do\n        if not is_http and prot.init_worker then\n            prot.init_worker()\n        end\n    end\nend\n\n\nfunction _M.check_schema(item, skip_disabled_plugin)\n    local name = item.name\n    local protocol = registered_protocol_schemas[name]\n    if not protocol and not skip_disabled_plugin then\n        -- like plugins, ignore unknown plugin if the schema is checked in the DP\n        return false, \"unknown protocol [\" .. name .. \"]\"\n    end\n\n    -- check protocol-specific configuration\n    if not item.conf then\n        return true\n    end\n    return protocol.check_schema(item.conf)\nend\n\n\nfunction _M.run_protocol(conf, ctx)\n    local name = conf.name\n    local protocol = registered_protocols[name]\n    local code = runner.run(protocol, ctx)\n    return ngx_exit(code)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/timers.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal process = require(\"ngx.process\")\nlocal pairs = pairs\nlocal unpack = unpack\nlocal thread_spawn = ngx.thread.spawn\nlocal thread_wait = ngx.thread.wait\n\nlocal check_interval = 1\n\nlocal timers = {}\n\n\nlocal _M = {}\n\n\nlocal function background_timer()\n    if core.table.nkeys(timers) == 0 then\n        return\n    end\n\n    local threads = {}\n    for name, timer in pairs(timers) do\n        core.log.info(\"run timer[\", name, \"]\")\n\n        local th, err = thread_spawn(timer)\n        if not th then\n            core.log.error(\"failed to spawn thread for timer [\", name, \"]: \", err)\n            goto continue\n        end\n\n        core.table.insert(threads, th)\n\n::continue::\n    end\n\n    local ok = thread_wait(unpack(threads))\n    if not ok then\n        core.log.error(\"failed to wait threads\")\n    end\nend\n\n\nlocal function is_privileged()\n    return process.type() == \"privileged agent\"\nend\n\n\nfunction _M.init_worker()\n    local opts = {\n        each_ttl = 0,\n        sleep_succ = 0,\n        check_interval = check_interval,\n    }\n    local timer, err = core.timer.new(\"background\", background_timer, opts)\n    if not timer then\n        core.log.error(\"failed to create background timer: \", err)\n        return\n    end\n\n    core.log.notice(\"succeed to create background timer\")\nend\n\n\nfunction _M.register_timer(name, f, privileged)\n    if privileged and not is_privileged() then\n        return\n    end\n\n    timers[name] = f\nend\n\n\nfunction _M.unregister_timer(name, privileged)\n    if privileged and not is_privileged() then\n        return\n    end\n\n    timers[name] = nil\nend\n\n\nfunction _M.check_interval()\n    return check_interval\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/tracer.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal tablepool = require(\"tablepool\")\nlocal span = require(\"apisix.utils.span\")\nlocal noop_span = require(\"apisix.utils.noop_span\").new()\nlocal span_kind = require(\"opentelemetry.trace.span_kind\")\nlocal span_status = require(\"opentelemetry.trace.span_status\")\nlocal local_conf = require(\"apisix.core.config_local\").local_conf()\nlocal ipairs = ipairs\nlocal ngx = ngx\n\nlocal enable_tracing = false\nif ngx.config.subsystem == \"http\" and type(local_conf.apisix.tracing) == \"boolean\" then\n    enable_tracing = local_conf.apisix.tracing\nend\n\nlocal _M = {\n    kind = span_kind,\n    status = span_status,\n}\n\nfunction _M.start(ctx, name, kind)\n    if not enable_tracing then\n        return noop_span\n    end\n\n    local tracing = ctx.tracing\n    if not tracing then\n        tracing = tablepool.fetch(\"tracing\", 0, 8)\n        tracing.spans = tablepool.fetch(\"tracing_spans\", 20, 0)\n        ctx.tracing = tracing\n        -- create a dummy root span as the invisible parent of all top-level spans\n        span.new(ctx, \"root\", nil)\n    end\n    if tracing.skip then\n        return noop_span\n    end\n\n    local sp = span.new(ctx, name, kind)\n    return sp\nend\n\n\nfunction _M.finish_all(ctx, code, message)\n    local tracing = ctx.tracing\n    if not tracing or not tracing.current_span then\n        return\n    end\n\n    tracing.current_span:set_status(code, message)\n    tracing.current_span:finish(ctx)\n\n    while tracing.current_span.parent_id do\n        tracing.current_span = tracing.spans[tracing.current_span.parent_id]\n        tracing.current_span:finish(ctx)\n    end\nend\n\n\nfunction _M.release(ctx)\n    local tracing = ctx.tracing\n    if not tracing then\n        return\n    end\n\n    for _, sp in ipairs(tracing.spans) do\n        sp:release()\n    end\n    tablepool.release(\"tracing_spans\", tracing.spans)\n    tablepool.release(\"tracing\", tracing)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/upstream.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal discovery = require(\"apisix.discovery.init\").discovery\nlocal upstream_util = require(\"apisix.utils.upstream\")\nlocal apisix_ssl = require(\"apisix.ssl\")\nlocal resource = require(\"apisix.resource\")\nlocal error = error\nlocal tostring = tostring\nlocal ipairs = ipairs\nlocal pairs = pairs\nlocal pcall = pcall\nlocal str_byte = string.byte\nlocal ngx_var = ngx.var\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal upstreams\nlocal healthcheck_manager\n\nlocal set_upstream_tls_client_param\nlocal ok, apisix_ngx_upstream = pcall(require, \"resty.apisix.upstream\")\nif ok then\n    set_upstream_tls_client_param = apisix_ngx_upstream.set_cert_and_key\nelse\n    set_upstream_tls_client_param = function ()\n        return nil, \"need to build APISIX-Runtime to support upstream mTLS\"\n    end\nend\n\nlocal set_stream_upstream_tls\nif not is_http then\n    local ok, apisix_ngx_stream_upstream = pcall(require, \"resty.apisix.stream.upstream\")\n    if ok then\n        set_stream_upstream_tls = apisix_ngx_stream_upstream.set_tls\n    else\n        set_stream_upstream_tls = function ()\n            return nil, \"need to build APISIX-Runtime to support TLS over TCP upstream\"\n        end\n    end\nend\n\n\n\nlocal HTTP_CODE_UPSTREAM_UNAVAILABLE = 503\nlocal _M = {}\n\n\nlocal function set_directly(ctx, key, ver, conf)\n    if not ctx then\n        error(\"missing argument ctx\", 2)\n    end\n    if not key then\n        error(\"missing argument key\", 2)\n    end\n    if not ver then\n        error(\"missing argument ver\", 2)\n    end\n    if not conf then\n        error(\"missing argument conf\", 2)\n    end\n\n    ctx.upstream_conf = conf\n    ctx.upstream_version = ver\n    ctx.upstream_key = key\n    return\nend\n_M.set = set_directly\n\n\nlocal function set_upstream_scheme(ctx, upstream)\n    -- plugins like proxy-rewrite may already set ctx.upstream_scheme\n    if not ctx.upstream_scheme then\n        -- the old configuration doesn't have scheme field, so fallback to \"http\"\n        ctx.upstream_scheme = upstream.scheme or \"http\"\n    end\n\n    ctx.var[\"upstream_scheme\"] = ctx.upstream_scheme\nend\n_M.set_scheme = set_upstream_scheme\n\nlocal scheme_to_port = {\n    http = 80,\n    https = 443,\n    grpc = 80,\n    grpcs = 443,\n}\n\n\n_M.scheme_to_port = scheme_to_port\n\n\nlocal function fill_node_info(up_conf, scheme, is_stream)\n    local nodes = up_conf.nodes\n    if up_conf.nodes_ref == nodes then\n        -- filled\n        return true\n    end\n\n    local need_filled = false\n    for _, n in ipairs(nodes) do\n        if not is_stream and not n.port then\n            if up_conf.scheme ~= scheme then\n                return nil, \"Can't detect upstream's scheme. \" ..\n                            \"You should either specify a port in the node \" ..\n                            \"or specify the upstream.scheme explicitly\"\n            end\n\n            need_filled = true\n        end\n\n        if not n.priority then\n            need_filled = true\n        end\n    end\n\n    if not need_filled then\n        up_conf.nodes_ref = nodes\n        return true\n    end\n\n    core.log.debug(\"fill node info for upstream: \",\n                core.json.delay_encode(up_conf, true))\n\n    -- keep the original nodes for slow path in `compare_upstream_node()`,\n    -- can't use `core.table.deepcopy()` for whole `nodes` array here,\n    -- because `compare_upstream_node()` compare `metadata` of node by address.\n    up_conf.original_nodes = core.table.new(#nodes, 0)\n    for i, n in ipairs(nodes) do\n        up_conf.original_nodes[i] = core.table.clone(n)\n        if not n.port or not n.priority then\n            nodes[i] = core.table.clone(n)\n\n            if not is_stream and not n.port then\n                nodes[i].port = scheme_to_port[scheme]\n            end\n\n            -- fix priority for non-array nodes and nodes from service discovery\n            if not n.priority then\n                nodes[i].priority = 0\n            end\n        end\n    end\n\n    up_conf.nodes_ref = nodes\n    return true\nend\n\n\nfunction _M.set_by_route(route, api_ctx)\n    if api_ctx.upstream_conf then\n        -- upstream_conf has been set by traffic-split plugin\n        return\n    end\n\n    local up_conf = api_ctx.matched_upstream\n    if not up_conf then\n        return 503, \"missing upstream configuration in Route or Service\"\n    end\n    -- core.log.info(\"up_conf: \", core.json.delay_encode(up_conf, true))\n\n    if up_conf.service_name then\n        if not discovery then\n            return 503, \"discovery is uninitialized\"\n        end\n        if not up_conf.discovery_type then\n            return 503, \"discovery server need appoint\"\n        end\n\n        local dis = discovery[up_conf.discovery_type]\n        if not dis then\n            local err = \"discovery \" .. up_conf.discovery_type .. \" is uninitialized\"\n            return 503, err\n        end\n\n        local new_nodes, err = dis.nodes(up_conf.service_name, up_conf.discovery_args)\n        if not new_nodes then\n            return HTTP_CODE_UPSTREAM_UNAVAILABLE, \"no valid upstream node: \" .. (err or \"nil\")\n        end\n\n        local same = upstream_util.compare_upstream_node(up_conf, new_nodes)\n        if not same then\n            local nodes_ver = resource.get_nodes_ver(up_conf.resource_key)\n            if not nodes_ver then\n                nodes_ver = 0\n            end\n            nodes_ver = nodes_ver + 1\n            up_conf._nodes_ver = nodes_ver\n            resource.set_nodes_ver_and_nodes(up_conf.resource_key, nodes_ver, new_nodes)\n            local pass, err = core.schema.check(core.schema.discovery_nodes, new_nodes)\n            if not pass then\n                return HTTP_CODE_UPSTREAM_UNAVAILABLE, \"invalid nodes format: \" .. err\n            end\n\n            core.log.info(\"discover new upstream from \", up_conf.service_name, \", type \",\n                          up_conf.discovery_type, \": \",\n                          core.json.delay_encode(up_conf, true))\n        end\n\n        -- in case the value of new_nodes is the same as the old one,\n        -- but discovery lib return a new table for it.\n        -- for example, when watch loop of kubernetes discovery is broken or done,\n        -- it will fetch full data again and return a new table for every services.\n        up_conf.nodes = new_nodes\n    end\n\n    local id = up_conf.resource_id\n    local conf_version = up_conf.resource_version\n    -- include the upstream object as part of the version, because the upstream will be changed\n    -- by service discovery or dns resolver.\n    set_directly(api_ctx, id, conf_version .. \"#\" .. tostring(up_conf) .. \"#\"\n                                    .. tostring(up_conf._nodes_ver or ''), up_conf)\n\n    local nodes_count = up_conf.nodes and #up_conf.nodes or 0\n    if nodes_count == 0 then\n        return HTTP_CODE_UPSTREAM_UNAVAILABLE, \"no valid upstream node\"\n    end\n\n    if not is_http then\n        local ok, err = fill_node_info(up_conf, nil, true)\n        if not ok then\n            return 503, err\n        end\n\n        local scheme = up_conf.scheme\n        if scheme == \"tls\" then\n            local ok, err = set_stream_upstream_tls()\n            if not ok then\n                return 503, err\n            end\n\n            local sni = apisix_ssl.server_name()\n            if sni then\n                ngx_var.upstream_sni = sni\n            end\n        end\n        local node_ver = resource.get_nodes_ver(up_conf.resource_key)\n        local resource_version = upstream_util.version(up_conf.resource_version,\n                                                                      node_ver)\n        local checker = healthcheck_manager.fetch_checker(up_conf.resource_key, resource_version)\n        api_ctx.up_checker = checker\n        return\n    end\n\n    set_upstream_scheme(api_ctx, up_conf)\n\n    local ok, err = fill_node_info(up_conf, api_ctx.upstream_scheme, false)\n    if not ok then\n        return 503, err\n    end\n    local node_ver = resource.get_nodes_ver(up_conf.resource_key)\n    local resource_version = upstream_util.version(up_conf.resource_version,\n                                                                  node_ver )\n    local checker = healthcheck_manager.fetch_checker(up_conf.resource_key, resource_version)\n    api_ctx.up_checker = checker\n    local scheme = up_conf.scheme\n    local tls_has_cert = up_conf.tls and (up_conf.tls.client_cert or up_conf.tls.client_cert_id)\n    if (scheme == \"https\" or scheme == \"grpcs\") and tls_has_cert then\n        local client_cert, client_key\n        if up_conf.tls.client_cert_id then\n            client_cert = api_ctx.upstream_ssl.cert\n            client_key = api_ctx.upstream_ssl.key\n        else\n            client_cert = up_conf.tls.client_cert\n            client_key = up_conf.tls.client_key\n        end\n\n        -- the sni here is just for logging\n        local sni = api_ctx.var.upstream_host\n        local cert, err = apisix_ssl.fetch_cert(sni, client_cert)\n        if not ok then\n            return 503, err\n        end\n\n        local key, err = apisix_ssl.fetch_pkey(sni, client_key)\n        if not ok then\n            return 503, err\n        end\n\n        if scheme == \"grpcs\" then\n            api_ctx.upstream_grpcs_cert = cert\n            api_ctx.upstream_grpcs_key = key\n        else\n            local ok, err = set_upstream_tls_client_param(cert, key)\n            if not ok then\n                return 503, err\n            end\n        end\n    end\n\n    return\nend\n\n\nfunction _M.set_grpcs_upstream_param(ctx)\n    if ctx.upstream_grpcs_cert then\n        local cert = ctx.upstream_grpcs_cert\n        local key = ctx.upstream_grpcs_key\n        local ok, err = set_upstream_tls_client_param(cert, key)\n        if not ok then\n            return 503, err\n        end\n    end\nend\n\n\nfunction _M.upstreams()\n    if not upstreams then\n        return nil, nil\n    end\n\n    return upstreams.values, upstreams.conf_version\nend\n\n\nlocal function check_schema(conf)\n    for _, node in ipairs(conf.nodes or {}) do\n        if core.utils.parse_ipv6(node.host) and str_byte(node.host, 1) ~= str_byte(\"[\") then\n            return false, \"IPv6 address must be enclosed with '[' and ']'\"\n        end\n    end\n    return core.schema.check(core.schema.upstream, conf)\nend\n\n_M.check_schema = check_schema\n\nlocal function get_chash_key_schema(hash_on)\n    if not hash_on then\n        return nil, \"hash_on is nil\"\n    end\n\n    if hash_on == \"vars\" then\n        return core.schema.upstream_hash_vars_schema\n    end\n\n    if hash_on == \"header\" or hash_on == \"cookie\" then\n        return core.schema.upstream_hash_header_schema\n    end\n\n    if hash_on == \"consumer\" then\n        return nil, nil\n    end\n\n    if hash_on == \"vars_combinations\" then\n        return core.schema.upstream_hash_vars_combinations_schema\n    end\n\n    return nil, \"invalid hash_on type \" .. hash_on\nend\n\n\nlocal function check_upstream_conf(in_dp, conf)\n    if not in_dp then\n        local ok, err = check_schema(conf)\n        if not ok then\n            return false, \"invalid configuration: \" .. err\n        end\n\n        if conf.nodes and not core.table.isarray(conf.nodes) then\n            local port\n            for addr,_ in pairs(conf.nodes) do\n                _, port = core.utils.parse_addr(addr)\n                if port then\n                    if port < 1 or port > 65535 then\n                        return false, \"invalid port \" .. tostring(port)\n                    end\n                end\n            end\n        end\n\n        local ssl_id = conf.tls and conf.tls.client_cert_id\n        if ssl_id then\n            local key = \"/ssls/\" .. ssl_id\n            local res, err = core.etcd.get(key)\n            if not res then\n                return nil, \"failed to fetch ssl info by \"\n                                    .. \"ssl id [\" .. ssl_id .. \"]: \" .. err\n            end\n\n            if res.status ~= 200 then\n                return nil, \"failed to fetch ssl info by \"\n                                    .. \"ssl id [\" .. ssl_id .. \"], \"\n                                    .. \"response code: \" .. res.status\n            end\n            if res.body and res.body.node and\n                res.body.node.value and res.body.node.value.type ~= \"client\" then\n\n                return nil, \"failed to fetch ssl info by \"\n                                    .. \"ssl id [\" .. ssl_id .. \"], \"\n                                    .. \"wrong ssl type\"\n            end\n        end\n    else\n        for i, node in ipairs(conf.nodes or {}) do\n            if core.utils.parse_ipv6(node.host) and str_byte(node.host, 1) ~= str_byte(\"[\") then\n                conf.nodes[i].host = \"[\" .. node.host .. \"]\"\n            end\n        end\n    end\n\n    if is_http then\n        if conf.pass_host == \"rewrite\" and\n            (conf.upstream_host == nil or conf.upstream_host == \"\")\n        then\n            return false, \"`upstream_host` can't be empty when `pass_host` is `rewrite`\"\n        end\n    end\n\n    if conf.tls and conf.tls.client_cert then\n        local cert = conf.tls.client_cert\n        local key = conf.tls.client_key\n        local ok, err = apisix_ssl.validate(cert, key)\n        if not ok then\n            return false, err\n        end\n    end\n\n    if conf.type ~= \"chash\" then\n        return true\n    end\n\n    if conf.hash_on ~= \"consumer\" and not conf.key then\n        return false, \"missing key\"\n    end\n\n    local key_schema, err = get_chash_key_schema(conf.hash_on)\n    if err then\n        return false, \"type is chash, err: \" .. err\n    end\n\n    if key_schema then\n        local ok, err = core.schema.check(key_schema, conf.key)\n        if not ok then\n            return false, \"invalid configuration: \" .. err\n        end\n    end\n\n    return true\nend\n\n\nfunction _M.check_upstream_conf(conf)\n    return check_upstream_conf(false, conf)\nend\n\n\nfunction _M.encrypt_conf(conf)\n    -- encrypt the key in the admin\n    if conf and conf.tls and conf.tls.client_key then\n        conf.tls.client_key = apisix_ssl.aes_encrypt_pkey(conf.tls.client_key)\n    end\nend\n\n\nlocal function filter_upstream(value, parent)\n    if not value then\n        return\n    end\n    value.resource_key = parent and parent.key\n    value.resource_version = ((parent and parent.modifiedIndex) or value.modifiedIndex)\n    value.resource_id = ((parent and parent.value.id) or value.id)\n    if not is_http and value.scheme == \"http\" then\n        -- For L4 proxy, the default scheme is \"tcp\"\n        value.scheme = \"tcp\"\n    end\n\n    if not value.nodes then\n        return\n    end\n\n    local nodes = value.nodes\n    if core.table.isarray(nodes) then\n        for _, node in ipairs(nodes) do\n            local host = node.host\n            if not core.utils.parse_ipv4(host) and\n                    not core.utils.parse_ipv6(host) then\n                parent.has_domain = true\n                break\n            end\n        end\n    else\n        local new_nodes = core.table.new(core.table.nkeys(nodes), 0)\n        for addr, weight in pairs(nodes) do\n            local host, port = core.utils.parse_addr(addr)\n            if not core.utils.parse_ipv4(host) and\n                    not core.utils.parse_ipv6(host) then\n                parent.has_domain = true\n            end\n            local node = {\n                host = host,\n                port = port,\n                weight = weight,\n            }\n            core.table.insert(new_nodes, node)\n        end\n        value.nodes = new_nodes\n    end\n    if parent.has_domain then\n        value.dns_nodes = value.nodes\n    end\nend\n_M.filter_upstream = filter_upstream\n\n\nfunction _M.init_worker()\n    local err\n    upstreams, err = core.config.new(\"/upstreams\", {\n            automatic = true,\n            item_schema = core.schema.upstream,\n            -- also check extra fields in the DP side\n            checker = function (item, schema_type)\n                return check_upstream_conf(true, item)\n            end,\n            filter = function(upstream)\n                upstream.has_domain = false\n\n                filter_upstream(upstream.value, upstream)\n\n                core.log.info(\"filter upstream: \", core.json.delay_encode(upstream, true))\n            end,\n        })\n    if not upstreams then\n        error(\"failed to create etcd instance for fetching upstream: \" .. err)\n        return\n    end\n    healthcheck_manager = require(\"apisix.healthcheck_manager\")\n    healthcheck_manager.init_worker()\nend\n\n\nfunction _M.get_by_id(up_id)\n    local upstream\n    local upstreams = core.config.fetch_created_obj(\"/upstreams\")\n    if upstreams then\n        upstream = upstreams:get(tostring(up_id))\n    end\n\n    if not upstream then\n        core.log.error(\"failed to find upstream by id: \", up_id)\n        return nil\n    end\n\n    if upstream.has_domain then\n        local err\n        upstream, err = upstream_util.parse_domain_in_up(upstream)\n        if err then\n            core.log.error(\"failed to get resolved upstream: \", err)\n            return nil\n        end\n    end\n\n    core.log.info(\"parsed upstream: \", core.json.delay_encode(upstream, true))\n    return upstream.value\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal _M = {}\n\nfunction _M.is_running_under_multi_auth(ctx)\n    return ctx._plugin_name == \"multi-auth\"\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/batch-processor-manager.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal batch_processor = require(\"apisix.utils.batch-processor\")\nlocal timer_at = ngx.timer.at\nlocal pairs = pairs\nlocal setmetatable = setmetatable\n\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\nfunction _M.new(name)\n    return setmetatable({\n        stale_timer_running = false,\n        buffers = {},\n        total_pushed_entries = 0,\n        name = name,\n    }, mt)\nend\n\n\nfunction _M:wrap_schema(schema)\n    local bp_schema = core.table.deepcopy(batch_processor.schema)\n    local properties = schema.properties\n    for k, v in pairs(bp_schema.properties) do\n        if not properties[k] then\n            properties[k] = v\n        end\n        -- don't touch if the plugin overrides the property\n    end\n\n    properties.name.default = self.name\n    return schema\nend\n\n\n-- remove stale objects from the memory after timer expires\nlocal function remove_stale_objects(premature, self)\n    if premature then\n        return\n    end\n\n    for key, batch in pairs(self.buffers) do\n        if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then\n            core.log.info(\"removing batch processor stale object, conf: \",\n                          core.json.delay_encode(key))\n           self.buffers[key] = nil\n        end\n    end\n\n    self.stale_timer_running = false\nend\n\n\nlocal check_stale\ndo\n    local interval = 1800\n\n    function check_stale(self)\n        if not self.stale_timer_running then\n            -- run the timer every 30 mins if any log is present\n            timer_at(interval, remove_stale_objects, self)\n            self.stale_timer_running = true\n        end\n    end\n\n    function _M.set_check_stale_interval(time)\n        interval = time\n    end\nend\n\n\nlocal function total_processed_entries(self)\n    local processed_entries = 0\n    for _, log_buffer in pairs(self.buffers) do\n        processed_entries = processed_entries + log_buffer.processed_entries\n    end\n    return processed_entries\nend\n\nfunction _M:add_entry(conf, entry, max_pending_entries)\n    if max_pending_entries then\n        local total_processed_entries_count = total_processed_entries(self)\n        if self.total_pushed_entries - total_processed_entries_count > max_pending_entries then\n            core.log.error(\"max pending entries limit exceeded. discarding entry.\",\n                           \" total_pushed_entries: \", self.total_pushed_entries,\n                           \" total_processed_entries: \", total_processed_entries_count,\n                           \" max_pending_entries: \", max_pending_entries)\n            return\n        end\n    end\n    check_stale(self)\n\n    local log_buffer = self.buffers[plugin.conf_version(conf)]\n    if not log_buffer then\n        return false\n    end\n\n    log_buffer:push(entry)\n    self.total_pushed_entries = self.total_pushed_entries + 1\n    return true\nend\n\n\nfunction _M:add_entry_to_new_processor(conf, entry, ctx, func, max_pending_entries)\n    if max_pending_entries then\n        local total_processed_entries_count = total_processed_entries(self)\n        if self.total_pushed_entries - total_processed_entries_count > max_pending_entries then\n            core.log.error(\"max pending entries limit exceeded. discarding entry.\",\n                           \" total_pushed_entries: \", self.total_pushed_entries,\n                           \" total_processed_entries: \", total_processed_entries_count,\n                           \" max_pending_entries: \", max_pending_entries)\n            return\n        end\n    end\n    check_stale(self)\n\n    local config = {\n        name = conf.name,\n        batch_max_size = conf.batch_max_size,\n        max_retry_count = conf.max_retry_count,\n        retry_delay = conf.retry_delay,\n        buffer_duration = conf.buffer_duration,\n        inactive_timeout = conf.inactive_timeout,\n        route_id = ctx.var.route_id,\n        server_addr = ctx.var.server_addr,\n    }\n\n    local log_buffer, err = batch_processor:new(func, config)\n    if not log_buffer then\n        core.log.error(\"error when creating the batch processor: \", err)\n        return false\n    end\n\n    log_buffer:push(entry)\n    self.buffers[plugin.conf_version(conf)] = log_buffer\n    self.total_pushed_entries = self.total_pushed_entries + 1\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/batch-processor.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal setmetatable = setmetatable\nlocal timer_at = ngx.timer.at\nlocal ipairs = ipairs\nlocal table = table\nlocal now = ngx.now\nlocal type = type\nlocal batch_processor = {}\nlocal batch_processor_mt = {\n    __index = batch_processor\n}\nlocal execute_func\nlocal create_buffer_timer\nlocal batch_metrics\nlocal prometheus\nif ngx.config.subsystem == \"http\" then\n    prometheus = require(\"apisix.plugins.prometheus.exporter\")\nend\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        name = {type = \"string\", default = \"log buffer\"},\n        max_retry_count = {type = \"integer\", minimum = 0, default= 0},\n        retry_delay = {type = \"integer\", minimum = 0, default= 1},\n        buffer_duration = {type = \"integer\", minimum = 1, default= 60},\n        inactive_timeout = {type = \"integer\", minimum = 1, default= 5},\n        batch_max_size = {type = \"integer\", minimum = 1, default= 1000},\n    }\n}\nbatch_processor.schema = schema\n\n\nlocal function schedule_func_exec(self, delay, batch)\n    local hdl, err = timer_at(delay, execute_func, self, batch)\n    if not hdl then\n        if err == \"process exiting\" then\n            -- it is allowed to create zero-delay timers even when\n            -- the Nginx worker process starts shutting down\n            timer_at(0, execute_func, self)\n        else\n            core.log.error(\"failed to create process timer: \", err)\n            return\n        end\n    end\nend\n\n\nlocal function set_metrics(self, count)\n    -- add batch metric for every route\n    if batch_metrics and self.name and self.route_id and self.server_addr then\n        self.label = {self.name, self.route_id, self.server_addr}\n        batch_metrics:set(count, self.label)\n    end\nend\n\n\nlocal function slice_batch(batch, n)\n    local slice = {}\n    local idx = 1\n    for i = n or 1, #batch do\n        slice[idx] = batch[i]\n        idx = idx + 1\n    end\n    return slice\nend\n\n\nfunction execute_func(premature, self, batch)\n    -- In case of \"err\" and a valid \"first_fail\" batch processor considers, all first_fail-1\n    -- entries have been successfully consumed and hence reschedule the job for entries with\n    -- index first_fail to #entries based on the current retry policy.\n    local ok, err, first_fail = self.func(batch.entries, self.batch_max_size)\n    if not ok then\n        if first_fail then\n            core.log.error(\"Batch Processor[\", self.name, \"] failed to process entries [\",\n                            #batch.entries + 1 - first_fail, \"/\", #batch.entries ,\"]: \", err)\n            batch.entries = slice_batch(batch.entries, first_fail)\n            self.processed_entries = self.processed_entries + first_fail - 1\n        else\n            core.log.error(\"Batch Processor[\", self.name,\n                           \"] failed to process entries: \", err)\n        end\n\n        batch.retry_count = batch.retry_count + 1\n        if batch.retry_count <= self.max_retry_count and #batch.entries > 0 then\n            schedule_func_exec(self, self.retry_delay,\n                               batch)\n        else\n            self.processed_entries = self.processed_entries + #batch.entries\n            core.log.error(\"Batch Processor[\", self.name,\"] exceeded \",\n                           \"the max_retry_count[\", batch.retry_count,\n                           \"] dropping the entries\")\n        end\n        return\n    end\n    self.processed_entries = self.processed_entries + #batch.entries\n    core.log.debug(\"Batch Processor[\", self.name,\n                   \"] successfully processed the entries\")\nend\n\n\nlocal function flush_buffer(premature, self)\n    if now() - self.last_entry_t >= self.inactive_timeout or\n       now() - self.first_entry_t >= self.buffer_duration\n    then\n        core.log.debug(\"Batch Processor[\", self.name ,\"] buffer \",\n            \"duration exceeded, activating buffer flush\")\n        self:process_buffer()\n        self.is_timer_running = false\n        return\n    end\n\n    -- buffer duration did not exceed or the buffer is active,\n    -- extending the timer\n    core.log.debug(\"Batch Processor[\", self.name ,\"] extending buffer timer\")\n    create_buffer_timer(self)\nend\n\n\nfunction create_buffer_timer(self)\n    local hdl, err = timer_at(self.inactive_timeout, flush_buffer, self)\n    if not hdl then\n        if err == \"process exiting\" then\n            timer_at(0, flush_buffer, self)\n        else\n            core.log.error(\"failed to create buffer timer: \", err)\n            return\n        end\n    end\n    self.is_timer_running = true\nend\n\n\nfunction batch_processor:new(func, config)\n    local ok, err = core.schema.check(schema, config)\n    if not ok then\n        return nil, err\n    end\n\n    if type(func) ~= \"function\" then\n        return nil, \"Invalid argument, arg #1 must be a function\"\n    end\n\n    core.log.debug(\"creating new batch processor with config: \",\n        core.json.delay_encode(config, true))\n\n    local processor = {\n        func = func,\n        buffer_duration = config.buffer_duration,\n        inactive_timeout = config.inactive_timeout,\n        max_retry_count = config.max_retry_count,\n        batch_max_size = config.batch_max_size,\n        retry_delay = config.retry_delay,\n        name = config.name,\n        batch_to_process = {},\n        entry_buffer = {entries = {}, retry_count = 0},\n        is_timer_running = false,\n        first_entry_t = 0,\n        last_entry_t = 0,\n        route_id = config.route_id,\n        server_addr = config.server_addr,\n        processed_entries = 0\n    }\n\n    return setmetatable(processor, batch_processor_mt)\nend\n\nfunction batch_processor:push(entry)\n    -- if the batch size is one then immediately send for processing\n    if self.batch_max_size == 1 then\n        local batch = {entries = {entry}, retry_count = 0}\n        schedule_func_exec(self, 0, batch)\n        return\n    end\n\n    if prometheus and prometheus.get_prometheus() and not batch_metrics and self.name\n       and self.route_id and self.server_addr then\n        batch_metrics = prometheus.get_prometheus():gauge(\"batch_process_entries\",\n                                                          \"batch process remaining entries\",\n                                                          {\"name\", \"route_id\", \"server_addr\"})\n    end\n\n    local entries = self.entry_buffer.entries\n    table.insert(entries, entry)\n    set_metrics(self, #entries)\n\n    if #entries == 1 then\n        self.first_entry_t = now()\n    end\n    self.last_entry_t = now()\n\n    if self.batch_max_size <= #entries then\n        core.log.debug(\"Batch Processor[\", self.name ,\n                       \"] batch max size has exceeded\")\n        self:process_buffer()\n    end\n\n    if not self.is_timer_running then\n        create_buffer_timer(self)\n    end\nend\n\n\nfunction batch_processor:process_buffer()\n    -- If entries are present in the buffer move the entries to processing\n    if #self.entry_buffer.entries > 0 then\n        core.log.debug(\"transferring buffer entries to processing pipe line, \",\n            \"buffercount[\", #self.entry_buffer.entries ,\"]\")\n        self.batch_to_process[#self.batch_to_process + 1] = self.entry_buffer\n        self.entry_buffer = {entries = {}, retry_count = 0}\n        set_metrics(self, 0)\n    end\n\n    for _, batch in ipairs(self.batch_to_process) do\n        schedule_func_exec(self, 0, batch)\n    end\n\n    self.batch_to_process = {}\nend\n\n\nreturn batch_processor\n"
  },
  {
    "path": "apisix/utils/content-decode.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal pcall = pcall\nlocal zlib = require(\"ffi-zlib\")\nlocal str_buffer = require(\"string.buffer\")\nlocal is_br_libs_loaded, brotli = pcall(require, \"brotli\")\nlocal content_decode_funcs = {}\nlocal _M = {}\n\n\nlocal function inflate_gzip(data)\n    local inputs = str_buffer.new():set(data)\n    local outputs = str_buffer.new()\n\n    local read_inputs = function(size)\n        local data = inputs:get(size)\n        if data == \"\" then\n            return nil\n        end\n        return data\n    end\n\n    local write_outputs = function(data)\n        return outputs:put(data)\n    end\n\n    local ok, err = zlib.inflateGzip(read_inputs, write_outputs)\n    if not ok then\n        return nil, \"inflate gzip err: \" .. err\n    end\n\n    return outputs:get()\nend\ncontent_decode_funcs.gzip = inflate_gzip\n\n\nlocal function brotli_stream_decode(read_inputs, write_outputs)\n    -- read 64k data per times\n    local read_size = 64 * 1024\n    local decompressor = brotli.decompressor:new()\n\n    local chunk, ok, res\n    repeat\n        chunk = read_inputs(read_size)\n        if chunk then\n            ok, res = pcall(function()\n                return decompressor:decompress(chunk)\n            end)\n        else\n            ok, res = pcall(function()\n                return decompressor:finish()\n            end)\n        end\n        if not ok then\n            return false, res\n        end\n        write_outputs(res)\n    until not chunk\n\n    return true, nil\nend\n\n\nlocal function brotli_decode(data)\n    local inputs = str_buffer.new():set(data)\n    local outputs = str_buffer.new()\n\n    local read_inputs = function(size)\n        local data = inputs:get(size)\n        if data == \"\" then\n            return nil\n        end\n        return data\n    end\n\n    local write_outputs = function(data)\n        return outputs:put(data)\n    end\n\n    local ok, err = brotli_stream_decode(read_inputs, write_outputs)\n    if not ok then\n        return nil, \"brotli decode err: \" .. err\n    end\n\n    return outputs:get()\nend\n\nif is_br_libs_loaded then\n    content_decode_funcs.br = brotli_decode\nend\n\n\nfunction _M.dispatch_decoder(response_encoding)\n    return content_decode_funcs[response_encoding]\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/google-cloud-oauth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal type = type\nlocal setmetatable = setmetatable\n\nlocal ngx_update_time = ngx.update_time\nlocal ngx_time = ngx.time\nlocal ngx_encode_args = ngx.encode_args\n\nlocal http = require(\"resty.http\")\nlocal jwt = require(\"resty.jwt\")\n\n\nlocal function get_timestamp()\n    ngx_update_time()\n    return ngx_time()\nend\n\n\nlocal _M = {}\n\n\nfunction _M.generate_access_token(self)\n    if not self.access_token or get_timestamp() > self.access_token_expire_time - 60 then\n        self:refresh_access_token()\n    end\n    return self.access_token\nend\n\n\nfunction _M.refresh_access_token(self)\n    local http_new = http.new()\n    local res, err = http_new:request_uri(self.token_uri, {\n        ssl_verify = self.ssl_verify,\n        method = \"POST\",\n        body = ngx_encode_args({\n            grant_type = \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\n            assertion = self:generate_jwt_token()\n        }),\n        headers = {\n            [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n        },\n    })\n\n    if not res then\n        core.log.error(\"failed to refresh google oauth access token, \", err)\n        return\n    end\n\n    if res.status ~= 200 then\n        core.log.error(\"failed to refresh google oauth access token: \", res.body)\n        return\n    end\n\n    res, err = core.json.decode(res.body)\n    if not res then\n        core.log.error(\"failed to parse google oauth response data: \", err)\n        return\n    end\n\n    self.access_token = res.access_token\n    self.access_token_type = res.token_type\n    self.access_token_ttl = res.expires_in\n    self.access_token_expire_time = get_timestamp() + res.expires_in\nend\n\n\nfunction _M.generate_jwt_token(self)\n    local payload = core.json.encode({\n        iss = self.client_email,\n        aud = self.token_uri,\n        scope = self.scope,\n        iat = get_timestamp(),\n        exp = get_timestamp() + (60 * 60)\n    })\n\n    local jwt_token = jwt:sign(self.private_key, {\n        header = { alg = \"RS256\", typ = \"JWT\" },\n        payload = payload,\n    })\n\n    return jwt_token\nend\n\n\nfunction _M.new(config, ssl_verify)\n    local oauth = {\n        client_email = config.client_email,\n        private_key = config.private_key,\n        project_id = config.project_id,\n        token_uri = config.token_uri or \"https://oauth2.googleapis.com/token\",\n        auth_uri = config.auth_uri or \"https://accounts.google.com/o/oauth2/auth\",\n        entries_uri = config.entries_uri,\n        access_token = nil,\n        access_token_type = nil,\n        access_token_expire_time = 0,\n    }\n\n    oauth.ssl_verify = ssl_verify\n\n    if config.scope then\n        if type(config.scope) == \"string\" then\n            oauth.scope = config.scope\n        end\n\n        if type(config.scope) == \"table\" then\n            oauth.scope = core.table.concat(config.scope, \" \")\n        end\n    else\n        oauth.scope = \"https://www.googleapis.com/auth/cloud-platform\"\n    end\n\n    return setmetatable(oauth, { __index = _M })\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/log-util.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal plugin = require(\"apisix.plugin\")\nlocal expr = require(\"resty.expr.v1\")\nlocal content_decode = require(\"apisix.utils.content-decode\")\nlocal ngx = ngx\nlocal pairs = pairs\nlocal type = type\nlocal ngx_now = ngx.now\nlocal ngx_header = ngx.header\nlocal os_date = os.date\nlocal str_byte = string.byte\nlocal str_sub  = string.sub\nlocal math_floor = math.floor\nlocal ngx_update_time = ngx.update_time\nlocal req_get_body_data = ngx.req.get_body_data\nlocal is_http = ngx.config.subsystem == \"http\"\nlocal req_get_body_file = ngx.req.get_body_file\nlocal MAX_REQ_BODY      = 524288      -- 512 KiB\nlocal MAX_RESP_BODY     = 524288      -- 512 KiB\nlocal MAX_LOG_FORMAT_DEPTH = 5\nlocal io                = io\nlocal req_read_body = ngx.req.read_body\n\nlocal lru_log_format = core.lrucache.new({\n    ttl = 300, count = 512\n})\n\nlocal _M = {}\n\n\nlocal function get_request_body(max_bytes)\n    local req_body = req_get_body_data()\n    if req_body then\n        if max_bytes and #req_body >= max_bytes then\n            req_body = str_sub(req_body, 1, max_bytes)\n        end\n        return req_body\n    end\n\n    local file_name = req_get_body_file()\n    if not file_name then\n        return nil\n    end\n\n    core.log.info(\"attempt to read body from file: \", file_name)\n\n    local f, err = io.open(file_name, 'r')\n    if not f then\n        return nil, \"fail to open file \" .. err\n    end\n\n    req_body = f:read(max_bytes)\n    f:close()\n\n    return req_body\nend\n\n\nlocal function do_gen_log_format(format, depth)\n    local log_format = {}\n    for k, var_name in pairs(format) do\n        if type(var_name) == \"table\" then\n            if depth >= MAX_LOG_FORMAT_DEPTH then\n                core.log.warn(\"log_format nesting exceeds max depth \",\n                              MAX_LOG_FORMAT_DEPTH, \", truncating\")\n                log_format[k] = {false, {}}\n            else\n                local nested_format = do_gen_log_format(var_name, depth + 1)\n                log_format[k] = {false, nested_format}\n            end\n        elseif type(var_name) == \"string\" and var_name:byte(1, 1) == str_byte(\"$\") then\n            log_format[k] = {true, var_name:sub(2)}\n        else\n            log_format[k] = {false, var_name}\n        end\n    end\n    return log_format\nend\n\nlocal function gen_log_format(format)\n    local log_format = do_gen_log_format(format, 1)\n    core.log.info(\"log_format: \", core.json.delay_encode(log_format))\n    return log_format\nend\n\n\nlocal function build_log_entry(ctx, log_format, max_req_body_bytes)\n    local entry = core.table.new(0, core.table.nkeys(log_format))\n    for k, var_attr in pairs(log_format) do\n        if var_attr[1] then\n            local key = var_attr[2]\n            if key == \"request_body\" then\n                local max_req_body_bytes = max_req_body_bytes or MAX_REQ_BODY\n                local req_body, err = get_request_body(max_req_body_bytes)\n                if err then\n                    core.log.error(\"fail to get request body: \", err)\n                else\n                    entry[k] = req_body\n                end\n            else\n                entry[k] = ctx.var[var_attr[2]]\n            end\n        elseif type(var_attr[2]) == \"table\" then\n            entry[k] = build_log_entry(ctx, var_attr[2], max_req_body_bytes)\n        else\n            entry[k] = var_attr[2]\n        end\n    end\n    return entry\nend\n\n\nlocal function get_custom_format_log(ctx, format, max_req_body_bytes)\n    local log_format = lru_log_format(format or \"\", nil, gen_log_format, format)\n    local entry = build_log_entry(ctx, log_format, max_req_body_bytes)\n\n    local matched_route = ctx.matched_route and ctx.matched_route.value\n    if matched_route then\n        entry.service_id = matched_route.service_id\n        entry.route_id = matched_route.id\n    end\n    return entry\nend\n-- export the log getter so we can mock in tests\n_M.get_custom_format_log = get_custom_format_log\n\n\n-- for test\nfunction _M.inject_get_custom_format_log(f)\n    get_custom_format_log = f\n    _M.get_custom_format_log = f\nend\n\n\nlocal function latency_details_in_ms(ctx)\n    local latency = (ngx_now() - ngx.req.start_time()) * 1000\n    local upstream_latency, apisix_latency = nil, latency\n\n    if ctx.var.upstream_response_time then\n        upstream_latency = ctx.var.upstream_response_time * 1000\n        apisix_latency = apisix_latency - upstream_latency\n\n        -- The latency might be negative, as Nginx uses different time measurements in\n        -- different metrics.\n        -- See https://github.com/apache/apisix/issues/5146#issuecomment-928919399\n        if apisix_latency < 0 then\n            apisix_latency = 0\n        end\n    end\n\n    return latency, upstream_latency, apisix_latency\nend\n_M.latency_details_in_ms = latency_details_in_ms\n\n\nlocal function get_full_log(ngx, conf)\n    local ctx = ngx.ctx.api_ctx\n    local var = ctx.var\n    local service_id\n    local route_id\n    local url = var.scheme .. \"://\" .. var.host .. \":\" .. var.server_port\n                .. var.request_uri\n    local matched_route = ctx.matched_route and ctx.matched_route.value\n\n    if matched_route then\n        service_id = matched_route.service_id or \"\"\n        route_id = matched_route.id\n    else\n        service_id = var.host\n    end\n\n    local consumer\n    if ctx.consumer then\n        consumer = {\n            username = ctx.consumer.username\n        }\n    end\n\n    local latency, upstream_latency, apisix_latency = latency_details_in_ms(ctx)\n\n    local log =  {\n        request = {\n            url = url,\n            uri = var.request_uri,\n            method = ngx.req.get_method(),\n            headers = ngx.req.get_headers(),\n            querystring = ngx.req.get_uri_args(),\n            size = var.request_length\n        },\n        response = {\n            status = ngx.status,\n            headers = ngx.resp.get_headers(),\n            size = var.bytes_sent\n        },\n        server = {\n            hostname = core.utils.gethostname(),\n            version = core.version.VERSION\n        },\n        upstream = var.upstream_addr,\n        service_id = service_id,\n        route_id = route_id,\n        consumer = consumer,\n        client_ip = core.request.get_remote_client_ip(ngx.ctx.api_ctx),\n        start_time = ngx.req.start_time() * 1000,\n        latency = latency,\n        upstream_latency = upstream_latency,\n        apisix_latency = apisix_latency\n    }\n\n    if conf.include_resp_body then\n        log.response.body = ctx.resp_body\n    end\n\n    if conf.include_req_body then\n\n        local log_request_body = true\n\n        if conf.include_req_body_expr then\n\n            if not conf.request_expr then\n                local request_expr, err = expr.new(conf.include_req_body_expr)\n                if not request_expr then\n                    core.log.error('generate request expr err ' .. err)\n                    return log\n                end\n                conf.request_expr = request_expr\n            end\n\n            local result = conf.request_expr:eval(ctx.var)\n\n            if not result then\n                log_request_body = false\n            end\n        end\n\n        if log_request_body then\n            local max_req_body_bytes = conf.max_req_body_bytes or MAX_REQ_BODY\n            local body, err = get_request_body(max_req_body_bytes)\n            if err then\n                core.log.error(\"fail to get request body: \", err)\n                return\n            end\n            log.request.body = body\n        end\n    end\n\n    return log\nend\n_M.get_full_log = get_full_log\n\n\n-- for test\nfunction _M.inject_get_full_log(f)\n    get_full_log = f\n    _M.get_full_log = f\nend\n\n\nlocal function is_match(match, ctx)\n    local match_result\n    for _, m in pairs(match) do\n        local expr, _ = expr.new(m)\n        match_result = expr:eval(ctx.var)\n        if match_result then\n            break\n        end\n    end\n\n    return match_result\nend\n\n\nfunction _M.get_log_entry(plugin_name, conf, ctx)\n    -- If the \"match\" configuration is set and the matching conditions are not met,\n    -- then do not log the message.\n    if conf.match and not is_match(conf.match, ctx) then\n        return\n    end\n\n    local metadata = plugin.plugin_metadata(plugin_name)\n    core.log.info(\"metadata: \", core.json.delay_encode(metadata))\n\n    local entry\n    local customized = false\n\n    local has_meta_log_format = metadata and metadata.value.log_format\n        and core.table.nkeys(metadata.value.log_format) > 0\n\n    if conf.log_format or has_meta_log_format then\n        customized = true\n        entry = get_custom_format_log(ctx, conf.log_format or metadata.value.log_format,\n                                      conf.max_req_body_bytes)\n    else\n        if is_http then\n            entry = get_full_log(ngx, conf)\n        else\n            -- get_full_log doesn't work in stream\n            core.log.error(plugin_name, \"'s log_format is not set\")\n        end\n    end\n\n    if ctx.llm_summary then\n        entry.llm_summary = ctx.llm_summary\n    end\n    if ctx.llm_request then\n        entry.llm_request = ctx.llm_request\n    end\n    if ctx.llm_response_text then\n        entry.llm_response_text = ctx.llm_response_text\n    end\n    return entry, customized\nend\n\n\nfunction _M.get_req_original(ctx, conf)\n    local data = {\n        ctx.var.request, \"\\r\\n\"\n    }\n    for k, v in pairs(ngx.req.get_headers()) do\n        core.table.insert_tail(data, k, \": \", v, \"\\r\\n\")\n    end\n    core.table.insert(data, \"\\r\\n\")\n\n    if conf.include_req_body then\n        local max_req_body_bytes = conf.max_req_body_bytes or MAX_REQ_BODY\n        local req_body = get_request_body(max_req_body_bytes)\n        core.table.insert(data, req_body)\n    end\n\n    return core.table.concat(data, \"\")\nend\n\n\nfunction _M.check_log_schema(conf)\n    if conf.include_req_body_expr then\n        local ok, err = expr.new(conf.include_req_body_expr)\n        if not ok then\n            return nil, \"failed to validate the 'include_req_body_expr' expression: \" .. err\n        end\n    end\n    if conf.include_resp_body_expr then\n        local ok, err = expr.new(conf.include_resp_body_expr)\n        if not ok then\n            return nil, \"failed to validate the 'include_resp_body_expr' expression: \" .. err\n        end\n    end\n    return true, nil\nend\n\n\nfunction _M.collect_body(conf, ctx)\n    if conf.include_resp_body then\n        local log_response_body = true\n\n        if conf.include_resp_body_expr then\n            if not conf.response_expr then\n                local response_expr, err = expr.new(conf.include_resp_body_expr)\n                if not response_expr then\n                    core.log.error('generate response expr err ' .. err)\n                    return\n                end\n                conf.response_expr = response_expr\n            end\n\n            if ctx.res_expr_eval_result == nil then\n                ctx.res_expr_eval_result = conf.response_expr:eval(ctx.var)\n            end\n\n            if not ctx.res_expr_eval_result then\n                log_response_body = false\n            end\n        end\n\n        if log_response_body then\n            local max_resp_body_bytes = conf.max_resp_body_bytes or MAX_RESP_BODY\n\n            if ctx._resp_body_bytes and ctx._resp_body_bytes >= max_resp_body_bytes then\n                return\n            end\n            local final_body = core.response.hold_body_chunk(ctx, true, max_resp_body_bytes)\n            if not final_body then\n                return\n            end\n\n            local response_encoding = ngx_header[\"Content-Encoding\"]\n            if not response_encoding then\n                ctx.resp_body = final_body\n                return\n            end\n\n            local decoder = content_decode.dispatch_decoder(response_encoding)\n            if not decoder then\n                core.log.warn(\"unsupported compression encoding type: \",\n                              response_encoding)\n                ctx.resp_body = final_body\n                return\n            end\n\n            local decoded_body, err = decoder(final_body)\n            if err ~= nil then\n                core.log.warn(\"try decode compressed data err: \", err)\n                ctx.resp_body = final_body\n                return\n            end\n\n            ctx.resp_body = decoded_body\n        end\n    end\nend\n\n\nfunction _M.get_rfc3339_zulu_timestamp(timestamp)\n    ngx_update_time()\n    local now = timestamp or ngx_now()\n    local second = math_floor(now)\n    local millisecond = math_floor((now - second) * 1000)\n    return os_date(\"!%Y-%m-%dT%T.\", second) .. core.string.format(\"%03dZ\", millisecond)\nend\n\n\nfunction _M.check_and_read_req_body(conf, ctx)\n    if conf.include_req_body then\n        local should_read_body = true\n        if conf.include_req_body_expr then\n            if not conf.request_expr then\n                local request_expr, err = expr.new(conf.include_req_body_expr)\n                if not request_expr then\n                    core.log.error('generate request expr err ', err)\n                    return\n                end\n                conf.request_expr = request_expr\n            end\n\n            local result = conf.request_expr:eval(ctx.var)\n\n            if not result then\n                should_read_body = false\n            end\n        end\n        if should_read_body then\n            req_read_body()\n        end\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/noop_span.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal setmetatable = setmetatable\n\nlocal _M = {}\n\n\nlocal mt = {\n    __index = _M\n}\n\nfunction _M.new(ctx, name, kind)\n    return setmetatable({}, mt)\nend\n\n\nfunction _M.set_status(self, code, message)\nend\n\n\nfunction _M.set_attributes(self, ...)\nend\n\n\nfunction _M.finish(self)\nend\n\n\nfunction _M.release(self)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/redis-schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal policy_to_additional_properties = {\n    redis = {\n        properties = {\n            redis_host = {\n                type = \"string\", minLength = 2\n            },\n            redis_port = {\n                type = \"integer\", minimum = 1, default = 6379,\n            },\n            redis_username = {\n                type = \"string\", minLength = 1,\n            },\n            redis_password = {\n                type = \"string\", minLength = 0,\n            },\n            redis_database = {\n                type = \"integer\", minimum = 0, default = 0,\n            },\n            redis_timeout = {\n                type = \"integer\", minimum = 1, default = 1000,\n            },\n            redis_ssl = {\n                type = \"boolean\", default = false,\n            },\n            redis_ssl_verify = {\n                type = \"boolean\", default = false,\n            },\n            redis_keepalive_timeout = {\n                type = \"integer\", minimum = 1000, default = 10000\n            },\n            redis_keepalive_pool = {\n                type = \"integer\", minimum = 1, default = 100\n            }\n        },\n        required = {\"redis_host\"},\n    },\n    [\"redis-cluster\"] = {\n        properties = {\n            redis_cluster_nodes = {\n                type = \"array\",\n                minItems = 1,\n                items = {\n                    type = \"string\", minLength = 2, maxLength = 100\n                },\n            },\n            redis_password = {\n                type = \"string\", minLength = 0,\n            },\n            redis_timeout = {\n                type = \"integer\", minimum = 1, default = 1000,\n            },\n            redis_cluster_name = {\n                type = \"string\",\n            },\n            redis_cluster_ssl = {\n                type = \"boolean\", default = false,\n            },\n            redis_cluster_ssl_verify = {\n                type = \"boolean\", default = false,\n            },\n            redis_keepalive_timeout = {\n                type = \"integer\", minimum = 1000, default = 10000\n            },\n            redis_keepalive_pool = {\n                type = \"integer\", minimum = 1, default = 100\n            }\n        },\n        required = {\"redis_cluster_nodes\", \"redis_cluster_name\"},\n    },\n}\n\nlocal limit_conn_redis_cluster_schema = policy_to_additional_properties[\"redis-cluster\"]\nlimit_conn_redis_cluster_schema.properties.key_ttl = {\n    type = \"integer\", default = 3600,\n}\n\nlocal limit_conn_redis_schema = policy_to_additional_properties[\"redis\"]\nlimit_conn_redis_schema.properties.key_ttl = {\n    type = \"integer\", default = 3600,\n}\n\nlocal _M = {\n    schema = policy_to_additional_properties,\n    limit_conn_redis_cluster_schema = limit_conn_redis_cluster_schema,\n    limit_conn_redis_schema = limit_conn_redis_schema,\n}\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/redis.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal redis_new     = require(\"resty.redis\").new\nlocal core          = require(\"apisix.core\")\n\n\nlocal _M = {version = 0.1}\n\nlocal function redis_cli(conf)\n    local red = redis_new()\n    local timeout = conf.redis_timeout or 1000    -- default 1sec\n\n    red:set_timeouts(timeout, timeout, timeout)\n\n    local sock_opts = {\n        ssl = conf.redis_ssl,\n        ssl_verify = conf.redis_ssl_verify\n    }\n\n    local ok, err = red:connect(conf.redis_host, conf.redis_port or 6379, sock_opts)\n    if not ok then\n        core.log.error(\" redis connect error, error: \", err)\n        return false, err\n    end\n\n    local count\n    count, err = red:get_reused_times()\n    core.log.debug(\"redis connection reused times: \", count)\n    if 0 == count then\n        if conf.redis_password and conf.redis_password ~= '' then\n            local ok, err\n            if conf.redis_username then\n                ok, err = red:auth(conf.redis_username, conf.redis_password)\n            else\n                ok, err = red:auth(conf.redis_password)\n            end\n            if not ok then\n                return nil, err\n            end\n        end\n\n        -- select db\n        if conf.redis_database ~= 0 then\n            local ok, err = red:select(conf.redis_database)\n            if not ok then\n                return false, \"failed to change redis db, err: \" .. err\n            end\n        end\n    elseif err then\n        -- core.log.info(\" err: \", err)\n        return nil, err\n    end\n    return red, nil\nend\n\n\nfunction _M.new(conf)\n    return redis_cli(conf)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/rediscluster.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal rediscluster      = require(\"resty.rediscluster\")\nlocal core              = require(\"apisix.core\")\nlocal ipairs            = ipairs\n\nlocal _M = {version = 0.1}\n\nlocal function new_redis_cluster(conf, dict_name)\n    local config = {\n        name = conf.redis_cluster_name,\n        serv_list = {},\n        keepalive_timeout = conf.redis_keepalive_timeout, -- redis connection pool idle timeout\n        keepalive_cons = conf.redis_keepalive_pool,       -- redis connection pool size\n        connect_timeout = conf.redis_timeout,\n        read_timeout = conf.redis_timeout,\n        send_timeout = conf.redis_timeout,\n        auth = conf.redis_password,\n        dict_name = dict_name,\n        connect_opts = {\n            ssl = conf.redis_cluster_ssl,\n            ssl_verify = conf.redis_cluster_ssl_verify,\n        }\n    }\n\n    for i, conf_item in ipairs(conf.redis_cluster_nodes) do\n        local host, port, err = core.utils.parse_addr(conf_item)\n        if err then\n            return nil, \"failed to parse address: \" .. conf_item\n                        .. \" err: \" .. err\n        end\n\n        config.serv_list[i] = {ip = host, port = port}\n    end\n\n    local red_cli, err = rediscluster:new(config)\n    if not red_cli then\n        return nil, \"failed to new redis cluster: \" .. err\n    end\n\n    return red_cli\nend\n\n\nfunction _M.new(conf, dict_name)\n     return new_redis_cluster(conf, dict_name)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/rfc5424.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal LOG_EMERG     = 0       --  system is unusable\nlocal LOG_ALERT     = 1       --  action must be taken immediately\nlocal LOG_CRIT      = 2       --  critical conditions\nlocal LOG_ERR       = 3       --  error conditions\nlocal LOG_WARNING   = 4       --  warning conditions\nlocal LOG_NOTICE    = 5       --  normal but significant condition\nlocal LOG_INFO      = 6       --  informational\nlocal LOG_DEBUG     = 7       --  debug-level messages\n\nlocal LOG_KERN      = 0       --  kernel messages\nlocal LOG_USER      = 1       --  random user-level messages\nlocal LOG_MAIL      = 2       --  mail system\nlocal LOG_DAEMON    = 3       --  system daemons\nlocal LOG_AUTH      = 4       --  security/authorization messages\nlocal LOG_SYSLOG    = 5       --  messages generated internally by syslogd\nlocal LOG_LPR       = 6       --  line printer subsystem\nlocal LOG_NEWS      = 7       --  network news subsystem\nlocal LOG_UUCP      = 8       --  UUCP subsystem\nlocal LOG_CRON      = 9       --  clock daemon\nlocal LOG_AUTHPRIV  = 10      --  security/authorization messages (private)\nlocal LOG_FTP       = 11      --  FTP daemon\nlocal LOG_LOCAL0    = 16      --  reserved for local use\nlocal LOG_LOCAL1    = 17      --  reserved for local use\nlocal LOG_LOCAL2    = 18      --  reserved for local use\nlocal LOG_LOCAL3    = 19      --  reserved for local use\nlocal LOG_LOCAL4    = 20      --  reserved for local use\nlocal LOG_LOCAL5    = 21      --  reserved for local use\nlocal LOG_LOCAL6    = 22      --  reserved for local use\nlocal LOG_LOCAL7    = 23      --  reserved for local use\n\nlocal Facility = {\n    KERN = LOG_KERN,\n    USER = LOG_USER,\n    MAIL = LOG_MAIL,\n    DAEMON = LOG_DAEMON,\n    AUTH = LOG_AUTH,\n    SYSLOG = LOG_SYSLOG,\n    LPR = LOG_LPR,\n    NEWS = LOG_NEWS,\n    UUCP = LOG_UUCP,\n    CRON = LOG_CRON,\n    AUTHPRIV = LOG_AUTHPRIV,\n    FTP = LOG_FTP,\n    LOCAL0 = LOG_LOCAL0,\n    LOCAL1 = LOG_LOCAL1,\n    LOCAL2 = LOG_LOCAL2,\n    LOCAL3 = LOG_LOCAL3,\n    LOCAL4 = LOG_LOCAL4,\n    LOCAL5 = LOG_LOCAL5,\n    LOCAL6 = LOG_LOCAL6,\n    LOCAL7 = LOG_LOCAL7,\n}\n\nlocal Severity = {\n    EMEGR = LOG_EMERG,\n    ALERT = LOG_ALERT,\n    CRIT = LOG_CRIT,\n    ERR = LOG_ERR,\n    WARNING = LOG_WARNING,\n    NOTICE = LOG_NOTICE,\n    INFO = LOG_INFO,\n    DEBUG = LOG_DEBUG,\n}\n\nlocal log_util = require(\"apisix.utils.log-util\")\nlocal ipairs = ipairs\nlocal str_format = string.format\n\nlocal _M = { version = 0.1 }\n\n\nfunction _M.encode(facility, severity, hostname, appname, pid, msg, structured_data)\n    local pri = (Facility[facility] * 8 + Severity[severity])\n    local t = log_util.get_rfc3339_zulu_timestamp()\n    if not hostname then\n        hostname = \"-\"\n    end\n\n    if not appname then\n        appname = \"-\"\n    end\n\n    local structured_data_str = \"-\"\n\n    if structured_data then\n        structured_data_str = \"[logservice\"\n        for _, sd_param in ipairs(structured_data) do\n            structured_data_str = structured_data_str .. \" \" .. sd_param.name\n                                  .. \"=\\\"\" .. sd_param.value .. \"\\\"\"\n        end\n        structured_data_str = structured_data_str .. \"]\"\n    end\n\n    return str_format(\"<%d>1 %s %s %s %d - %s %s\\n\", pri, t, hostname,\n                    appname, pid, structured_data_str, msg)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/router.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal resty_router = require(\"resty.radixtree\")\n\n\nlocal _M = {}\n\ndo\n    local router_opts = {\n        no_param_match = true\n    }\n\nfunction _M.new(routes)\n    return resty_router.new(routes, router_opts)\nend\n\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/span.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal tablepool = require(\"tablepool\")\nlocal util = require(\"opentelemetry.util\")\nlocal span_status = require(\"opentelemetry.trace.span_status\")\nlocal setmetatable = setmetatable\nlocal table = table\nlocal new_tab = require(\"table.new\")\nlocal select = select\nlocal pool_name = \"opentelemetry_span\"\nlocal update_time = ngx.update_time\n\nlocal _M = {}\n\n\nlocal mt = {\n    __index = _M\n}\n\nlocal function get_time()\n    update_time()\n    return util.time_nano()\nend\n\n\n\nlocal function append_child(sp, child_id)\n    if not sp.child_ids then\n        sp.child_ids = new_tab(10, 0)\n    end\n    table.insert(sp.child_ids, child_id)\nend\n\n\nlocal function set_parent(sp, parent_id)\n    sp.parent_id = parent_id\nend\n\n\nfunction _M.new(ctx, name, kind)\n    local tracing = ctx.tracing\n\n    local self = tablepool.fetch(pool_name, 0, 16)\n    self.start_time = get_time()\n    self.name = name\n    self.kind = kind\n\n    table.insert(tracing.spans, self)\n    local id = #tracing.spans\n    self.id = id\n\n    local parent = tracing.current_span\n    if parent then\n        set_parent(self, parent.id)\n        append_child(parent, id)\n    else\n        tracing.root_span = self\n    end\n\n    ctx.tracing.current_span = self\n    return setmetatable(self, mt)\nend\n\n\nfunction _M.set_status(self, code, message)\n    code = span_status.validate(code)\n    local status = self.status\n    if not status then\n        status = {\n            code = code,\n            message = \"\"\n        }\n        self.status = status\n    else\n        status.code = code\n    end\n\n    if code == span_status.ERROR then\n        status.message = message\n    end\nend\n\n\nfunction _M.set_attributes(self, ...)\n    if not self.attributes then\n        self.attributes = new_tab(10, 0)\n    end\n    local count = select('#', ...)\n    for i = 1, count do\n        local attr = select(i, ...)\n        table.insert(self.attributes, attr)\n    end\nend\n\n\nfunction _M.finish(self, ctx)\n    local tracing = ctx.tracing\n    self.end_time = get_time()\n    if not self.parent_id then\n        return\n    end\n    tracing.current_span = tracing.spans[self.parent_id]\nend\n\n\nfunction _M.release(self)\n    tablepool.release(pool_name, self)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/trusted-addresses.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal require       = require\nlocal core          = require(\"apisix.core\")\n\nlocal trusted_addresses_matcher\n\nlocal _M = {}\n\n\nfunction _M.init_worker()\n    local local_conf = core.config.local_conf()\n    local trusted_addresses = core.table.try_read_attr(local_conf, \"apisix\", \"trusted_addresses\")\n\n    if not trusted_addresses then\n        core.log.info(\"trusted_addresses is not configured\")\n        return\n    end\n\n    local matcher, err = core.ip.create_ip_matcher(trusted_addresses)\n    if not matcher then\n        core.log.error(\"failed to create ip matcher for trusted_addresses: \", err)\n        return\n    end\n\n    trusted_addresses_matcher = matcher\nend\n\n\nfunction _M.is_trusted(address)\n    if not trusted_addresses_matcher then\n        core.log.info(\"trusted_addresses_matcher is not initialized\")\n        return false\n    end\n    return trusted_addresses_matcher:match(address)\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/utils/upstream.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal ipmatcher = require(\"resty.ipmatcher\")\nlocal ipairs = ipairs\nlocal type = type\nlocal tostring = tostring\nlocal resource = require(\"apisix.resource\")\nlocal tracer    = require(\"apisix.tracer\")\nlocal ngx = ngx\n\nlocal _M = {}\n\n\nlocal function sort_by_key_host(a, b)\n    return a.host < b.host\nend\n\n\nlocal function compare_upstream_node(up_conf, new_t)\n    if up_conf == nil then\n        return false\n    end\n\n    -- fast path\n    local old_t = up_conf.nodes\n    if old_t == new_t then\n        return true\n    end\n\n    if type(old_t) ~= \"table\" then\n        return false\n    end\n\n    -- slow path\n    core.log.debug(\"compare upstream nodes by value, \",\n                    \"old: \", tostring(old_t) , \" \", core.json.delay_encode(old_t, true),\n                    \"new: \", tostring(new_t) , \" \", core.json.delay_encode(new_t, true))\n\n    if up_conf.original_nodes then\n        -- if original_nodes is set, it means that the upstream nodes\n        -- are changed by `fill_node_info`, so we need to compare the new nodes with the\n        -- original nodes.\n        old_t = up_conf.original_nodes\n    end\n\n    if #new_t ~= #old_t then\n        return false\n    end\n\n    core.table.sort(old_t, sort_by_key_host)\n    core.table.sort(new_t, sort_by_key_host)\n\n    for i = 1, #new_t do\n        local new_node = new_t[i]\n        local old_node = old_t[i]\n        for _, name in ipairs({\"host\", \"port\", \"weight\", \"priority\", \"metadata\"}) do\n            if new_node[name] ~= old_node[name] then\n                return false\n            end\n        end\n    end\n\n    return true\nend\n_M.compare_upstream_node = compare_upstream_node\n\n\nlocal function parse_domain_for_nodes(nodes)\n    local span = tracer.start(ngx.ctx, \"resolve_dns\", tracer.kind.internal)\n    local new_nodes = core.table.new(#nodes, 0)\n    for _, node in ipairs(nodes) do\n        local host = node.host\n        if not ipmatcher.parse_ipv4(host) and\n                not ipmatcher.parse_ipv6(host) then\n            local ip, err = core.resolver.parse_domain(host)\n            if ip then\n                local new_node = core.table.clone(node)\n                new_node.host = ip\n                new_node.domain = host\n                core.table.insert(new_nodes, new_node)\n            end\n\n            if err then\n                core.log.error(\"dns resolver domain: \", host, \" error: \", err)\n            end\n        else\n            core.table.insert(new_nodes, node)\n        end\n    end\n    span:finish(ngx.ctx)\n    return new_nodes\nend\n_M.parse_domain_for_nodes = parse_domain_for_nodes\n\n\nfunction _M.parse_domain_in_up(up)\n    local nodes = up.value.dns_nodes\n    local new_nodes, err = parse_domain_for_nodes(nodes)\n    if not new_nodes then\n        return nil, err\n    end\n\n    local ok = compare_upstream_node(up.value, new_nodes)\n    if ok then\n        return up\n    end\n\n    local nodes_ver = resource.get_nodes_ver(up.value.resource_key)\n    if not nodes_ver then\n        nodes_ver = 0\n    end\n    nodes_ver = nodes_ver + 1\n    up.value._nodes_ver = nodes_ver\n    up.value.nodes = new_nodes\n    resource.set_nodes_ver_and_nodes(up.value.resource_key, nodes_ver, new_nodes)\n\n    core.log.info(\"resolve upstream which contain domain: \",\n                  core.json.delay_encode(up, true))\n    return up\nend\n\n\nfunction _M.version(index, nodes_ver)\n    if not index then\n        return\n    end\n    return index .. tostring(nodes_ver or '')\nend\n\nreturn _M\n"
  },
  {
    "path": "apisix/wasm.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal type = type\nlocal support_wasm, wasm = pcall(require, \"resty.proxy-wasm\")\nlocal ngx_var = ngx.var\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        conf = {\n            oneOf = {\n                { type = \"object\", minProperties = 1},\n                { type = \"string\", minLength = 1},\n            }\n        },\n    },\n    required = {\"conf\"}\n}\nlocal _M = {}\n\n\nlocal function check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nlocal function get_plugin_ctx_key(ctx)\n    return ctx.conf_type .. \"#\" .. ctx.conf_id\nend\n\nlocal function fetch_plugin_ctx(conf, ctx, plugin)\n    if not conf.plugin_ctxs then\n        conf.plugin_ctxs = {}\n    end\n\n    local ctxs = conf.plugin_ctxs\n    local key = get_plugin_ctx_key(ctx)\n    local plugin_ctx = ctxs[key]\n    local err\n    if not plugin_ctx then\n        if type(conf.conf) == \"table\" then\n            plugin_ctx, err = wasm.on_configure(plugin, core.json.encode(conf.conf))\n        elseif type(conf.conf) == \"string\" then\n            plugin_ctx, err = wasm.on_configure(plugin, conf.conf)\n        else\n            return nil, \"invalid conf type\"\n        end\n        if not plugin_ctx then\n            return nil, err\n        end\n\n        ctxs[key] = plugin_ctx\n    end\n\n    return plugin_ctx\nend\n\n\nlocal function http_request_wrapper(self, conf, ctx)\n    local name = self.name\n    local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)\n    if not plugin_ctx then\n        core.log.error(name, \": failed to fetch wasm plugin ctx: \", err)\n        return 503\n    end\n\n    local ok, err = wasm.on_http_request_headers(plugin_ctx)\n    if not ok then\n        core.log.error(name, \": failed to run wasm plugin: \", err)\n        return 503\n    end\n\n    -- $wasm_process_req_body is predefined in ngx_tpl.lua\n    local handle_body = ngx_var.wasm_process_req_body\n    if handle_body ~= '' then\n        -- reset the flag so we can use it for the next Wasm plugin\n        -- use ngx.var to bypass the cache\n        ngx_var.wasm_process_req_body = ''\n\n        local body, err = core.request.get_body()\n        if err ~= nil then\n            core.log.error(name, \": failed to get request body: \", err)\n            return 503\n        end\n\n        local ok, err = wasm.on_http_request_body(plugin_ctx, body, true)\n        if not ok then\n            core.log.error(name, \": failed to run wasm plugin: \", err)\n            return 503\n        end\n    end\nend\n\n\nlocal function header_filter_wrapper(self, conf, ctx)\n    local name = self.name\n    local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)\n    if not plugin_ctx then\n        core.log.error(name, \": failed to fetch wasm plugin ctx: \", err)\n        return 503\n    end\n\n    local ok, err = wasm.on_http_response_headers(plugin_ctx)\n    if not ok then\n        core.log.error(name, \": failed to run wasm plugin: \", err)\n        return 503\n    end\n\n    -- $wasm_process_resp_body is predefined in ngx_tpl.lua\n    local handle_body = ngx_var.wasm_process_resp_body\n    if handle_body ~= '' then\n        -- reset the flag so we can use it for the next Wasm plugin\n        -- use ngx.var to bypass the cache\n        ngx_var.wasm_process_resp_body = \"\"\n        ctx[\"wasm_\" .. name .. \"_process_resp_body\"] = true\n    end\nend\n\n\nlocal function body_filter_wrapper(self, conf, ctx)\n    local name = self.name\n\n    local enabled = ctx[\"wasm_\" .. name .. \"_process_resp_body\"]\n    if not enabled then\n        return\n    end\n\n    local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)\n    if not plugin_ctx then\n        core.log.error(name, \": failed to fetch wasm plugin ctx: \", err)\n        return\n    end\n\n    local ok, err = wasm.on_http_response_body(plugin_ctx)\n    if not ok then\n        core.log.error(name, \": failed to run wasm plugin: \", err)\n        return\n    end\nend\n\n\nfunction _M.require(attrs)\n    if not support_wasm then\n        return nil, \"need to build APISIX-Runtime to support wasm\"\n    end\n\n    local name = attrs.name\n    local priority = attrs.priority\n    local plugin, err = wasm.load(name, attrs.file)\n    if not plugin then\n        return nil, err\n    end\n\n    local mod = {\n        version = 0.1,\n        name = name,\n        priority = priority,\n        schema = schema,\n        check_schema = check_schema,\n        plugin = plugin,\n        type = \"wasm\",\n    }\n\n    if attrs.http_request_phase == \"rewrite\" then\n        mod.rewrite = function (conf, ctx)\n            return http_request_wrapper(mod, conf, ctx)\n        end\n    else\n        mod.access = function (conf, ctx)\n            return http_request_wrapper(mod, conf, ctx)\n        end\n    end\n\n    mod.header_filter = function (conf, ctx)\n        return header_filter_wrapper(mod, conf, ctx)\n    end\n\n    mod.body_filter = function (conf, ctx)\n        return body_filter_wrapper(mod, conf, ctx)\n    end\n\n    -- the returned values need to be the same as the Lua's 'require'\n    return true, mod\nend\n\n\nreturn _M\n"
  },
  {
    "path": "apisix-master-0.rockspec",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\npackage = \"apisix\"\nversion = \"master-0\"\nsupported_platforms = {\"linux\"}\n\nsource = {\n    url = \"git://github.com/apache/apisix\",\n    branch = \"master\",\n}\n\ndescription = {\n    summary = \"Apache APISIX is a cloud-native microservices API gateway, delivering the ultimate performance, security, open source and scalable platform for all your APIs and microservices.\",\n    homepage = \"https://github.com/apache/apisix\",\n    license = \"Apache License 2.0\",\n}\n\ndependencies = {\n    \"lua-resty-ctxdump = 0.1-0\",\n    \"lyaml = 6.2.8-1\",\n    \"api7-lua-resty-dns-client = 7.1.0-0\",\n    \"lua-resty-template = 2.0-1\",\n    \"lua-resty-etcd = 1.10.6-0\",\n    \"api7-lua-resty-http = 0.2.3-0\",\n    \"lua-resty-balancer = 0.05-0\",\n    \"lua-resty-ngxvar = 0.5.2-0\",\n    \"lua-resty-jit-uuid = 0.0.7-2\",\n    \"lua-resty-ksuid = 1.0.1-0\",\n    \"lua-resty-healthcheck-api7 = 3.2.1-0\",\n    \"api7-lua-resty-jwt = 0.2.6-0\",\n    \"lua-resty-hmac-ffi = 0.06-1\",\n    \"lua-resty-cookie = 0.4.1-1\",\n    \"lua-resty-session = 4.1.5-1\",\n    \"opentracing-openresty = 0.1-0\",\n    \"lua-resty-radixtree = 2.9.2-0\",\n    \"lua-protobuf = 0.5.3-1\",\n    \"lua-resty-openidc = 1.8.0-1\",\n    \"luafilesystem = 1.8.0-1\",\n    \"nginx-lua-prometheus-api7 = 0.20250302-1\",\n    \"jsonschema = 0.9.9-0\",\n    \"lua-resty-ipmatcher = 0.6.1-0\",\n    \"lua-resty-kafka = 0.23-0\",\n    \"lua-resty-logger-socket = 2.0.3-0\",\n    \"skywalking-nginx-lua = 1.0.1-0\",\n    \"base64 = 1.5-3\",\n    \"binaryheap = 0.4-1\",\n    \"api7-dkjson = 0.1.1-0\",\n    \"resty-redis-cluster = 1.05-1\",\n    \"lua-resty-expr = 1.3.2\",\n    \"graphql = 0.0.2-1\",\n    \"argparse = 0.7.1-1\",\n    \"luasocket = 3.1.0-1\",\n    \"luasec = 1.3.2-1\",\n    \"lua-resty-consul = 0.3-2\",\n    \"penlight = 1.14.0-3\",\n    \"ext-plugin-proto = 0.6.1-0\",\n    \"casbin = 1.46.0-1\",\n    \"inspect == 3.1.3-0\",\n    \"lua-resty-rocketmq = 0.4.2-0\",\n    \"opentelemetry-lua = 0.2-6\",\n    \"net-url = 1.2-1\",\n    \"xml2lua = 1.6-2\",\n    \"nanoid = 0.1-1\",\n    \"lua-resty-mediador = 0.1.2-1\",\n    \"lua-resty-ldap = 0.1.0-0\",\n    \"lua-resty-t1k = 1.1.6-0\",\n    \"brotli-ffi = 0.3-1\",\n    \"lua-ffi-zlib = 0.6-0\",\n    \"jsonpath = 1.0-1\",\n    \"api7-lua-resty-aws == 2.0.2-1\",\n    \"multipart = 0.5.11-1\",\n    \"luautf8 = 0.2.0-1\",\n}\n\nbuild = {\n    type = \"make\",\n    build_variables = {\n        CFLAGS=\"$(CFLAGS)\",\n        LIBFLAG=\"$(LIBFLAG)\",\n        LUA_LIBDIR=\"$(LUA_LIBDIR)\",\n        LUA_BINDIR=\"$(LUA_BINDIR)\",\n        LUA_INCDIR=\"$(LUA_INCDIR)\",\n        LUA=\"$(LUA)\",\n        OPENSSL_INCDIR=\"$(OPENSSL_INCDIR)\",\n        OPENSSL_LIBDIR=\"$(OPENSSL_LIBDIR)\",\n    },\n    install_variables = {\n        ENV_INST_PREFIX=\"$(PREFIX)\",\n        ENV_INST_BINDIR=\"$(BINDIR)\",\n        ENV_INST_LIBDIR=\"$(LIBDIR)\",\n        ENV_INST_LUADIR=\"$(LUADIR)\",\n        ENV_INST_CONFDIR=\"$(CONFDIR)\",\n    },\n}\n"
  },
  {
    "path": "autodocs/config.ld",
    "content": "project='Apache APISIX'\ntitle='Plugin Develop Docs'\ndescription='Functions in APISIX core'\nformat='markdown'\nbacktick_references = false\nno_lua_ref = true\nall = false\nno_space_before_args = true\next = \"md\"\ntemplate = true -- use the ldoc.ltp as markdown template\ntemplate_escape = \">\"\n"
  },
  {
    "path": "autodocs/generate.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\n# workdir is the root of the apisix, use command: autodocs/generate.sh build to generate the docs,\n# and the output will be in the workdir/autodocs/output/ directory.\nbuild() {\n    # install dependencies\n    apt-get -y update --fix-missing\n    apt-get -y install lua5.1 liblua5.1-0-dev\n    curl https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh -sL | bash -\n    luarocks install ldoc\n\n    # generate docs\n    rm -rf autodocs/output || true\n    mkdir autodocs/output || true\n    cd autodocs/output\n    find ../../apisix/core -name \"*.lua\" -type f -exec ldoc -c ../config.ld {} \\;\n\n    # generate the markdown files' name\n    rm ../md_files_name.txt || true\n    output=\"./\"\n    mds=$(ls $output)\n    for md in $mds\n    do\n       echo $md >> ../md_files_name.txt\n    done\n}\n\ncase_opt=$1\ncase $case_opt in\n    (build)\n        build\n        ;;\nesac\n"
  },
  {
    "path": "autodocs/ldoc.ltp",
    "content": "> local iter = ldoc.modules.iter\n> local display_name = ldoc.display_name\n> local function trim_newline(s)\n>   return (s:gsub(\"\\n\", \"\\r\"))\n> end\n---\ntitle: APISIX Plugin Development Docs\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## $(ldoc.title)\n\n### $(module.name)\n\n$(module.summary) $(module.description)\n>\n> for kind, items in module.kinds() do\n>    for item in items() do\n\n#### $(trim_newline(display_name(item)))\n>       if item.type == \"function\" then\n>          if item.summary and item.summary ~= '' then\n\n**Summary**: $(item.summary)\n>          end -- if item.summary\n>          if item.description and item.description ~= '' then\n\n**Description**:\n\n```text$(trim_newline(item.description))\n```\n>          end -- if item.description\n>       end -- if item.type\n>       if item.params and #item.params > 0 then\n>          local subnames = module.kinds:type_of(item).subnames\n>          if subnames then\n\n**$(subnames)**\n\n>          end -- if subnames\n>          -- print the parameters\n>          for par in iter(item.params) do\n>             local param = item:subparam(par)\n>             for p in iter(param) do\n>                local name = item:display_name_of(p)\n>                local tp  = item:type_of_param(p)\n* **$(name)**($(tp)):$(item.params.map[p])\n>                if tp ~= '' then\n>                end -- if tp\n>\n>             end -- for p\n>          end -- for par\n>       end -- if item.params and #item.params > 0\n>\n>       -- print the returns\n>       if item.retgroups then\n>           local groups = item.retgroups\n\n**Returns:**\n\n>          for i, group in ldoc.ipairs(groups) do\n>             for r in group:iter() do\n>                local type, ctypes = item:return_type(r);\n* `$(type)`: $(r.text)\n>             end -- for r in group:iter()\n>          end -- for i,group\n>       end -- if item.retgroups\n\n>       if item.usage then\n**Usage**\n\n>          for usage in item.usage:iter() do\n```lua\n$(trim_newline(usage))\n```\n>          end -- for usage in item.usage:iter()\n>          local usage = item.usage\n>       end -- if item.usage\n>    end -- end for item in items()\n> end -- for kinds, items\n"
  },
  {
    "path": "benchmark/fake-apisix/conf/cert/apisix.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "benchmark/fake-apisix/conf/cert/apisix.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5\njhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo\neLj0efMiOepOSZflj9Ob4yKR2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5s\nmPtW1Oc/BV5terhscJdOgmRrabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt\n6iMWEGeQU6mwPENgvj1olji2WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiy\nVt1TmtMWn1ztk6FfLRqwJWR/Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1o\nnpRVeXhrBajbCRDRBMwaNw/1/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2\nfzaqpIfyUbPST4GdqNG9NyIh/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI\n1cGrGwyXbrieNp63AgMBAAECggGBAJM8g0duoHmIYoAJzbmKe4ew0C5fZtFUQNmu\nO2xJITUiLT3ga4LCkRYsdBnY+nkK8PCnViAb10KtIT+bKipoLsNWI9Xcq4Cg4G3t\n11XQMgPPgxYXA6m8t+73ldhxrcKqgvI6xVZmWlKDPn+CY/Wqj5PA476B5wEmYbNC\nGIcd1FLl3E9Qm4g4b/sVXOHARF6iSvTR+6ol4nfWKlaXSlx2gNkHuG8RVpyDsp9c\nz9zUqAdZ3QyFQhKcWWEcL6u9DLBpB/gUjyB3qWhDMe7jcCBZR1ALyRyEjmDwZzv2\njlv8qlLFfn9R29UI0pbuL1eRAz97scFOFme1s9oSU9a12YHfEd2wJOM9bqiKju8y\nDZzePhEYuTZ8qxwiPJGy7XvRYTGHAs8+iDlG4vVpA0qD++1FTpv06cg/fOdnwshE\nOJlEC0ozMvnM2rZ2oYejdG3aAnUHmSNa5tkJwXnmj/EMw1TEXf+H6+xknAkw05nh\nzsxXrbuFUe7VRfgB5ElMA/V4NsScgQKBwQDmMRtnS32UZjw4A8DsHOKFzugfWzJ8\nGc+3sTgs+4dNIAvo0sjibQ3xl01h0BB2Pr1KtkgBYB8LJW/FuYdCRS/KlXH7PHgX\n84gYWImhNhcNOL3coO8NXvd6+m+a/Z7xghbQtaraui6cDWPiCNd/sdLMZQ/7LopM\nRbM32nrgBKMOJpMok1Z6zsPzT83SjkcSxjVzgULNYEp03uf1PWmHuvjO1yELwX9/\ngoACViF+jst12RUEiEQIYwr4y637GQBy+9cCgcEA3pN9W5OjSPDVsTcVERig8++O\nBFURiUa7nXRHzKp2wT6jlMVcu8Pb2fjclxRyaMGYKZBRuXDlc/RNO3uTytGYNdC2\nIptU5N4M7iZHXj190xtDxRnYQWWo/PR6EcJj3f/tc3Itm1rX0JfuI3JzJQgDb9Z2\ns/9/ub8RRvmQV9LM/utgyOwNdf5dyVoPcTY2739X4ZzXNH+CybfNa+LWpiJIVEs2\ntxXbgZrhmlaWzwA525nZ0UlKdfktdcXeqke9eBghAoHARVTHFy6CjV7ZhlmDEtqE\nU58FBOS36O7xRDdpXwsHLnCXhbFu9du41mom0W4UdzjgVI9gUqG71+SXrKr7lTc3\ndMHcSbplxXkBJawND/Q1rzLG5JvIRHO1AGJLmRgIdl8jNgtxgV2QSkoyKlNVbM2H\nWy6ZSKM03lIj74+rcKuU3N87dX4jDuwV0sPXjzJxL7NpR/fHwgndgyPcI14y2cGz\nzMC44EyQdTw+B/YfMnoZx83xaaMNMqV6GYNnTHi0TO2TAoHBAKmdrh9WkE2qsr59\nIoHHygh7Wzez+Ewr6hfgoEK4+QzlBlX+XV/9rxIaE0jS3Sk1txadk5oFDebimuSk\nlQkv1pXUOqh+xSAwk5v88dBAfh2dnnSa8HFN3oz+ZfQYtnBcc4DR1y2X+fVNgr3i\nnxruU2gsAIPFRnmvwKPc1YIH9A6kIzqaoNt1f9VM243D6fNzkO4uztWEApBkkJgR\n4s/yOjp6ovS9JG1NMXWjXQPcwTq3sQVLnAHxZRJmOvx69UmK4QKBwFYXXjeXiU3d\nbcrPfe6qNGjfzK+BkhWznuFUMbuxyZWDYQD5yb6ukUosrj7pmZv3BxKcKCvmONU+\nCHgIXB+hG+R9S2mCcH1qBQoP/RSm+TUzS/Bl2UeuhnFZh2jSZQy3OwryUi6nhF0u\nLDzMI/6aO1ggsI23Ri0Y9ZtqVKczTkxzdQKR9xvoNBUufjimRlS80sJCEB3Qm20S\nwzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "benchmark/fake-apisix/conf/cert/openssl.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n[req]\ndistinguished_name = req_distinguished_name\nx509_extensions = v3_req\nprompt = no\n\n[req_distinguished_name]\nC = CN\nST = GuangDong\nL = ZhuHai\nO = iresty\nCN = test.com\n\n[v3_req]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = CA:TRUE\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = test.com\nDNS.2 = *.test.com\n\n## openssl genrsa -out apisix.key 3072 -nodes\n## openssl req -new -x509 -key apisix.key -sha256 -config openssl.conf -out apisix.crt -days 36500\n"
  },
  {
    "path": "benchmark/fake-apisix/conf/nginx.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nmaster_process on;\n\nworker_processes 1;\n\nerror_log logs/error.log warn;\npid logs/nginx.pid;\n\nworker_rlimit_nofile 20480;\n\nevents {\n    worker_connections 10620;\n}\n\nworker_shutdown_timeout 3;\n\nhttp {\n    lua_package_path  \"$prefix/lua/?.lua;;\";\n\n    log_format main '$remote_addr - $remote_user [$time_local] $http_host \"$request\" $status $body_bytes_sent $request_time \"$http_referer\" \"$http_user_agent\" $upstream_addr $upstream_status $upstream_response_time';\n    access_log logs/access.log main buffer=16384 flush=5;\n\n    init_by_lua_block {\n        require \"resty.core\"\n        apisix = require(\"apisix\")\n        apisix.http_init()\n    }\n\n    init_worker_by_lua_block {\n        apisix.http_init_worker()\n    }\n\n    upstream apisix_backend {\n        server 0.0.0.1;\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n\n        keepalive 320;\n    }\n\n    server {\n        listen 9443 ssl;\n        ssl_certificate      cert/apisix.crt;\n        ssl_certificate_key  cert/apisix.key;\n        ssl_session_cache    shared:SSL:1m;\n\n        listen 9080;\n\n        server_tokens off;\n        more_set_headers 'Server: APISIX web server';\n\n        location = /apisix/nginx_status {\n            allow 127.0.0.0/24;\n            access_log off;\n            stub_status;\n        }\n\n        location /apisix/admin {\n            allow 127.0.0.0/24;\n            content_by_lua_block {\n                apisix.http_admin()\n            }\n        }\n\n        ssl_certificate_by_lua_block {\n            apisix.http_ssl_phase()\n        }\n\n        location / {\n            set $upstream_scheme             'http';\n            set $upstream_host               $http_host;\n            set $upstream_upgrade            '';\n            set $upstream_connection         '';\n            set $upstream_uri                '';\n\n            access_by_lua_block {\n                apisix.http_access_phase()\n            }\n\n            proxy_http_version 1.1;\n            proxy_set_header   Host              $upstream_host;\n            proxy_set_header   Upgrade           $upstream_upgrade;\n            proxy_set_header   Connection        $upstream_connection;\n            proxy_set_header   X-Real-IP         $remote_addr;\n            proxy_pass_header  Server;\n            proxy_pass_header  Date;\n\n            ### the following x-forwarded-* headers is to send to upstream server\n\n            set $var_x_forwarded_proto      $scheme;\n            set $var_x_forwarded_host       $host;\n            set $var_x_forwarded_port       $server_port;\n\n            proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;\n            proxy_set_header   X-Forwarded-Proto    $var_x_forwarded_proto;\n            proxy_set_header   X-Forwarded-Host     $var_x_forwarded_host;\n            proxy_set_header   X-Forwarded-Port     $var_x_forwarded_port;\n\n            # proxy pass\n            proxy_pass         $upstream_scheme://apisix_backend$upstream_uri;\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "benchmark/fake-apisix/lua/apisix.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal balancer = require \"ngx.balancer\"\nlocal _M = {version = 0.1}\n\nfunction _M.http_init()\nend\n\nfunction _M.http_init_worker()\nend\n\nlocal function fake_fetch()\n    ngx.ctx.ip = \"127.0.0.1\"\n    ngx.ctx.port = 1980\nend\n\nfunction _M.http_access_phase()\n    local uri = ngx.var.uri\n    local host = ngx.var.host\n    local method = ngx.req.get_method()\n    local remote_addr = ngx.var.remote_addr\n    fake_fetch(uri, host, method, remote_addr)\nend\n\nfunction _M.http_header_filter_phase()\n    if ngx.ctx then\n        -- do something\n    end\nend\n\nfunction _M.http_body_filter_phase()\n    if ngx.ctx then\n        -- do something\n    end\nend\n\nfunction _M.http_log_phase()\n    if ngx.ctx then\n        -- do something\n    end\nend\n\nfunction _M.http_admin()\nend\n\nfunction _M.http_ssl_phase()\n    if ngx.ctx then\n        -- do something\n    end\nend\n\nfunction _M.http_balancer_phase()\n    local ok, err = balancer.set_current_peer(ngx.ctx.ip, ngx.ctx.port)\n    if not ok then\n        ngx.log(ngx.ERR, \"failed to set the current peer: \", err)\n        return ngx.exit(500)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "benchmark/run.sh",
    "content": "#! /bin/bash -x\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nif [ -n \"$1\" ]; then\n    worker_cnt=$1\nelse\n    worker_cnt=1\nfi\n\nif [ -n \"$2\" ]; then\n    upstream_cnt=$2\nelse\n    upstream_cnt=1\nfi\n\nmkdir -p benchmark/server/logs\nmkdir -p benchmark/fake-apisix/logs\n\n\nmake init\n\nfake_apisix_cmd=\"openresty -p $PWD/benchmark/fake-apisix -c $PWD/benchmark/fake-apisix/conf/nginx.conf\"\nserver_cmd=\"openresty -p $PWD/benchmark/server -c $PWD/benchmark/server/conf/nginx.conf\"\n\ntrap 'onCtrlC' INT\nfunction onCtrlC () {\n    sudo killall wrk\n    sudo killall openresty\n    sudo ${fake_apisix_cmd} -s stop || exit 1\n    sudo ${server_cmd} -s stop || exit 1\n}\n\nfor up_cnt in $(seq 1 $upstream_cnt);\ndo\n    port=$((1979+$up_cnt))\n    nginx_listen=$nginx_listen\"listen $port;\"\n    upstream_nodes=$upstream_nodes\"\\\"127.0.0.1:$port\\\":1\"\n\n    if [ $up_cnt -lt $upstream_cnt ]; then\n        upstream_nodes=$upstream_nodes\",\"\n    fi\ndone\n\nsed  -i \"s/\\- proxy-mirror/#\\- proxy-mirror/g\" conf/config-default.yaml\nsed  -i \"s/\\- proxy-cache/#\\- proxy-cache/g\" conf/config-default.yaml\nsed  -i \"s/listen .*;/$nginx_listen/g\" benchmark/server/conf/nginx.conf\n\necho \"\nnginx_config:\n  worker_processes: ${worker_cnt}\n\" > conf/config.yaml\n\nsudo ${server_cmd} || exit 1\n\nmake run\n\nsleep 3\n\n#############################################\necho -e \"\\n\\napisix: $worker_cnt worker + $upstream_cnt upstream + no plugin\"\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            '$upstream_nodes'\n        }\n    }\n}'\n\nsleep 1\n\nwrk -d 5 -c 16 http://127.0.0.1:9080/hello\n\nsleep 1\n\nwrk -d 5 -c 16 http://127.0.0.1:9080/hello\n\nsleep 1\n\n#############################################\necho -e \"\\n\\napisix: $worker_cnt worker + $upstream_cnt upstream + 2 plugins (limit-count + prometheus)\"\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2000000000000,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        },\n        \"prometheus\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            '$upstream_nodes'\n        }\n    }\n}'\n\nsleep 3\n\nwrk -d 5 -c 16 http://127.0.0.1:9080/hello\n\nsleep 1\n\nwrk -d 5 -c 16 http://127.0.0.1:9080/hello\n\nsleep 1\n\nmake stop\n\n#############################################\necho -e \"\\n\\nfake empty apisix server: $worker_cnt worker\"\n\nsleep 1\n\nsed  -i \"s/worker_processes [0-9]*/worker_processes $worker_cnt/g\" benchmark/fake-apisix/conf/nginx.conf\n\nsudo ${fake_apisix_cmd} || exit 1\n\nsleep 1\n\nwrk -d 5 -c 16 http://127.0.0.1:9080/hello\n\nsleep 1\n\nwrk -d 5 -c 16 http://127.0.0.1:9080/hello\n\nsudo ${fake_apisix_cmd} -s stop || exit 1\n\nsudo ${server_cmd} -s stop || exit 1\n"
  },
  {
    "path": "benchmark/server/conf/nginx.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nmaster_process on;\n\nworker_processes 2;\n\nerror_log logs/error.log warn;\npid logs/nginx.pid;\n\nworker_rlimit_nofile 20480;\n\nevents {\n    accept_mutex off;\n    worker_connections 10620;\n}\n\nworker_shutdown_timeout 3;\n\nhttp {\n    server {\n        listen 1980;\n\n        access_log off;\n        location / {\n            echo_duplicate 1 \"1234567890\";\n        }\n    }\n}\n"
  },
  {
    "path": "bin/apisix",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nif [ -s './apisix/cli/apisix.lua' ]; then\n    # install via source\n    APISIX_LUA=./apisix/cli/apisix.lua\nelif [ -s '/usr/local/share/lua/5.1/apisix/cli/apisix.lua' ]; then\n    # install via luarock\n    APISIX_LUA=/usr/local/share/lua/5.1/apisix/cli/apisix.lua\nelse\n    # install via official rpm or docker\n    APISIX_LUA=/usr/local/apisix/apisix/cli/apisix.lua\nfi\n\n# find the openresty\nOR_BIN=$(command -v openresty || exit 1)\nOR_EXEC=${OR_BIN:-'/usr/local/openresty-debug/bin/openresty'}\nOR_VER=$(openresty -v 2>&1 | awk -F '/' '{print $2}' | awk -F '.' '{print $1 * 100 + $2}')\nLUA_VERSION=$(lua -v 2>&1| grep -E -o  \"Lua [0-9]+.[0-9]+\")\n\nif [[ -e $OR_EXEC && \"$OR_VER\" -ge 119 ]]; then\n    # OpenResty version is >= 1.19, use luajit by default\n    ROOT=$(${OR_EXEC} -V 2>&1 | grep prefix | grep -Eo 'prefix=(.*)/nginx\\s+--' | grep -Eo '/.*/')\n    # find the luajit binary of openresty\n    LUAJIT_BIN=\"$ROOT\"/luajit/bin/luajit\n\n    # use the luajit of openresty\n    echo \"$LUAJIT_BIN $APISIX_LUA $*\"\n    exec $LUAJIT_BIN $APISIX_LUA $*\nelse\n    echo \"ERROR: Please check the version of OpenResty and Lua, OpenResty 1.19+ + LuaJIT is required for Apache APISIX.\"\nfi\n"
  },
  {
    "path": "ci/backup-docker-images.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ntest_type=$1\n\necho \"started backing up, time: $(date)\"\nmkdir docker-images-backup\nsum=$(cat ci/pod/docker-compose.$test_type.yml | grep image | wc -l)\nspecial_tag=$(cat ci/pod/docker-compose.$test_type.yml | grep image: | awk '{print $2}' | awk 'ORS=NR%\"'$sum'\"?\" \":\"\\n\"{print}')\necho special: $special_tag\nopenwhisk_tag=\"openwhisk/action-nodejs-v14:nightly openwhisk/standalone:nightly\"\necho\necho special_tag: $special_tag\necho openwhisk_tag: $openwhisk_tag\necho\nall_tags=\"${special_tag} ${openwhisk_tag}\"\nto_pull=\"\"\n\nfor tag in $all_tags\ndo\n    if ! ( docker inspect $tag &> /dev/null )\n    then\n        to_pull=\"${to_pull} ${tag}\"\n    fi\ndone\n\necho to pull : $to_pull\n\nif [[ -n $to_pull ]]\nthen\n    echo \"$to_pull\" | xargs -P10 -n1 docker pull\nfi\n\ndocker save $special_tag $openwhisk_tag -o docker-images-backup/apisix-images.tar\necho \"docker save done, time: $(date)\"\n"
  },
  {
    "path": "ci/check_changelog_prs.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { execSync } from 'child_process';\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\n// Types\ninterface Version {\n    tag: string;\n    ref: string;\n}\n\ninterface PR {\n    number: number;\n    title: string;\n    commit: string;\n}\n\n// Configuration\nconst IGNORE_TYPES = [\n    'docs',\n    'chore',\n    'test',\n    'ci'\n];\n\nconst IGNORE_PRS = [\n    // 3.9.0\n    10655, 10857, 10858, 10887, 10959, 11029, 11041, 11053, 11055, 11061, 10976, 10984, 11025,\n    // 3.10.0\n    11105, 11128, 11169, 11171, 11280, 11333, 11081, 11202, 11469,\n    // 3.11.0\n    11463, 11570,\n    // 3.12.0\n    11769, 11816, 11881, 11905, 11924, 11926, 11973, 11991, 11992, 11829,\n    // 3.13.0\n    9945, 11420, 11765, 12036, 12048, 12057, 12076, 12122, 12123, 12168, 12199, 12218, 12225, 12272, 12277, 12300, 12306, 12329, 12353, 12364, 12375, 12358,\n    //3.14.0\n    8772, 12655,\n    // 3.15.0\n    12761, 12805, 12844, 12863, 12829, 12725, 12948\n];\n\n\nfunction getGitRef(version: string): string {\n    try {\n        execSync(`git rev-parse ${version}`, { stdio: 'ignore' });\n        return version;\n    } catch {\n        return 'HEAD';\n    }\n}\n\nfunction extractVersionsFromChangelog(): Version[] {\n    const changelogPath = join(process.cwd(), '..', 'CHANGELOG.md');\n    const content = readFileSync(changelogPath, 'utf-8');\n    const versionRegex = /^## ([0-9]+\\.[0-9]+\\.[0-9]+)/gm;\n    const versions: Version[] = [];\n    let match;\n\n    while ((match = versionRegex.exec(content)) !== null) {\n        const tag = match[1];\n        versions.push({\n            tag,\n            ref: getGitRef(tag)\n        });\n    }\n\n    return versions;\n}\n\nfunction extractPRsFromChangelog(startTag: string, endTag: string): number[] {\n    const changelogPath = join(process.cwd(), '..', 'CHANGELOG.md');\n    const content = readFileSync(changelogPath, 'utf-8');\n    const lines = content.split('\\n');\n    let inRange = false;\n    const prs: number[] = [];\n\n    for (const line of lines) {\n        if (line.startsWith(`## ${startTag}`)) {\n            inRange = true;\n            continue;\n        }\n        if (inRange && line.startsWith(`## ${endTag}`)) {\n            break;\n        }\n        if (inRange) {\n            const match = line.match(/#(\\d+)/);\n            if (match) {\n                prs.push(parseInt(match[1], 10));\n            }\n        }\n    }\n\n    return prs.sort((a, b) => a - b);\n}\n\nfunction shouldIgnoreCommitMessage(message: string): boolean {\n    // Extract the commit message part (remove the commit hash)\n    const messagePart = message.split(' ').slice(1).join(' ');\n\n    // Check if the message starts with any of the ignored types\n    for (const type of IGNORE_TYPES) {\n        // Check simple format: \"type: message\"\n        if (messagePart.startsWith(`${type}:`)) {\n            return true;\n        }\n        // Check format with scope: \"type(scope): message\"\n        if (messagePart.startsWith(`${type}(`)) {\n            const closingBracketIndex = messagePart.indexOf('):');\n            if (closingBracketIndex !== -1) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nfunction extractPRsFromGitLog(oldRef: string, newRef: string): PR[] {\n    const log = execSync(`git log ${oldRef}..${newRef} --oneline`, { encoding: 'utf-8' });\n    const prs: PR[] = [];\n\n    for (const line of log.split('\\n')) {\n        if (!line.trim()) continue;\n\n        // Check if this commit should be ignored\n        if (shouldIgnoreCommitMessage(line)) continue;\n\n        // Find PR number\n        const prMatch = line.match(/#(\\d+)/);\n        if (prMatch) {\n            const prNumber = parseInt(prMatch[1], 10);\n            if (!IGNORE_PRS.includes(prNumber)) {\n                prs.push({\n                    number: prNumber,\n                    title: line,\n                    commit: line.split(' ')[0]\n                });\n            }\n        }\n    }\n\n    return prs.sort((a, b) => a.number - b.number);\n}\n\nfunction findMissingPRs(changelogPRs: number[], gitPRs: PR[]): PR[] {\n    const changelogPRSet = new Set(changelogPRs);\n    return gitPRs.filter(pr => !changelogPRSet.has(pr.number));\n}\n\nfunction versionGreaterThan(v1: string, v2: string): boolean {\n    // Remove 'v' prefix if present\n    const cleanV1 = v1.replace(/^v/, '');\n    const cleanV2 = v2.replace(/^v/, '');\n\n    // Split version strings into arrays of numbers\n    const v1Parts = cleanV1.split('.').map(Number);\n    const v2Parts = cleanV2.split('.').map(Number);\n\n    // Compare each part\n    for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {\n        const v1Part = v1Parts[i] || 0;\n        const v2Part = v2Parts[i] || 0;\n\n        if (v1Part > v2Part) return true;\n        if (v1Part < v2Part) return false;\n    }\n\n    // If all parts are equal, return false\n    return false;\n}\n\n// Main function\nasync function main() {\n    try {\n        const versions = extractVersionsFromChangelog();\n        let hasErrors = false;\n\n        for (let i = 0; i < versions.length - 1; i++) {\n            const newVersion = versions[i];\n            const oldVersion = versions[i + 1];\n\n            // Skip if new version is less than or equal to 3.8.0\n            if (!versionGreaterThan(newVersion.tag, '3.8.0')) {\n                continue;\n            }\n\n            console.log(`\\n=== Checking changes between ${newVersion.tag} (${newVersion.ref}) and ${oldVersion.tag} (${oldVersion.ref}) ===`);\n\n            const changelogPRs = extractPRsFromChangelog(newVersion.tag, oldVersion.tag);\n            const gitPRs = extractPRsFromGitLog(oldVersion.ref, newVersion.ref);\n            const missingPRs = findMissingPRs(changelogPRs, gitPRs);\n\n            console.log(`\\n=== PR Comparison Results for ${newVersion.tag} ===`);\n\n            if (missingPRs.length === 0) {\n                console.log(`\\n✅ All PRs are included in CHANGELOG.md for version ${newVersion.tag}`);\n            } else {\n                console.log(`\\n❌ Missing PRs in CHANGELOG.md for version ${newVersion.tag} (sorted):`);\n                missingPRs.forEach(pr => {\n                    console.log(`  #${pr.number}`);\n                });\n\n                console.log(`\\nDetailed information about missing PRs for version ${newVersion.tag}:`);\n                missingPRs.forEach(pr => {\n                    console.log(`\\nPR #${pr.number}:`);\n                    console.log(`  - ${pr.title}`);\n                    console.log(`  - PR URL: https://github.com/apache/apisix/pull/${pr.number}`);\n                });\n\n                console.log('Note: If you confirm that a PR should not appear in the changelog, please add its number to the IGNORE_PRS array in this script.');\n                hasErrors = true;\n            }\n        }\n\n        if (hasErrors) {\n            process.exit(1);\n        }\n    } catch (error) {\n        console.error('Error:', error);\n        process.exit(1);\n    }\n}\n\n(async () => {\n    await main();\n})();\n"
  },
  {
    "path": "ci/common.sh",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\nexport_version_info() {\n    source ./.requirements\n}\n\nexport_or_prefix() {\n    export OPENRESTY_PREFIX=\"/usr/local/openresty\"\n\n    export PATH=$OPENRESTY_PREFIX/nginx/sbin:$OPENRESTY_PREFIX/luajit/bin:$OPENRESTY_PREFIX/bin:$PATH\n    export OPENSSL_PREFIX=$OPENRESTY_PREFIX/openssl3\n    export OPENSSL_BIN=$OPENSSL_PREFIX/bin/openssl\n}\n\ncreate_lua_deps() {\n    echo \"Create lua deps\"\n\n    make deps\n\n    # just for jwt-auth test\n    luarocks install lua-resty-openssl --tree deps\n\n    # maybe reopen this feature later\n    # luarocks install luacov-coveralls --tree=deps --local > build.log 2>&1 || (cat build.log && exit 1)\n    # for github action cache\n    chmod -R a+r deps\n}\n\nrerun_flaky_tests() {\n    if tail -1 \"$1\" | grep \"Result: PASS\"; then\n        exit 0\n    fi\n\n    if ! tail -1 \"$1\" | grep \"Result: FAIL\"; then\n        # CI failure not caused by failed test\n        exit 1\n    fi\n\n    local tests\n    local n_test\n    tests=\"$(awk '/^t\\/.*.t\\s+\\(.+ Failed: .+\\)/{ print $1 }' \"$1\")\"\n    n_test=\"$(echo \"$tests\" | wc -l)\"\n    if [ \"$n_test\" -gt 10 ]; then\n        # too many tests failed\n        exit 1\n    fi\n\n    echo \"Rerun $(echo \"$tests\" | xargs)\"\n    FLUSH_ETCD=1 prove --timer -I./test-nginx/lib -I./ $(echo \"$tests\" | xargs)\n}\n\nfail_on_bailout() {\n    local test_output_file=\"$1\"\n\n    # Check for bailout message in test output\n    if grep -q \"Bailout called.  Further testing stopped:\" \"$test_output_file\"; then\n        echo \"Error: Bailout detected in test output\"\n        exit 1\n    fi\n}\ninstall_curl () {\n    CURL_VERSION=\"8.13.0\"\n    wget -q https://github.com/stunnel/static-curl/releases/download/${CURL_VERSION}/curl-linux-x86_64-glibc-${CURL_VERSION}.tar.xz\n    tar -xf curl-linux-x86_64-glibc-${CURL_VERSION}.tar.xz\n    sudo cp curl /usr/bin\n    curl -V\n}\n\ninstall_apisix_runtime() {\n    export runtime_version=${APISIX_RUNTIME}\n    wget \"https://raw.githubusercontent.com/api7/apisix-build-tools/apisix-runtime/${APISIX_RUNTIME}/build-apisix-runtime.sh\"\n    chmod +x build-apisix-runtime.sh\n    ./build-apisix-runtime.sh latest\n}\n\ninstall_grpcurl () {\n    # For more versions, visit https://github.com/fullstorydev/grpcurl/releases\n    GRPCURL_VERSION=\"1.8.5\"\n    wget -q https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL_VERSION}/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz\n    tar -xvf grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz -C /usr/local/bin\n}\n\ninstall_vault_cli () {\n    VAULT_VERSION=\"1.9.0\"\n    wget -q https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip\n    unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin\n}\n\ninstall_nodejs () {\n    curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install --cleanup lts\n    export PNPM_HOME=\"/pnpm\"\n    export PATH=\"$PNPM_HOME:$PATH\"\n    corepack enable pnpm\n    pnpm setup\n}\n\ninstall_brotli () {\n    local BORTLI_VERSION=\"1.1.0\"\n    wget -q https://github.com/google/brotli/archive/refs/tags/v${BORTLI_VERSION}.zip\n    unzip v${BORTLI_VERSION}.zip && cd ./brotli-${BORTLI_VERSION} && mkdir build && cd build\n    local CMAKE=$(command -v cmake3 > /dev/null 2>&1 && echo cmake3 || echo cmake)\n    ${CMAKE} -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local/brotli ..\n    sudo ${CMAKE} --build . --config Release --target install\n    if [ -d \"/usr/local/brotli/lib64\" ]; then\n        echo /usr/local/brotli/lib64 | sudo tee /etc/ld.so.conf.d/brotli.conf\n    else\n        echo /usr/local/brotli/lib | sudo tee /etc/ld.so.conf.d/brotli.conf\n    fi\n    sudo ldconfig\n    cd ../..\n    rm -rf brotli-${BORTLI_VERSION}\n}\n\nset_coredns() {\n    # test a domain name is configured as upstream\n    echo \"127.0.0.1 test.com\" | sudo tee -a /etc/hosts\n    echo \"::1 ipv6.local\" | sudo tee -a /etc/hosts\n    # test certificate verification\n    echo \"127.0.0.1 admin.apisix.dev\" | sudo tee -a /etc/hosts\n    cat /etc/hosts # check GitHub Action's configuration\n\n    # override DNS configures\n    if [ -f \"/etc/netplan/50-cloud-init.yaml\" ]; then\n        sudo pip3 install yq\n\n        tmp=$(mktemp)\n        yq -y '.network.ethernets.eth0.\"dhcp4-overrides\".\"use-dns\"=false' /etc/netplan/50-cloud-init.yaml | \\\n        yq -y '.network.ethernets.eth0.\"dhcp4-overrides\".\"use-domains\"=false' | \\\n        yq -y '.network.ethernets.eth0.nameservers.addresses[0]=\"8.8.8.8\"' | \\\n        yq -y '.network.ethernets.eth0.nameservers.search[0]=\"apache.org\"' > $tmp\n        mv $tmp /etc/netplan/50-cloud-init.yaml\n        cat /etc/netplan/50-cloud-init.yaml\n        sudo netplan apply\n        sleep 3\n\n        sudo mv /etc/resolv.conf /etc/resolv.conf.bak\n        sudo ln -s /run/systemd/resolve/resolv.conf /etc/\n    fi\n    cat /etc/resolv.conf\n\n    mkdir -p build-cache\n\n    if [ ! -f \"build-cache/coredns_1_8_1\" ]; then\n        wget -q https://github.com/coredns/coredns/releases/download/v1.8.1/coredns_1.8.1_linux_amd64.tgz\n        tar -xvf coredns_1.8.1_linux_amd64.tgz\n        mv coredns build-cache/\n\n        touch build-cache/coredns_1_8_1\n    fi\n\n    pushd t/coredns || exit 1\n    ../../build-cache/coredns -dns.port=1053 &\n    popd || exit 1\n\n    touch build-cache/test_resolve.conf\n    echo \"nameserver 127.0.0.1:1053\" > build-cache/test_resolve.conf\n}\n\nGRPC_SERVER_EXAMPLE_VER=20210819\n\nlinux_get_dependencies () {\n    apt update\n    apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl libpcre3 libpcre3-dev libpcre2-dev xz-utils redis-tools\n    apt remove -y curl\n    apt-get install -y libyaml-dev\n    wget https://github.com/mikefarah/yq/releases/download/3.4.1/yq_linux_amd64 -O /usr/bin/yq && sudo chmod +x /usr/bin/yq\n\n    # install curl with http3 support\n    install_curl\n}\n\nfunction start_grpc_server_example() {\n    ./t/grpc_server_example/grpc_server_example \\\n        -grpc-address :10051 -grpcs-address :10052 -grpcs-mtls-address :10053 -grpc-http-address :10054 \\\n        -crt ./t/certs/apisix.crt -key ./t/certs/apisix.key -ca ./t/certs/mtls_ca.crt \\\n        > grpc_server_example.log 2>&1 &\n\n    for (( i = 0; i <= 10; i++ )); do\n        sleep 0.5\n        GRPC_PROC=`ps -ef | grep grpc_server_example | grep -v grep || echo \"none\"`\n        if [[ $GRPC_PROC == \"none\" || \"$i\" -eq 10 ]]; then\n            echo \"failed to start grpc_server_example\"\n            ss -antp | grep 1005 || echo \"no proc listen port 1005x\"\n            cat grpc_server_example.log\n\n            exit 1\n        fi\n\n        ss -lntp | grep 10051 | grep grpc_server && break\n    done\n}\n\n\nfunction start_sse_server_example() {\n    # build sse_server_example\n    pushd t/sse_server_example\n    go build\n    ./sse_server_example 7737 2>&1 &\n\n    for (( i = 0; i <= 10; i++ )); do\n        sleep 0.5\n        SSE_PROC=`ps -ef | grep sse_server_example | grep -v grep || echo \"none\"`\n        if [[ $SSE_PROC == \"none\" || \"$i\" -eq 10 ]]; then\n            echo \"failed to start sse_server_example\"\n            ss -antp | grep 7737 || echo \"no proc listen port 7737\"\n            exit 1\n        else\n            break\n        fi\n    done\n    popd\n}\n"
  },
  {
    "path": "ci/free_disk_space.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# GitHub Action CI runner comes with a limited disk space, due to several reasons\n# it may become full. For example, caching docker images creates an archive of\n# several GBs of size, this sometimes leads to disk usage becoming full.\n# To keep CI functional, we delete large directories that we do not need.\n\necho \"==============================================================================\"\necho \"Freeing up disk space on CI system\"\necho \"==============================================================================\"\n\necho \"Initial disk usage:\"\ndf -h\n\necho \"Removing large directories and runtimes...\"\nsudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /usr/share/swift\n\necho \"Removing large packages and performing clean-up...\"\nsudo apt-get remove -y '^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*' \\\nazure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri google-cloud-sdk google-cloud-cli --fix-missing\nsudo apt-get autoremove -y\nsudo apt-get clean\n\necho \"Removing Docker images...\"\nsudo docker image prune --all --force\n\necho \"Removing and Swap storage...\"\nsudo swapoff -a\nsudo rm -f /mnt/swapfile\n\necho \"Final disk usage:\"\ndf -h\n"
  },
  {
    "path": "ci/init-common-test-service.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# prepare vault kv engine\nsleep 3s\ndocker exec -i vault sh -c \"VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv\"\n\n# prepare localstack\nsleep 3s\ndocker exec -i localstack sh -c \"awslocal secretsmanager create-secret --name apisix-key --description 'APISIX Secret' --secret-string '{\\\"jack\\\":\\\"value\\\"}'\"\nsleep 3s\ndocker exec -i localstack sh -c \"awslocal secretsmanager create-secret --name apisix-mysql --description 'APISIX Secret' --secret-string 'secret'\"\n\n# prepare filesystem mcp server\nsleep 3s\n./ci/prepare_filesystem_mcp.sh\n"
  },
  {
    "path": "ci/init-last-test-service.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nbefore() {\n    # generating SSL certificates for Kafka\n    sudo keytool -genkeypair -keyalg RSA -dname \"CN=127.0.0.1\" -alias 127.0.0.1 -keystore ./ci/pod/kafka/kafka-server/selfsigned.jks -validity 365 -keysize 2048 -storepass changeit\n}\n\nafter() {\n    docker exec -i apache-apisix-kafka-server1-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 1 --topic test2\n    docker exec -i apache-apisix-kafka-server1-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 3 --topic test3\n    docker exec -i apache-apisix-kafka-server2-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server2:2181 --replication-factor 1 --partitions 1 --topic test4\n    docker exec -i apache-apisix-kafka-server1-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 1 --topic test-consumer\n    # create messages for test-consumer\n    for i in `seq 30`\n    do\n        docker exec -i apache-apisix-kafka-server1-1 bash -c \"echo \"testmsg$i\" | /opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic test-consumer\"\n        echo \"Produces messages to the test-consumer topic, msg: testmsg$i\"\n    done\n    echo \"Kafka service initialization completed\"\n}\n\ncase $1 in\n    'after')\n        after\n        ;;\n    'before')\n        before\n        ;;\nesac\n"
  },
  {
    "path": "ci/init-plugin-test-service.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nafter() {\n    docker exec -i apache-apisix-kafka-server1-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 1 --topic test2\n    docker exec -i apache-apisix-kafka-server1-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 3 --topic test3\n    docker exec -i apache-apisix-kafka-server2-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server2:2181 --replication-factor 1 --partitions 1 --topic test4\n    docker exec -i apache-apisix-kafka-server3-scram-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server3:2181 --replication-factor 1 --partitions 1 --topic test-scram-256\n    docker exec -i apache-apisix-kafka-server3-scram-1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server3:2181 --replication-factor 1 --partitions 1 --topic test-scram-512\n    # Create user with SCRAM-SHA-512\n    docker exec apache-apisix-kafka-server3-scram-1 /opt/bitnami/kafka/bin/kafka-configs.sh \\\n        --zookeeper zookeeper-server3:2181 \\\n        --alter \\\n        --add-config 'SCRAM-SHA-256=[password=admin-secret],SCRAM-SHA-512=[password=admin-secret]' \\\n        --entity-type users \\\n        --entity-name admin\n    # prepare openwhisk env\n    docker pull openwhisk/action-nodejs-v14:1.20.0\n    docker run --rm -d --name openwhisk -p 3233:3233 -p 3232:3232 -v /var/run/docker.sock:/var/run/docker.sock openwhisk/standalone:1.0.0\n    docker exec -i openwhisk waitready\n    docker exec -i openwhisk bash -c \"wsk package create pkg\"\n    docker exec -i openwhisk bash -c \"wsk action update /guest/pkg/testpkg <(echo 'function main(args){return {\\\"hello\\\": \\\"world\\\"}}') --kind nodejs:14\"\n    docker exec -i openwhisk bash -c \"wsk action update test <(echo 'function main(args){return {\\\"hello\\\": \\\"test\\\"}}') --kind nodejs:14\"\n    docker exec -i openwhisk bash -c \"wsk action update test-params <(echo 'function main(args){return {\\\"hello\\\": args.name || \\\"test\\\"}}') --kind nodejs:14\"\n    docker exec -i openwhisk bash -c \"wsk action update test-statuscode <(echo 'function main(args){return {\\\"statusCode\\\": 407}}') --kind nodejs:14\"\n    docker exec -i openwhisk bash -c \"wsk action update test-headers <(echo 'function main(args){return {\\\"headers\\\": {\\\"test\\\":\\\"header\\\"}}}') --kind nodejs:14\"\n    docker exec -i openwhisk bash -c \"wsk action update test-body <(echo 'function main(args){return {\\\"body\\\": {\\\"test\\\":\\\"body\\\"}}}') --kind nodejs:14\"\n\n\n    docker exec -i rmqnamesrv rm /home/rocketmq/rocketmq-4.6.0/conf/tools.yml\n    docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test -c DefaultCluster\n    docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test2 -c DefaultCluster\n    docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test3 -c DefaultCluster\n    docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test4 -c DefaultCluster\n\n    # wait for keycloak ready\n    bash -c 'while true; do curl -s localhost:8080 &>/dev/null; ret=$?; [[ $ret -eq 0 ]] && break; sleep 3; done'\n\n    # install jq\n    wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -O jq\n    chmod +x jq\n    docker cp jq apisix_keycloak:/usr/bin/\n\n    # configure keycloak\n    docker exec apisix_keycloak bash /tmp/kcadm_configure_cas.sh\n    docker exec apisix_keycloak bash /tmp/kcadm_configure_university.sh\n    docker exec apisix_keycloak bash /tmp/kcadm_configure_basic.sh\n\n    # configure clickhouse\n    echo 'CREATE TABLE default.test (`host` String, `client_ip` String, `route_id` String, `service_id` String, `@timestamp` String, PRIMARY KEY(`@timestamp`)) ENGINE = MergeTree()' | curl 'http://localhost:8123/' --data-binary @-\n    echo 'CREATE TABLE default.test (`host` String, `client_ip` String, `route_id` String, `service_id` String, `@timestamp` String, PRIMARY KEY(`@timestamp`)) ENGINE = MergeTree()' | curl 'http://localhost:8124/' --data-binary @-\n}\n\nbefore() {\n    # download keycloak cas provider\n    sudo wget -q https://github.com/jacekkow/keycloak-protocol-cas/releases/download/18.0.2/keycloak-protocol-cas-18.0.2.jar -O /opt/keycloak-protocol-cas-18.0.2.jar\n}\n\ncase $1 in\n    'after')\n        after\n        ;;\n    'before')\n        before\n        ;;\nesac\n"
  },
  {
    "path": "ci/kubernetes-ci.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./ci/common.sh\n\nrun_case() {\n    export_or_prefix\n    export PERL5LIB=.:$PERL5LIB\n    prove -Itest-nginx/lib -I./ -r t/kubernetes | tee test-result\n    rerun_flaky_tests test-result\n}\n\ncase_opt=$1\ncase $case_opt in\n    (run_case)\n        run_case\n        ;;\nesac\n"
  },
  {
    "path": "ci/linux-install-etcd-client.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nETCD_ARCH=\"amd64\"\nETCD_VERSION=${ETCD_VERSION:-'3.5.4'}\nARCH=${ARCH:-`(uname -m | tr '[:upper:]' '[:lower:]')`}\n\nif [[ $ARCH == \"arm64\" ]] || [[ $ARCH == \"aarch64\" ]]; then\n    ETCD_ARCH=\"arm64\"\nfi\n\nwget -q https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}.tar.gz\ntar xf etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}.tar.gz\nsudo cp etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}/etcdctl /usr/local/bin/\nrm -rf etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}\n"
  },
  {
    "path": "ci/linux-install-openresty.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -euo pipefail\n\nsource ./ci/common.sh\n\nexport_version_info\n\nARCH=${ARCH:-`(uname -m | tr '[:upper:]' '[:lower:]')`}\narch_path=\"\"\nif [[ $ARCH == \"arm64\" ]] || [[ $ARCH == \"aarch64\" ]]; then\n    arch_path=\"arm64/\"\nfi\n\nwget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -\nwget -qO - http://repos.apiseven.com/pubkey.gpg | sudo apt-key add -\nsudo apt-get -y update --fix-missing\nsudo apt-get -y install software-properties-common\nsudo add-apt-repository -y \"deb https://openresty.org/package/${arch_path}ubuntu $(lsb_release -sc) main\"\nsudo add-apt-repository -y \"deb http://repos.apiseven.com/packages/${arch_path}debian bullseye main\"\n\nsudo apt-get update\nsudo apt-get install -y openresty-pcre-dev openresty-zlib-dev build-essential gcc g++ cpanminus\n\nSSL_LIB_VERSION=${SSL_LIB_VERSION-openssl}\nENABLE_FIPS=${ENABLE_FIPS:-\"false\"}\n\nif [ \"$OPENRESTY_VERSION\" == \"source\" ]; then\n    if [ \"$SSL_LIB_VERSION\" == \"tongsuo\" ]; then\n        export openssl_prefix=/usr/local/tongsuo\n        export zlib_prefix=$OPENRESTY_PREFIX/zlib\n        export pcre_prefix=$OPENRESTY_PREFIX/pcre\n\n        export cc_opt=\"-DNGX_LUA_ABORT_AT_PANIC -I${zlib_prefix}/include -I${pcre_prefix}/include -I${openssl_prefix}/include\"\n        export ld_opt=\"-L${zlib_prefix}/lib -L${pcre_prefix}/lib -L${openssl_prefix}/lib64 -Wl,-rpath,${zlib_prefix}/lib:${pcre_prefix}/lib:${openssl_prefix}/lib64\"\n    fi\nfi\n\ninstall_apisix_runtime\n\nif [ ! \"$ENABLE_FIPS\" == \"true\" ]; then\ncurl -o /usr/local/openresty/openssl3/ssl/openssl.cnf \\\n    https://raw.githubusercontent.com/api7/apisix-build-tools/apisix-runtime/${APISIX_RUNTIME}/conf/openssl3/openssl.cnf\nfi\n\n# patch lua-resty-events\nsed -i 's/log(ERR, \"event worker failed: \", perr)/log(ngx.WARN, \"event worker failed: \", perr)/' /usr/local/openresty/lualib/resty/events/worker.lua\n"
  },
  {
    "path": "ci/linux_apisix_current_luarocks_in_customed_nginx_runner.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nexport OPENRESTY_VERSION=source\n. ./ci/linux_apisix_current_luarocks_runner.sh\n"
  },
  {
    "path": "ci/linux_apisix_current_luarocks_runner.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./ci/common.sh\n\ndo_install() {\n    linux_get_dependencies\n    install_brotli\n\n    export_or_prefix\n\n    ./ci/linux-install-openresty.sh\n    ./utils/linux-install-luarocks.sh\n    ./ci/linux-install-etcd-client.sh\n}\n\nscript() {\n    export_or_prefix\n    openresty -V\n\n    sudo rm -rf /usr/local/share/lua/5.1/apisix\n\n    # install APISIX with local version\n    luarocks install apisix-master-0.rockspec --only-deps > build.log 2>&1 || (cat build.log && exit 1)\n    luarocks make apisix-master-0.rockspec > build.log 2>&1 || (cat build.log && exit 1)\n    # ensure all files under apisix is installed\n    diff -rq apisix /usr/local/share/lua/5.1/apisix\n\n    mkdir cli_tmp && cd cli_tmp\n\n    # show install file\n    luarocks show apisix\n\n    sudo PATH=$PATH apisix help\n    sudo PATH=$PATH apisix init\n    sudo PATH=$PATH apisix start\n    sudo PATH=$PATH apisix stop\n\n    grep '\\[error\\]' /usr/local/apisix/logs/error.log > /tmp/error.log | true\n    if [ -s /tmp/error.log ]; then\n        echo \"=====found error log=====\"\n        cat /usr/local/apisix/logs/error.log\n        exit 1\n    fi\n\n    cd ..\n\n    # apisix cli test\n    set_coredns\n\n    # install test dependencies\n    sudo pip install requests\n\n    # dismiss \"maximum number of open file descriptors too small\" warning\n    ulimit -n 10240\n    ulimit -n -S\n    ulimit -n -H\n\n    for f in ./t/cli/test_*.sh; do\n        # skip docker test - runs in separate container\n        [[ \"$f\" == \"./t/cli/test_standalone_docker.sh\" ]] && continue\n        PATH=\"$PATH\" \"$f\"\n    done\n}\n\ncase_opt=$1\nshift\n\ncase ${case_opt} in\ndo_install)\n    do_install \"$@\"\n    ;;\nscript)\n    script \"$@\"\n    ;;\nesac\n"
  },
  {
    "path": "ci/linux_openresty_common_runner.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./ci/common.sh\n\nbefore_install() {\n    linux_get_dependencies\n\n    sudo cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1)\n}\n\ndo_install() {\n    export_or_prefix\n\n    ./ci/linux-install-openresty.sh\n\n    ./utils/linux-install-luarocks.sh\n\n    ./ci/linux-install-etcd-client.sh\n\n    create_lua_deps\n\n    # sudo apt-get install tree -y\n    # tree deps\n\n    # The latest version of test-nginx is not compatible with the current set of tests with ---http2\n    # due to this commit: https://github.com/openresty/test-nginx/commit/0ccd106cbe6878318e5a591634af8f1707c411a6\n    # This change pins test-nginx to a commit before this one.\n    git clone --depth 1 https://github.com/openresty/test-nginx.git test-nginx\n    cd test-nginx\n    git fetch --depth=1 origin ced30a31bafab6c68873efb17b6d80f39bcd95f5\n    git checkout ced30a31bafab6c68873efb17b6d80f39bcd95f5\n    cd ..\n\n    make utils\n\n    mkdir -p build-cache\n    # install and start grpc_server_example\n    cd t/grpc_server_example\n\n    CGO_ENABLED=0 go build\n    cd ../../\n\n    # install grpcurl\n    install_grpcurl\n\n    # install nodejs\n    install_nodejs\n\n    # install common jest test suite\n    pushd t\n    pnpm install\n    popd\n\n    # grpc-web server && client\n    pushd t/plugin/grpc-web\n    ./setup.sh\n    popd\n\n    # install vault cli capabilities\n    install_vault_cli\n\n    # install brotli\n    install_brotli\n}\n\nscript() {\n    export_or_prefix\n    openresty -V\n\n    make init\n\n    set_coredns\n\n    start_grpc_server_example\n\n    start_sse_server_example\n\n    # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t\n    FLUSH_ETCD=1 prove --timer -Itest-nginx/lib -I./ -r $TEST_FILE_SUB_DIR | tee /tmp/test.result\n    fail_on_bailout /tmp/test.result\n    rerun_flaky_tests /tmp/test.result\n}\n\nafter_success() {\n    # cat luacov.stats.out\n    # luacov-coveralls\n    echo \"done\"\n}\n\ncase_opt=$1\nshift\n\ncase ${case_opt} in\nbefore_install)\n    before_install \"$@\"\n    ;;\ndo_install)\n    do_install \"$@\"\n    ;;\nscript)\n    script \"$@\"\n    ;;\nafter_success)\n    after_success \"$@\"\n    ;;\nesac\n"
  },
  {
    "path": "ci/linux_openresty_runner.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nexport OPENRESTY_VERSION=source\n. ./ci/linux_openresty_common_runner.sh\n"
  },
  {
    "path": "ci/linux_openresty_tongsuo_runner.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nexport OPENRESTY_VERSION=source\nexport SSL_LIB_VERSION=tongsuo\n\n\nbefore_install() {\n    if [ -n \"$COMPILE_TONGSUO\" ]; then\n        git clone https://github.com/api7/tongsuo --depth 1\n        pushd tongsuo\n        # build binary\n        ./config enable-ntls -static\n        make -j2\n        mv apps/openssl apps/static-openssl\n        ./config shared enable-ntls -g --prefix=/usr/local/tongsuo\n        make -j2\n        popd\n    fi\n\n    pushd tongsuo\n    sudo make install_sw\n    sudo cp apps/static-openssl /usr/local/tongsuo/bin/openssl\n    export PATH=/usr/local/tongsuo/bin:$PATH\n    openssl version\n    popd\n}\n\n\ncase_opt=$1\n\ncase ${case_opt} in\nbefore_install)\n    # shellcheck disable=SC2218\n    before_install\n    ;;\nesac\n\n. ./ci/linux_openresty_common_runner.sh\n"
  },
  {
    "path": "ci/pod/docker-compose.common.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nversion: \"3.8\"\n\nservices:\n  ## Redis\n  apisix_redis:\n    # The latest image is the latest stable version\n    image: redis:latest\n    restart: unless-stopped\n    volumes:\n      - ./t/certs:/certs\n    command: \"--tls-port 6380 \\\n            --tls-cert-file /certs/mtls_server.crt \\\n            --tls-key-file /certs/mtls_server.key \\\n            --tls-ca-cert-file /certs/mtls_ca.crt \\\n            --tls-auth-clients no \\\n            --user alice on +@all ~* \\\\&* \\\\>somepassword\"\n    ports:\n      - \"6379:6379\"\n      - \"6380:6380\"\n\n  ## Etcd\n  etcd_old:\n    image: bitnamilegacy/etcd:3.3.8\n    restart: unless-stopped\n    env_file:\n      - ci/pod/etcd/env/common.env\n    environment:\n      ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379\n    ports:\n      - \"3379:2379\"\n      - \"3380:2380\"\n\n  etcd:\n    image: bitnamilegacy/etcd:3.5.4\n    restart: unless-stopped\n    env_file:\n      - ci/pod/etcd/env/common.env\n    environment:\n        ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379\n    ports:\n      - \"2379:2379\"\n      - \"2380:2380\"\n\n  etcd_tls:\n    image: bitnamilegacy/etcd:3.5.4\n    restart: unless-stopped\n    env_file:\n      - ci/pod/etcd/env/common.env\n    environment:\n      ETCD_ADVERTISE_CLIENT_URLS: https://0.0.0.0:12379\n      ETCD_LISTEN_CLIENT_URLS: https://0.0.0.0:12379\n      ETCD_CERT_FILE: /certs/etcd.pem\n      ETCD_KEY_FILE: /certs/etcd.key\n    ports:\n      - \"12379:12379\"\n      - \"12380:12380\"\n    volumes:\n      - ./t/certs:/certs\n\n  etcd_mtls:\n    image: bitnamilegacy/etcd:3.5.4\n    restart: unless-stopped\n    env_file:\n      - ci/pod/etcd/env/common.env\n    environment:\n      ETCD_ADVERTISE_CLIENT_URLS: https://0.0.0.0:22379\n      ETCD_LISTEN_CLIENT_URLS: https://0.0.0.0:22379\n      ETCD_CERT_FILE: /certs/mtls_server.crt\n      ETCD_KEY_FILE: /certs/mtls_server.key\n      ETCD_CLIENT_CERT_AUTH: \"true\"\n      ETCD_TRUSTED_CA_FILE: /certs/mtls_ca.crt\n    ports:\n      - \"22379:22379\"\n      - \"22380:22380\"\n    volumes:\n      - ./t/certs:/certs\n\n\n  ## Redis cluster\n  redis-cluster:\n    image: vishnunair/docker-redis-cluster:latest\n    restart: unless-stopped\n    ports:\n      - \"5000:6379\"\n      - \"5002:6380\"\n      - \"5003:6381\"\n      - \"5004:6382\"\n      - \"5005:6383\"\n      - \"5006:6384\"\n\n\n  ## HashiCorp Vault\n  vault:\n    image: vault:1.9.0\n    container_name: vault\n    restart: unless-stopped\n    ports:\n      - \"8200:8200\"\n    cap_add:\n      - IPC_LOCK\n    environment:\n      VAULT_DEV_ROOT_TOKEN_ID: root\n      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200\n    command: [ \"vault\", \"server\", \"-dev\" ]\n\n\n  ## LocalStack\n  localstack:\n    image: localstack/localstack\n    container_name: localstack\n    restart: unless-stopped\n    ports:\n      - \"127.0.0.1:4566:4566\"            # LocalStack Gateway\n\n\n  ## httpbin - HTTP Request & Response Service\n  httpbin:\n    image: kennethreitz/httpbin\n    container_name: httpbin\n    restart: unless-stopped\n    ports:\n      - \"8280:80\"\n"
  },
  {
    "path": "ci/pod/docker-compose.first.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nversion: \"3.8\"\n\nservices:\n  ## Eureka\n  eureka:\n    image: bitinit/eureka\n    env_file:\n      - ci/pod/eureka/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"8761:8761\"\n\n  ## Consul\n  consul_1:\n    image: consul:1.7\n    restart: unless-stopped\n    ports:\n      - \"8500:8500\"\n    command: [ \"consul\", \"agent\", \"-server\", \"-bootstrap-expect=1\", \"-client\", \"0.0.0.0\", \"-log-level\", \"info\", \"-data-dir=/consul/data\", \"-enable-script-checks\" ]\n    networks:\n      consul_net:\n\n  consul_2:\n    image: consul:1.7\n    restart: unless-stopped\n    ports:\n      - \"8600:8500\"\n    command: [ \"consul\", \"agent\", \"-server\", \"-bootstrap-expect=1\", \"-client\", \"0.0.0.0\", \"-log-level\", \"info\", \"-data-dir=/consul/data\", \"-enable-script-checks\" ]\n    networks:\n      consul_net:\n\n  consul_3:\n    image: hashicorp/consul:1.16.2\n    restart: unless-stopped\n    ports:\n      - \"8502:8500\"\n    command: [ \"consul\", \"agent\", \"-server\", \"-bootstrap-expect=1\", \"-client\", \"0.0.0.0\", \"-log-level\", \"info\", \"-data-dir=/consul/data\", \"-enable-script-checks\", \"-ui\", \"-hcl\", \"acl = {\\nenabled = true\\ndefault_policy = \\\"deny\\\"\\nenable_token_persistence = true\\ntokens = {\\nagent = \\\"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\\\"\\n}}\" ]\n    networks:\n      consul_net:\n\n  ## Consul cluster\n  consul_node_1:\n    image: consul:1.7\n    restart: unless-stopped\n    ports:\n      - \"9500:8500\"\n      - \"8300:8300\"\n      - \"8301:8301\"\n      - \"8302:8302\"\n      - \"9600:8600\"\n    command: [ \"consul\", \"agent\", \"-server\", \"-bootstrap-expect=1\", \"-bind\", \"0.0.0.0\", \"-client\", \"0.0.0.0\", \"-node\", \"node-1\", \"-log-level\", \"info\", \"-data-dir=/consul/data\", \"-enable-script-checks\" ]\n    healthcheck:\n        test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8500/\"]\n        interval: 10s\n        timeout: 10s\n        retries: 5\n    networks:\n      consul_cluster_net:\n        aliases:\n          - consul.cluster\n\n  consul_node_2:\n    image: consul:1.7\n    restart: unless-stopped\n    environment:\n      - CONSUL_BIND_INTERFACE=eth0\n    ports:\n      - \"9501:8500\"\n    command: [ \"consul\", \"agent\", \"-server\", \"-bind\", \"0.0.0.0\", \"-client\", \"0.0.0.0\", \"-retry-join\", \"consul.cluster\", \"-node\", \"node-2\", \"-log-level\", \"info\", \"-data-dir=/consul/data\", \"-enable-script-checks\" ]\n    depends_on:\n      consul_node_1:\n        condition: service_healthy\n    networks:\n      consul_cluster_net:\n        aliases:\n          - consul.cluster\n\n  consul_node_3:\n    image: consul:1.7\n    restart: unless-stopped\n    environment:\n      - CONSUL_BIND_INTERFACE=eth0\n    ports:\n      - \"9502:8500\"\n    command: [ \"consul\", \"agent\", \"-server\", \"-bind\", \"0.0.0.0\", \"-client\", \"0.0.0.0\", \"-retry-join\", \"consul.cluster\", \"-node\", \"node-3\", \"-log-level\", \"info\", \"-data-dir=/consul/data\", \"-enable-script-checks\" ]\n    depends_on:\n      consul_node_1:\n        condition: service_healthy\n    networks:\n      consul_cluster_net:\n        aliases:\n          - consul.cluster\n\n  ## Nacos cluster\n  nacos_auth:\n    hostname: nacos1\n    image: nacos/nacos-server:1.4.1\n    env_file:\n      - ci/pod/nacos/env/common.env\n    environment:\n      NACOS_AUTH_ENABLE: \"true\"\n    restart: unless-stopped\n    ports:\n      - \"8848:8848\"\n    networks:\n      nacos_net:\n\n  nacos_no_auth:\n    hostname: nacos2\n    image: nacos/nacos-server:1.4.1\n    env_file:\n      - ci/pod/nacos/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"8858:8848\"\n    networks:\n      nacos_net:\n\n  nacos_server_health_check:\n    build:\n      context: ci/pod/nacos/healthcheck\n      dockerfile: Dockerfile\n    environment:\n      CHECK_URI: \"http://nacos2:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=2\"\n    tty: true\n    # debug healthcheck script\n#    volumes:\n#     - ./ci/pod/nacos/healthcheck/nacos-server-healthcheck.sh:/nacos-server-healthcheck.sh\n    healthcheck:\n      test: [ \"CMD\", \"bash\", \"/nacos-server-healthcheck.sh\" ]\n      interval: 5s\n      timeout: 5s\n      retries: 60\n      start_period: 10s\n    networks:\n      nacos_net:\n\n  nacos_service_health_check:\n    build:\n      context: ci/pod/nacos/healthcheck\n      dockerfile: Dockerfile\n    # debug healthcheck script\n#    volumes:\n#     - ./ci/pod/nacos/healthcheck/nacos-service-healthcheck.sh:/nacos-service-healthcheck.sh\n    tty: true\n    healthcheck:\n      test: [ \"CMD\", \"bash\", \"/nacos-service-healthcheck.sh\" ]\n      interval: 5s\n      timeout: 30s\n      retries: 60\n      start_period: 10s\n    networks:\n      nacos_net:\n\n  ### Nacos services\n  nacos-service1:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 1\n    restart: unless-stopped\n    ports:\n      - \"18001:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n  nacos-service2:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 2\n    restart: unless-stopped\n    ports:\n      - \"18002:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n  nacos-service3:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 1\n      NAMESPACE: test_ns\n    restart: unless-stopped\n    ports:\n      - \"18003:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n  nacos-service4:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 1\n      GROUP: test_group\n    restart: unless-stopped\n    ports:\n      - \"18004:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n  nacos-service5:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 1\n      GROUP: test_group\n      NAMESPACE: test_ns\n    restart: unless-stopped\n    ports:\n      - \"18005:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n  nacos-service6:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 3\n      GROUP: test_group2\n      NAMESPACE: test_ns\n    restart: unless-stopped\n    ports:\n      - \"18006:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n  nacos-service7:\n    build:\n      context: ci/pod/nacos/service\n      dockerfile: Dockerfile\n    env_file:\n      - ci/pod/nacos/env/service.env\n    environment:\n      SUFFIX_NUM: 4\n      GROUP: test_group\n      NAMESPACE: test_ns2\n    restart: unless-stopped\n    ports:\n      - \"18007:18001\"\n    depends_on:\n      nacos_server_health_check:\n        condition: service_healthy\n    networks:\n      nacos_net:\n\n\nnetworks:\n  consul_cluster_net:\n  consul_net:\n  nacos_net:\n"
  },
  {
    "path": "ci/pod/docker-compose.last.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nversion: \"3.8\"\n\nservices:\n  ## Redis\n  apisix_redis:\n    # The latest image is the latest stable version\n    image: redis:latest\n    restart: unless-stopped\n    ports:\n      - \"6379:6379\"\n    networks:\n      apisix_net:\n\n  ## kafka-cluster\n  zookeeper-server1:\n    image: bitnamilegacy/zookeeper:3.6.0\n    env_file:\n      - ci/pod/kafka/zookeeper-server/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"2181:2181\"\n    networks:\n      kafka_net:\n\n  zookeeper-server2:\n    image: bitnamilegacy/zookeeper:3.6.0\n    env_file:\n      - ci/pod/kafka/zookeeper-server/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"12181:12181\"\n    networks:\n      kafka_net:\n\n  kafka-server1:\n    image: bitnamilegacy/kafka:2.8.1\n    env_file:\n      - ci/pod/kafka/kafka-server/env/last.env\n    environment:\n      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server1:2181\n    restart: unless-stopped\n    ports:\n      - \"9092:9092\"\n      - \"9093:9093\"\n      - \"9094:9094\"\n    depends_on:\n      - zookeeper-server1\n      - zookeeper-server2\n    networks:\n      kafka_net:\n    volumes:\n      - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro\n      - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n      - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n\n  kafka-server2:\n    image: bitnamilegacy/kafka:2.8.1\n    env_file:\n      - ci/pod/kafka/kafka-server/env/last.env\n    environment:\n      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server2:2181\n    restart: unless-stopped\n    ports:\n      - \"19092:9092\"\n      - \"19093:9093\"\n      - \"19094:9094\"\n    depends_on:\n      - zookeeper-server1\n      - zookeeper-server2\n    networks:\n      kafka_net:\n    volumes:\n      - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro\n      - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro\n      - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro\n\n\nnetworks:\n  apisix_net:\n  kafka_net:\n"
  },
  {
    "path": "ci/pod/docker-compose.plugin.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nversion: \"3.8\"\n\nservices:\n  ## keycloak\n  apisix_keycloak:\n    container_name: apisix_keycloak\n    image: quay.io/keycloak/keycloak:18.0.2\n    # use host network because in CAS auth,\n    # keycloak needs to send back-channel POST to apisix.\n    network_mode: host\n    environment:\n      KEYCLOAK_ADMIN: admin\n      KEYCLOAK_ADMIN_PASSWORD: admin\n      KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/server.crt.pem\n      KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/server.key.pem\n    restart: unless-stopped\n    command: [\"start-dev\"]\n    volumes:\n      - /opt/keycloak-protocol-cas-18.0.2.jar:/opt/keycloak/providers/keycloak-protocol-cas-18.0.2.jar\n      - ./ci/pod/keycloak/server.crt.pem:/opt/keycloak/conf/server.crt.pem\n      - ./ci/pod/keycloak/server.key.pem:/opt/keycloak/conf/server.key.pem\n      - ./ci/pod/keycloak/kcadm_configure_cas.sh:/tmp/kcadm_configure_cas.sh\n      - ./ci/pod/keycloak/kcadm_configure_university.sh:/tmp/kcadm_configure_university.sh\n      - ./ci/pod/keycloak/kcadm_configure_basic.sh:/tmp/kcadm_configure_basic.sh\n\n  ## kafka-cluster\n  zookeeper-server1:\n    image: bitnamilegacy/zookeeper:3.6.0\n    env_file:\n      - ci/pod/kafka/zookeeper-server/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"2181:2181\"\n    networks:\n      kafka_net:\n\n  zookeeper-server2:\n    image: bitnamilegacy/zookeeper:3.6.0\n    env_file:\n      - ci/pod/kafka/zookeeper-server/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"12181:12181\"\n    networks:\n      kafka_net:\n\n  zookeeper-server3:\n    image: bitnamilegacy/zookeeper:3.6.0\n    env_file:\n      - ci/pod/kafka/zookeeper-server/env/common.env\n    restart: unless-stopped\n    ports:\n      - \"12182:12181\"\n    networks:\n      kafka_net_2:\n\n  kafka-server1:\n    image: bitnamilegacy/kafka:2.8.1\n    env_file:\n      - ci/pod/kafka/kafka-server/env/common.env\n    environment:\n      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server1:2181\n    restart: unless-stopped\n    ports:\n      - \"9092:9092\"\n    depends_on:\n      - zookeeper-server1\n      - zookeeper-server2\n    networks:\n      kafka_net:\n\n  kafka-server2:\n    image: bitnamilegacy/kafka:2.8.1\n    env_file:\n      - ci/pod/kafka/kafka-server/env/common2.env\n    environment:\n      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server2:2181\n    restart: unless-stopped\n    ports:\n      - \"19092:19092\"\n      - \"19094:19094\"\n    depends_on:\n      - zookeeper-server1\n      - zookeeper-server2\n    networks:\n      kafka_net:\n    volumes:\n      - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro\n\n  kafka-server3-scram:\n    image: bitnamilegacy/kafka:2.8.1\n    env_file:\n      - ci/pod/kafka/kafka-server/env/common3-scram.env\n    environment:\n      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server3:2181\n    restart: unless-stopped\n    ports:\n      - \"29092:29092\"  # PLAINTEXT for inter-broker communication\n      - \"29094:29094\"  # SASL_SCRAM for clients\n    depends_on:\n      - zookeeper-server1\n      - zookeeper-server2\n      - zookeeper-server3\n    networks:\n      kafka_net_2:\n    volumes:\n      - ./ci/pod/kafka/kafka-server/kafka_scram_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro\n\n  ## SkyWalking\n  skywalking:\n    image: apache/skywalking-oap-server:8.7.0-es6\n    restart: unless-stopped\n    ports:\n      - \"1234:1234\"\n      - \"11800:11800\"\n      - \"12800:12800\"\n    networks:\n      skywalk_net:\n\n\n  ## OpenLDAP\n  openldap:\n    image: bitnamilegacy/openldap:2.5.8\n    environment:\n      - LDAP_ADMIN_USERNAME=amdin\n      - LDAP_ADMIN_PASSWORD=adminpassword\n      - LDAP_USERS=user01,user02\n      - LDAP_PASSWORDS=password1,password2\n      - LDAP_ENABLE_TLS=yes\n      - LDAP_TLS_CERT_FILE=/certs/localhost_slapd_cert.pem\n      - LDAP_TLS_KEY_FILE=/certs/localhost_slapd_key.pem\n      - LDAP_TLS_CA_FILE=/certs/apisix.crt\n    ports:\n      - \"1389:1389\"\n      - \"1636:1636\"\n    volumes:\n      - ./t/certs:/certs\n\n\n  ## Grafana Loki\n  loki:\n    image: grafana/loki:2.8.0\n    command: -config.file=/etc/loki/local-config.yaml -auth.enabled -querier.multi-tenant-queries-enabled\n    ports:\n      - \"3100:3100\"\n    networks:\n      - loki_net\n\n  rocketmq_namesrv:\n    image: apacherocketmq/rocketmq:4.6.0\n    container_name: rmqnamesrv\n    restart: unless-stopped\n    ports:\n      - \"9876:9876\"\n    command: sh mqnamesrv\n    networks:\n      rocketmq_net:\n\n  rocketmq_broker:\n    image: apacherocketmq/rocketmq:4.6.0\n    container_name: rmqbroker\n    restart: unless-stopped\n    ports:\n      - \"10909:10909\"\n      - \"10911:10911\"\n      - \"10912:10912\"\n    depends_on:\n      - rocketmq_namesrv\n    command: sh mqbroker -n rocketmq_namesrv:9876 -c ../conf/broker.conf\n    networks:\n      rocketmq_net:\n\n  # Open Policy Agent\n  opa:\n    image: openpolicyagent/opa:0.35.0\n    restart: unless-stopped\n    ports:\n      - 8181:8181\n    command: run -s /example.rego /echo.rego /data.json /with_route.rego\n    volumes:\n      - type: bind\n        source: ./ci/pod/opa/with_route.rego\n        target: /with_route.rego\n      - type: bind\n        source: ./ci/pod/opa/example.rego\n        target: /example.rego\n      - type: bind\n        source: ./ci/pod/opa/echo.rego\n        target: /echo.rego\n      - type: bind\n        source: ./ci/pod/opa/data.json\n        target: /data.json\n    networks:\n      opa_net:\n\n  # Elasticsearch Logger Service\n  elasticsearch-noauth:\n    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0\n    restart: unless-stopped\n    ports:\n      - \"9200:9200\"\n      - \"9300:9300\"\n    environment:\n      ES_JAVA_OPTS: -Xms512m -Xmx512m\n      discovery.type: single-node\n      xpack.security.enabled: 'false'\n\n  elasticsearch-auth:\n    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0\n    restart: unless-stopped\n    ports:\n      - \"9201:9201\"\n    environment:\n      ES_JAVA_OPTS: -Xms512m -Xmx512m\n      discovery.type: single-node\n      ELASTIC_USERNAME: elastic\n      ELASTIC_PASSWORD: 123456\n      http.port: 9201\n      xpack.security.enabled: 'true'\n\n  elasticsearch-auth-2:\n    image: docker.elastic.co/elasticsearch/elasticsearch:9.0.2\n    restart: unless-stopped\n    ports:\n      - \"9301:9201\"\n    environment:\n      ES_JAVA_OPTS: -Xms512m -Xmx512m\n      discovery.type: single-node\n      ELASTIC_USERNAME: elastic\n      ELASTIC_PASSWORD: 123456\n      http.port: 9201\n      xpack.security.enabled: 'true'\n\n  elasticsearch-auth-3:\n    image: docker.elastic.co/elasticsearch/elasticsearch:7.0.0\n    restart: unless-stopped\n    ports:\n      - \"9401:9201\"\n    environment:\n      ES_JAVA_OPTS: -Xms512m -Xmx512m\n      discovery.type: single-node\n      ELASTIC_USERNAME: elastic\n      ELASTIC_PASSWORD: 123456\n      http.port: 9201\n      xpack.security.enabled: 'true'\n\n  elasticsearch-auth-4:\n    image: docker.elastic.co/elasticsearch/elasticsearch:6.7.0\n    restart: unless-stopped\n    ports:\n      - \"9501:9201\"\n    environment:\n      ES_JAVA_OPTS: -Xms512m -Xmx512m\n      discovery.type: single-node\n      ELASTIC_USERNAME: elastic\n      ELASTIC_PASSWORD: 123456\n      http.port: 9201\n      xpack.security.enabled: 'true'\n\n  # The function services of OpenFunction\n  test-header:\n    image: test-header-image:latest\n    restart: unless-stopped\n    ports:\n      - \"30583:8080\"\n    environment:\n      CONTEXT_MODE: \"self-host\"\n      FUNC_CONTEXT: \"{\\\"name\\\":\\\"HelloWorld\\\",\\\"version\\\":\\\"v1.0.0\\\",\\\"port\\\":\\\"8080\\\",\\\"runtime\\\":\\\"Knative\\\"}\"\n\n  test-uri:\n    image: test-uri-image:latest\n    restart: unless-stopped\n    ports:\n      - \"30584:8080\"\n    environment:\n      CONTEXT_MODE: \"self-host\"\n      FUNC_CONTEXT: \"{\\\"name\\\":\\\"HelloWorld\\\",\\\"version\\\":\\\"v1.0.0\\\",\\\"port\\\":\\\"8080\\\",\\\"runtime\\\":\\\"Knative\\\"}\"\n\n  test-body:\n    image: test-body-image:latest\n    restart: unless-stopped\n    ports:\n      - \"30585:8080\"\n    environment:\n      CONTEXT_MODE: \"self-host\"\n      FUNC_CONTEXT: \"{\\\"name\\\":\\\"HelloWorld\\\",\\\"version\\\":\\\"v1.0.0\\\",\\\"port\\\":\\\"8080\\\",\\\"runtime\\\":\\\"Knative\\\"}\"\n\n  ## RedisCluster Enable TLS\n  redis-node-0:\n    image: docker.io/bitnamilegacy/redis-cluster:7.0\n    volumes:\n      - ./t/certs:/certs\n    environment:\n      - 'ALLOW_EMPTY_PASSWORD=yes'\n      - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2'\n      - 'REDIS_TLS_ENABLED=yes'\n      - 'REDIS_TLS_CERT_FILE=/certs/mtls_server.crt'\n      - 'REDIS_TLS_KEY_FILE=/certs/mtls_server.key'\n      - 'REDIS_TLS_CA_FILE=/certs/mtls_ca.crt'\n      - 'REDIS_TLS_AUTH_CLIENTS=no'\n    ports:\n      - '7000:6379'\n\n  redis-node-1:\n    image: docker.io/bitnamilegacy/redis-cluster:7.0\n    volumes:\n      - ./t/certs:/certs\n    environment:\n      - 'ALLOW_EMPTY_PASSWORD=yes'\n      - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2'\n      - 'REDIS_TLS_ENABLED=yes'\n      - 'REDIS_TLS_CERT_FILE=/certs/mtls_server.crt'\n      - 'REDIS_TLS_KEY_FILE=/certs/mtls_server.key'\n      - 'REDIS_TLS_CA_FILE=/certs/mtls_ca.crt'\n      - 'REDIS_TLS_AUTH_CLIENTS=no'\n    ports:\n      - '7001:6379'\n\n  redis-node-2:\n    image: docker.io/bitnamilegacy/redis-cluster:7.0\n    volumes:\n      - ./t/certs:/certs\n    depends_on:\n      - redis-node-0\n      - redis-node-1\n    environment:\n      - 'ALLOW_EMPTY_PASSWORD=yes'\n      - 'REDIS_CLUSTER_REPLICAS=0'\n      - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2'\n      - 'REDIS_CLUSTER_CREATOR=yes'\n      - 'REDIS_TLS_ENABLED=yes'\n      - 'REDIS_TLS_CERT_FILE=/certs/mtls_server.crt'\n      - 'REDIS_TLS_KEY_FILE=/certs/mtls_server.key'\n      - 'REDIS_TLS_CA_FILE=/certs/mtls_ca.crt'\n      - 'REDIS_TLS_AUTH_CLIENTS=no'\n    ports:\n      - '7002:6379'\n\n  graphql-demo:\n    # the owner doesn't provide a semver tag\n    image: npalm/graphql-java-demo:latest\n    ports:\n      - '8888:8080'\n\n  vector:\n    image: timberio/vector:0.29.1-debian\n    container_name: vector\n    volumes:\n      - ./ci/pod/vector:/etc/vector/\n      - ./t/certs:/certs\n    ports:\n      - '3000:3000' #tcp logger\n      - '8127:8127/udp'\n      - '43000:43000'\n      - '5140:5140'\n      - \"18088:18088\" # For splunk logging tests\n      - '5150:5150/udp'\n      - \"3001:3001\" #http logger\n    networks:\n      vector_net:\n\n  clickhouse:\n    image: clickhouse/clickhouse-server:23.4.2-alpine\n    container_name: clickhouse\n    ports:\n      - '8123:8123'\n    networks:\n      clickhouse_net:\n\n  clickhouse2:\n    image: clickhouse/clickhouse-server:23.4.2-alpine\n    container_name: clickhouse2\n    ports:\n      - '8124:8123'\n    networks:\n      clickhouse_net:\n  otel-collector:\n    image: otel/opentelemetry-collector-contrib\n    volumes:\n    - ./ci/pod/otelcol-contrib:/etc/otelcol-contrib:rw\n    ports:\n      - '4318:4318'\n\n\nnetworks:\n  apisix_net:\n  kafka_net:\n  kafka_net_2:\n  skywalk_net:\n  rocketmq_net:\n  opa_net:\n  vector_net:\n  clickhouse_net:\n  loki_net:\n"
  },
  {
    "path": "ci/pod/etcd/env/common.env",
    "content": "ALLOW_NONE_AUTHENTICATION=yes\n"
  },
  {
    "path": "ci/pod/eureka/env/common.env",
    "content": "ENVIRONMENT=apisix\nspring.application.name=apisix-eureka\nserver.port=8761\neureka.instance.ip-address=localhost\neureka.client.registerWithEureka=true\neureka.client.fetchRegistry=false\neureka.client.serviceUrl.defaultZone=http://127.0.0.1:8761/eureka/\n"
  },
  {
    "path": "ci/pod/kafka/kafka-server/env/common.env",
    "content": "ALLOW_PLAINTEXT_LISTENER=yes\nKAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true\nKAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092\n"
  },
  {
    "path": "ci/pod/kafka/kafka-server/env/common2.env",
    "content": "ALLOW_PLAINTEXT_LISTENER=yes\nKAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false\nKAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:19092,SASL_PLAINTEXT://0.0.0.0:19094\nKAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:19092,SASL_PLAINTEXT://127.0.0.1:19094\nKAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM=\nKAFKA_CFG_SSL_KEYSTORE_LOCATION=/opt/bitnami/kafka/config/certs/kafka.keystore.jks\nKAFKA_CFG_SSL_KEYSTORE_PASSWORD=changeit\nKAFKA_CFG_SSL_KEY_PASSWORD=changeit\n"
  },
  {
    "path": "ci/pod/kafka/kafka-server/env/common3-scram.env",
    "content": "ALLOW_PLAINTEXT_LISTENER=yes\nKAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false\n\n# CORRECTED: Use SASL_PLAINTEXT protocol with SCRAM mechanism\nKAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:29092,SASL_PLAINTEXT://0.0.0.0:29094\nKAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka-server3-scram:29092,SASL_PLAINTEXT://127.0.0.1:29094\n\n# SCRAM-specific configuration\nKAFKA_CFG_SASL_ENABLED_MECHANISMS=SCRAM-SHA-256,SCRAM-SHA-512\nKAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAINTEXT\n\n# Security protocol for inter-broker communication (since it's a single-node cluster)\nKAFKA_CFG_SECURITY_INTER_BROKER_PROTOCOL=PLAINTEXT\n\n# Optional: Explicitly set the security protocol map\nKAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT\n\n# Other configurations\nKAFKA_CFG_OFFSETS_TOPIC_NUM_PARTITIONS=1\nKAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1\n"
  },
  {
    "path": "ci/pod/kafka/kafka-server/env/last.env",
    "content": "ALLOW_PLAINTEXT_LISTENER=yes\nKAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false\nKAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9093,SASL_PLAINTEXT://0.0.0.0:9094\nKAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9093,SASL_PLAINTEXT://127.0.0.1:9094\nKAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM=\nKAFKA_CFG_SSL_KEYSTORE_LOCATION=/opt/bitnami/kafka/config/certs/kafka.keystore.jks\nKAFKA_CFG_SSL_KEYSTORE_PASSWORD=changeit\nKAFKA_CFG_SSL_KEY_PASSWORD=changeit\n"
  },
  {
    "path": "ci/pod/kafka/kafka-server/kafka_jaas.conf",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nKafkaServer {\n    org.apache.kafka.common.security.plain.PlainLoginModule required\n    username=\"admin\"\n    password=\"admin-secret\"\n    user_admin=\"admin-secret\";\n};\n"
  },
  {
    "path": "ci/pod/kafka/kafka-server/kafka_scram_jaas.conf",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nKafkaServer {\n    org.apache.kafka.common.security.scram.ScramLoginModule required\n    username=\"admin\"\n    password=\"admin-secret\";\n    org.apache.kafka.common.security.plain.PlainLoginModule required\n    username=\"admin\"\n    password=\"admin-secret\"\n    user_admin=\"admin-secret\";\n};\n"
  },
  {
    "path": "ci/pod/kafka/zookeeper-server/env/common.env",
    "content": "ALLOW_ANONYMOUS_LOGIN=yes\n"
  },
  {
    "path": "ci/pod/keycloak/kcadm_configure_basic.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nexport PATH=/opt/keycloak/bin:$PATH\n\nkcadm.sh config credentials --server http://127.0.0.1:8080 --realm master --user admin --password admin\n\n# create realm\nkcadm.sh create realms -s realm=basic -s enabled=true\n\n# set realm keys with specific private key, reuse tls cert and key\nPRIVATE_KEY=$(awk 'NF {sub(/\\r/, \"\"); printf \"%s\\\\n\", $0}' /opt/keycloak/conf/server.key.pem)\nCERTIFICATE=$(awk 'NF {sub(/\\r/, \"\"); printf \"%s\\\\n\", $0}' /opt/keycloak/conf/server.crt.pem)\nkcadm.sh create components -r basic -s name=rsa-apisix -s providerId=rsa \\\n    -s providerType=org.keycloak.keys.KeyProvider \\\n    -s 'config.priority=[\"1000\"]' \\\n    -s 'config.enabled=[\"true\"]' \\\n    -s 'config.active=[\"true\"]' \\\n    -s \"config.privateKey=[\\\"$PRIVATE_KEY\\\"]\" \\\n    -s \"config.certificate=[\\\"$CERTIFICATE\\\"]\" \\\n    -s 'config.algorithm=[\"RS256\"]'\n\n# create client apisix\nkcadm.sh create clients \\\n    -r basic \\\n    -s clientId=apisix \\\n    -s enabled=true \\\n    -s clientAuthenticatorType=client-secret \\\n    -s secret=secret \\\n    -s 'redirectUris=[\"*\"]' \\\n    -s 'directAccessGrantsEnabled=true'\n\n# add audience to client apisix, so that the access token will contain the client id (\"apisix\") as audience\nAPISIX_CLIENT_UUID=$(kcadm.sh get clients -r basic -q clientId=apisix | jq -r '.[0].id')\nkcadm.sh create clients/$APISIX_CLIENT_UUID/protocol-mappers/models \\\n  -r basic \\\n  -s protocol=openid-connect \\\n  -s name=aud \\\n  -s protocolMapper=oidc-audience-mapper \\\n  -s 'config.\"id.token.claim\"=false' \\\n  -s 'config.\"access.token.claim\"=true' \\\n  -s 'config.\"included.client.audience\"=apisix'\n\n# create client apisix\nkcadm.sh create clients \\\n    -r basic \\\n    -s clientId=apisix \\\n    -s enabled=true \\\n    -s clientAuthenticatorType=client-secret \\\n    -s secret=secret \\\n    -s 'redirectUris=[\"*\"]' \\\n    -s 'directAccessGrantsEnabled=true'\n\n# create client apisix-no-aud, without client id audience\n# according to Keycloak's default implementation, when unconfigured,\n# only the account is listed as an audience, not the client id\n\nkcadm.sh create clients \\\n    -r basic \\\n    -s clientId=apisix-no-aud \\\n    -s enabled=true \\\n    -s clientAuthenticatorType=client-secret \\\n    -s secret=secret \\\n    -s 'redirectUris=[\"*\"]' \\\n    -s 'directAccessGrantsEnabled=true'\n\n# create user jack\nkcadm.sh create users -r basic -s username=jack -s enabled=true\nkcadm.sh set-password -r basic --username jack --new-password jack\n"
  },
  {
    "path": "ci/pod/keycloak/kcadm_configure_cas.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\nexport PATH=/opt/keycloak/bin:$PATH\n\nkcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin\n\nkcadm.sh create realms -s realm=test -s enabled=true\n\nkcadm.sh create users -r test -s username=test -s enabled=true\nkcadm.sh set-password -r test --username test --new-password test\n\nclients=(\"cas1\" \"cas2\")\nrootUrls=(\"http://127.0.0.1:1984\" \"http://127.0.0.2:1984\")\n\nfor i in ${!clients[@]}; do\n    kcadm.sh create clients -r test -s clientId=${clients[$i]} -s enabled=true \\\n        -s protocol=cas -s frontchannelLogout=false -s rootUrl=${rootUrls[$i]} -s 'redirectUris=[\"/*\"]'\ndone\n"
  },
  {
    "path": "ci/pod/keycloak/kcadm_configure_university.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nexport PATH=/opt/keycloak/bin:$PATH\n\nkcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin\n\n# create realm University\nkcadm.sh create realms -s realm=University -s enabled=true\n\n# create roles `Teacher, Student`\nkcadm.sh create roles -r University -s name=Teacher\nkcadm.sh create roles -r University -s name=Student\n\n# create users `teacher@gmail.com, student@gmail.com`\nkcadm.sh create users -r University -s username=teacher@gmail.com -s enabled=true\nkcadm.sh create users -r University -s username=student@gmail.com -s enabled=true\n\n# set password\nkcadm.sh set-password -r University --username teacher@gmail.com --new-password 123456\nkcadm.sh set-password -r University --username student@gmail.com --new-password 123456\n\n# bind roles to users\nkcadm.sh add-roles -r University --uusername teacher@gmail.com --rolename Teacher\nkcadm.sh add-roles -r University --uusername student@gmail.com --rolename Student\n\n# create client course_management\nkcadm.sh create clients -r University -s clientId=course_management -s enabled=true -s clientAuthenticatorType=client-secret -s secret=d1ec69e9-55d2-4109-a3ea-befa071579d5\n\nclient_id=$(kcadm.sh get clients -r University --fields id,clientId 2>/dev/null | jq -r '.[] | select(.clientId=='\\\"course_management\\\"') | .id')\nteacher_id=$(kcadm.sh get roles -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"Teacher\\\"') | .id')\nstudent_id=$(kcadm.sh get roles -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"Student\\\"') | .id')\n\n# update client course_management\nkcadm.sh update clients/${client_id} -r University -s protocol=openid-connect -s standardFlowEnabled=true \\\n  -s implicitFlowEnabled=true -s directAccessGrantsEnabled=true -s serviceAccountsEnabled=true \\\n  -s authorizationServicesEnabled=true -s 'redirectUris=[\"*\"]' -s 'webOrigins=[\"*\"]'\n\nkcadm.sh update clients/${client_id}/authz/resource-server -r University -s allowRemoteResourceManagement=false -s policyEnforcementMode=\"ENFORCING\"\n\n# create authz-resource with name `course_resource`, uri `/course/*`, scope `DELETE, delete, view, GET`\nkcadm.sh create clients/${client_id}/authz/resource-server/resource -r University -s name=course_resource \\\n  -s ownerManagedAccess=false -s uris='[\"/course/*\"]' -s scopes='[{\"name\": \"DELETE\"},{\"name\": \"view\"},{\"name\": \"GET\"},{\"name\": \"delete\"}]'\n\ncourse_resource_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/resource -r University --fields _id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"course_resource\\\"') | ._id')\nDELETE_scope_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/scope -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"DELETE\\\"') | .id')\ndelete_scope_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/scope -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"delete\\\"') | .id')\nGET_scope_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/scope -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"GET\\\"') | .id')\nview_scope_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/scope -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"view\\\"') | .id')\n\n# create authz-policy `AllowTeacherPolicy, AllowStudentPolicy`\nkcadm.sh create clients/${client_id}/authz/resource-server/policy/role -r University \\\n  -s name=\"AllowTeacherPolicy\" -s logic=\"POSITIVE\" -s decisionStrategy=\"UNANIMOUS\" \\\n  -s roles='[{\"id\": '\\\"${teacher_id}\\\"'}]'\n\nkcadm.sh create clients/${client_id}/authz/resource-server/policy/role -r University \\\n  -s name=\"AllowStudentPolicy\" -s logic=\"POSITIVE\" -s decisionStrategy=\"UNANIMOUS\" \\\n  -s roles='[{\"id\": '\\\"${student_id}\\\"'}]'\n\nallow_teacher_policy_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/policy -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"AllowTeacherPolicy\\\"') | .id')\nallow_student_policy_id=$(kcadm.sh get clients/${client_id}/authz/resource-server/policy -r University --fields id,name 2>/dev/null | jq -r '.[] | select(.name=='\\\"AllowStudentPolicy\\\"') | .id')\n\n# create authz-permission `Delete Course Permission` and `View Course Permission`\nkcadm.sh create clients/${client_id}/authz/resource-server/permission/scope -r University \\\n  -s name=\"Delete Course Permission\" -s logic=\"POSITIVE\" -s decisionStrategy=\"UNANIMOUS\" \\\n  -s policies='['\\\"${allow_teacher_policy_id}\\\"']' \\\n  -s scopes='['\\\"${DELETE_scope_id}\\\"', '\\\"${delete_scope_id}\\\"']' \\\n  -s resources='['\\\"${course_resource_id}\\\"']'\n\nkcadm.sh create clients/${client_id}/authz/resource-server/permission/scope -r University \\\n  -s name=\"View Course Permission\" -s logic=\"POSITIVE\" -s decisionStrategy=\"AFFIRMATIVE\" \\\n  -s policies='['\\\"${allow_teacher_policy_id}\\\"', '\\\"${allow_student_policy_id}\\\"']' \\\n  -s scopes='['\\\"${GET_scope_id}\\\"', '\\\"${view_scope_id}\\\"']' \\\n  -s resources='['\\\"${course_resource_id}\\\"']'\n"
  },
  {
    "path": "ci/pod/keycloak/server.crt.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUbZfnhty/ZiHPz5Aq8kK5Kr8kcSQwDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA0MTgxMTQzNDJaFw0zMzA0\nMTUxMTQzNDJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQC/F4wK7eMTVAKGDMLCXE+Y6REdA5GU6/AakJf3NEKQ\nwCrtrqO+VBPIz445+edf3EEXhjFFGPdU6p0EkF0SMLaMsVBQQJ2qcP6FloIYiyT3\nWCs/gbtdoWq53ucAfWueIyHWsovLc0VhOXm0rhTYg88nMjJ7y6vYkfLMT6qlwASn\n9Tozgjat09fWATbN7yBi4ivVVsKDo2S3jkOyVnYYMjzZO3CSkyUSMl+ZsSesseSK\nA9c2zogfKIU833njraA8blMFfdinEMI/9yceEx57IUjnpY1iWHLSItiZF+LKEpeL\nvp9gpr88ghR85ISusqAqwcmnsdAqjjw7gbPm1DIvUgVBAgMBAAGjUzBRMB0GA1Ud\nDgQWBBRvlz5ZiE2fD9ikPRqpYwsVrxZfxTAfBgNVHSMEGDAWgBRvlz5ZiE2fD9ik\nPRqpYwsVrxZfxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCX\n5fOeFnX67eHI5dJB8p3U2GS21qykDVLV5ZV+JZfZwXJEygIvr/T9vs772EPxv+0/\nTO0+pGdcVswXq/6BoUFCV0rWWTDP5wTS3sV1ZsSSHil5zEutXuAI1LQGlit6w5xn\niDURFZw3ZmOFytXKXNbca1ma4yaCZtOwVe3O36GZeOiZFzBYE2DELqy77Nz1E5+3\njZaDnx0vonV8/hhX6FAPRPQnIXkaEH3BnVQZGD1jxipbFQQtmeeNPELy18MQo30N\nW1wOsbMMouniKUjdT16tdtzJzC+l9pVqRC+8df5PJfN56Uv9Ed6pjytkSF1SvHyJ\niTWmyxJL9AonUkc5Oiri\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ci/pod/keycloak/server.key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/F4wK7eMTVAKG\nDMLCXE+Y6REdA5GU6/AakJf3NEKQwCrtrqO+VBPIz445+edf3EEXhjFFGPdU6p0E\nkF0SMLaMsVBQQJ2qcP6FloIYiyT3WCs/gbtdoWq53ucAfWueIyHWsovLc0VhOXm0\nrhTYg88nMjJ7y6vYkfLMT6qlwASn9Tozgjat09fWATbN7yBi4ivVVsKDo2S3jkOy\nVnYYMjzZO3CSkyUSMl+ZsSesseSKA9c2zogfKIU833njraA8blMFfdinEMI/9yce\nEx57IUjnpY1iWHLSItiZF+LKEpeLvp9gpr88ghR85ISusqAqwcmnsdAqjjw7gbPm\n1DIvUgVBAgMBAAECggEBAKUrrkGYI2mGePPzPbiP38E02zTv67sEQLJFfwUOp+bE\nI5b0F9agh8VQGghkyKgkEiNKO3YVQVuluvjB66CYeIGdleT4JQ+4wVcoo+ShCN++\n1wr6kMA6kKx+Tb8vqYCzr0ELbSf6x+Jksp0Ixz3qmHixu88jWbNFW89boQ3JrnyZ\nTUgRSRdPoXcxspwcbhy6mMhwUfUSy8Zcck81dBEAjokvzbYh4jtFYMipWqro66KJ\nB9uqQme2J/rN/2PSrA6chI85Wa+JaGOSPDaGNp+DrADjoVZf1tXgzGCsA/lmVtQ0\n8YN4Dh21EjLxz4Dj5GE7RWET4Ejvv1XEih1p+zKne00CgYEA327raCD5Fnr1nGTb\nQ4ZWkcDR6EGSD6JGD0ur+UqqJhirM/5b4iGcsVK5uufb5dwk9+9z0EucXOVq/il0\nvgG2FbgRYM8kx3CDLvMYAqKJ8e5NsGJWwJVq6DsmsO1SaEId+SVFH83RHfG5/ksq\n/DgRg0Wl9FoL7sHchuSIP2QiLrMCgYEA2vHcKsMZk/KGMBHVffY3PUckirIM6vLa\nidMmm0T0HSAdviZRxQGyOnjd93ZhMqFJPTrmHOq0uAxfdFt+oRoHk/pGarBCv76L\nNnPrSnVe1pJOh7Mm7LHLgrAgeM2WW7xz6jZwc8On+9qHK97I/wAnJB8J7DvQJ2hR\nsWCDSbfKtjsCgYEAnVE77tVIjMuGo9dfiuvLiFR7d0yzys43Bg4ByEUKCEjWQoWV\nrGJ+MVxN6YvXCME4RloS8VZLgh0GeG44BJCv5Br2IXO4MbTGqQgAn9pRxkZD7S1Q\nZ8jMvTboxypSG5ZyBDp5sSr5Ulwg2SuT2IKh0gv4DVRZkoJtA41lYTzf1IECgYBd\n3NJGgt20T4S3lu2v0p5b5uQDkdF36CVIcP1cE3OUCPC3VDY5/0ApUSfXryh8TCjZ\n1yZPv086mBNUDuV6q24UQndtxaLYERgdgBSfFzJRSuffxS4qyw40OM2y/HA5Y9FN\n14jeGEMr9cN9S0VgDPC6y5O1cu8J9e8P3BBsyh5dgQKBgHMlIhOJDO/neVnax79X\nd3+5GaiggUnkd27OkYC4LhXEc/QWeHE0ByA0bDhhnsE7IVK2CVC18axOLmEJVy2g\nF6ZtxcpNrlVtF4YaOiRVUcDNnz9gX48efrpdoX2iBSFEd1NRDo/bjkVXI1L08LNf\nBbMB104PadChoGpl5R3NQQsP\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "ci/pod/nacos/env/common.env",
    "content": "EMBEDDED_STORAGE=embedded\nPREFER_HOST_MODE=hostname\nMODE=cluster\nNACOS_SERVERS=\"nacos1:8848 nacos2:8848\"\nJVM_XMS=512m\nJVM_XMX=512m\n"
  },
  {
    "path": "ci/pod/nacos/env/service.env",
    "content": "SERVICE_NAME=APISIX-NACOS\nNACOS_ADDR=nacos2:8848\n"
  },
  {
    "path": "ci/pod/nacos/healthcheck/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM alpine:latest\n\n# change workdir to /\nWORKDIR /\n\n# install curl\nRUN apk --no-cache add bash curl\n\n# add healthcheck script\nCOPY *.sh /\n\n# add hosted process\nCMD [\"cat\"]\n"
  },
  {
    "path": "ci/pod/nacos/healthcheck/nacos-server-healthcheck.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nset -ex\n\n# nacos server healthcheck\nREQ_STATUS=$(curl -s -o /dev/null -w '%{http_code}' \"${CHECK_URI}\")\n\nif [ \"${REQ_STATUS}\" -ne \"200\" ]; then\n  exit 1;\nfi\n"
  },
  {
    "path": "ci/pod/nacos/healthcheck/nacos-service-healthcheck.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\n# nacos service healthcheck\nURI_LIST=(\n  \"http://nacos2:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=2\"\n  \"http://nacos2:8848/nacos/v1/ns/service/list?groupName=test_group&pageNo=1&pageSize=2\"\n  \"http://nacos2:8848/nacos/v1/ns/service/list?groupName=DEFAULT_GROUP&namespaceId=test_ns&pageNo=1&pageSize=2\"\n  \"http://nacos2:8848/nacos/v1/ns/service/list?groupName=test_group&namespaceId=test_ns&pageNo=1&pageSize=2\"\n)\n\nfor URI in \"${URI_LIST[@]}\"; do\n  if [[ $(curl -s \"${URI}\" | grep \"APISIX-NACOS\") ]]; then\n    continue\n  else\n    exit 1;\n  fi\ndone\n\n\nfor IDX in {1..7..1}; do\n  REQ_STATUS=$(curl -s -o /dev/null -w '%{http_code}' \"http://nacos-service${IDX}:18001/hello\")\n  if [ \"${REQ_STATUS}\" -ne \"200\" ]; then\n    exit 1;\n  fi\ndone\n"
  },
  {
    "path": "ci/pod/nacos/service/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM eclipse-temurin:8\n\nENV SUFFIX_NUM=${SUFFIX_NUM:-1}\nENV NACOS_ADDR=${NACOS_ADDR:-127.0.0.1:8848}\nENV SERVICE_NAME=${SERVICE_NAME:-gateway-service}\nENV NAMESPACE=${NAMESPACE}\nENV GROUP=${GROUP:-DEFAULT_GROUP}\n\nADD https://raw.githubusercontent.com/api7/nacos-test-service/main/spring-nacos-1.0-SNAPSHOT.jar /app.jar\n\nENTRYPOINT [\"java\",\"-Djava.security.egd=file:/dev/./urandom\",\"-jar\",\"/app.jar\",\\\n            \"--suffix.num=${SUFFIX_NUM}\",\"--spring.cloud.nacos.discovery.server-addr=${NACOS_ADDR}\",\\\n            \"--spring.application.name=${SERVICE_NAME}\",\"--spring.cloud.nacos.discovery.group=${GROUP}\",\\\n            \"--spring.cloud.nacos.discovery.namespace=${NAMESPACE}\"]\nEXPOSE 18001\n"
  },
  {
    "path": "ci/pod/opa/data.json",
    "content": "{\n    \"users\": {\n        \"alice\": {\n            \"headers\": {\n                \"Location\": \"http://example.com/auth\"\n            },\n            \"status_code\": 302\n        },\n        \"bob\": {\n            \"headers\": {\n                \"test\": \"abcd\",\n                \"abcd\": \"test\"\n            }\n        },\n        \"carla\": {\n            \"reason\": \"Give you a string reason\"\n        },\n        \"dylon\": {\n            \"reason\": {\n                \"code\": 40001,\n                \"desc\": \"Give you a object reason\"\n            }\n        },\n        \"elisa\": {\n            \"reason\": {\n               \"info\": []\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "ci/pod/opa/echo.rego",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\npackage echo\n\nallow = false\nreason = input\n"
  },
  {
    "path": "ci/pod/opa/example.rego",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\npackage example\n\nimport input.request\nimport data.users\n\ndefault allow = false\n\nallow {\n    request.headers[\"test-header\"] == \"only-for-test\"\n    request.method == \"GET\"\n    startswith(request.path, \"/hello\")\n    request.query[\"test\"] != \"abcd\"\n    request.query[\"user\"]\n}\n\nallow {\n    request.method == \"GET\"\n    startswith(request.path, \"/echo\")\n}\n\nreason = users[request.query[\"user\"]].reason {\n    not allow\n    request.query[\"user\"]\n}\n\nheaders = users[request.query[\"user\"]].headers {\n    not allow\n    request.query[\"user\"]\n}\n\nheaders = {\"user\": request.query[\"user\"]} {\n    allow\n    request.query[\"user\"]\n}\n\nstatus_code = users[request.query[\"user\"]].status_code {\n    not allow\n    request.query[\"user\"]\n}\n"
  },
  {
    "path": "ci/pod/opa/with_route.rego",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\npackage with_route\ndefault allow = false\n\nallow {\n  input.route.name == \"valid\"\n}\n\nstatus_code = 403 {not allow}\n"
  },
  {
    "path": "ci/pod/openfunction/build-function-image.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -xeuo pipefail\n\nif [ ! -f \"./pack\" ]; then\n    wget -q https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz\n    tar -zxvf pack-v0.27.0-linux.tgz\nfi\n\n# please update function-example/*/hello.go if you want to update function\n./pack build test-uri-image --path ./ci/pod/openfunction/function-example/test-uri  --builder openfunction/builder-go:v2.4.0-1.17 --env FUNC_NAME=\"HelloWorld\"  --env FUNC_CLEAR_SOURCE=true --env FUNC_GOPROXY=\"https://proxy.golang.org\"\n./pack build test-body-image --path ./ci/pod/openfunction/function-example/test-body --builder openfunction/builder-go:v2.4.0-1.17 --env FUNC_NAME=\"HelloWorld\"  --env FUNC_CLEAR_SOURCE=true --env FUNC_GOPROXY=\"https://proxy.golang.org\"\n./pack build test-header-image --path ./ci/pod/openfunction/function-example/test-header  --builder openfunction/builder-go:v2.4.0-1.17 --env FUNC_NAME=\"HelloWorld\"  --env FUNC_CLEAR_SOURCE=true --env FUNC_GOPROXY=\"https://proxy.golang.org\"\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-body/go.mod",
    "content": "module example.com/hello\n\ngo 1.17\n\nrequire github.com/OpenFunction/functions-framework-go v0.3.0\n\nrequire (\n\tgithub.com/SkyAPM/go2sky v1.4.1 // indirect\n\tgithub.com/cloudevents/sdk-go/v2 v2.4.1 // indirect\n\tgithub.com/dapr/dapr v1.6.0 // indirect\n\tgithub.com/dapr/go-sdk v1.3.1 // indirect\n\tgithub.com/go-logr/logr v1.2.0 // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/uuid v1.3.0 // indirect\n\tgithub.com/json-iterator/go v1.1.11 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgo.uber.org/atomic v1.9.0 // indirect\n\tgo.uber.org/multierr v1.7.0 // indirect\n\tgo.uber.org/zap v1.19.1 // indirect\n\tgolang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect\n\tgolang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect\n\tgolang.org/x/text v0.3.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect\n\tgoogle.golang.org/grpc v1.40.0 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.0 // indirect\n\tk8s.io/klog/v2 v2.30.0 // indirect\n\tskywalking.apache.org/repo/goapi v0.0.0-20220401015832-2c9eee9481eb // indirect\n)\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-body/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.86.0/go.mod h1:YG2MRW8zzPSZaztnTZtxbMPK2VYaHg4NTDYZMG+5ZqQ=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/pubsub v1.12.2/go.mod h1:BmI/dqa6eXfm8WTp+JIN6d6vtVGq+vcsnglFKn/aVkY=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncontrib.go.opencensus.io/exporter/prometheus v0.4.0/go.mod h1:o7cosnyfuPVK0tB8q0QmaQNhGnptITnPQB+z1+qeFB0=\ncontrib.go.opencensus.io/exporter/zipkin v0.1.1/go.mod h1:GMvdSl3eJ2gapOaLKzTKE3qDgUkJ86k9k3yY2eqwkzc=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/99designs/keyring v1.1.5/go.mod h1:7hsVvt2qXgtadGevGJ4ujg+u8m6SpJ5TpHqTozIPqf0=\ngithub.com/AdhityaRamadhanus/fasthttpcors v0.0.0-20170121111917-d4c07198763a/go.mod h1:C0A1KeiVHs+trY6gUTPhhGammbrZ30ZfXRW/nuT7HLw=\ngithub.com/AthenZ/athenz v1.10.15/go.mod h1:7KMpEuJ9E4+vMCMI3UQJxwWs0RZtQq7YXZ1IteUjdsc=\ngithub.com/Azure/azure-amqp-common-go/v3 v3.0.1/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0=\ngithub.com/Azure/azure-amqp-common-go/v3 v3.1.0/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0=\ngithub.com/Azure/azure-event-hubs-go/v3 v3.3.10/go.mod h1:sszMsQpFy8Au2s2NColbnJY8lRVm1koW0XxBJ3rN5TY=\ngithub.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=\ngithub.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=\ngithub.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=\ngithub.com/Azure/azure-sdk-for-go v37.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0/go.mod h1:ZPW/Z0kLCTdDZaDbYTetxc9Cxl/2lNqxYHYNOF2bti0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v0.12.0/go.mod h1:GJzjM4SR9T0KyX5gKCVyz1ytD8FeWeUPCwtFCt1AyfE=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=\ngithub.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.3.0/go.mod h1:aJ4Pej3ivJnoNJ4UPgh/snHVLSSV2Mcc62srBQZ4TWE=\ngithub.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.1.0/go.mod h1:qKJHexVLI0iqKFeV/2WnqbRBQtJTPOMeBdmHOxs+E88=\ngithub.com/Azure/azure-service-bus-go v0.10.10/go.mod h1:o5z/3lDG1iT/T/G7vgIwIqVDTx9Qa2wndf5OdzSzpF8=\ngithub.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=\ngithub.com/Azure/azure-storage-blob-go v0.10.0/go.mod h1:ep1edmW+kNQx4UfWM9heESNmQdijykocJ0YOxmMX8SE=\ngithub.com/Azure/azure-storage-queue-go v0.0.0-20191125232315-636801874cdd/go.mod h1:K6am8mT+5iFXgingS9LUc7TmbsW6XBw3nxaRyaMyWc8=\ngithub.com/Azure/go-amqp v0.13.0/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs=\ngithub.com/Azure/go-amqp v0.13.1/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs=\ngithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=\ngithub.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=\ngithub.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=\ngithub.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=\ngithub.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=\ngithub.com/Azure/go-autorest/autorest v0.11.7/go.mod h1:V6p3pKZx1KKkJubbxnDWrzNhEIfOy/pTGasLqzHIPHs=\ngithub.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=\ngithub.com/Azure/go-autorest/autorest v0.11.23/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs=\ngithub.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=\ngithub.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=\ngithub.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=\ngithub.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=\ngithub.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=\ngithub.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=\ngithub.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=\ngithub.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=\ngithub.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=\ngithub.com/DataDog/zstd v1.4.6-0.20210211175136-c6db21d202f4/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=\ngithub.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=\ngithub.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=\ngithub.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=\ngithub.com/OpenFunction/functions-framework-go v0.3.0 h1:yiVwk7IysrMPnG3eCOgRLZbpsCUaYU3gRYA7dqIPREo=\ngithub.com/OpenFunction/functions-framework-go v0.3.0/go.mod h1:DbssgwZJRVd8VOls6aLpQwqBWu6gbDM4G+7RwwCJMEQ=\ngithub.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/SkyAPM/go2sky v1.4.1 h1:FV0jUB8UeC5CW0Z12j8xgrK0LoVV85Z92ShQU0G3Xfo=\ngithub.com/SkyAPM/go2sky v1.4.1/go.mod h1:cebzbFtq5oc9VrgJy0Sv7oePj/TjIlXPdj2ntHdCXd0=\ngithub.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=\ngithub.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/a8m/documentdb v1.3.1-0.20211026005403-13c3593b3c3a/go.mod h1:4Z0mpi7fkyqjxUdGiNMO3vagyiUoiwLncaIX6AsW5z0=\ngithub.com/aerospike/aerospike-client-go v4.5.0+incompatible/go.mod h1:zj8LBEnWBDOVEIJt8LvaRvDG5ARAoa5dBeHaB472NRc=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=\ngithub.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b/go.mod h1:Tie46d3UWzXpj+Fh9+DQTyaUxEpFBPOLXrnx7nxlKRo=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alibaba/sentinel-golang v1.0.3/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.2/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=\ngithub.com/alibabacloud-go/darabonba-openapi v0.1.4/go.mod h1:j03z4XUkIC9aBj/w5Bt7H0cygmPNt5sug8NXle68+Og=\ngithub.com/alibabacloud-go/darabonba-openapi v0.1.14/go.mod h1:w4CosR7O/kapCtEEMBm3JsQqWBU/CnZ2o0pHorsTWDI=\ngithub.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=\ngithub.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=\ngithub.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=\ngithub.com/alibabacloud-go/oos-20190601 v1.0.1/go.mod h1:t7g1ubvGwLe0cP+uLSrTza2S6xthOFZw43h9Zajt+Kw=\ngithub.com/alibabacloud-go/openapi-util v0.0.7/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=\ngithub.com/alibabacloud-go/openapi-util v0.0.10/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=\ngithub.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=\ngithub.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.15/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=\ngithub.com/alibabacloud-go/tea-utils v1.3.9/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=\ngithub.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=\ngithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=\ngithub.com/alicebob/miniredis/v2 v2.13.3/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg=\ngithub.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=\ngithub.com/aliyun/aliyun-oss-go-sdk v2.0.7+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=\ngithub.com/aliyun/aliyun-tablestore-go-sdk v1.6.0/go.mod h1:jixoiNNRR/4ziq0yub1fTlxmDcQwlpkaujpaWIATQWM=\ngithub.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=\ngithub.com/aliyunmq/mq-http-go-sdk v1.0.3/go.mod h1:JYfRMQoPexERvnNNBcal0ZQ2TVQ5ialDiW9ScjaadEM=\ngithub.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=\ngithub.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=\ngithub.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=\ngithub.com/apache/pulsar-client-go v0.6.1-0.20211027182823-171ef578e91a/go.mod h1:EauTUv9sTmP9QRznRgK9hxnzCsIVfS8fyhTfGcuJBrE=\ngithub.com/apache/pulsar-client-go/oauth2 v0.0.0-20201120111947-b8bd55bc02bd/go.mod h1:0UtvvETGDdvXNDCHa8ZQpxl+w3HbdFtfYZvDHLgWGTY=\ngithub.com/apache/rocketmq-client-go v1.2.5/go.mod h1:Kap8oXIVLlHF50BGUbN9z97QUp1GaK1nOoCfsZnR2bw=\ngithub.com/apache/rocketmq-client-go/v2 v2.1.0/go.mod h1:oEZKFDvS7sz/RWU0839+dQBupazyBV7WX5cP6nrio0Q=\ngithub.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/apache/thrift v0.14.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI=\ngithub.com/ardielle/ardielle-tools v1.5.4/go.mod h1:oZN+JRMnqGiIhrzkRN9l26Cej9dEx4jeNG6A+AdkShk=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=\ngithub.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=\ngithub.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=\ngithub.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=\ngithub.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=\ngithub.com/aws/aws-sdk-go v1.41.7/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=\ngithub.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=\ngithub.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f/go.mod h1:SghidfnxvX7ribW6nHI7T+IBbc9puZ9kk5Tx/88h8P4=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=\ngithub.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=\ngithub.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=\ngithub.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=\ngithub.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/camunda-cloud/zeebe/clients/go v1.0.1/go.mod h1:slW2ZP0pMmiZdxBLJHjGxax+E2AjjLFB608DRhounJI=\ngithub.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=\ngithub.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/cinience/go_rocketmq v0.0.2/go.mod h1:2YNY7emT546dcFpMEWLesmAEi4ndW7+tX5VfNf1Zsgs=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudevents/sdk-go/v2 v2.4.1 h1:rZJoz9QVLbWQmnvLPDFEmv17Czu+CfSPwMO6lhJ72xQ=\ngithub.com/cloudevents/sdk-go/v2 v2.4.1/go.mod h1:MZiMwmAh5tGj+fPFvtHv9hKurKqXtdB9haJYMJ/7GJY=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=\ngithub.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=\ngithub.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=\ngithub.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=\ngithub.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=\ngithub.com/dancannon/gorethink v4.0.0+incompatible/go.mod h1:BLvkat9KmZc1efyYwhz3WnybhRZtgF1K929FD8z1avU=\ngithub.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=\ngithub.com/dapr/components-contrib v1.6.0-rc.2/go.mod h1:30BaLseZXoK+UPD5E93dCTGZwlG3nWLNJazoJ9bKGlU=\ngithub.com/dapr/dapr v1.6.0 h1:zc6/jHVkD4LkNosVM+PNVDPBnmwYqnXXPD7knvE9etU=\ngithub.com/dapr/dapr v1.6.0/go.mod h1:ilH7anASii1b6hBRy2GTmf63Kj1/ejjaN9GcQJ2z5R8=\ngithub.com/dapr/go-sdk v1.3.1 h1:VI7vp3ZwZu+O8k9vPZ0gTTCRywj+ZsLm7MIQqB9S7FU=\ngithub.com/dapr/go-sdk v1.3.1/go.mod h1:tFH/t0z3qypmk5CXHvYSjf/1dGVi04voXfNnhbGgy/A=\ngithub.com/dapr/kit v0.0.2-0.20210614175626-b9074b64d233/go.mod h1:y8r0VqUNKyd6xBXp7gQjwA59wlCLGfKzL5J8iJsN09w=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/deepmap/oapi-codegen v1.3.6/go.mod h1:aBozjEveG+33xPiP55Iw/XbVkhtZHEGLq3nxlX0+hfU=\ngithub.com/deepmap/oapi-codegen v1.8.1/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20210411162248-d9abbec934ba/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=\ngithub.com/dghubble/go-twitter v0.0.0-20190719072343-39e5462e111f/go.mod h1:xfg4uS5LEzOj8PgZV7SQYRHbG7jPUnelEiaAVJxmhJE=\ngithub.com/dghubble/oauth1 v0.6.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk=\ngithub.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=\ngithub.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=\ngithub.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=\ngithub.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=\ngithub.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=\ngithub.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=\ngithub.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=\ngithub.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5/go.mod h1:MQXNGeXkpojWTxbN7vXoE3f7EmlA11MlJbsrJpVBINA=\ngithub.com/fasthttp/router v1.3.8/go.mod h1:DQBvuHvYbn3SUN6pGjwjPbpCNpWfCFc5Ipn/Fj6XxFc=\ngithub.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=\ngithub.com/getkin/kin-openapi v0.2.0/go.mod h1:V1z9xl9oF5Wt7v32ne4FmiF1alpS4dM6mNzoywPOXlk=\ngithub.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=\ngithub.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=\ngithub.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=\ngithub.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU=\ngithub.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=\ngithub.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=\ngithub.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=\ngithub.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=\ngithub.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=\ngithub.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=\ngithub.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=\ngithub.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=\ngithub.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=\ngithub.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=\ngithub.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=\ngithub.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=\ngithub.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=\ngithub.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=\ngithub.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=\ngithub.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=\ngithub.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=\ngithub.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=\ngithub.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=\ngithub.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=\ngithub.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=\ngithub.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=\ngithub.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=\ngithub.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=\ngithub.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=\ngithub.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=\ngithub.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=\ngithub.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=\ngithub.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=\ngithub.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=\ngithub.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=\ngithub.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=\ngithub.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=\ngithub.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=\ngithub.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=\ngithub.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=\ngithub.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=\ngithub.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=\ngithub.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=\ngithub.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=\ngithub.com/go-redis/redis/v8 v8.8.0/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y=\ngithub.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=\ngithub.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=\ngithub.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=\ngithub.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=\ngithub.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=\ngithub.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=\ngithub.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=\ngithub.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=\ngithub.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=\ngithub.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=\ngithub.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=\ngithub.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=\ngithub.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=\ngithub.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=\ngithub.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=\ngithub.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=\ngithub.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=\ngithub.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=\ngithub.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=\ngithub.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=\ngithub.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=\ngithub.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=\ngithub.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=\ngithub.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=\ngithub.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=\ngithub.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=\ngithub.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogap/errors v0.0.0-20200228125012-531a6449b28c/go.mod h1:tbRYYYC7g/H7QlCeX0Z2zaThWKowF4QQCFIsGgAsqRo=\ngithub.com/gogap/stack v0.0.0-20150131034635-fef68dddd4f8/go.mod h1:6q1WEv2BiAO4FSdwLQTJbWQYAn1/qDNJHUGJNXCj9kM=\ngithub.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=\ngithub.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=\ngithub.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=\ngithub.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v0.0.0-20181025225059-d3de96c4c28e/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w=\ngithub.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=\ngithub.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=\ngithub.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c/go.mod h1:YjKB0WsLXlMkO9p+wGTCoPIDGRJH0mz7E526PxkQVxI=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=\ngithub.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=\ngithub.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=\ngithub.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hazelcast/hazelcast-go-client v0.0.0-20190530123621-6cf767c2f31a/go.mod h1:VhwtcZ7sg3xq7REqGzEy7ylSWGKz4jZd05eCJropNzI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/influxdata/influxdb-client-go v1.4.0/go.mod h1:S+oZsPivqbcP1S9ur+T+QqXvrYS3NCZeMQtBoH4D1dw=\ngithub.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=\ngithub.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=\ngithub.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=\ngithub.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=\ngithub.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=\ngithub.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=\ngithub.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=\ngithub.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=\ngithub.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=\ngithub.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik=\ngithub.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=\ngithub.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=\ngithub.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=\ngithub.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=\ngithub.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg=\ngithub.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jawher/mow.cli v1.0.4/go.mod h1:5hQj2V8g+qYmLUVWqu4Wuja1pI57M83EChYLVZ0sMKk=\ngithub.com/jawher/mow.cli v1.2.0/go.mod h1:y+pcA3jBAdo/GIZx/0rFjw/K2bVEODP9rfZOfaiq8Ko=\ngithub.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=\ngithub.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=\ngithub.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=\ngithub.com/kataras/go-errors v0.0.3/go.mod h1:K3ncz8UzwI3bpuksXt5tQLmrRlgxfv+52ARvAu1+I+o=\ngithub.com/kataras/go-serializer v0.0.4/go.mod h1:/EyLBhXKQOJ12dZwpUZZje3lGy+3wnvG7QKaVJtm/no=\ngithub.com/keighl/postmark v0.0.0-20190821160221-28358b1a94e3/go.mod h1:Pz+php+2qQ4fWYwCa5O/rcnovTT2ylkKg3OnMLuFUbg=\ngithub.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=\ngithub.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.10.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=\ngithub.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=\ngithub.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE=\ngithub.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod h1:UGmTpUd3rjbtfIpwAPrcfmGf/Z1HS95TATB+m57TPB8=\ngithub.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=\ngithub.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=\ngithub.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=\ngithub.com/linkedin/goavro/v2 v2.9.8/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=\ngithub.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=\ngithub.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=\ngithub.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=\ngithub.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=\ngithub.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=\ngithub.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=\ngithub.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=\ngithub.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/microcosm-cc/bluemonday v1.0.7/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=\ngithub.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=\ngithub.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=\ngithub.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=\ngithub.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=\ngithub.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=\ngithub.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=\ngithub.com/nats-io/jwt v0.3.3-0.20200519195258-f2bf5ce574c7/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M=\ngithub.com/nats-io/jwt v1.1.0/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M=\ngithub.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=\ngithub.com/nats-io/jwt/v2 v2.0.0-20200916203241-1f8ce17dff02/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ=\ngithub.com/nats-io/jwt/v2 v2.0.0-20201015190852-e11ce317263c/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ=\ngithub.com/nats-io/jwt/v2 v2.0.0-20210125223648-1c24d462becc/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ=\ngithub.com/nats-io/jwt/v2 v2.0.0-20210208203759-ff814ca5f813/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ=\ngithub.com/nats-io/jwt/v2 v2.0.1/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY=\ngithub.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=\ngithub.com/nats-io/nats-server/v2 v2.1.8-0.20200524125952-51ebd92a9093/go.mod h1:rQnBf2Rv4P9adtAs/Ti6LfFmVtFG6HLhl/H7cVshcJU=\ngithub.com/nats-io/nats-server/v2 v2.1.8-0.20200601203034-f8d6dd992b71/go.mod h1:Nan/1L5Sa1JRW+Thm4HNYcIDcVRFc5zK9OpSZeI2kk4=\ngithub.com/nats-io/nats-server/v2 v2.1.8-0.20200929001935-7f44d075f7ad/go.mod h1:TkHpUIDETmTI7mrHN40D1pzxfzHZuGmtMbtb83TGVQw=\ngithub.com/nats-io/nats-server/v2 v2.1.8-0.20201129161730-ebe63db3e3ed/go.mod h1:XD0zHR/jTXdZvWaQfS5mQgsXj6x12kMjKLyAk/cOGgY=\ngithub.com/nats-io/nats-server/v2 v2.1.8-0.20210205154825-f7ab27f7dad4/go.mod h1:kauGd7hB5517KeSqspW2U1Mz/jhPbTrE8eOXzUPk1m0=\ngithub.com/nats-io/nats-server/v2 v2.1.8-0.20210227190344-51550e242af8/go.mod h1:/QQ/dpqFavkNhVnjvMILSQ3cj5hlmhB66adlgNbjuoA=\ngithub.com/nats-io/nats-server/v2 v2.1.9/go.mod h1:9qVyoewoYXzG1ME9ox0HwkkzyYvnlBDugfR4Gg/8uHU=\ngithub.com/nats-io/nats-server/v2 v2.2.1-0.20210330155036-61cbd74e213d/go.mod h1:eKlAaGmSQHZMFQA6x56AaP5/Bl9N3mWF4awyT2TTpzc=\ngithub.com/nats-io/nats-server/v2 v2.2.1/go.mod h1:A+5EOqdnhH7FvLxtAK6SEDx6hyHriVOwf+FT/eEV99c=\ngithub.com/nats-io/nats-streaming-server v0.21.2/go.mod h1:2W8QfNVOtcFpmf0bRiwuLtRb0/hkX4NuOxPOFNOThVQ=\ngithub.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=\ngithub.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE=\ngithub.com/nats-io/nats.go v1.10.1-0.20200531124210-96f2130e4d55/go.mod h1:ARiFsjW9DVxk48WJbO3OSZ2DG8fjkMi7ecLmXoY/n9I=\ngithub.com/nats-io/nats.go v1.10.1-0.20200606002146-fc6fed82929a/go.mod h1:8eAIv96Mo9QW6Or40jUHejS7e4VwZ3VRYD6Sf0BTDp4=\ngithub.com/nats-io/nats.go v1.10.1-0.20201021145452-94be476ad6e0/go.mod h1:VU2zERjp8xmF+Lw2NH4u2t5qWZxwc7jB3+7HVMWQXPI=\ngithub.com/nats-io/nats.go v1.10.1-0.20210127212649-5b4924938a9a/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI=\ngithub.com/nats-io/nats.go v1.10.1-0.20210211000709-75ded9c77585/go.mod h1:uBWnCKg9luW1g7hgzPxUjHFRI40EuTSX7RCzgnc74Jk=\ngithub.com/nats-io/nats.go v1.10.1-0.20210228004050-ed743748acac/go.mod h1:hxFvLNbNmT6UppX5B5Tr/r3g+XSwGjJzFn6mxPNJEHc=\ngithub.com/nats-io/nats.go v1.10.1-0.20210330225420-a0b1f60162f8/go.mod h1:Zq9IEHy7zurF0kFbU5aLIknnFI7guh8ijHk+2v+Vf5g=\ngithub.com/nats-io/nats.go v1.12.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=\ngithub.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/nats-io/stan.go v0.8.3/go.mod h1:Ejm8bbHnMTSptU6uNMAVuxeapMJYBB/Ml3ej6z4GoSY=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=\ngithub.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/open-policy-agent/opa v0.23.2/go.mod h1:rrwxoT/b011T0cyj+gg2VvxqTtn6N3gp/jzmr3fjW44=\ngithub.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=\ngithub.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=\ngithub.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=\ngithub.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=\ngithub.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=\ngithub.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/oracle/oci-go-sdk/v54 v54.0.0/go.mod h1:+t+yvcFGVp+3ZnztnyxqXfQDsMlq8U25faBLa+mqCMc=\ngithub.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=\ngithub.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=\ngithub.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=\ngithub.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=\ngithub.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=\ngithub.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=\ngithub.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=\ngithub.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=\ngithub.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=\ngithub.com/prometheus/statsd_exporter v0.22.3/go.mod h1:N4Z1+iSqc9rnxlT1N8Qn3l65Vzb5t4Uq0jpg8nxyhio=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=\ngithub.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=\ngithub.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=\ngithub.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=\ngithub.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/savsgio/gotils v0.0.0-20210217112953-d4a072536008/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sendgrid/rest v2.6.3+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=\ngithub.com/sendgrid/sendgrid-go v3.5.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=\ngithub.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=\ngithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/sony/gobreaker v0.4.2-0.20210216022020-dd874f9dd33b/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=\ngithub.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v0.0.0-20181024212040-082b515c9490/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/supplyon/gremcos v0.1.0/go.mod h1:ZnXsXGVbGCYDFU5GLPX9HZLWfD+ZWkiPo30KUjNoOtw=\ngithub.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=\ngithub.com/testcontainers/testcontainers-go v0.9.0/go.mod h1:b22BFXhRbg4PJmeMVWh6ftqjyZHgiIl3w274e9r3C2E=\ngithub.com/tidwall/gjson v1.2.1/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=\ngithub.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=\ngithub.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=\ngithub.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=\ngithub.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=\ngithub.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3/go.mod h1:QDlpd3qS71vYtakd2hmdpqhJ9nwv6mD6A30bQ1BPBFE=\ngithub.com/trusch/grpc-proxy v0.0.0-20190529073533-02b64529f274/go.mod h1:dzrPb02OTNDVimdCCBR1WAPu9a69n3VnfDyCX/GT/gE=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.21.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A=\ngithub.com/valyala/fasthttp v1.31.1-0.20211216042702-258a4c17b4f4/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=\ngithub.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=\ngithub.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=\ngithub.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=\ngithub.com/vmware/vmware-go-kcl v1.5.0/go.mod h1:P92YfaWfQyudNf62BNx+E2rJn9pd165MhHsRt8ajkpM=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=\ngithub.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=\ngithub.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=\ngithub.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=\ngithub.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=\ngithub.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=\ngithub.com/yuin/gopher-lua v0.0.0-20200603152657-dc2b0ca8b37e/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=\ngithub.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=\ngo.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=\ngo.etcd.io/etcd v0.5.0-alpha.5.0.20200819165624-17cef6e3e9d5/go.mod h1:skWido08r9w6Lq/w70DO5XYIKMu4QFu1+4VsqLQuJy8=\ngo.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=\ngo.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=\ngo.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=\ngo.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=\ngo.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=\ngo.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=\ngo.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc=\ngo.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA=\ngo.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=\ngo.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=\ngo.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=\ngo.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=\ngo.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngo.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=\ngo.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=\ngo.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=\ngoji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=\ngolang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181023182221-1baf3a9d7d67/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=\ngolang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180828065106-d99a578cf41b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=\ngolang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180810170437-e96c4e24768d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU=\ngoogle.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210701133433-6b8dcf568a95/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=\ngoogle.golang.org/genproto v0.0.0-20210707164411-8c882eb9abba/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 h1:NHN4wOCScVzKhPenJ2dt+BTs3X/XkBVI/Rh4iDt55T8=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/couchbase/gocb.v1 v1.6.4/go.mod h1:Ri5Qok4ZKiwmPr75YxZ0uELQy45XJgUSzeUnK806gTY=\ngopkg.in/couchbase/gocbcore.v7 v7.1.18/go.mod h1:48d2Be0MxRtsyuvn+mWzqmoGUG9uA00ghopzOs148/E=\ngopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4/go.mod h1:ZjII0iKx4Veo6N6da+pEZu/ptNyKLg9QTVt7fFmR6sw=\ngopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4/go.mod h1:jl/gd/aQ2S8whKVSTnsPs6n7BPeaAuw9UglBD/OF7eo=\ngopkg.in/couchbaselabs/jsonx.v1 v1.0.0/go.mod h1:oR201IRovxvLW/eISevH12/+MiKHtNQAKfcX8iWZvJY=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=\ngopkg.in/gorethink/gorethink.v4 v4.1.0/go.mod h1:M7JgwrUAmshJ3iUbEK0Pt049MPyPK+CYDGGaEjdZb/c=\ngopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=\ngopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=\ngopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=\ngopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=\ngopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=\ngopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=\ngopkg.in/kataras/go-serializer.v0 v0.0.4/go.mod h1:v2jHg/3Wp7uncDNzenTsX75PRDxhzlxoo/qDvM4ZGxk=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=\ngopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools v0.0.0-20181223230014-1083505acf35/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\nhonnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.19.2/go.mod h1:IQpK0zFQ1xc5iNIQPqzgoOwuFugaYHK4iCknlAQP9nI=\nk8s.io/api v0.20.0/go.mod h1:HyLC5l5eoS/ygQYl1BXBgFzWNlkHiAuyNAbevIn+FKg=\nk8s.io/apiextensions-apiserver v0.19.2/go.mod h1:EYNjpqIAvNZe+svXVx9j4uBaVhTB4C94HkY3w058qcg=\nk8s.io/apiextensions-apiserver v0.20.0/go.mod h1:ZH+C33L2Bh1LY1+HphoRmN1IQVLTShVcTojivK3N9xg=\nk8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=\nk8s.io/apimachinery v0.20.0/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=\nk8s.io/apiserver v0.19.2/go.mod h1:FreAq0bJ2vtZFj9Ago/X0oNGC51GfubKK/ViOKfVAOA=\nk8s.io/apiserver v0.20.0/go.mod h1:6gRIWiOkvGvQt12WTYmsiYoUyYW0FXSiMdNl4m+sxY8=\nk8s.io/cli-runtime v0.20.0/go.mod h1:C5tewU1SC1t09D7pmkk83FT4lMAw+bvMDuRxA7f0t2s=\nk8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA=\nk8s.io/client-go v0.20.0/go.mod h1:4KWh/g+Ocd8KkCwKF8vUNnmqgv+EVnQDK4MBF4oB5tY=\nk8s.io/code-generator v0.19.2/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk=\nk8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg=\nk8s.io/component-base v0.19.2/go.mod h1:g5LrsiTiabMLZ40AR6Hl45f088DevyGY+cCE2agEIVo=\nk8s.io/component-base v0.20.0/go.mod h1:wKPj+RHnAr8LW2EIBIK7AxOHPde4gme2lzXwVSoRXeA=\nk8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=\nk8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=\nk8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=\nk8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=\nk8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=\nk8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=\nk8s.io/metrics v0.20.0/go.mod h1:9yiRhfr8K8sjdj2EthQQE9WvpYDvsXIV3CjN4Ruq4Jw=\nk8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nnhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\nnhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQbTRyDlZPJX2SUPEqvnB+j7AJjtlox7PEwigU0=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/controller-runtime v0.7.0/go.mod h1:pJ3YBrJiAqMAZKi6UVGuE98ZrroV1p+pIhoHsMm9wdU=\nsigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nskywalking.apache.org/repo/goapi v0.0.0-20220401015832-2c9eee9481eb h1:+PP2DpKFN/rEporLdPI4A7bPWQjwfARlUDKNhSab8iM=\nskywalking.apache.org/repo/goapi v0.0.0-20220401015832-2c9eee9481eb/go.mod h1:uWwwvhcwe2MD/nJCg0c1EE/eL6KzaBosLHDfMFoEJ30=\nsourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=\nstathat.com/c/consistent v1.0.0/go.mod h1:QkzMWzcbB+yQBL2AttO6sgsQS/JSTapcDISJalmCDS0=\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-body/hello.go",
    "content": "/*\n * Copyright 2022 The OpenFunction Authors.\n *\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage hello\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/OpenFunction/functions-framework-go/functions\"\n)\n\nfunc init() {\n\tfunctions.HTTP(\"HelloWorld\", HelloWorld)\n}\n\nfunc HelloWorld(w http.ResponseWriter, r *http.Request) {\n\tbody, _ := io.ReadAll(r.Body)\n\tfmt.Fprintf(w, \"Hello, %s!\\n\", string(body))\n}\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-header/go.mod",
    "content": "module example.com/hello\n\ngo 1.17\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-header/hello.go",
    "content": "/*\n * Copyright 2022 The OpenFunction Authors.\n *\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage hello\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc HelloWorld(w http.ResponseWriter, r *http.Request) {\n\theader := r.Header\n\tfmt.Fprintf(w, \"%s\", header[\"Authorization\"])\n}\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-uri/go.mod",
    "content": "module example.com/hello\n\ngo 1.17\n\nrequire github.com/OpenFunction/functions-framework-go v0.4.0\n\nrequire (\n\tgithub.com/SkyAPM/go2sky v1.4.1 // indirect\n\tgithub.com/cloudevents/sdk-go/v2 v2.4.1 // indirect\n\tgithub.com/dapr/dapr v1.8.3 // indirect\n\tgithub.com/dapr/go-sdk v1.5.0 // indirect\n\tgithub.com/go-logr/logr v1.2.3 // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/uuid v1.3.0 // indirect\n\tgithub.com/gorilla/mux v1.8.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgo.uber.org/atomic v1.9.0 // indirect\n\tgo.uber.org/multierr v1.7.0 // indirect\n\tgo.uber.org/zap v1.21.0 // indirect\n\tgolang.org/x/net v0.0.0-20220621193019-9d032be2e588 // indirect\n\tgolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect\n\tgolang.org/x/text v0.3.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20220622171453-ea41d75dfa0f // indirect\n\tgoogle.golang.org/grpc v1.47.0 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/klog/v2 v2.30.0 // indirect\n\tskywalking.apache.org/repo/goapi v0.0.0-20220401015832-2c9eee9481eb // indirect\n)\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-uri/go.sum",
    "content": "bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=\nbazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.86.0/go.mod h1:YG2MRW8zzPSZaztnTZtxbMPK2VYaHg4NTDYZMG+5ZqQ=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=\ncloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=\ncloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=\ncloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/pubsub v1.12.2/go.mod h1:BmI/dqa6eXfm8WTp+JIN6d6vtVGq+vcsnglFKn/aVkY=\ncloud.google.com/go/secretmanager v1.4.0/go.mod h1:h2VZz7Svt1W9/YVl7mfcX9LddvS6SOLOvMoOXBhYT1k=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncode.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8=\ncontrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U=\ncontrib.go.opencensus.io/exporter/zipkin v0.1.1/go.mod h1:GMvdSl3eJ2gapOaLKzTKE3qDgUkJ86k9k3yY2eqwkzc=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ndubbo.apache.org/dubbo-go/v3 v3.0.3-0.20220610080020-48691a404537/go.mod h1:O7eTHAilCWlqBjEkG2MW9khZFImiARb/tSOE8PJas+g=\ngithub.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=\ngithub.com/99designs/keyring v1.1.6/go.mod h1:16e0ds7LGQQcT59QqkTg72Hh5ShM51Byv5PEmW6uoRU=\ngithub.com/99designs/keyring v1.2.0/go.mod h1:ETJn2A9cfvJKq1Q4FeOc+eetK52Ik0kUGog7Uy+xvX8=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=\ngithub.com/AdhityaRamadhanus/fasthttpcors v0.0.0-20170121111917-d4c07198763a/go.mod h1:C0A1KeiVHs+trY6gUTPhhGammbrZ30ZfXRW/nuT7HLw=\ngithub.com/AthenZ/athenz v1.10.39/go.mod h1:3Tg8HLsiQZp81BJY58JBeU2BR6B/H4/0MQGfCwhHNEA=\ngithub.com/Azure/azure-amqp-common-go/v3 v3.0.1/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0=\ngithub.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=\ngithub.com/Azure/azure-event-hubs-go/v3 v3.3.18/go.mod h1:R5H325+EzgxcBDkUerEwtor7ZQg77G7HiOTwpcuIVXY=\ngithub.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=\ngithub.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=\ngithub.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=\ngithub.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=\ngithub.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v37.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=\ngithub.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1/go.mod h1:l3wvZkG9oW07GLBW5Cd0WwG5asOfJ8aqE8raUvNzLpk=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=\ngithub.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.7.1/go.mod h1:WcC2Tk6JyRlqjn2byvinNnZzgdXmZ1tOiIOWNh1u0uA=\ngithub.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.5.0/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA=\ngithub.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.1/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=\ngithub.com/Azure/azure-service-bus-go v0.10.10/go.mod h1:o5z/3lDG1iT/T/G7vgIwIqVDTx9Qa2wndf5OdzSzpF8=\ngithub.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=\ngithub.com/Azure/azure-storage-blob-go v0.10.0/go.mod h1:ep1edmW+kNQx4UfWM9heESNmQdijykocJ0YOxmMX8SE=\ngithub.com/Azure/azure-storage-queue-go v0.0.0-20191125232315-636801874cdd/go.mod h1:K6am8mT+5iFXgingS9LUc7TmbsW6XBw3nxaRyaMyWc8=\ngithub.com/Azure/go-amqp v0.13.0/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs=\ngithub.com/Azure/go-amqp v0.13.1/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs=\ngithub.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=\ngithub.com/Azure/go-amqp v0.17.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=\ngithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=\ngithub.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=\ngithub.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=\ngithub.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=\ngithub.com/Azure/go-autorest/autorest v0.11.7/go.mod h1:V6p3pKZx1KKkJubbxnDWrzNhEIfOy/pTGasLqzHIPHs=\ngithub.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=\ngithub.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY=\ngithub.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=\ngithub.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U=\ngithub.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=\ngithub.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.15/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=\ngithub.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=\ngithub.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=\ngithub.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=\ngithub.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=\ngithub.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=\ngithub.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=\ngithub.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=\ngithub.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=\ngithub.com/Flaque/filet v0.0.0-20201012163910-45f684403088/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=\ngithub.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=\ngithub.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=\ngithub.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=\ngithub.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=\ngithub.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=\ngithub.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=\ngithub.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=\ngithub.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=\ngithub.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=\ngithub.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=\ngithub.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=\ngithub.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=\ngithub.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=\ngithub.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=\ngithub.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=\ngithub.com/Microsoft/hcsshim v0.9.1/go.mod h1:Y/0uV2jUab5kBI7SQgl62at0AVX7uaruzADAVmxm3eM=\ngithub.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=\ngithub.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=\ngithub.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=\ngithub.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=\ngithub.com/OpenFunction/functions-framework-go v0.4.0 h1:WHuKHRgwFNiTe+6/lJqDiQC0zOU7cS+HVf/XN/dA1j4=\ngithub.com/OpenFunction/functions-framework-go v0.4.0/go.mod h1:+uYjTEYmn2uqIyViZtg9OF+bUNdjbkWNd7jrQWc7iEc=\ngithub.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/RoaringBitmap/roaring v1.1.0/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=\ngithub.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/SkyAPM/go2sky v1.4.1 h1:FV0jUB8UeC5CW0Z12j8xgrK0LoVV85Z92ShQU0G3Xfo=\ngithub.com/SkyAPM/go2sky v1.4.1/go.mod h1:cebzbFtq5oc9VrgJy0Sv7oePj/TjIlXPdj2ntHdCXd0=\ngithub.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=\ngithub.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA=\ngithub.com/a8m/documentdb v1.3.1-0.20220405205223-5b41ba0aaeb1/go.mod h1:4Z0mpi7fkyqjxUdGiNMO3vagyiUoiwLncaIX6AsW5z0=\ngithub.com/aerospike/aerospike-client-go v4.5.0+incompatible/go.mod h1:zj8LBEnWBDOVEIJt8LvaRvDG5ARAoa5dBeHaB472NRc=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=\ngithub.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b/go.mod h1:Tie46d3UWzXpj+Fh9+DQTyaUxEpFBPOLXrnx7nxlKRo=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=\ngithub.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk=\ngithub.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=\ngithub.com/alibabacloud-go/darabonba-openapi v0.1.4/go.mod h1:j03z4XUkIC9aBj/w5Bt7H0cygmPNt5sug8NXle68+Og=\ngithub.com/alibabacloud-go/darabonba-openapi v0.1.16/go.mod h1:ZjyqRbbZOaUBSh7keeH8VQN/BzCPvxCQwMuJGDdbmXQ=\ngithub.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=\ngithub.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=\ngithub.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=\ngithub.com/alibabacloud-go/oos-20190601 v1.0.1/go.mod h1:t7g1ubvGwLe0cP+uLSrTza2S6xthOFZw43h9Zajt+Kw=\ngithub.com/alibabacloud-go/openapi-util v0.0.7/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=\ngithub.com/alibabacloud-go/openapi-util v0.0.10/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=\ngithub.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=\ngithub.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.15/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=\ngithub.com/alibabacloud-go/tea-utils v1.3.9/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=\ngithub.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=\ngithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=\ngithub.com/alicebob/miniredis/v2 v2.13.3/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg=\ngithub.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=\ngithub.com/aliyun/aliyun-oss-go-sdk v2.0.7+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=\ngithub.com/aliyun/aliyun-tablestore-go-sdk v1.6.0/go.mod h1:jixoiNNRR/4ziq0yub1fTlxmDcQwlpkaujpaWIATQWM=\ngithub.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=\ngithub.com/aliyunmq/mq-http-go-sdk v1.0.3/go.mod h1:JYfRMQoPexERvnNNBcal0ZQ2TVQ5ialDiW9ScjaadEM=\ngithub.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=\ngithub.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=\ngithub.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=\ngithub.com/apache/dubbo-getty v1.4.9-0.20220610060150-8af010f3f3dc/go.mod h1:cPJlbcHUTNTpiboMQjMHhE9XBni11LiBiG8FdrDuVzk=\ngithub.com/apache/dubbo-go-hessian2 v1.9.1/go.mod h1:xQUjE7F8PX49nm80kChFvepA/AvqAZ0oh/UaB6+6pBE=\ngithub.com/apache/dubbo-go-hessian2 v1.9.3/go.mod h1:xQUjE7F8PX49nm80kChFvepA/AvqAZ0oh/UaB6+6pBE=\ngithub.com/apache/dubbo-go-hessian2 v1.11.0/go.mod h1:7rEw9guWABQa6Aqb8HeZcsYPHsOS7XT1qtJvkmI6c5w=\ngithub.com/apache/pulsar-client-go v0.8.1/go.mod h1:yJNcvn/IurarFDxwmoZvb2Ieylg630ifxeO/iXpk27I=\ngithub.com/apache/pulsar-client-go/oauth2 v0.0.0-20220120090717-25e59572242e/go.mod h1:Xee4tgYLFpYcPMcTfBYWE1uKRzeciodGTSEDMzsR6i8=\ngithub.com/apache/rocketmq-client-go v1.2.5/go.mod h1:Kap8oXIVLlHF50BGUbN9z97QUp1GaK1nOoCfsZnR2bw=\ngithub.com/apache/rocketmq-client-go/v2 v2.1.0/go.mod h1:oEZKFDvS7sz/RWU0839+dQBupazyBV7WX5cP6nrio0Q=\ngithub.com/apache/rocketmq-client-go/v2 v2.1.1-rc2/go.mod h1:DDYjQ9wxYmJLjgNK4+RqyFE8/13gLK/Bugz4U6zD5MI=\ngithub.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/apache/thrift v0.14.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc/go.mod h1:w648aMHEgFYS6xb0KVMMtZ2uMeemhiKCuD2vj6gY52A=\ngithub.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI=\ngithub.com/ardielle/ardielle-tools v1.5.4/go.mod h1:oZN+JRMnqGiIhrzkRN9l26Cej9dEx4jeNG6A+AdkShk=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=\ngithub.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=\ngithub.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=\ngithub.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=\ngithub.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=\ngithub.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=\ngithub.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=\ngithub.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=\ngithub.com/aws/aws-sdk-go v1.41.7/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=\ngithub.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=\ngithub.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=\ngithub.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=\ngithub.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=\ngithub.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=\ngithub.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f/go.mod h1:SghidfnxvX7ribW6nHI7T+IBbc9puZ9kk5Tx/88h8P4=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=\ngithub.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=\ngithub.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=\ngithub.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=\ngithub.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=\ngithub.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=\ngithub.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=\ngithub.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=\ngithub.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=\ngithub.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=\ngithub.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=\ngithub.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=\ngithub.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=\ngithub.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=\ngithub.com/bytecodealliance/wasmtime-go v0.35.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI=\ngithub.com/camunda/zeebe/clients/go/v8 v8.0.3/go.mod h1:iOEgFlCYAPdqae6iPp0ajeo2RSxJirU39i+UAN74NOY=\ngithub.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=\ngithub.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=\ngithub.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=\ngithub.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=\ngithub.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=\ngithub.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=\ngithub.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=\ngithub.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=\ngithub.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=\ngithub.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=\ngithub.com/cinience/go_rocketmq v0.0.2/go.mod h1:2YNY7emT546dcFpMEWLesmAEi4ndW7+tX5VfNf1Zsgs=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudevents/sdk-go/v2 v2.4.1 h1:rZJoz9QVLbWQmnvLPDFEmv17Czu+CfSPwMO6lhJ72xQ=\ngithub.com/cloudevents/sdk-go/v2 v2.4.1/go.mod h1:MZiMwmAh5tGj+fPFvtHv9hKurKqXtdB9haJYMJ/7GJY=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=\ngithub.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=\ngithub.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=\ngithub.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=\ngithub.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=\ngithub.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=\ngithub.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=\ngithub.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=\ngithub.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=\ngithub.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=\ngithub.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=\ngithub.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=\ngithub.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=\ngithub.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=\ngithub.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=\ngithub.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=\ngithub.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=\ngithub.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=\ngithub.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=\ngithub.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=\ngithub.com/containerd/cgroups v1.0.2/go.mod h1:qpbpJ1jmlqsR9f2IyaLPsdkCdnt0rbDVqIDlhuu5tRY=\ngithub.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8=\ngithub.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=\ngithub.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=\ngithub.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=\ngithub.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=\ngithub.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=\ngithub.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=\ngithub.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=\ngithub.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=\ngithub.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=\ngithub.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=\ngithub.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=\ngithub.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ=\ngithub.com/containerd/containerd v1.6.2/go.mod h1:sidY30/InSE1j2vdD1ihtKoJz+lWdaXMdiAeIupaf+s=\ngithub.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=\ngithub.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=\ngithub.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=\ngithub.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=\ngithub.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk=\ngithub.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=\ngithub.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=\ngithub.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=\ngithub.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=\ngithub.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=\ngithub.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=\ngithub.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=\ngithub.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk=\ngithub.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA=\ngithub.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA=\ngithub.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=\ngithub.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=\ngithub.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=\ngithub.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=\ngithub.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=\ngithub.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=\ngithub.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=\ngithub.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=\ngithub.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms=\ngithub.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4=\ngithub.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=\ngithub.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=\ngithub.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=\ngithub.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM=\ngithub.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=\ngithub.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=\ngithub.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=\ngithub.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=\ngithub.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=\ngithub.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ=\ngithub.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=\ngithub.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=\ngithub.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=\ngithub.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=\ngithub.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=\ngithub.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=\ngithub.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=\ngithub.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=\ngithub.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=\ngithub.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y=\ngithub.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=\ngithub.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=\ngithub.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE=\ngithub.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=\ngithub.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=\ngithub.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=\ngithub.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=\ngithub.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=\ngithub.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=\ngithub.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=\ngithub.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creasty/defaults v1.5.2/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=\ngithub.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=\ngithub.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=\ngithub.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=\ngithub.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=\ngithub.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=\ngithub.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=\ngithub.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=\ngithub.com/dancannon/gorethink v4.0.0+incompatible/go.mod h1:BLvkat9KmZc1efyYwhz3WnybhRZtgF1K929FD8z1avU=\ngithub.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=\ngithub.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=\ngithub.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=\ngithub.com/dapr/components-contrib v1.8.0-rc.6/go.mod h1:gxrCpaosbI0n3SFW7fKSvJU/ymjryHqrdRgqmsknuno=\ngithub.com/dapr/components-contrib v1.8.1-rc.1/go.mod h1:gxrCpaosbI0n3SFW7fKSvJU/ymjryHqrdRgqmsknuno=\ngithub.com/dapr/dapr v1.8.0/go.mod h1:yAsDiK5oecG0htw2S8JG9RFaeHJVdlTfZyOrL57AvRM=\ngithub.com/dapr/dapr v1.8.3 h1:wAmP8lXeI1OeCnLGi3XT1PokbSaM0/N71ChZhjPdTCw=\ngithub.com/dapr/dapr v1.8.3/go.mod h1:/0JyKebxzz0vPwYXc/2qHBXIicUi01HUWnpQ8AiJ0zM=\ngithub.com/dapr/go-sdk v1.5.0 h1:OVkrupquJEOL1qRtwKcMVrFKYhw4UJQvgOJNduo2VxE=\ngithub.com/dapr/go-sdk v1.5.0/go.mod h1:Cvz3taCVu22WCNEUbc9/szvG/yJxWPAV4dcaG+zDWA4=\ngithub.com/dapr/kit v0.0.2-0.20210614175626-b9074b64d233/go.mod h1:y8r0VqUNKyd6xBXp7gQjwA59wlCLGfKzL5J8iJsN09w=\ngithub.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/deepmap/oapi-codegen v1.3.6/go.mod h1:aBozjEveG+33xPiP55Iw/XbVkhtZHEGLq3nxlX0+hfU=\ngithub.com/deepmap/oapi-codegen v1.8.1/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20210411162248-d9abbec934ba/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=\ngithub.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=\ngithub.com/dghubble/go-twitter v0.0.0-20190719072343-39e5462e111f/go.mod h1:xfg4uS5LEzOj8PgZV7SQYRHbG7jPUnelEiaAVJxmhJE=\ngithub.com/dghubble/oauth1 v0.6.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk=\ngithub.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY=\ngithub.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=\ngithub.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=\ngithub.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=\ngithub.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=\ngithub.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=\ngithub.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=\ngithub.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac=\ngithub.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=\ngithub.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=\ngithub.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=\ngithub.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker v20.10.14+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=\ngithub.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=\ngithub.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=\ngithub.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/dubbogo/go-zookeeper v1.0.3/go.mod h1:fn6n2CAEer3novYgk9ULLwAjuV8/g4DdC2ENwRb6E+c=\ngithub.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5/go.mod h1:fn6n2CAEer3novYgk9ULLwAjuV8/g4DdC2ENwRb6E+c=\ngithub.com/dubbogo/gost v1.9.0/go.mod h1:pPTjVyoJan3aPxBPNUX0ADkXjPibLo+/Ib0/fADXSG8=\ngithub.com/dubbogo/gost v1.11.18/go.mod h1:vIcP9rqz2KsXHPjsAwIUtfJIJjppQLQDcYaZTy/61jI=\ngithub.com/dubbogo/gost v1.11.23/go.mod h1:PhJ8+qZJx+Txjx1KthNPuVkCvUca0jRLgKWj/noGgeI=\ngithub.com/dubbogo/gost v1.11.25/go.mod h1:iovrPhv0hyakhQGVr4jwiECBL9HXNuBY4VV3HWK5pM0=\ngithub.com/dubbogo/grpc-go v1.42.9/go.mod h1:F1T9hnUvYGW4JLK1QNriavpOkhusU677ovPzLkk6zHM=\ngithub.com/dubbogo/jsonparser v1.0.1/go.mod h1:tYAtpctvSP/tWw4MeelsowSPgXQRVHHWbqL6ynps8jU=\ngithub.com/dubbogo/net v0.0.4/go.mod h1:1CGOnM7X3he+qgGNqjeADuE5vKZQx/eMSeUkpU3ujIc=\ngithub.com/dubbogo/triple v1.0.9/go.mod h1:1t9me4j4CTvNDcsMZy6/OGarbRyAUSY0tFXGXHCp7Iw=\ngithub.com/dubbogo/triple v1.1.8/go.mod h1:9pgEahtmsY/avYJp3dzUQE8CMMVe1NtGBmUhfICKLJk=\ngithub.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=\ngithub.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=\ngithub.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.0/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=\ngithub.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=\ngithub.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=\ngithub.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=\ngithub.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=\ngithub.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5/go.mod h1:MQXNGeXkpojWTxbN7vXoE3f7EmlA11MlJbsrJpVBINA=\ngithub.com/fasthttp/router v1.3.8/go.mod h1:DQBvuHvYbn3SUN6pGjwjPbpCNpWfCFc5Ipn/Fj6XxFc=\ngithub.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=\ngithub.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=\ngithub.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=\ngithub.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=\ngithub.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=\ngithub.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=\ngithub.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=\ngithub.com/getkin/kin-openapi v0.2.0/go.mod h1:V1z9xl9oF5Wt7v32ne4FmiF1alpS4dM6mNzoywPOXlk=\ngithub.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=\ngithub.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=\ngithub.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=\ngithub.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=\ngithub.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=\ngithub.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=\ngithub.com/go-co-op/gocron v1.9.0/go.mod h1:DbJm9kdgr1sEvWpHCA7dFFs/PGHPMil9/97EXCRPr4k=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-ini/ini v1.66.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=\ngithub.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=\ngithub.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro=\ngithub.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=\ngithub.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=\ngithub.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=\ngithub.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=\ngithub.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=\ngithub.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=\ngithub.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=\ngithub.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=\ngithub.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=\ngithub.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=\ngithub.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=\ngithub.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=\ngithub.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=\ngithub.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=\ngithub.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=\ngithub.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=\ngithub.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=\ngithub.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=\ngithub.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=\ngithub.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=\ngithub.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=\ngithub.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=\ngithub.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=\ngithub.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=\ngithub.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=\ngithub.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=\ngithub.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=\ngithub.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=\ngithub.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=\ngithub.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=\ngithub.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=\ngithub.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=\ngithub.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=\ngithub.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=\ngithub.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=\ngithub.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=\ngithub.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=\ngithub.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=\ngithub.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=\ngithub.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=\ngithub.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=\ngithub.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=\ngithub.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=\ngithub.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=\ngithub.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=\ngithub.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogap/errors v0.0.0-20200228125012-531a6449b28c/go.mod h1:tbRYYYC7g/H7QlCeX0Z2zaThWKowF4QQCFIsGgAsqRo=\ngithub.com/gogap/stack v0.0.0-20150131034635-fef68dddd4f8/go.mod h1:6q1WEv2BiAO4FSdwLQTJbWQYAn1/qDNJHUGJNXCj9kM=\ngithub.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=\ngithub.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=\ngithub.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=\ngithub.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=\ngithub.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=\ngithub.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=\ngithub.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=\ngithub.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=\ngithub.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=\ngithub.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg=\ngithub.com/gonum/integrate v0.0.0-20181209220457-a422b5c0fdf2/go.mod h1:pDgmNM6seYpwvPos3q+zxlXMsbve6mOIPucUnUOrI7Y=\ngithub.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=\ngithub.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A=\ngithub.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=\ngithub.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=\ngithub.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w=\ngithub.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=\ngithub.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=\ngithub.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=\ngithub.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=\ngithub.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=\ngithub.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c/go.mod h1:YjKB0WsLXlMkO9p+wGTCoPIDGRJH0mz7E526PxkQVxI=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=\ngithub.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=\ngithub.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=\ngithub.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=\ngithub.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=\ngithub.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=\ngithub.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=\ngithub.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=\ngithub.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=\ngithub.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=\ngithub.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=\ngithub.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=\ngithub.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=\ngithub.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=\ngithub.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=\ngithub.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=\ngithub.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=\ngithub.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=\ngithub.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=\ngithub.com/hazelcast/hazelcast-go-client v0.0.0-20190530123621-6cf767c2f31a/go.mod h1:VhwtcZ7sg3xq7REqGzEy7ylSWGKz4jZd05eCJropNzI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s=\ngithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.0.87/go.mod h1:IvF+Pe06JMUivVgN6B4wcsPEoFvVa40IYaOPZyUt5HE=\ngithub.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=\ngithub.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=\ngithub.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/influxdata/influxdb-client-go v1.4.0/go.mod h1:S+oZsPivqbcP1S9ur+T+QqXvrYS3NCZeMQtBoH4D1dw=\ngithub.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=\ngithub.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=\ngithub.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=\ngithub.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=\ngithub.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=\ngithub.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=\ngithub.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=\ngithub.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=\ngithub.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=\ngithub.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=\ngithub.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=\ngithub.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=\ngithub.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=\ngithub.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=\ngithub.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=\ngithub.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=\ngithub.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=\ngithub.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=\ngithub.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=\ngithub.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=\ngithub.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=\ngithub.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=\ngithub.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=\ngithub.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jawher/mow.cli v1.0.4/go.mod h1:5hQj2V8g+qYmLUVWqu4Wuja1pI57M83EChYLVZ0sMKk=\ngithub.com/jawher/mow.cli v1.2.0/go.mod h1:y+pcA3jBAdo/GIZx/0rFjw/K2bVEODP9rfZOfaiq8Ko=\ngithub.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=\ngithub.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=\ngithub.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=\ngithub.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=\ngithub.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=\ngithub.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=\ngithub.com/kataras/go-errors v0.0.3/go.mod h1:K3ncz8UzwI3bpuksXt5tQLmrRlgxfv+52ARvAu1+I+o=\ngithub.com/kataras/go-serializer v0.0.4/go.mod h1:/EyLBhXKQOJ12dZwpUZZje3lGy+3wnvG7QKaVJtm/no=\ngithub.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=\ngithub.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.10.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/knadh/koanf v1.4.1/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs=\ngithub.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7/go.mod h1:Y2SaZf2Rzd0pXkLVhLlCiAXFCLSXAIbTKDivVgff/AM=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/labd/commercetools-go-sdk v0.3.2/go.mod h1:I+KKNALlg6PcSertsVA7E442koO99GT7gldWqwZlUGo=\ngithub.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=\ngithub.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=\ngithub.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=\ngithub.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE=\ngithub.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod h1:UGmTpUd3rjbtfIpwAPrcfmGf/Z1HS95TATB+m57TPB8=\ngithub.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=\ngithub.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=\ngithub.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=\ngithub.com/linkedin/goavro/v2 v2.9.8/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=\ngithub.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=\ngithub.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=\ngithub.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=\ngithub.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=\ngithub.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=\ngithub.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=\ngithub.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=\ngithub.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=\ngithub.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=\ngithub.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=\ngithub.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=\ngithub.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=\ngithub.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=\ngithub.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=\ngithub.com/microcosm-cc/bluemonday v1.0.7/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=\ngithub.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=\ngithub.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=\ngithub.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=\ngithub.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=\ngithub.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=\ngithub.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=\ngithub.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM=\ngithub.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=\ngithub.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=\ngithub.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=\ngithub.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=\ngithub.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=\ngithub.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=\ngithub.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=\ngithub.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=\ngithub.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=\ngithub.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=\ngithub.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=\ngithub.com/mrz1836/postmark v1.2.9/go.mod h1:xNRms8jgTfqBneqg0+PzvBrhuojefqXIWc6Np0nHiEM=\ngithub.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=\ngithub.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=\ngithub.com/nacos-group/nacos-sdk-go v1.1.1/go.mod h1:UHOtQNQY/qpk2dhg6gDq8u5+/CEIc3+lWmrmxEzX0/g=\ngithub.com/nacos-group/nacos-sdk-go/v2 v2.0.1/go.mod h1:SlhyCAv961LcZ198XpKfPEQqlJWt2HkL1fDLas0uy/w=\ngithub.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=\ngithub.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=\ngithub.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=\ngithub.com/nats-io/jwt v1.1.0/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M=\ngithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=\ngithub.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=\ngithub.com/nats-io/nats-server/v2 v2.1.9/go.mod h1:9qVyoewoYXzG1ME9ox0HwkkzyYvnlBDugfR4Gg/8uHU=\ngithub.com/nats-io/nats-server/v2 v2.7.4/go.mod h1:1vZ2Nijh8tcyNe8BDVyTviCd9NYzRbubQYiEHsvOQWc=\ngithub.com/nats-io/nats-streaming-server v0.21.2/go.mod h1:2W8QfNVOtcFpmf0bRiwuLtRb0/hkX4NuOxPOFNOThVQ=\ngithub.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=\ngithub.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE=\ngithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/nats-io/stan.go v0.8.3/go.mod h1:Ejm8bbHnMTSptU6uNMAVuxeapMJYBB/Ml3ej6z4GoSY=\ngithub.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=\ngithub.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=\ngithub.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=\ngithub.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=\ngithub.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=\ngithub.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=\ngithub.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/open-policy-agent/opa v0.40.0/go.mod h1:UQqv8nJ1njs2+Od1lrPFzUAApdj22ABxTO35+Vpsjz4=\ngithub.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=\ngithub.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=\ngithub.com/opencontainers/runc v1.0.3/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=\ngithub.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=\ngithub.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=\ngithub.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=\ngithub.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=\ngithub.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=\ngithub.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=\ngithub.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=\ngithub.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=\ngithub.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=\ngithub.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=\ngithub.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=\ngithub.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/oracle/oci-go-sdk/v54 v54.0.0/go.mod h1:+t+yvcFGVp+3ZnztnyxqXfQDsMlq8U25faBLa+mqCMc=\ngithub.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=\ngithub.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=\ngithub.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=\ngithub.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=\ngithub.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/polarismesh/polaris-go v1.1.0/go.mod h1:tquawfjEKp1W3ffNJQSzhfditjjoZ7tvhOCElN7Efzs=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=\ngithub.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=\ngithub.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=\ngithub.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=\ngithub.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=\ngithub.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=\ngithub.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=\ngithub.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=\ngithub.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=\ngithub.com/prometheus/statsd_exporter v0.22.3/go.mod h1:N4Z1+iSqc9rnxlT1N8Qn3l65Vzb5t4Uq0jpg8nxyhio=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rabbitmq/amqp091-go v1.3.4/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=\ngithub.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=\ngithub.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=\ngithub.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=\ngithub.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=\ngithub.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=\ngithub.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=\ngithub.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=\ngithub.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=\ngithub.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/savsgio/gotils v0.0.0-20210217112953-d4a072536008/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=\ngithub.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=\ngithub.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=\ngithub.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=\ngithub.com/sendgrid/rest v2.6.3+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=\ngithub.com/sendgrid/sendgrid-go v3.5.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=\ngithub.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=\ngithub.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sijms/go-ora/v2 v2.2.22/go.mod h1:jzfAFD+4CXHE+LjGWFl6cPrtiIpQVxakI2gvrMF2w6Y=\ngithub.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=\ngithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=\ngithub.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/sony/gobreaker v0.4.2-0.20210216022020-dd874f9dd33b/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=\ngithub.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=\ngithub.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=\ngithub.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=\ngithub.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=\ngithub.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=\ngithub.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=\ngithub.com/stathat/consistent v1.0.0/go.mod h1:uajTPbgSygZBJ+V+0mY7meZ8i0XAcZs7AQ6V121XSxw=\ngithub.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=\ngithub.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/supplyon/gremcos v0.1.0/go.mod h1:ZnXsXGVbGCYDFU5GLPX9HZLWfD+ZWkiPo30KUjNoOtw=\ngithub.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=\ngithub.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=\ngithub.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=\ngithub.com/testcontainers/testcontainers-go v0.12.0/go.mod h1:SIndOQXZng0IW8iWU1Js0ynrfZ8xcxrTtDfF6rD2pxs=\ngithub.com/tetratelabs/wazero v0.0.0-20220425003459-ad61d9a6ff43/go.mod h1:Y4X/zO4sC2dJjZG9GDYNRbJGogfqFYJY/BbyKlOxXGI=\ngithub.com/tevid/gohamcrest v1.1.1/go.mod h1:3UvtWlqm8j5JbwYZh80D/PVBt0mJ1eJiYgZMibh0H/k=\ngithub.com/tidwall/gjson v1.2.1/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=\ngithub.com/tidwall/gjson v1.8.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=\ngithub.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=\ngithub.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=\ngithub.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=\ngithub.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3/go.mod h1:QDlpd3qS71vYtakd2hmdpqhJ9nwv6mD6A30bQ1BPBFE=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=\ngithub.com/uber/jaeger-client-go v2.29.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=\ngithub.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=\ngithub.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=\ngithub.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.21.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A=\ngithub.com/valyala/fasthttp v1.31.1-0.20211216042702-258a4c17b4f4/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=\ngithub.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=\ngithub.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=\ngithub.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=\ngithub.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=\ngithub.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=\ngithub.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=\ngithub.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=\ngithub.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vmware/vmware-go-kcl v1.5.0/go.mod h1:P92YfaWfQyudNf62BNx+E2rJn9pd165MhHsRt8ajkpM=\ngithub.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=\ngithub.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=\ngithub.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=\ngithub.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=\ngithub.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=\ngithub.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=\ngithub.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=\ngithub.com/yuin/gopher-lua v0.0.0-20200603152657-dc2b0ca8b37e/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=\ngithub.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=\ngithub.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=\ngithub.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=\ngithub.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=\ngithub.com/zouyx/agollo/v3 v3.4.5/go.mod h1:LJr3kDmm23QSW+F1Ol4TMHDa7HvJvscMdVxJ2IpUTVc=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=\ngo.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=\ngo.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=\ngo.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=\ngo.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw=\ngo.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU=\ngo.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=\ngo.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=\ngo.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8=\ngo.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=\ngo.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=\ngo.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY=\ngo.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE=\ngo.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w=\ngo.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc=\ngo.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ=\ngo.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4=\ngo.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=\ngo.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=\ngo.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0/go.mod h1:PFmBsWbldL1kiWZk9+0LBZz2brhByaGsvp6pRICMlPE=\ngo.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=\ngo.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=\ngo.opentelemetry.io/otel v1.6.0/go.mod h1:bfJD2DZVw0LBxghOTlgnlI0CV3hLDu9XF/QKOUXMTQQ=\ngo.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ=\ngo.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=\ngo.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk=\ngo.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=\ngo.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=\ngo.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3/go.mod h1:NEu79Xo32iVb+0gVNV8PMd7GoWqnyDXRlj04yFjqz40=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.3/go.mod h1:UJmXdiVVBaZ63umRUTwJuCMAV//GCMvDiQwn703/GoY=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.3/go.mod h1:ycItY/esVj8c0dKgYTOztTERXtPzcfDU/0o8EdwCjoA=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=\ngo.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=\ngo.opentelemetry.io/otel/metric v0.28.0/go.mod h1:TrzsfQAmQaB1PDcdhBauLMk7nyyg9hm+GoQq/ekE9Iw=\ngo.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=\ngo.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=\ngo.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=\ngo.opentelemetry.io/otel/sdk v1.6.3/go.mod h1:A4iWF7HTXa+GWL/AaqESz28VuSBIcZ+0CV+IzJ5NMiQ=\ngo.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=\ngo.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=\ngo.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=\ngo.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=\ngo.opentelemetry.io/otel/trace v1.6.0/go.mod h1:qs7BrU5cZ8dXQHBGxHMOxwME/27YH2qEp4/+tZLLwJE=\ngo.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0=\ngo.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=\ngo.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ=\ngo.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=\ngo.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=\ngo.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=\ngo.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=\ngo.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=\ngo.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngo.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=\ngo.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=\ngo.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=\ngo.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=\ngo.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=\ngoji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=\ngolang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220621193019-9d032be2e588 h1:9ubFuySsnAJYGyJrZ3koiEv8FyqofCBdz3G9Mbf2YFc=\ngolang.org/x/net v0.0.0-20220621193019-9d032be2e588/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180828065106-d99a578cf41b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=\ngolang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM=\ngolang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=\ngolang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=\ngonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=\ngonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=\ngoogle.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=\ngoogle.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=\ngoogle.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=\ngoogle.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210701133433-6b8dcf568a95/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=\ngoogle.golang.org/genproto v0.0.0-20210707164411-8c882eb9abba/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220622171453-ea41d75dfa0f h1:kYlCnpX4eB0QEnXm12j4DAX4yrjjhJmsyuWtSSZ+Buo=\ngoogle.golang.org/genproto v0.0.0-20220622171453-ea41d75dfa0f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=\ngoogle.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=\ngopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/couchbase/gocb.v1 v1.6.4/go.mod h1:Ri5Qok4ZKiwmPr75YxZ0uELQy45XJgUSzeUnK806gTY=\ngopkg.in/couchbase/gocbcore.v7 v7.1.18/go.mod h1:48d2Be0MxRtsyuvn+mWzqmoGUG9uA00ghopzOs148/E=\ngopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4/go.mod h1:ZjII0iKx4Veo6N6da+pEZu/ptNyKLg9QTVt7fFmR6sw=\ngopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4/go.mod h1:jl/gd/aQ2S8whKVSTnsPs6n7BPeaAuw9UglBD/OF7eo=\ngopkg.in/couchbaselabs/jsonx.v1 v1.0.1/go.mod h1:oR201IRovxvLW/eISevH12/+MiKHtNQAKfcX8iWZvJY=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=\ngopkg.in/gorethink/gorethink.v4 v4.1.0/go.mod h1:M7JgwrUAmshJ3iUbEK0Pt049MPyPK+CYDGGaEjdZb/c=\ngopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=\ngopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=\ngopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=\ngopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=\ngopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=\ngopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=\ngopkg.in/kataras/go-serializer.v0 v0.0.4/go.mod h1:v2jHg/3Wp7uncDNzenTsX75PRDxhzlxoo/qDvM4ZGxk=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\nhonnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.20.0/go.mod h1:HyLC5l5eoS/ygQYl1BXBgFzWNlkHiAuyNAbevIn+FKg=\nk8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=\nk8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=\nk8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=\nk8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs=\nk8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg=\nk8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4=\nk8s.io/apimachinery v0.20.0/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=\nk8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=\nk8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=\nk8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=\nk8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=\nk8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U=\nk8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc=\nk8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=\nk8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=\nk8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=\nk8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ=\nk8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4=\nk8s.io/cli-runtime v0.23.0/go.mod h1:B5N3YH0KP1iKr6gEuJ/RRmGjO0mJQ/f/JrsmEiPQAlU=\nk8s.io/client-go v0.20.0/go.mod h1:4KWh/g+Ocd8KkCwKF8vUNnmqgv+EVnQDK4MBF4oB5tY=\nk8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=\nk8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=\nk8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=\nk8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y=\nk8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA=\nk8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=\nk8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg=\nk8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE=\nk8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=\nk8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=\nk8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=\nk8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI=\nk8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI=\nk8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=\nk8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=\nk8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=\nk8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=\nk8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4=\nk8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=\nk8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=\nk8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=\nk8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=\nk8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=\nk8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=\nk8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=\nk8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=\nk8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=\nk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk=\nk8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=\nk8s.io/metrics v0.20.0/go.mod h1:9yiRhfr8K8sjdj2EthQQE9WvpYDvsXIV3CjN4Ruq4Jw=\nk8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nnhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\nnhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\noras.land/oras-go v1.1.0/go.mod h1:1A7vR/0KknT2UkJVWh+xMi95I/AhK8ZrxrnUSmXN0bQ=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I=\nsigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA=\nsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=\nsigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8=\nsigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=\nskywalking.apache.org/repo/goapi v0.0.0-20220401015832-2c9eee9481eb h1:+PP2DpKFN/rEporLdPI4A7bPWQjwfARlUDKNhSab8iM=\nskywalking.apache.org/repo/goapi v0.0.0-20220401015832-2c9eee9481eb/go.mod h1:uWwwvhcwe2MD/nJCg0c1EE/eL6KzaBosLHDfMFoEJ30=\nsourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=\nstathat.com/c/consistent v1.0.0/go.mod h1:QkzMWzcbB+yQBL2AttO6sgsQS/JSTapcDISJalmCDS0=\n"
  },
  {
    "path": "ci/pod/openfunction/function-example/test-uri/hello.go",
    "content": "/*\n * Copyright 2022 The OpenFunction Authors.\n *\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage hello\n\nimport (\n\t\"fmt\"\n\tofctx \"github.com/OpenFunction/functions-framework-go/context\"\n\t\"net/http\"\n\n\t\"github.com/OpenFunction/functions-framework-go/functions\"\n)\n\nfunc init() {\n\tfunctions.HTTP(\"HelloWorld\", HelloWorld,\n\t\tfunctions.WithFunctionPath(\"/{greeting}\"))\n}\n\nfunc HelloWorld(w http.ResponseWriter, r *http.Request) {\n\tvars := ofctx.VarsFromCtx(r.Context())\n\tfmt.Fprintf(w, \"Hello, %s!\\n\", vars[\"greeting\"])\n}\n"
  },
  {
    "path": "ci/pod/otelcol-contrib/config.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nreceivers:\n  otlp:\n    protocols:\n      grpc:\n        endpoint: 0.0.0.0:4317\n      http:\n        endpoint: 0.0.0.0:4318\nexporters:\n  file:\n    path: /etc/otelcol-contrib/data-otlp.json\n    append: true\nservice:\n  pipelines:\n    traces:\n      receivers: [otlp]\n      exporters: [file]\n"
  },
  {
    "path": "ci/pod/vector/vector.toml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[sources.log-from-tcp]\ntype = \"socket\"\naddress = \"0.0.0.0:3000\"\nhost_key = \"host\"\nmode = \"tcp\"\nport_key = \"port\"\nshutdown_timeout_secs = 30\nsocket_file_mode = 511\n\n[sources.log-from-http]\ntype = \"http_server\"\naddress = \"0.0.0.0:3001\"\n\n[sources.log-from-udp]\ntype = \"socket\"\naddress = \"0.0.0.0:8127\"\nhost_key = \"host\"\nmode = \"udp\"\nport_key = \"port\"\n\n[sources.log-from-tls]\ntype = \"socket\"\naddress = \"0.0.0.0:43000\"\nhost_key = \"host\"\nmode = \"tcp\"\nport_key = \"port\"\ntls.enabled = true\ntls.verify = true\ntls.ca_file = \"/certs/vector_logs_ca.crt\"\ntls.crt_file = \"/certs/vector_logs_server.crt\"\ntls.key_file = \"/certs/vector_logs_server.key\"\n\n[sources.log-from-syslog-tcp]\ntype = \"syslog\"\naddress = \"0.0.0.0:5140\"\nmode = \"tcp\"\n\n[sources.log-from-syslog-udp]\ntype = \"syslog\"\naddress = \"0.0.0.0:5150\"\nmode = \"udp\"\n\n[sources.log-from-splunk]\ntype = \"splunk_hec\"\naddress = \"0.0.0.0:18088\"\nvalid_tokens = [\n  \"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n]\n\n[sinks.log-2-console]\ninputs = [ \"log-from-tcp\",  \"log-from-tls\", \"log-from-syslog-tcp\", \"log-from-syslog-udp\", \"log-from-udp\", \"log-from-splunk\", \"log-from-http\"]\ntype = \"console\"\nencoding.codec = \"json\"\n\n[sinks.log-2-tcp-file]\ninputs = [ \"log-from-tcp\" ]\ntype = \"file\"\nencoding.codec = \"text\"\npath = \"/etc/vector/tcp.log\"\n\n[sinks.log-2-http-file]\ninputs = [ \"log-from-http\" ]\ntype = \"file\"\nencoding.codec = \"text\"\npath = \"/etc/vector/http.log\"\n\n[sinks.log-2-udp-file]\ninputs = [ \"log-from-udp\" ]\ntype = \"file\"\nencoding.codec = \"json\"\npath = \"/etc/vector/udp.log\"\n\n[sinks.tls-log-2-file]\ninputs = [ \"log-from-tls\" ]\ntype = \"file\"\nencoding.codec = \"json\"\npath = \"/etc/vector/tls-datas.log\"\n\n[sinks.log-2-syslog-tcp-file]\ninputs = [ \"log-from-syslog-tcp\" ]\ntype = \"file\"\nencoding.codec = \"text\"\npath = \"/etc/vector/syslog-tcp.log\"\n\n[sinks.log-2-splunk-file]\ninputs = [ \"log-from-splunk\" ]\ntype = \"file\"\nencoding.codec = \"json\"\npath = \"/etc/vector/splunk.log\"\n\n[sinks.log-2-syslog-udp-file]\ninputs = [ \"log-from-syslog-udp\" ]\ntype = \"file\"\nencoding.codec = \"text\"\npath = \"/etc/vector/syslog-udp.log\"\n"
  },
  {
    "path": "ci/prepare_filesystem_mcp.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\n\nVERSION=\"2025.7.1\"\nURL=\"https://github.com/modelcontextprotocol/servers/archive/refs/tags/${VERSION}.tar.gz\"\n\nWORKDIR=\"$(mktemp -d)\"\nDEST_DIR=\"${REPO_ROOT}/t/plugin/mcp/servers\"\n\ncurl -L \"${URL}\" | tar -xz -C \"${WORKDIR}\"\n\nrm -rf \"${DEST_DIR}\"\nmkdir -p \"$(dirname \"${DEST_DIR}\")\"\n\ncp -R \"${WORKDIR}/servers-${VERSION}\" \"${DEST_DIR}\"\n\n(\n  cd \"${DEST_DIR}\"\n  npm install\n  # Note: Although dlx specifies the package version, it does not use a lockfile,\n  # so dependency resolution is not reproducible. Only the package-lock.json included\n  # in the release package can ensure that the entire dependency tree is fully locked.\n  npm run build -w @modelcontextprotocol/server-filesystem\n)\n\necho \"[OK] filesystem MCP ready: ${DEST_DIR}\"\n"
  },
  {
    "path": "ci/redhat-ci.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./ci/common.sh\ninstall_dependencies() {\n    export_version_info\n    export_or_prefix\n\n    # install build & runtime deps\n    yum install -y --disablerepo=* --enablerepo=ubi-8-appstream-rpms --enablerepo=ubi-8-baseos-rpms \\\n    wget tar gcc gcc-c++ automake autoconf libtool make unzip git sudo openldap-devel hostname patch \\\n    which ca-certificates pcre pcre-devel pcre2 pcre2-devel xz \\\n    openssl-devel\n    yum install -y libyaml-devel\n    yum install -y --disablerepo=* --enablerepo=ubi-8-appstream-rpms --enablerepo=ubi-8-baseos-rpms cpanminus perl\n\n    # install newer curl\n    yum makecache\n    yum install -y xz\n    install_curl\n\n    # install apisix-runtime to make apisix's rpm test work\n    yum install -y yum-utils && yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo\n    yum install -y openresty-pcre-devel openresty-zlib-devel\n\n    install_apisix_runtime\n    curl -o /usr/local/openresty/openssl3/ssl/openssl.cnf \\\n        https://raw.githubusercontent.com/api7/apisix-build-tools/apisix-runtime/${APISIX_RUNTIME}/conf/openssl3/openssl.cnf\n\n    # patch lua-resty-events\n    sed -i 's/log(ERR, \"event worker failed: \", perr)/log(ngx.WARN, \"event worker failed: \", perr)/' /usr/local/openresty/lualib/resty/events/worker.lua\n\n    # install luarocks\n    ./utils/linux-install-luarocks.sh\n\n    # install etcdctl\n    ./ci/linux-install-etcd-client.sh\n\n    # install vault cli capabilities\n    install_vault_cli\n\n    # install brotli\n    yum install -y cmake3\n    install_brotli\n\n    # install test::nginx\n    cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)\n\n    # add go1.15 binary to the path\n    mkdir build-cache\n    pushd build-cache/\n    # Go is required inside the container.\n    wget -q https://golang.org/dl/go1.17.linux-amd64.tar.gz && tar -xf go1.17.linux-amd64.tar.gz\n    export PATH=$PATH:$(pwd)/go/bin\n    popd\n    # install and start grpc_server_example\n    pushd t/grpc_server_example\n\n    CGO_ENABLED=0 go build\n    popd\n\n    yum install -y iproute procps\n    start_grpc_server_example\n\n    start_sse_server_example\n\n    # installing grpcurl\n    install_grpcurl\n\n    # install nodejs\n    install_nodejs\n\n    # grpc-web server && client\n    pushd t/plugin/grpc-web\n    ./setup.sh\n    # back to home directory\n    popd\n\n    # install dependencies\n    git clone https://github.com/openresty/test-nginx.git test-nginx\n    create_lua_deps\n}\n\nrun_case() {\n    export_or_prefix\n    make init\n    set_coredns\n    # run test cases\n    FLUSH_ETCD=1 prove --timer -Itest-nginx/lib -I./ -r ${TEST_FILE_SUB_DIR} | tee /tmp/test.result\n    fail_on_bailout /tmp/test.result\n    rerun_flaky_tests /tmp/test.result\n}\n\ncase_opt=$1\ncase $case_opt in\n    (install_dependencies)\n        install_dependencies\n        ;;\n    (run_case)\n        run_case\n        ;;\nesac\n"
  },
  {
    "path": "ci/tars-ci.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./ci/common.sh\n\nrun_case() {\n    export_or_prefix\n    export PERL5LIB=.:$PERL5LIB\n    prove -Itest-nginx/lib -I./ -r t/tars | tee test-result\n    rerun_flaky_tests test-result\n}\n\ncase_opt=$1\ncase $case_opt in\n    (run_case)\n        run_case\n        ;;\nesac\n"
  },
  {
    "path": "conf/cert/ssl_PLACE_HOLDER.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "conf/cert/ssl_PLACE_HOLDER.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5\njhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo\neLj0efMiOepOSZflj9Ob4yKR2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5s\nmPtW1Oc/BV5terhscJdOgmRrabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt\n6iMWEGeQU6mwPENgvj1olji2WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiy\nVt1TmtMWn1ztk6FfLRqwJWR/Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1o\nnpRVeXhrBajbCRDRBMwaNw/1/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2\nfzaqpIfyUbPST4GdqNG9NyIh/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI\n1cGrGwyXbrieNp63AgMBAAECggGBAJM8g0duoHmIYoAJzbmKe4ew0C5fZtFUQNmu\nO2xJITUiLT3ga4LCkRYsdBnY+nkK8PCnViAb10KtIT+bKipoLsNWI9Xcq4Cg4G3t\n11XQMgPPgxYXA6m8t+73ldhxrcKqgvI6xVZmWlKDPn+CY/Wqj5PA476B5wEmYbNC\nGIcd1FLl3E9Qm4g4b/sVXOHARF6iSvTR+6ol4nfWKlaXSlx2gNkHuG8RVpyDsp9c\nz9zUqAdZ3QyFQhKcWWEcL6u9DLBpB/gUjyB3qWhDMe7jcCBZR1ALyRyEjmDwZzv2\njlv8qlLFfn9R29UI0pbuL1eRAz97scFOFme1s9oSU9a12YHfEd2wJOM9bqiKju8y\nDZzePhEYuTZ8qxwiPJGy7XvRYTGHAs8+iDlG4vVpA0qD++1FTpv06cg/fOdnwshE\nOJlEC0ozMvnM2rZ2oYejdG3aAnUHmSNa5tkJwXnmj/EMw1TEXf+H6+xknAkw05nh\nzsxXrbuFUe7VRfgB5ElMA/V4NsScgQKBwQDmMRtnS32UZjw4A8DsHOKFzugfWzJ8\nGc+3sTgs+4dNIAvo0sjibQ3xl01h0BB2Pr1KtkgBYB8LJW/FuYdCRS/KlXH7PHgX\n84gYWImhNhcNOL3coO8NXvd6+m+a/Z7xghbQtaraui6cDWPiCNd/sdLMZQ/7LopM\nRbM32nrgBKMOJpMok1Z6zsPzT83SjkcSxjVzgULNYEp03uf1PWmHuvjO1yELwX9/\ngoACViF+jst12RUEiEQIYwr4y637GQBy+9cCgcEA3pN9W5OjSPDVsTcVERig8++O\nBFURiUa7nXRHzKp2wT6jlMVcu8Pb2fjclxRyaMGYKZBRuXDlc/RNO3uTytGYNdC2\nIptU5N4M7iZHXj190xtDxRnYQWWo/PR6EcJj3f/tc3Itm1rX0JfuI3JzJQgDb9Z2\ns/9/ub8RRvmQV9LM/utgyOwNdf5dyVoPcTY2739X4ZzXNH+CybfNa+LWpiJIVEs2\ntxXbgZrhmlaWzwA525nZ0UlKdfktdcXeqke9eBghAoHARVTHFy6CjV7ZhlmDEtqE\nU58FBOS36O7xRDdpXwsHLnCXhbFu9du41mom0W4UdzjgVI9gUqG71+SXrKr7lTc3\ndMHcSbplxXkBJawND/Q1rzLG5JvIRHO1AGJLmRgIdl8jNgtxgV2QSkoyKlNVbM2H\nWy6ZSKM03lIj74+rcKuU3N87dX4jDuwV0sPXjzJxL7NpR/fHwgndgyPcI14y2cGz\nzMC44EyQdTw+B/YfMnoZx83xaaMNMqV6GYNnTHi0TO2TAoHBAKmdrh9WkE2qsr59\nIoHHygh7Wzez+Ewr6hfgoEK4+QzlBlX+XV/9rxIaE0jS3Sk1txadk5oFDebimuSk\nlQkv1pXUOqh+xSAwk5v88dBAfh2dnnSa8HFN3oz+ZfQYtnBcc4DR1y2X+fVNgr3i\nnxruU2gsAIPFRnmvwKPc1YIH9A6kIzqaoNt1f9VM243D6fNzkO4uztWEApBkkJgR\n4s/yOjp6ovS9JG1NMXWjXQPcwTq3sQVLnAHxZRJmOvx69UmK4QKBwFYXXjeXiU3d\nbcrPfe6qNGjfzK+BkhWznuFUMbuxyZWDYQD5yb6ukUosrj7pmZv3BxKcKCvmONU+\nCHgIXB+hG+R9S2mCcH1qBQoP/RSm+TUzS/Bl2UeuhnFZh2jSZQy3OwryUi6nhF0u\nLDzMI/6aO1ggsI23Ri0Y9ZtqVKczTkxzdQKR9xvoNBUufjimRlS80sJCEB3Qm20S\nwzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "conf/config.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# If you want to set the specified configuration value, you can set the new\n# in this file. For example if you want to specify the etcd address:\n#\n# deployment:\n#   role: traditional\n#   role_traditional:\n#     config_provider: etcd\n#   etcd:\n#     host:\n#       - http://127.0.0.1:2379\n#\n# To configure via environment variables, you can use `${{VAR}}` syntax. For instance:\n#\n# deployment:\n#   role: traditional\n#   role_traditional:\n#     config_provider: etcd\n#   etcd:\n#     host:\n#       - http://${{ETCD_HOST}}:2379\n#\n# And then run `export ETCD_HOST=$your_host` before `make init`.\n#\n# If the configured environment variable can't be found, an error will be thrown.\n#\n# Also, If you want to use default value when the environment variable not set,\n# Use `${{VAR:=default_value}}` instead. For instance:\n#\n# deployment:\n#   role: traditional\n#   role_traditional:\n#     config_provider: etcd\n#   etcd:\n#     host:\n#       - http://${{ETCD_HOST:=localhost}}:2379\n#\n# This will find environment variable `ETCD_HOST` first, and if it's not exist it will use `localhost` as default value.\n#\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key:\n      - name: admin\n        key: ''  # using fixed API token has security risk, please update it when you deploy to production environment. If passed empty then will be autogenerated by APISIX and will be written back here. Recommended is to use external mechanism to generate and store the token.\n        role: admin\n"
  },
  {
    "path": "conf/config.yaml.example",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# CAUTION: DO NOT MODIFY DEFAULT CONFIGURATIONS IN THIS FILE.\n# Keep the custom configurations in conf/config.yaml.\n#\n\napisix:\n  # node_listen: 9080          # APISIX listening port.\n  node_listen:                 # APISIX listening ports.\n    - 9080\n  #   - port: 9081\n  #   - ip: 127.0.0.2          # If not set, default to `0.0.0.0`\n  #     port: 9082\n  enable_admin: true           # Admin API\n  enable_dev_mode: false       # If true, set nginx `worker_processes` to 1.\n  enable_reuseport: true       # If true, enable nginx SO_REUSEPORT option.\n  show_upstream_status_in_response_header: false  # If true, include the upstream HTTP status code in\n                                                  # the response header `X-APISIX-Upstream-Status`.\n                                                  # If false, show `X-APISIX-Upstream-Status` only if\n                                                  # the upstream response code is 5xx.\n  enable_ipv6: true\n  enable_http2: true\n\n  # proxy_protocol:                    # PROXY Protocol configuration\n  #   listen_http_port: 9181           # APISIX listening port for HTTP traffic with PROXY protocol.\n  #   listen_https_port: 9182          # APISIX listening port for HTTPS traffic with PROXY protocol.\n  #   enable_tcp_pp: true              # Enable the PROXY protocol when stream_proxy.tcp is set.\n  #   enable_tcp_pp_to_upstream: true  # Enable the PROXY protocol.\n\n  enable_server_tokens: true           # If true, show APISIX version in the `Server` response header.\n  extra_lua_path: \"\"                   # Extend lua_package_path to load third-party code.\n  extra_lua_cpath: \"\"                  # Extend lua_package_cpath to load third-party code.\n  # lua_module_hook: \"my_project.my_hook\"  # Hook module used to inject third-party code into APISIX.\n\n  proxy_cache:      # Proxy Caching configuration\n    cache_ttl: 10s  # The default caching time on disk if the upstream does not specify a caching time.\n    zones:\n      - name: disk_cache_one    # Name of the cache.\n        memory_size: 50m        # Size of the memory to store the cache index.\n        disk_size: 1G           # Size of the disk to store the cache data.\n        disk_path: /tmp/disk_cache_one  # Path to the cache file for disk cache.\n        cache_levels: \"1:2\"               # Cache hierarchy levels of disk cache.\n      # - name: disk_cache_two\n      #  memory_size: 50m\n      #  disk_size: 1G\n      #  disk_path: \"/tmp/disk_cache_two\"\n      #  cache_levels: \"1:2\"\n      - name: memory_cache\n        memory_size: 50m\n\n  delete_uri_tail_slash: false        # Delete the '/' at the end of the URI\n  normalize_uri_like_servlet: false   # If true, use the same path normalization rules as the Java\n                                      # servlet specification. See https://github.com/jakartaee/servlet/blob/master/spec/src/main/asciidoc/servlet-spec-body.adoc#352-uri-path-canonicalization, which is used in Tomcat.\n\n  router:\n    http: radixtree_host_uri    # radixtree_host_uri: match route by host and URI\n                                # radixtree_uri: match route by URI\n                                # radixtree_uri_with_parameter: similar to radixtree_uri but match URI with parameters. See https://github.com/api7/lua-resty-radixtree/#parameters-in-path for more details.\n    ssl: radixtree_sni          # radixtree_sni: match route by SNI\n\n  # http is the default proxy mode. proxy_mode can be one of `http`, `stream`, or `http&stream`\n  proxy_mode: \"http\"\n  # stream_proxy:                 # TCP/UDP L4 proxy\n  #   tcp:\n  #     - addr: 9100              # Set the TCP proxy listening ports.\n  #       tls: true\n  #     - addr: \"127.0.0.1:9101\"\n  #   udp:                        # Set the UDP proxy listening ports.\n  #     - 9200\n  #     - \"127.0.0.1:9201\"\n\n  # dns_resolver:                 # If not set, read from `/etc/resolv.conf`\n  #   - 1.1.1.1\n  #   - 8.8.8.8\n  # dns_resolver_valid: 30        # Override the default TTL of the DNS records.\n  resolver_timeout: 5             # Set the time in seconds that the server will wait for a response from the\n                                  # DNS resolver before timing out.\n  enable_resolv_search_opt: true  # If true, use search option in the resolv.conf file in DNS lookups.\n\n  ssl:\n    enable: true\n    listen:                                       # APISIX listening port for HTTPS traffic.\n      - port: 9443\n        enable_http3: false                       # Enable HTTP/3 (with QUIC). If not set default to `false`.\n      # - ip: 127.0.0.3                           # If not set, default to `0.0.0.0`.\n      #   port: 9445\n      #   enable_http3: true\n    #ssl_trusted_certificate: system              # Specifies a file path with trusted CA certificates in the PEM format. The default value is \"system\".\n    ssl_protocols: TLSv1.2 TLSv1.3                # TLS versions supported.\n    ssl_ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384\n    ssl_session_tickets: false  # If true, session tickets are used for SSL/TLS connections.\n                                # Disabled by default because it renders Perfect Forward Secrecy (FPS)\n                                # useless. See https://github.com/mozilla/server-side-tls/issues/135.\n\n    # fallback_sni: \"my.default.domain\"      # Fallback SNI to be used if the client does not send SNI during\n    #                                        # the handshake.\n\n  enable_control: true  # Control API\n  # control:\n  #  ip: 127.0.0.1\n  #  port: 9090\n\n  disable_sync_configuration_during_start: false  # Safe exit. TO BE REMOVED.\n\n  # This time will be used to distinguish whether the worker is started first time or restarted due to a crash, unit: second.\n  worker_startup_time_threshold: 60\n\n  data_encryption:                # Data encryption settings.\n    enable_encrypt_fields: true   # Whether enable encrypt fields specified in `encrypt_fields` in plugin schema.\n    keyring:                      # This field is used to encrypt the private key of SSL and the `encrypt_fields`\n                                  # in plugin schema.\n      - qeddd145sfvddff3          # Set the encryption key for AES-128-CBC. It should be a hexadecimal string\n                                  # of length 16.\n      - edd1c9f0985e76a2          # If not set, APISIX saves the original data into etcd.\n                                  # CAUTION: If you would like to update the key, add the new key as the\n                                  # first item in the array and keep the older keys below the newly added\n                                  # key, so that data can be decrypted with the older keys and encrypted\n                                  # with the new key. Removing the old keys directly can render the data\n                                  # unrecoverable.\n\n# status:                       # When enabled, APISIX will provide `/status` and `/status/ready` endpoints\n  #   ip: 127.0.0.1               # /status endpoint will return 200 status code if APISIX has successfully started and running correctly\n  #   port: 7085                  # /status/ready endpoint will return 503 status code if any of the workers do not receive config from etcd\n                                  # or (standalone mode) the config isn't loaded yet either via file or Admin API.\n  # disable_upstream_healthcheck: false # A global switch for healthcheck. Defaults to false.\n                                        # When set to true, it overrides all upstream healthcheck configurations and globally disabling healthchecks.\n# trusted_addresses:              # When configured, APISIX will trust the `X-Forwarded-*` Headers\n#   - 127.0.0.1                   # passed in requests from the IP/CIDR in the list.\n#   - 172.18.0.0/16               # CAUTION: When not configured or the request from an untrusted address,\n                                  # APISIX will override `X-Forwarded-*` headers with trusted values.\n  # fine tune the parameters of LRU cache for some features like secret\n  lru:\n    secret:\n      ttl: 300          # Global TTL fallback\n      count: 512        # Cache size\n      neg_ttl: 60       # Negative cache TTL\n      neg_count: 512    # Negative cache size\n\n  tracing: false                    # Enable comprehensive request lifecycle tracing (SSL/SNI, rewrite, access, header_filter, body_filter, and log).\n                                    # When disabled, OpenTelemetry collects only a single span per request.\n\nnginx_config:                     # Config for render the template to generate nginx.conf\n  # user: root                    # Set the execution user of the worker process. This is only\n                                  # effective if the master process runs with super-user privileges.\n  error_log: logs/error.log       # Location of the error log.\n  error_log_level:  warn          # Logging level: info, debug, notice, warn, error, crit, alert, or emerg.\n  worker_processes: auto          # Automatically determine the optimal number of worker processes based\n                                  # on the available system resources.\n                                  # If you want use multiple cores in container, you can inject the number of\n                                  # CPU cores as environment variable \"APISIX_WORKER_PROCESSES\".\n  enable_cpu_affinity: false      # Disable CPU affinity by default as worker_cpu_affinity affects the\n                                  # behavior of APISIX in containers. For example, multiple instances could\n                                  # be bound to one CPU core, which is not desirable.\n                                  # If APISIX is deployed on a physical machine, CPU affinity can be enabled.\n  worker_rlimit_nofile: 20480     # The number of files a worker process can open.\n                                  # The value should be larger than worker_connections.\n  worker_shutdown_timeout: 240s   # Timeout for a graceful shutdown of worker processes.\n\n  max_pending_timers: 16384       # The maximum number of pending timers that can be active at any given time.\n                                  # Error \"too many pending timers\" indicates the threshold is reached.\n  max_running_timers: 4096        # The maximum number of running timers that can be active at any given time.\n                                  # Error \"lua_max_running_timers are not enough\" error indicates the\n                                  # threshold is reached.\n\n  event:\n    worker_connections: 10620\n\n  # envs:                         # Get environment variables.\n  #  - TEST_ENV\n\n  meta:\n    lua_shared_dict:              # Nginx Lua shared memory zone. Size units are m or k.\n      prometheus-metrics: 15m\n      prometheus-cache: 10m       # Cache the calculated metrics data text.\n                                  # Please resize when the `error.log` prompts that the data is full.\n                                  # NOTE: Restart APISIX to take effect.\n      standalone-config: 10m\n      upstream-healthcheck: 10m\n\n  stream:\n    enable_access_log: false                 # Enable stream proxy access logging.\n    access_log: logs/access_stream.log       # Location of the stream access log.\n    access_log_format: |\n      \"$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time\" # Customize log format: http://nginx.org/en/docs/varindex.html\n    access_log_format_escape: default        # Escape default or json characters in variables.\n    lua_shared_dict:                         # Nginx Lua shared memory zone. Size units are m or k.\n      etcd-cluster-health-check-stream: 10m\n      lrucache-lock-stream: 10m\n      plugin-limit-conn-stream: 10m\n      worker-events-stream: 10m\n      tars-stream: 1m\n\n  # Add other custom Nginx configurations.\n  # Users are responsible for validating the custom configurations\n  # to ensure they are not in conflict with APISIX configurations.\n  main_configuration_snippet: |\n    # Add custom Nginx main configuration to nginx.conf.\n    # The configuration should be well indented!\n  http_configuration_snippet: |\n    # Add custom Nginx http configuration to nginx.conf.\n    # The configuration should be well indented!\n  http_server_configuration_snippet: |\n    # Add custom Nginx http server configuration to nginx.conf.\n    # The configuration should be well indented!\n  http_server_location_configuration_snippet: |\n    # Add custom Nginx http server location configuration to nginx.conf.\n    # The configuration should be well indented!\n  http_admin_configuration_snippet: |\n    # Add custom Nginx admin server configuration to nginx.conf.\n    # The configuration should be well indented!\n  http_end_configuration_snippet: |\n    # Add custom Nginx http end configuration to nginx.conf.\n    # The configuration should be well indented!\n  stream_configuration_snippet: |\n    # Add custom Nginx stream configuration to nginx.conf.\n    # The configuration should be well indented!\n\n  http:\n    enable_access_log: true             # Enable HTTP proxy access logging.\n    access_log: logs/access.log         # Location of the access log.\n    access_log_buffer: 16384            # buffer size of access log.\n    # available variables:\n    # request_type: traditional_http / ai_chat / ai_stream\n    # llm_time_to_first_token: duration from the start send request to ai server to the first token received\n    # llm_prompt_tokens: number of tokens in the prompt\n    # llm_completion_tokens: number of tokens in the chat completion\n    access_log_format: |\n      \"$remote_addr - $remote_user [$time_local] $http_host \\\"$request\\\" $status $body_bytes_sent $request_time \\\"$http_referer\\\" \\\"$http_user_agent\\\" $upstream_addr $upstream_status $upstream_response_time \\\"$upstream_scheme://$upstream_host$upstream_uri\\\"\"\n    # Customize log format: http://nginx.org/en/docs/varindex.html\n    access_log_format_escape: default   # Escape default or json characters in variables.\n    keepalive_timeout: 60s              # Set the maximum time for which TCP connection keeps alive.\n    client_header_timeout: 60s          # Set the maximum time waiting for client to send the entire HTTP\n                                        # request header before closing the connection.\n    client_body_timeout: 60s            # Set the maximum time waiting for client to send the request body.\n    client_max_body_size: 0             # Set the maximum allowed size of the client request body.\n                                        # Default to 0, unlimited.\n                                        # Unlike Nginx, APISIX does not limit the body size by default.\n                                        # If exceeded, the 413 (Request Entity Too Large) error is returned.\n    send_timeout: 10s   # Set the maximum time for transmitting a response to the client before closing.\n    underscores_in_headers: \"on\"  # Allow HTTP request headers to contain underscores in their names.\n    real_ip_header: X-Real-IP     # https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header\n    real_ip_recursive: \"off\" # http://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_recursive\n    real_ip_from:            # http://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from\n      - 127.0.0.1\n      - \"unix:\"\n\n    # custom_lua_shared_dict:     # Custom Nginx Lua shared memory zone for nginx.conf. Size units are m or k.\n    #  ipc_shared_dict: 100m      # Custom shared cache, format: `cache-key: cache-size`\n\n    proxy_ssl_server_name: true   # Send the server name in the SNI extension when establishing an SSL/TLS\n                                  # connection with the upstream server, allowing the upstream server to\n                                  # select the appropriate SSL/TLS certificate and configuration based on\n                                  # the requested server name.\n\n    upstream:\n      keepalive: 320              # Set the maximum time of keep-alive connections to the upstream servers.\n                                  # When the value is exceeded, the least recently used connection is closed.\n      keepalive_requests: 1000    # Set the maximum number of requests that can be served through one\n                                  # keep-alive connection.\n                                  # After the maximum number of requests is made, the connection is closed.\n      keepalive_timeout: 60s      # Set the maximum time for which TCP connection keeps alive.\n    charset: utf-8                # Add the charset to the \"Content-Type\" response header field.\n                                  # See http://nginx.org/en/docs/http/ngx_http_charset_module.html#charset\n    variables_hash_max_size: 2048 # Set the maximum size of the variables hash table.\n\n    lua_shared_dict:              # Nginx Lua shared memory zone. Size units are m or k.\n      internal-status: 10m\n      plugin-limit-req: 10m\n      plugin-limit-count: 10m\n      prometheus-metrics: 10m     # In production, less than 50m is recommended\n      plugin-limit-conn: 10m\n      worker-events: 10m\n      lrucache-lock: 10m\n      balancer-ewma: 10m\n      balancer-ewma-locks: 10m\n      balancer-ewma-last-touched-at: 10m\n      plugin-limit-req-redis-cluster-slot-lock: 1m\n      plugin-limit-count-redis-cluster-slot-lock: 1m\n      plugin-limit-conn-redis-cluster-slot-lock: 1m\n      tracing_buffer: 10m\n      plugin-api-breaker: 10m\n      etcd-cluster-health-check: 10m\n      discovery: 1m\n      jwks: 1m\n      introspection: 10m\n      access-tokens: 1m\n      ext-plugin: 1m\n      tars: 1m\n      cas-auth: 10m\n      ocsp-stapling: 10m\n      mcp-session: 10m\n\n# discovery:                      # Service Discovery\n#  dns:\n#    servers:\n#      - \"127.0.0.1:8600\"         # Replace with the address of your DNS server.\n#    resolv_conf: /etc/resolv.conf # Replace with the path to the local DNS resolv config. Configure either \"servers\" or \"resolv_conf\".\n#    order:                       # Resolve DNS records this order.\n#      - last                     # Try the latest successful type for a hostname.\n#      - SRV\n#      - A\n#      - AAAA\n#      - CNAME\n#  eureka:                        # Eureka\n#    host:                        # Eureka address(es)\n#      - \"http://127.0.0.1:8761\"\n#    prefix: /eureka/\n#    fetch_interval: 30           # Default 30s\n#    weight: 100                  # Default weight for node\n#    timeout:\n#      connect: 2000              # Default 2000ms\n#      send: 2000                 # Default 2000ms\n#      read: 5000                 # Default 5000ms\n#  nacos:                         # Nacos\n#    host:                        # Nacos address(es)\n#      - \"http://${username}:${password}@${host1}:${port1}\"\n#    prefix: \"/nacos/v1/\"\n#    fetch_interval: 30    # Default 30s\n# `weight` is the `default_weight` that will be attached to each discovered node that\n# doesn't have a weight explicitly provided in nacos results\n#    weight: 100           # Default 100.\n#    timeout:\n#      connect: 2000       # Default 2000ms\n#      send: 2000          # Default 2000ms\n#      read: 5000          # Default 5000ms\n#    access_key: \"\"        # Nacos AccessKey ID in Alibaba Cloud, notice that it's for Nacos instances on Microservices Engine (MSE)\n#    secret_key: \"\"        # Nacos AccessKey Secret in Alibaba Cloud, notice that it's for Nacos instances on Microservices Engine (MSE)\n#  consul_kv:              # Consul KV\n#    servers:              # Consul KV address(es)\n#      - \"http://127.0.0.1:8500\"\n#      - \"http://127.0.0.1:8600\"\n#    prefix: \"upstreams\"\n#    skip_keys:                     # Skip special keys\n#      - \"upstreams/unused_api/\"\n#    timeout:\n#      connect: 2000                # Default 2000ms\n#      read: 2000                   # Default 2000ms\n#      wait: 60                     # Default 60s\n#    weight: 1                      # Default 1\n#    fetch_interval: 3              # Default 3s. Effective only when keepalive is false.\n#    keepalive: true                # Default to true. Use long pull to query Consul.\n#    default_server:                # Define default server to route traffic to.\n#      host: \"127.0.0.1\"\n#      port: 20999\n#      metadata:\n#        fail_timeout: 1            # Default 1ms\n#        weight: 1                  # Default 1\n#        max_fails: 1               # Default 1\n#    dump:                          # Dump the Consul key-value (KV) store to a file.\n#       path: \"logs/consul_kv.dump\" # Location of the dump file.\n#       expire: 2592000             # Specify the expiration time of the dump file in units of seconds.\n#  consul:                          # Consul\n#    servers:                       # Consul address(es)\n#      - \"http://127.0.0.1:8500\"\n#      - \"http://127.0.0.1:8600\"\n#    skip_services:                 # Skip services during service discovery.\n#      - \"service_a\"\n#    timeout:\n#      connect: 2000                # Default 2000ms\n#      read: 2000                   # Default 2000ms\n#      wait: 60                     # Default 60s\n#    weight: 1                      # Default 1\n#    fetch_interval: 3              # Default 3s. Effective only when keepalive is false.\n#    keepalive: true                # Default to true. Use long pull to query Consul.\n#    default_service:               # Define the default service to route traffic to.\n#      host: \"127.0.0.1\"\n#      port: 20999\n#      metadata:\n#        fail_timeout: 1           # Default 1ms\n#        weight: 1                 # Default 1\n#        max_fails: 1              # Default 1\n#    dump:                           # Dump the Consul key-value (KV) store to a file.\n#       path: \"logs/consul_kv.dump\"  # Location of the dump file.\n#       expire: 2592000              # Specify the expiration time of the dump file in units of seconds.\n#       load_on_init: true           # Default true, load the consul dump file on init\n#  kubernetes:                     # Kubernetes service discovery\n#    ### kubernetes service discovery both support single-cluster and multi-cluster mode\n#    ### applicable to the case where the service is distributed in a single or multiple kubernetes clusters.\n#    ### single-cluster mode ###\n#    service:\n#      schema: https                     # apiserver schema, options [http, https], default https\n#      host: ${KUBERNETES_SERVICE_HOST}  # apiserver host, options [ipv4, ipv6, domain, environment variable], default ${KUBERNETES_SERVICE_HOST}\n#      port: ${KUBERNETES_SERVICE_PORT}  # apiserver port, options [port number, environment variable], default ${KUBERNETES_SERVICE_PORT}\n#    client:\n#      # serviceaccount token or path of serviceaccount token_file\n#      token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n#      # token: |-\n#       # eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEif\n#       # 6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEifeyJhbGciOiJSUzI1NiIsImtpZCI\n#    # kubernetes discovery plugin support use namespace_selector\n#    # you can use one of [equal, not_equal, match, not_match] filter namespace\n#    namespace_selector:\n#      # only save endpoints with namespace equal default\n#      equal: default\n#      # only save endpoints with namespace not equal default\n#      #not_equal: default\n#      # only save endpoints with namespace match one of [default, ^my-[a-z]+$]\n#      #match:\n#      #- default\n#      #- ^my-[a-z]+$\n#      # only save endpoints with namespace not match one of [default, ^my-[a-z]+$ ]\n#      #not_match:\n#      #- default\n#      #- ^my-[a-z]+$\n#    # kubernetes discovery plugin support use label_selector\n#    # for the expression of label_selector, please refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\n#    label_selector: |-\n#      first=\"a\",second=\"b\"\n#    # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint\n#    shared_size: 1m #default 1m\n#    ### single-cluster mode ###\n#    ### multi-cluster mode ###\n#  - id: release  # a custom name refer to the cluster, pattern ^[a-z0-9]{1,8}\n#    service:\n#      schema: https                     # apiserver schema, options [http, https], default https\n#      host: ${KUBERNETES_SERVICE_HOST}  # apiserver host, options [ipv4, ipv6, domain, environment variable]\n#      port: ${KUBERNETES_SERVICE_PORT}  # apiserver port, options [port number, environment variable]\n#    client:\n#      # serviceaccount token or path of serviceaccount token_file\n#      token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n#      # token: |-\n#       # eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEif\n#       # 6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEifeyJhbGciOiJSUzI1NiIsImtpZCI\n#    # kubernetes discovery plugin support use namespace_selector\n#    # you can use one of [equal, not_equal, match, not_match] filter namespace\n#    namespace_selector:\n#      # only save endpoints with namespace equal default\n#      equal: default\n#      # only save endpoints with namespace not equal default\n#      #not_equal: default\n#      # only save endpoints with namespace match one of [default, ^my-[a-z]+$]\n#      #match:\n#      #- default\n#      #- ^my-[a-z]+$\n#      # only save endpoints with namespace not match one of [default, ^my-[a-z]+$ ]\n#      #not_match:\n#      #- default\n#      #- ^my-[a-z]+$\n#    # kubernetes discovery plugin support use label_selector\n#    # for the expression of label_selector, please refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\n#    label_selector: |-\n#      first=\"a\",second=\"b\"\n#    # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint\n#    shared_size: 1m #default 1m\n#    ### multi-cluster mode ###\n\ngraphql:\n  max_size: 1048576                # Set the maximum size limitation of graphql in bytes. Default to 1MiB.\n\n# ext-plugin:\n#   cmd: [\"ls\", \"-l\"]\n\nplugins:                           # plugin list (sorted by priority)\n  - real-ip                        # priority: 23000\n  - ai                             # priority: 22900\n  - client-control                 # priority: 22000\n  - proxy-control                  # priority: 21990\n  - request-id                     # priority: 12015\n  - zipkin                         # priority: 12011\n  #- skywalking                    # priority: 12010\n  #- opentelemetry                 # priority: 12009\n  - ext-plugin-pre-req             # priority: 12000\n  - fault-injection                # priority: 11000\n  - mocking                        # priority: 10900\n  - serverless-pre-function        # priority: 10000\n  #- batch-requests                # priority: 4010\n  - cors                           # priority: 4000\n  - ip-restriction                 # priority: 3000\n  - ua-restriction                 # priority: 2999\n  - referer-restriction            # priority: 2990\n  - csrf                           # priority: 2980\n  - uri-blocker                    # priority: 2900\n  - request-validation             # priority: 2800\n  - chaitin-waf                    # priority: 2700\n  - multi-auth                     # priority: 2600\n  - openid-connect                 # priority: 2599\n  - cas-auth                       # priority: 2597\n  - authz-casbin                   # priority: 2560\n  - authz-casdoor                  # priority: 2559\n  - wolf-rbac                      # priority: 2555\n  - ldap-auth                      # priority: 2540\n  - hmac-auth                      # priority: 2530\n  - basic-auth                     # priority: 2520\n  - jwt-auth                       # priority: 2510\n  - jwe-decrypt                    # priority: 2509\n  - key-auth                       # priority: 2500\n  - consumer-restriction           # priority: 2400\n  - attach-consumer-label          # priority: 2399\n  - forward-auth                   # priority: 2002\n  - opa                            # priority: 2001\n  - authz-keycloak                 # priority: 2000\n  #- error-log-logger              # priority: 1091\n  - proxy-cache                    # priority: 1085\n  - body-transformer               # priority: 1080\n  - ai-prompt-template             # priority: 1071\n  - ai-prompt-decorator            # priority: 1070\n  - ai-prompt-guard                # priority: 1072\n  - ai-rag                         # priority: 1060\n  - ai-aws-content-moderation      # priority: 1050\n  - ai-proxy-multi                 # priority: 1041\n  - ai-proxy                       # priority: 1040\n  - ai-rate-limiting               # priority: 1030\n  - proxy-mirror                   # priority: 1010\n  - proxy-rewrite                  # priority: 1008\n  - workflow                       # priority: 1006\n  - api-breaker                    # priority: 1005\n  - limit-conn                     # priority: 1003\n  - limit-count                    # priority: 1002\n  - limit-req                      # priority: 1001\n  #- node-status                   # priority: 1000\n  #- brotli                        # priority: 996\n  - gzip                           # priority: 995\n  #- server-info                    # priority: 990\n  - traffic-split                  # priority: 966\n  - redirect                       # priority: 900\n  - response-rewrite               # priority: 899\n  - mcp-bridge                     # priority: 510\n  - degraphql                      # priority: 509\n  - kafka-proxy                    # priority: 508\n  #- dubbo-proxy                   # priority: 507\n  - grpc-transcode                 # priority: 506\n  - grpc-web                       # priority: 505\n  - http-dubbo                     # priority: 504\n  - public-api                     # priority: 501\n  - prometheus                     # priority: 500\n  - datadog                        # priority: 495\n  - lago                           # priority: 415\n  - loki-logger                    # priority: 414\n  - elasticsearch-logger           # priority: 413\n  - echo                           # priority: 412\n  - loggly                         # priority: 411\n  - http-logger                    # priority: 410\n  - splunk-hec-logging             # priority: 409\n  - skywalking-logger              # priority: 408\n  - google-cloud-logging           # priority: 407\n  - sls-logger                     # priority: 406\n  - tcp-logger                     # priority: 405\n  - kafka-logger                   # priority: 403\n  - rocketmq-logger                # priority: 402\n  - syslog                         # priority: 401\n  - udp-logger                     # priority: 400\n  - file-logger                    # priority: 399\n  - clickhouse-logger              # priority: 398\n  - tencent-cloud-cls              # priority: 397\n  - inspect                        # priority: 200\n  #- log-rotate                    # priority: 100\n  # <- recommend to use priority (0, 100) for your custom plugins\n  - example-plugin                 # priority: 0\n  #- gm                            # priority: -43\n  #- ocsp-stapling                 # priority: -44\n  - aws-lambda                     # priority: -1899\n  - azure-functions                # priority: -1900\n  - openwhisk                      # priority: -1901\n  - openfunction                   # priority: -1902\n  - serverless-post-function       # priority: -2000\n  - ext-plugin-post-req            # priority: -3000\n  - ext-plugin-post-resp           # priority: -4000\n\nstream_plugins:                    # stream plugin list (sorted by priority)\n  - ip-restriction                 # priority: 3000\n  - limit-conn                     # priority: 1003\n  - mqtt-proxy                     # priority: 1000\n  #- prometheus                    # priority: 500\n  - syslog                         # priority: 401\n  # <- recommend to use priority (0, 100) for your custom plugins\n\n\n# wasm:\n#   plugins:\n#     - name: wasm_log\n#       priority: 7999\n#       file: t/wasm/log/main.go.wasm\n\n# xrpc:\n#   protocols:\n#     - name: pingpong\nplugin_attr:          # Plugin attributes\n  log-rotate:         # Plugin: log-rotate\n    timeout: 10000    # maximum wait time for a log rotation(unit: millisecond)\n    interval: 3600    # Set the log rotate interval in seconds.\n    max_kept: 168     # Set the maximum number of log files to keep. If exceeded, historic logs are deleted.\n    max_size: -1      # Set the maximum size of log files in bytes before a rotation.\n                      # Skip size check if max_size is less than 0.\n    enable_compression: false    # Enable log file compression (gzip).\n  skywalking:                                     # Plugin: skywalking\n    service_name: APISIX                          # Set the service name for SkyWalking reporter.\n    service_instance_name: APISIX Instance Name   # Set the service instance name for SkyWalking reporter.\n    endpoint_addr: http://127.0.0.1:12800         # Set the SkyWalking HTTP endpoint.\n    report_interval: 3                            # Set the reporting interval in second.\n  opentelemetry:      # Plugin: opentelemetry\n    trace_id_source: x-request-id   # Specify the source of the trace ID for OpenTelemetry traces.\n    resource:\n      service.name: APISIX          # Set the service name for OpenTelemetry traces.\n    collector:\n      address: 127.0.0.1:4318       # Set the address of the OpenTelemetry collector to send traces to.\n      request_timeout: 3            # Set the timeout for requests to the OpenTelemetry collector in seconds.\n      request_headers:              # Set the headers to include in requests to the OpenTelemetry collector.\n        Authorization: token        # Set the authorization header to include an access token.\n    batch_span_processor:\n      drop_on_queue_full: false     # Drop spans when the export queue is full.\n      max_queue_size: 1024          # Set the maximum size of the span export queue.\n      batch_timeout: 2              # Set the timeout for span batches to wait in the export queue before\n                                    # being sent.\n      inactive_timeout: 1           # Set the timeout for spans to wait in the export queue before being sent,\n                                    # if the queue is not full.\n      max_export_batch_size: 16     # Set the maximum number of spans to include in each batch sent to the\n                                    # OpenTelemetry collector.\n    set_ngx_var: false              # Export opentelemetry variables to NGINX variables.\n  prometheus:                               # Plugin: prometheus\n    export_uri: /apisix/prometheus/metrics  # Set the URI for the Prometheus metrics endpoint.\n    metric_prefix: apisix_                  # Set the prefix for Prometheus metrics generated by APISIX.\n    enable_export_server: true              # Enable the Prometheus export server.\n    export_addr:                            # Set the address for the Prometheus export server.\n      ip: 127.0.0.1                         # Set the IP.\n      port: 9091                            # Set the port.\n    refresh_interval: 15                    # Set the interval for refreshing cached metric data. unit: second.\n    # metrics:    # Create extra labels from nginx variables: https://nginx.org/en/docs/varindex.html\n    #  http_status:\n    #    expire: 0 # The expiration time after which metrics are removed. unit: second.\n    #              # 0 means the metrics will not expire\n    #    extra_labels:\n    #      - upstream_addr: $upstream_addr\n    #      - status: $upstream_status  # The label name does not need to be the same as the variable name.\n    #  http_latency:\n    #    expire: 0 # The expiration time after which metrics are removed. unit: second.\n    #              # 0 means the metrics will not expire\n    #    extra_labels:\n    #      - upstream_addr: $upstream_addr\n    #  bandwidth:\n    #    expire: 0 # The expiration time after which metrics are removed. unit: second.\n    #              # 0 means the metrics will not expire\n    #    extra_labels:\n    #      - upstream_addr: $upstream_addr\n    #  upstream_status:\n    #    expire: 0 # The expiration time after which metrics are removed. unit: second.\n    # default_buckets:\n    #   - 10\n    #   - 50\n    #   - 100\n    #   - 200\n    #   - 500\n  server-info:                        # Plugin: server-info\n    report_ttl: 60                    # Set the TTL in seconds for server info in etcd.\n                                      # Maximum: 86400. Minimum: 3.\n  dubbo-proxy:                        # Plugin: dubbo-proxy\n    upstream_multiplex_count: 32      # Set the maximum number of connections that can be multiplexed over\n                                      # a single network connection between the Dubbo Proxy and the upstream\n                                      # Dubbo services.\n  proxy-mirror:                       # Plugin: proxy-mirror\n    timeout:                          # Set the timeout for mirrored requests.\n      connect: 60s\n      read: 60s\n      send: 60s\n  # redirect:                         # Plugin: redirect\n  #   https_port: 8443                # Set the default port used to redirect HTTP to HTTPS.\n  inspect:                            # Plugin: inspect\n    delay: 3                          # Set the delay in seconds for the frequency of checking the hooks file.\n    hooks_file: \"/usr/local/apisix/plugin_inspect_hooks.lua\"  # Set the path to the Lua file that defines\n                                                              # hooks. Only administrators should have\n                                                              # write access to this file for security.\n  zipkin:                             # Plugin: zipkin\n    set_ngx_var: false                # export zipkin variables to nginx variables\n\ndeployment:                    # Deployment configurations\n  role: traditional            # Set deployment mode: traditional, control_plane, or data_plane.\n  role_traditional:\n    config_provider: etcd      # Set the configuration center.\n\n  #role_data_plane:            # Set data plane details if role is data_plane.\n  #  config_provider: etcd     # Set the configuration center: etcd, xds, or yaml.\n\n  #role_control_plane:         # Set control plane details if role is control_plane.\n  #  config_provider: etcd     # Set the configuration center.\n\n  admin:                       # Admin API\n    admin_key_required: true   # Enable Admin API authentication by default for security.\n    admin_key:\n      -\n        name: admin                             # admin: write access to configurations.\n        key: ''   # Set API key for the admin of Admin API.\n        role: admin\n      # -\n      #   name: viewer                            # viewer: read-only to configurations.\n      #   key: 4054f7cf07e344346cd3f287985e76a2   # Set API key for the viewer of Admin API.\n      #   role: viewer\n\n    enable_admin_cors: true       # Enable Admin API CORS response header `Access-Control-Allow-Origin`.\n    enable_admin_ui: true         # Enable embedded APISIX Dashboard UI.\n    allow_admin:                  # Limit Admin API access by IP addresses.\n      - 127.0.0.0/24              # If not set, any IP address is allowed.\n      # - \"::/64\"\n    admin_listen:                 # Set the Admin API listening addresses.\n      ip: 0.0.0.0                 # Set listening IP.\n      port: 9180                  # Set listening port. Beware of port conflict with node_listen.\n\n    # https_admin: true           # Enable SSL for Admin API on IP and port specified in admin_listen.\n                                  # Use admin_api_mtls.admin_ssl_cert and admin_api_mtls.admin_ssl_cert_key.\n    # admin_api_mtls:             # Set this if `https_admin` is true.\n    #   admin_ssl_cert: \"\"        # Set path to SSL/TLS certificate.\n    #   admin_ssl_cert_key: \"\"    # Set path to SSL/TLS key.\n    #   admin_ssl_ca_cert: \"\"     # Set path to CA certificate used to sign client certificates.\n\n    admin_api_version: v3         # Set the version of Admin API (latest: v3).\n\n  etcd:\n    host:                         # Set etcd address(es) in the same etcd cluster.\n      - \"http://127.0.0.1:2379\"   # If TLS is enabled for etcd, use https://127.0.0.1:2379.\n    prefix: /apisix               # Set etcd prefix.\n    timeout: 30                   # The timeout when connect/read/write to etcd, Set timeout in seconds.\n    watch_timeout: 50             # The timeout when watch etcd\n    # resync_delay: 5             # Set resync time in seconds after a sync failure.\n                                  # The actual resync time would be resync_delay plus 50% random jitter.\n    # health_check_timeout: 10    # Set timeout in seconds for etcd health check.\n                                  # Default to 10 if not set or a negative value is provided.\n    startup_retry: 2              # Set the number of retries to etcd on startup. Default to 2.\n    # user: root                  # Set the root username for etcd.\n    # password: 5tHkHhYkjr6cQ     # Set the root password for etcd.\n    tls:\n      # cert: /path/to/cert       # Set the path to certificate used by the etcd client\n      # key: /path/to/key         # Set the path to path of key used by the etcd client\n      verify: true                # Verify the etcd certificate when establishing a TLS connection with etcd.\n      # sni:                      # The SNI for etcd TLS requests.\n                                  # If not set, the host from the URL is used.\n"
  },
  {
    "path": "conf/debug.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nbasic:\n  enable: false         # Enable the basic debug mode.\nhttp_filter:\n  enable: false         # Enable HTTP filter to dynamically apply advanced debug settings.\n  enable_header_name: X-APISIX-Dynamic-Debug   # If the header is present in a request, apply the advanced debug settings.\nhook_conf:\n  enable: false                 # Enable hook debug trace to log the target module function's input arguments or returned values.\n  name: hook_phase              # Name of module and function list.\n  log_level: warn               # Severity level for input arguments and returned values in the error log.\n  is_print_input_args: true     # Print the input arguments.\n  is_print_return_value: true   # Print the return value.\n\nhook_phase:                     # Name of module and function list.\n  apisix:                       # Required module name.\n    - http_access_phase         # Required function names.\n    - http_header_filter_phase\n    - http_body_filter_phase\n    - http_log_phase\n\n#END\n"
  },
  {
    "path": "conf/mime.types",
    "content": "\ntypes {\n    text/html                                        html htm shtml;\n    text/css                                         css;\n    text/xml                                         xml;\n    image/gif                                        gif;\n    image/jpeg                                       jpeg jpg;\n    application/javascript                           js;\n    application/atom+xml                             atom;\n    application/rss+xml                              rss;\n\n    text/mathml                                      mml;\n    text/plain                                       txt;\n    text/vnd.sun.j2me.app-descriptor                 jad;\n    text/vnd.wap.wml                                 wml;\n    text/x-component                                 htc;\n\n    image/png                                        png;\n    image/svg+xml                                    svg svgz;\n    image/tiff                                       tif tiff;\n    image/vnd.wap.wbmp                               wbmp;\n    image/webp                                       webp;\n    image/x-icon                                     ico;\n    image/x-jng                                      jng;\n    image/x-ms-bmp                                   bmp;\n\n    font/woff                                        woff;\n    font/woff2                                       woff2;\n\n    application/java-archive                         jar war ear;\n    application/json                                 json;\n    application/mac-binhex40                         hqx;\n    application/msword                               doc;\n    application/pdf                                  pdf;\n    application/postscript                           ps eps ai;\n    application/rtf                                  rtf;\n    application/vnd.apple.mpegurl                    m3u8;\n    application/vnd.google-earth.kml+xml             kml;\n    application/vnd.google-earth.kmz                 kmz;\n    application/vnd.ms-excel                         xls;\n    application/vnd.ms-fontobject                    eot;\n    application/vnd.ms-powerpoint                    ppt;\n    application/vnd.oasis.opendocument.graphics      odg;\n    application/vnd.oasis.opendocument.presentation  odp;\n    application/vnd.oasis.opendocument.spreadsheet   ods;\n    application/vnd.oasis.opendocument.text          odt;\n    application/vnd.openxmlformats-officedocument.presentationml.presentation\n                                                     pptx;\n    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\n                                                     xlsx;\n    application/vnd.openxmlformats-officedocument.wordprocessingml.document\n                                                     docx;\n    application/vnd.wap.wmlc                         wmlc;\n    application/wasm                                 wasm;\n    application/x-7z-compressed                      7z;\n    application/x-cocoa                              cco;\n    application/x-java-archive-diff                  jardiff;\n    application/x-java-jnlp-file                     jnlp;\n    application/x-makeself                           run;\n    application/x-perl                               pl pm;\n    application/x-pilot                              prc pdb;\n    application/x-rar-compressed                     rar;\n    application/x-redhat-package-manager             rpm;\n    application/x-sea                                sea;\n    application/x-shockwave-flash                    swf;\n    application/x-stuffit                            sit;\n    application/x-tcl                                tcl tk;\n    application/x-x509-ca-cert                       der pem crt;\n    application/x-xpinstall                          xpi;\n    application/xhtml+xml                            xhtml;\n    application/xspf+xml                             xspf;\n    application/zip                                  zip;\n\n    application/octet-stream                         bin exe dll;\n    application/octet-stream                         deb;\n    application/octet-stream                         dmg;\n    application/octet-stream                         iso img;\n    application/octet-stream                         msi msp msm;\n\n    audio/midi                                       mid midi kar;\n    audio/mpeg                                       mp3;\n    audio/ogg                                        ogg;\n    audio/x-m4a                                      m4a;\n    audio/x-realaudio                                ra;\n\n    video/3gpp                                       3gpp 3gp;\n    video/mp2t                                       ts;\n    video/mp4                                        mp4;\n    video/mpeg                                       mpeg mpg;\n    video/quicktime                                  mov;\n    video/webm                                       webm;\n    video/x-flv                                      flv;\n    video/x-m4v                                      m4v;\n    video/x-mng                                      mng;\n    video/x-ms-asf                                   asx asf;\n    video/x-ms-wmv                                   wmv;\n    video/x-msvideo                                  avi;\n}\n"
  },
  {
    "path": "docker/compose/apisix_conf/master/config.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\napisix:\n  node_listen: 9080 # APISIX listening port\n  enable_ipv6: false\n\ndeployment:\n  admin:\n    allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow\n      - 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.\n\n    admin_key:\n      - name: \"admin\"\n        key: edd1c9f034335f136f87ad84b625c8f1\n        role: admin # admin: manage all configuration data\n\n  etcd:\n    host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.\n      - \"http://etcd:2379\" # multiple etcd address\n    prefix: \"/apisix\" # apisix configurations prefix\n    timeout: 30 # 30 seconds\n"
  },
  {
    "path": "docker/compose/docker-compose-master.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nversion: \"3\"\n\nservices:\n  apisix:\n    image: \"apache/apisix:${APISIX_DOCKER_TAG}\"\n    restart: always\n    volumes:\n      - ./apisix_conf/master/config.yaml:/usr/local/apisix/conf/config.yaml:ro\n    depends_on:\n      - etcd\n    ports:\n      - \"9180:9180/tcp\"\n      - \"9080:9080/tcp\"\n      - \"9091:9091/tcp\"\n      - \"9443:9443/tcp\"\n    networks:\n      - apisix\n\n  etcd:\n    image: bitnamilegacy/etcd:3.6\n    restart: always\n    environment:\n      ETCD_DATA_DIR: /etcd_data\n      ETCD_ENABLE_V2: \"true\"\n      ALLOW_NONE_AUTHENTICATION: \"yes\"\n      ETCD_ADVERTISE_CLIENT_URLS: \"http://etcd:2379\"\n      ETCD_LISTEN_CLIENT_URLS: \"http://0.0.0.0:2379\"\n    ports:\n      - \"2379:2379/tcp\"\n    networks:\n      - apisix\n\n  httpbin:\n    image: ghcr.io/mccutchen/go-httpbin\n    restart: always\n    ports:\n      - \"8280:8080/tcp\"\n    networks:\n      - apisix\n\nnetworks:\n  apisix:\n    driver: bridge\n"
  },
  {
    "path": "docker/debian-dev/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nFROM debian:bullseye-slim AS build\n\nARG ENABLE_PROXY=false\nARG CODE_PATH\n\nENV DEBIAN_FRONTEND=noninteractive\nENV ENV_INST_LUADIR=/usr/local/apisix\n\nCOPY ${CODE_PATH} /apisix\n\nWORKDIR /apisix\n\nRUN set -x \\\n    && apt-get -y update --fix-missing \\\n    && apt-get install -y \\\n        make \\\n        git  \\\n        sudo \\\n        libyaml-dev \\\n    && ls -al \\\n    && make deps \\\n    && mkdir -p ${ENV_INST_LUADIR} \\\n    && cp -r deps ${ENV_INST_LUADIR} \\\n    && make install\n\nFROM debian:bullseye-slim\n\nARG ENTRYPOINT_PATH=./docker-entrypoint.sh\nARG INSTALL_BROTLI=./install-brotli.sh\n\n# Install the runtime libyaml package\nRUN apt-get -y update --fix-missing \\\n    && apt-get install -y libldap2-dev libyaml-0-2 \\\n    && apt-get remove --purge --auto-remove -y \\\n    && mkdir -p /usr/local/apisix/ui\n\nCOPY --from=build /usr/local/apisix /usr/local/apisix\nCOPY --from=build /usr/local/openresty /usr/local/openresty\nCOPY --from=build /usr/bin/apisix /usr/bin/apisix\nCOPY --chown=nobody:root ui/ /usr/local/apisix/ui/\n\nCOPY ${INSTALL_BROTLI} /install-brotli.sh\nRUN chmod +x /install-brotli.sh \\\n    && cd / && ./install-brotli.sh && rm -rf /install-brotli.sh \\\n    && chgrp -R 0 /usr/local/apisix \\\n    && chmod -R g=u /usr/local/apisix\n\nENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin\n\nWORKDIR /usr/local/apisix\n\nRUN ln -sf /dev/stdout /usr/local/apisix/logs/access.log \\\n    && ln -sf /dev/stderr /usr/local/apisix/logs/error.log\n\nEXPOSE 9080 9443\n\nCOPY ${ENTRYPOINT_PATH} /docker-entrypoint.sh\nRUN chmod +x /docker-entrypoint.sh\n\nENTRYPOINT [\"/docker-entrypoint.sh\"]\n\nCMD [\"docker-start\"]\n\nSTOPSIGNAL SIGQUIT\n"
  },
  {
    "path": "docker/debian-dev/docker-entrypoint.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -eo pipefail\n\nPREFIX=${APISIX_PREFIX:=/usr/local/apisix}\n\nif [[ \"$1\" == \"docker-start\" ]]; then\n    if [ \"$APISIX_STAND_ALONE\" = \"true\" ]; then\n      # If the file is not present then initialise the content otherwise update relevant keys for standalone mode\n      if [ ! -f \"${PREFIX}/conf/config.yaml\" ]; then\n          cat > ${PREFIX}/conf/config.yaml << _EOC_\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n_EOC_\n      fi\n\n        if [ ! -f \"${PREFIX}/conf/apisix.yaml\" ]; then\n          cat > ${PREFIX}/conf/apisix.yaml << _EOC_\nroutes:\n  -\n#END\n_EOC_\n        fi\n        /usr/bin/apisix init\n    else\n        /usr/bin/apisix init\n        /usr/bin/apisix init_etcd\n    fi\n\n    # For versions below 3.5.0 whose conf_server has not been removed.\n    if [ -e \"/usr/local/apisix/conf/config_listen.sock\" ]; then\n        rm -f \"/usr/local/apisix/conf/config_listen.sock\"\n    fi\n\n    if [ -e \"/usr/local/apisix/logs/worker_events.sock\" ]; then\n        rm -f \"/usr/local/apisix/logs/worker_events.sock\"\n    fi\n\n    if [ -e \"/usr/local/apisix/logs/stream_worker_events.sock\" ]; then\n        rm -f \"/usr/local/apisix/logs/stream_worker_events.sock\"\n    fi\n\n    exec /usr/local/openresty/bin/openresty -p /usr/local/apisix -g 'daemon off;'\nfi\n\nexec \"$@\"\n"
  },
  {
    "path": "docker/debian-dev/install-brotli.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ninstall_brotli() {\n    apt-get -qy update\n    apt-get install -y sudo cmake wget unzip\n    local BORTLI_VERSION=\"1.1.0\"\n    wget -q https://github.com/google/brotli/archive/refs/tags/v${BORTLI_VERSION}.zip || exit 1\n    unzip v${BORTLI_VERSION}.zip && cd ./brotli-${BORTLI_VERSION} && mkdir build && cd build || exit 1\n    local CMAKE=$(command -v cmake3 >/dev/null 2>&1 && echo cmake3 || echo cmake) || exit 1\n    ${CMAKE} -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local/brotli .. || exit 1\n    sudo ${CMAKE} --build . --config Release --target install || exit 1\n    if [ -d \"/usr/local/brotli/lib64\" ]; then\n        echo /usr/local/brotli/lib64 | sudo tee /etc/ld.so.conf.d/brotli.conf\n    else\n        echo /usr/local/brotli/lib | sudo tee /etc/ld.so.conf.d/brotli.conf\n    fi\n    sudo ldconfig || exit 1\n    ln -sf /usr/local/brotli/bin/brotli /usr/bin/brotli\n    cd ../..\n    rm -rf brotli-${BORTLI_VERSION}\n    rm -rf /v${BORTLI_VERSION}.zip\n    export SUDO_FORCE_REMOVE=yes\n    apt purge -qy cmake sudo wget unzip\n    apt-get remove --purge --auto-remove -y\n}\ninstall_brotli\n"
  },
  {
    "path": "docs/assets/other/json/apisix-grafana-dashboard.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"7.3.7\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"graph\",\n      \"name\": \"Graph\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"singlestat\",\n      \"name\": \"Singlestat\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"stat\",\n      \"name\": \"Stat\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"${DS_PROMETHEUS}\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"limit\": 100,\n        \"name\": \"Annotations & Alerts\",\n        \"showIn\": 0,\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"description\": \"MicroService API Gateway Apache APISIX\",\n  \"editable\": true,\n  \"gnetId\": 11719,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"iteration\": 1617695812393,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 10,\n      \"panels\": [],\n      \"title\": \"Nginx\",\n      \"type\": \"row\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {}\n        },\n        \"overrides\": []\n      },\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 1\n      },\n      \"id\": 8,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true,\n        \"ymax\": null,\n        \"ymin\": null\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_http_requests_total{instance=~\\\"$instance\\\"})\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"Total\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Total Requests\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {}\n        },\n        \"overrides\": []\n      },\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 1\n      },\n      \"id\": 16,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true,\n        \"ymax\": null,\n        \"ymin\": null\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_nginx_http_current_connections{state=\\\"accepted\\\", instance=~\\\"$instance\\\"})\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"Accepted\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Accepted Connections\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {}\n        },\n        \"overrides\": []\n      },\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 1\n      },\n      \"id\": 11,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": true,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": true,\n        \"ymax\": null,\n        \"ymin\": null\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_nginx_http_current_connections{state=\\\"handled\\\", instance=~\\\"$instance\\\"})\",\n          \"intervalFactor\": 2,\n          \"legendFormat\": \"Total\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Handled Connections\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 6\n      },\n      \"hiddenSeries\": false,\n      \"id\": 17,\n      \"legend\": {\n        \"alignAsTable\": false,\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_nginx_http_current_connections{state=~\\\"active|reading|writing|waiting\\\", instance=~\\\"$instance\\\"}) by (state)\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{state}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Nginx connection state\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": null,\n          \"format\": \"short\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"decimals\": null,\n          \"format\": \"Misc\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 12\n      },\n      \"id\": 13,\n      \"panels\": [],\n      \"title\": \"Bandwidth\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 13\n      },\n      \"hiddenSeries\": false,\n      \"id\": 6,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"rightSide\": true,\n        \"show\": true,\n        \"sort\": \"total\",\n        \"sortDesc\": true,\n        \"total\": true,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(apisix_bandwidth{instance=~\\\"$instance\\\"}[$__rate_interval])) by (type)\",\n          \"legendFormat\": \"{{type}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Total Bandwidth\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"decbytes\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 19\n      },\n      \"hiddenSeries\": false,\n      \"id\": 21,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"rightSide\": true,\n        \"show\": true,\n        \"sort\": \"total\",\n        \"sortDesc\": true,\n        \"total\": true,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(irate(apisix_bandwidth{type=\\\"ingress\\\", service =~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (service)\",\n          \"legendFormat\": \"service:{{service}}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"sum(irate(apisix_bandwidth{type=\\\"ingress\\\", service =~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (route)\",\n          \"legendFormat\": \"route:{{route}}\",\n          \"refId\": \"B\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Ingress per service/route\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"decbytes\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 25\n      },\n      \"hiddenSeries\": false,\n      \"id\": 19,\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"rightSide\": true,\n        \"show\": true,\n        \"sort\": \"total\",\n        \"sortDesc\": true,\n        \"total\": true,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(apisix_bandwidth{type=\\\"egress\\\", service =~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (service)\",\n          \"interval\": \"\",\n          \"legendFormat\": \"service:{{service}}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"sum(rate(apisix_bandwidth{type=\\\"egress\\\", service =~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (route)\",\n          \"legendFormat\": \"route:{{route}}\",\n          \"refId\": \"B\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Egress per service/route\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"decbytes\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 31\n      },\n      \"id\": 15,\n      \"panels\": [],\n      \"title\": \"HTTP\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {\n        \"HTTP Status:200\": \"green\",\n        \"HTTP Status:500\": \"red\"\n      },\n      \"bars\": false,\n      \"cacheTimeout\": null,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 3,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 32\n      },\n      \"hiddenSeries\": false,\n      \"id\": 2,\n      \"interval\": \"\",\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [\n        {\n          \"alias\": \"state\",\n          \"lines\": true\n        }\n      ],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(apisix_http_status{service=~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (code)\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"HTTP Status:{{code}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Requests per second (RPS) by status code\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"cacheTimeout\": null,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 3,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 32\n      },\n      \"hiddenSeries\": false,\n      \"id\": 2,\n      \"interval\": \"\",\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [\n        {\n          \"alias\": \"state\",\n          \"lines\": true\n        }\n      ],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(apisix_http_status{instance=~\\\"$instance\\\"}[$__rate_interval]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\",\n          \"legendFormat\": \"Requests/second\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Total requests per second (RPS)\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"cacheTimeout\": null,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 3,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 38\n      },\n      \"hiddenSeries\": false,\n      \"id\": 32,\n      \"interval\": \"\",\n      \"legend\": {\n        \"alignAsTable\": true,\n        \"avg\": true,\n        \"current\": true,\n        \"max\": true,\n        \"min\": true,\n        \"rightSide\": true,\n        \"show\": true,\n        \"sort\": \"total\",\n        \"sortDesc\": true,\n        \"total\": true,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [\n        {\n          \"alias\": \"state\",\n          \"lines\": true\n        }\n      ],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(rate(apisix_http_status{service=~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (service)\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"service:{{service}}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"sum(rate(apisix_http_status{service=~\\\"$service\\\",route=~\\\"$route\\\",instance=~\\\"$instance\\\"}[$__rate_interval])) by (route)\",\n          \"interval\": \"\",\n          \"legendFormat\": \"route:{{route}}\",\n          \"refId\": \"D\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Requests per second (RPS) per service/route\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 44\n      },\n      \"hiddenSeries\": false,\n      \"id\": 27,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"histogram_quantile(0.90, sum(rate(apisix_http_latency_bucket{type=~\\\"request\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P90\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"histogram_quantile(0.95, sum(rate(apisix_http_latency_bucket{type=~\\\"request\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P95\",\n          \"refId\": \"B\"\n        },\n        {\n          \"expr\": \"histogram_quantile(0.99, sum(rate(apisix_http_latency_bucket{type=~\\\"request\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P99\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Request Latency\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"ms\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 44\n      },\n      \"hiddenSeries\": false,\n      \"id\": 28,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"histogram_quantile(0.90, sum(rate(apisix_http_latency_bucket{type=~\\\"apisix\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"P90\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"histogram_quantile(0.95, sum(rate(apisix_http_latency_bucket{type=~\\\"apisix\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P95\",\n          \"refId\": \"B\"\n        },\n        {\n          \"expr\": \"histogram_quantile(0.99, sum(rate(apisix_http_latency_bucket{type=~\\\"apisix\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P99\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"APISIX Latency\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"ms\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 44\n      },\n      \"hiddenSeries\": false,\n      \"id\": 33,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"histogram_quantile(0.90, sum(rate(apisix_http_latency_bucket{type=~\\\"upstream\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P90\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"histogram_quantile(0.95, sum(rate(apisix_http_latency_bucket{type=~\\\"upstream\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P95\",\n          \"refId\": \"B\"\n        },\n        {\n          \"expr\": \"histogram_quantile(0.99, sum(rate(apisix_http_latency_bucket{type=~\\\"upstream\\\",service=~\\\"$service\\\",consumer=~\\\"$consumer\\\",node=~\\\"$node\\\",route=~\\\"$route\\\"}[$__rate_interval])) by (le))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"P99\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Upstream Latency\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"ms\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 50\n      },\n      \"id\": 23,\n      \"panels\": [],\n      \"title\": \"Misc\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 18,\n        \"x\": 0,\n        \"y\": 51\n      },\n      \"hiddenSeries\": false,\n      \"id\": 30,\n      \"legend\": {\n        \"alignAsTable\": false,\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_etcd_modify_indexes{key=~\\\"consumers|global_rules|max_modify_index|prev_index|protos|routes|services|ssls|stream_routes|upstreams|x_etcd_index\\\"}) by (key)\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{key}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Etcd modify indexes\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"decimals\": null,\n          \"format\": \"short\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 0\n              },\n              {\n                \"color\": \"green\",\n                \"value\": 1\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 3,\n        \"x\": 18,\n        \"y\": 51\n      },\n      \"id\": 25,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"last\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"7.3.7\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_etcd_reachable{instance=~\\\"$instance\\\"})\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Etcd reachable\",\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"yellow\",\n                \"value\": 1\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 3,\n        \"x\": 21,\n        \"y\": 51\n      },\n      \"id\": 29,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"last\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"7.3.7\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(apisix_nginx_metric_errors_total{instance=~\\\"$instance\\\"})\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Nginx metric errors\",\n      \"type\": \"stat\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"${DS_PROMETHEUS}\",\n      \"description\": \"The free space percent of each nginx shared DICT since APISIX start\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"links\": []\n        },\n        \"overrides\": []\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 57\n      },\n      \"hiddenSeries\": false,\n      \"id\": 35,\n      \"legend\": {\n        \"alignAsTable\": false,\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.7\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"(apisix_shared_dict_free_space_bytes * 100) / on (name) apisix_shared_dict_capacity_bytes\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{state}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Nginx shared dict free space percent\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"$$hashKey\": \"object:117\",\n          \"decimals\": null,\n          \"format\": \"percent\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"$$hashKey\": \"object:118\",\n          \"decimals\": null,\n          \"format\": \"Misc\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 26,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"allValue\": \".*\",\n        \"current\": {},\n        \"datasource\": \"${DS_PROMETHEUS}\",\n        \"definition\": \"label_values(apisix_http_status,service)\",\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": true,\n        \"name\": \"service\",\n        \"options\": [],\n        \"query\": \"label_values(apisix_http_status,service)\",\n        \"refresh\": 2,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": \".*\",\n        \"current\": {},\n        \"datasource\": \"${DS_PROMETHEUS}\",\n        \"definition\": \"label_values(apisix_http_status,route)\",\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": true,\n        \"name\": \"route\",\n        \"options\": [],\n        \"query\": \"label_values(apisix_http_status,route)\",\n        \"refresh\": 2,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": \".*\",\n        \"current\": {},\n        \"datasource\": \"${DS_PROMETHEUS}\",\n        \"definition\": \"label_values(apisix_nginx_http_current_connections,instance)\",\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": true,\n        \"name\": \"instance\",\n        \"options\": [],\n        \"query\": \"label_values(apisix_http_status,instance)\",\n        \"refresh\": 2,\n        \"regex\": \".*\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": \".*\",\n        \"current\": {},\n        \"datasource\": \"${DS_PROMETHEUS}\",\n        \"definition\": \"label_values(apisix_http_status,consumer)\",\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": true,\n        \"name\": \"consumer\",\n        \"options\": [],\n        \"query\": \"label_values(apisix_http_status,consumer)\",\n        \"refresh\": 2,\n        \"regex\": \".*\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": \".*\",\n        \"current\": {},\n        \"datasource\": \"${DS_PROMETHEUS}\",\n        \"definition\": \"label_values(apisix_http_status,node)\",\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": true,\n        \"name\": \"node\",\n        \"options\": [],\n        \"query\": \"label_values(apisix_http_status,node)\",\n        \"refresh\": 2,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-30m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ]\n  },\n  \"timezone\": \"\",\n  \"title\": \"Apache APISIX\",\n  \"uid\": \"bLlNuRLWz\",\n  \"version\": 13\n}\n"
  },
  {
    "path": "docs/en/latest/FAQ.md",
    "content": "---\ntitle: FAQ\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - FAQ\ndescription: This article lists solutions to common problems when using Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Why do I need a new API gateway?\n\nAs organizations move towards cloud native microservices, there is a need for an API gateway that is performant, flexible, secure and scalable.\n\nAPISIX outperforms other API gateways in these metrics while being platform agnostic and fully dynamic delivering features like supporting multiple protocols, fine-grained routing and multi-language support.\n\n## How does Apache APISIX differ from other API gateways?\n\nApache APISIX differs in the following ways:\n\n- It uses etcd to save and synchronize configurations rather than relational databases like PostgreSQL or MySQL. The real-time event notification system in etcd is easier to scale than in these alternatives. This allows APISIX to synchronize the configuration in real-time, makes the code concise and avoids a single point of failure.\n- Fully dynamic.\n- Supports [hot loading of Plugins](./terminology/plugin.md#hot-reload).\n\n## What is the performance impact of using Apache APISIX?\n\nApache APISIX delivers the best performance among other API gateways with a single-core QPS of 18,000 with an average delay of 0.2 ms.\n\nSpecific results of the performance benchmarks can be found [here](benchmark.md).\n\n## Which platforms does Apache APISIX support?\n\nApache APISIX is platform agnostic and avoids vendor lock-in. It is built for cloud native environments and can run on bare-metal machines to Kubernetes. It even support Apple Silicon chips.\n\n## What does it mean by \"Apache APISIX is fully dynamic\"?\n\nApache APISIX is fully dynamic in the sense that it doesn't require restarts to change its behavior.\n\nIt does the following dynamically:\n\n- Reloading Plugins\n- Proxy rewrites\n- Proxy mirror\n- Response rewrites\n- Health checks\n- Traffic split\n\n## Does Apache APISIX have a user interface?\n\nAPISIX has a powerful built-in Dashboard [APISIX Dashboard](https://github.com/apache/apisix-dashboard). You can manage APISIX configurations through the [APISIX Dashboard](https://github.com/apache/apisix-dashboard) user interface.\n\n## Can I write my own Plugins for Apache APISIX?\n\nYes. Apache APISIX is flexible and extensible through the use of custom Plugins that can be specific to user needs.\n\nYou can write your own Plugins by referring to [How to write your own Plugins](plugin-develop.md).\n\n## Why does Apache APISIX use etcd for the configuration center?\n\nIn addition to the basic functionality of storing the configurations, Apache APISIX also needs a storage system that supports these features:\n\n1. Distributed deployments in clusters.\n2. Guarded transactions by comparisons.\n3. Multi-version concurrency control.\n4. Notifications and watch streams.\n5. High performance with minimum read/write latency.\n\netcd provides these features and more making it ideal over other databases like PostgreSQL and MySQL.\n\nTo learn more on how etcd compares with other alternatives see this [comparison chart](https://etcd.io/docs/latest/learning/why/#comparison-chart).\n\n## When installing Apache APISIX dependencies with LuaRocks, why does it cause a timeout or result in a slow or unsuccessful installation?\n\nThis is likely because the LuaRocks server used is blocked.\n\nTo solve this you can use https_proxy or use the `--server` flag to specify a faster LuaRocks server.\n\nYou can run the command below to see the available servers (needs LuaRocks 3.0+):\n\n```shell\nluarocks config rocks_servers\n```\n\nMainland China users can use `luarocks.cn` as the LuaRocks server. You can use this wrapper with the Makefile to set this up:\n\n```bash\nmake deps ENV_LUAROCKS_SERVER=https://luarocks.cn\n```\n\nIf this does not solve your problem, you can try getting a detailed log by using the `--verbose` or `-v` flag to diagnose the problem.\n\n## How do I build the APISIX-Runtime environment?\n\nSome functions need to introduce additional NGINX modules, which requires APISIX to run on APISIX-Runtime. If you need these functions, you can refer to the code in [api7/apisix-build-tools](https://github.com/api7/apisix-build-tools) to build your own APISIX-Runtime environment.\n\n## How can I make a gray release with Apache APISIX?\n\nLet's take an example query `foo.com/product/index.html?id=204&page=2` and consider that you need to make a gray release based on the `id` in the query string with this condition:\n\n1. Group A: `id <= 1000`\n2. Group B: `id > 1000`\n\nThere are two different ways to achieve this in Apache APISIX:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. Using the `vars` field in a [Route](terminology/route.md):\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"vars\": [\n        [\"arg_id\", \"<=\", \"1000\"]\n    ],\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"/test?group_id=1\"\n        }\n    }\n}'\n\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"vars\": [\n        [\"arg_id\", \">\", \"1000\"]\n    ],\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"/test?group_id=2\"\n        }\n    }\n}'\n```\n\nAll the available operators of the current `lua-resty-radixtree` are listed [here](https://github.com/api7/lua-resty-radixtree#operator-list).\n\n2. Using the [traffic-split](plugins/traffic-split.md) Plugin.\n\n## How do I redirect HTTP traffic to HTTPS with Apache APISIX?\n\nFor example, you need to redirect traffic from `http://foo.com` to `https://foo.com`.\n\nApache APISIX provides several different ways to achieve this:\n\n1. Setting `http_to_https` to `true` in the [redirect](plugins/redirect.md) Plugin:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"host\": \"foo.com\",\n    \"plugins\": {\n        \"redirect\": {\n            \"http_to_https\": true\n        }\n    }\n}'\n```\n\n2. Advanced routing with `vars` in the redirect Plugin:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"host\": \"foo.com\",\n    \"vars\": [\n        [\n            \"scheme\",\n            \"==\",\n            \"http\"\n        ]\n    ],\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"https://$host$request_uri\",\n            \"ret_code\": 301\n        }\n    }\n}'\n```\n\n3. Using the `serverless` Plugin:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\"return function() if ngx.var.scheme == \\\"http\\\" and ngx.var.host == \\\"foo.com\\\" then ngx.header[\\\"Location\\\"] = \\\"https://foo.com\\\" .. ngx.var.request_uri; ngx.exit(ngx.HTTP_MOVED_PERMANENTLY); end; end\"]\n        }\n    }\n}'\n```\n\nTo test this serverless Plugin:\n\n```shell\ncurl -i -H 'Host: foo.com' http://127.0.0.1:9080/hello\n```\n\nThe response should be:\n\n```\nHTTP/1.1 301 Moved Permanently\nDate: Mon, 18 May 2020 02:56:04 GMT\nContent-Type: text/html\nContent-Length: 166\nConnection: keep-alive\nLocation: https://foo.com/hello\nServer: APISIX web server\n\n<html>\n<head><title>301 Moved Permanently</title></head>\n<body>\n<center><h1>301 Moved Permanently</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n## How do I change Apache APISIX's log level?\n\nBy default the log level of Apache APISIX is set to `warn`. You can set this to `info` to trace the messages printed by `core.log.info`.\n\nFor this, you can set the `error_log_level` parameter in your configuration file (conf/config.yaml) as shown below and reload Apache APISIX.\n\n```yaml\nnginx_config:\n  error_log_level: \"info\"\n```\n\n## How do I reload my custom Plugins for Apache APISIX?\n\nAll Plugins in Apache APISIX are hot reloaded.\n\nYou can learn more about hot reloading of Plugins [here](./terminology/plugin.md#hot-reload).\n\n## How do I configure Apache APISIX to listen on multiple ports when handling HTTP or HTTPS requests?\n\nBy default, Apache APISIX listens only on port 9080 when handling HTTP requests.\n\nTo configure Apache APISIX to listen on multiple ports, you can:\n\n1. Modify the parameter `node_listen` in `conf/config.yaml`:\n\n   ```\n    apisix:\n      node_listen:\n        - 9080\n        - 9081\n        - 9082\n   ```\n\n   Similarly for HTTPS requests, modify the parameter `ssl.listen` in `conf/config.yaml`:\n\n   ```\n   apisix:\n     ssl:\n       enable: true\n       listen:\n         - port: 9443\n         - port: 9444\n         - port: 9445\n   ```\n\n2. Reload or restart Apache APISIX.\n\n## After uploading the SSL certificate, why can't the corresponding route be accessed through HTTPS + IP?\n\nIf you directly use HTTPS + IP address to access the server, the server will use the IP address to compare with the bound SNI. Since the SSL certificate is bound to the domain name, the corresponding resource cannot be found in the SNI, so that the certificate will be verified. The authentication fails, and the user cannot access the gateway via HTTPS + IP.\n\nYou can implement this function by setting the `fallback_sni` parameter in the configuration file and configuring the domain name. When the user uses HTTPS + IP to access the gateway, when the SNI is empty, it will fall back to the default SNI to achieve HTTPS + IP access to the gateway.\n\n```yaml title=\"./conf/config.yaml\"\napisix\n  ssl：\n    fallback_sni: \"${your sni}\"\n```\n\n## How does Apache APISIX achieve millisecond-level configuration synchronization?\n\nApache APISIX uses etcd for its configuration center. etcd provides subscription functions like [watch](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watch) and [watchdir](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watchdir) that can monitor changes to specific keywords or directories.\n\nIn Apache APISIX, we use [etcd.watchdir](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watchdir) to monitor changes in a directory.\n\nIf there is no change in the directory being monitored, the process will be blocked until it times out or run into any errors.\n\nIf there are changes in the directory being monitored, etcd will return this new data within milliseconds and Apache APISIX will update the cache memory.\n\n## How do I customize the Apache APISIX instance id?\n\nBy default, Apache APISIX reads the instance id from `conf/apisix.uid`. If this is not found and no id is configured, Apache APISIX will generate a `uuid` for the instance id.\n\nTo specify a meaningful id to bind Apache APISIX to your internal system, set the `id` in your `conf/config.yaml` file:\n\n```yaml\napisix:\n  id: \"your-id\"\n```\n\n## Why are there errors saying \"failed to fetch data from etcd, failed to read etcd dir, etcd key: xxxxxx\" in the error.log?\n\nPlease follow the troubleshooting steps described below:\n\n1. Make sure that there aren't any networking issues between Apache APISIX and your etcd deployment in your cluster.\n2. If your network is healthy, check whether you have enabled the [gRPC gateway](https://etcd.io/docs/v3.4/dev-guide/api_grpc_gateway/) for etcd. The default state depends on whether you used command line options or a configuration file to start the etcd server.\n\n   - If you used command line options, gRPC gateway is enabled by default. You can enable it manually as shown below:\n\n   ```sh\n   etcd --enable-grpc-gateway --data-dir=/path/to/data\n   ```\n\n   **Note**: This flag is not shown while running `etcd --help`.\n\n   - If you used a configuration file, gRPC gateway is disabled by default. You can manually enable it as shown below:\n\n   In `etcd.json`:\n\n   ```json\n   {\n     \"enable-grpc-gateway\": true,\n     \"data-dir\": \"/path/to/data\"\n   }\n   ```\n\n   In `etcd.conf.yml`:\n\n   ```yml\n   enable-grpc-gateway: true\n   ```\n\n**Note**: This distinction was eliminated by etcd in their latest master branch but wasn't backported to previous versions.\n\n## How do I setup high availability Apache APISIX clusters?\n\nApache APISIX can be made highly available by adding a load balancer in front of it as APISIX's data plane is stateless and can be scaled when needed.\n\nThe control plane of Apache APISIX is highly available as it relies only on an etcd cluster.\n\n## Why does the `make deps` command fail when installing Apache APISIX from source?\n\nWhen executing `make deps` to install Apache APISIX from source, you can get an error as shown below:\n\n```shell\n$ make deps\n......\nError: Failed installing dependency: https://luarocks.org/luasec-0.9-1.src.rock - Could not find header file for OPENSSL\n  No file openssl/ssl.h in /usr/local/include\nYou may have to install OPENSSL in your system and/or pass OPENSSL_DIR or OPENSSL_INCDIR to the luarocks command.\nExample: luarocks install luasec OPENSSL_DIR=/usr/local\nmake: *** [deps] Error 1\n```\n\nThis is caused by the missing OpenResty openssl development kit. To install it, refer [installing dependencies](install-dependencies.md).\n\n## How do I use regular expressions (regex) for matching `uri` in a Route?\n\nYou can use the `vars` field in a Route for matching regular expressions:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"vars\": [\n        [\"uri\", \"~~\", \"^/[a-z]+$\"]\n    ],\n    \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n    }\n}'\n```\n\nAnd to test this request:\n\n```shell\n# uri matched\n$ curl http://127.0.0.1:9080/hello -i\nHTTP/1.1 200 OK\n...\n\n# uri didn't match\n$ curl http://127.0.0.1:9080/12ab -i\nHTTP/1.1 404 Not Found\n...\n```\n\nFor more info on using `vars` refer to [lua-resty-expr](https://github.com/api7/lua-resty-expr).\n\n## Does the Upstream node support configuring a [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) address?\n\nYes. The example below shows configuring the FQDN `httpbin.default.svc.cluster.local` (a Kubernetes service):\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/ip\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.default.svc.cluster.local\": 1\n        }\n    }\n}'\n```\n\nTo test this Route:\n\n```shell\n$ curl http://127.0.0.1:9080/ip -i\nHTTP/1.1 200 OK\n...\n```\n\n## What is the `X-API-KEY` of the Admin API? Can it be modified?\n\n`X-API-KEY` of the Admin API refers to the `apisix.admin_key.key` in your `conf/config.yaml` file. It is the access token for the Admin API.\n\nBy default, it is set to `edd1c9f034335f136f87ad84b625c8f1` and can be modified by changing the parameter in your `conf/config.yaml` file:\n\n```yaml\napisix:\n  admin_key\n    -\n      name: \"admin\"\n      key: newkey\n      role: admin\n```\n\nNow, to access the Admin API:\n\n```shell\n$ curl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H 'X-API-KEY: newkey' -X PUT -d '\n{\n    \"uris\":[ \"/*\" ],\n    \"name\":\"admin-token-test\",\n    \"upstream\":{\n        \"nodes\":[\n            {\n                \"host\":\"127.0.0.1\",\n                \"port\":1980,\n                \"weight\":1\n            }\n        ],\n        \"type\":\"roundrobin\"\n    }\n}'\n\nHTTP/1.1 200 OK\n......\n```\n\n**Note**: By using the default token, you could be exposed to security risks. It is required to update it when deploying to a production environment.\n\n## How do I allow all IPs to access Apache APISIX's Admin API?\n\nBy default, Apache APISIX only allows IPs in the range `127.0.0.0/24` to access the Admin API.\n\nTo allow IPs in all ranges, you can update your configuration file as show below and restart or reload Apache APISIX.\n\n```yaml\ndeployment:\n  admin:\n    allow_admin:\n      - 0.0.0.0/0\n```\n\n**Note**: This should only be used in non-production environments to allow all clients to access Apache APISIX and is not safe for production environments. Always authorize specific IP addresses or address ranges for production environments.\n\n## How do I auto renew SSL certificates with acme.sh?\n\nYou can run the commands below to achieve this:\n\n```bash\ncurl --output /root/.acme.sh/renew-hook-update-apisix.sh --silent https://gist.githubusercontent.com/anjia0532/9ebf8011322f43e3f5037bc2af3aeaa6/raw/65b359a4eed0ae990f9188c2afa22bacd8471652/renew-hook-update-apisix.sh\n```\n\n```bash\nchmod +x /root/.acme.sh/renew-hook-update-apisix.sh\n```\n\n```bash\nacme.sh  --issue  --staging  -d demo.domain --renew-hook \"/root/.acme.sh/renew-hook-update-apisix.sh  -h http://apisix-admin:port -p /root/.acme.sh/demo.domain/demo.domain.cer -k /root/.acme.sh/demo.domain/demo.domain.key -a xxxxxxxxxxxxx\"\n```\n\n```bash\nacme.sh --renew --domain demo.domain\n```\n\nYou can check [this post](https://juejin.cn/post/6965778290619449351) for a more detailed instruction on setting this up.\n\n## How do I strip a prefix from a path before forwarding to Upstream in Apache APISIX?\n\nTo strip a prefix from a path in your route, like to take `/foo/get` and strip it to `/get`, you can use the [proxy-rewrite](plugins/proxy-rewrite.md) Plugin:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/foo/*\",\n    \"plugins\": {\n        \"proxy-rewrite\": {\n            \"regex_uri\": [\"^/foo/(.*)\",\"/$1\"]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n\nAnd to test this configuration:\n\n```shell\ncurl http://127.0.0.1:9080/foo/get -i\nHTTP/1.1 200 OK\n...\n{\n  ...\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n## How do I fix the error `unable to get local issuer certificate` in Apache APISIX?\n\nYou can manually set the path to your certificate by adding it to your `conf/config.yaml` file as shown below:\n\n```yaml\napisix:\n  ssl:\n    ssl_trusted_certificate: /path/to/certs/ca-certificates.crt\n```\n\n**Note**: When you are trying to connect TLS services with cosocket and if APISIX does not trust the peer's TLS certificate, you should set the parameter `apisix.ssl.ssl_trusted_certificate`.\n\nFor example, if you are using Nacos for service discovery in APISIX, and Nacos has TLS enabled (configured host starts with `https://`), you should set `apisix.ssl.ssl_trusted_certificate` and use the same CA certificate as Nacos.\n\n## How do I fix the error `module 'resty.worker.events' not found` in Apache APISIX?\n\nThis error is caused by installing Apache APISIX in the `/root` directory. The worker process would by run by the user \"nobody\" and it would not have enough permissions to access the files in the `/root` directory.\n\nTo fix this, you can change the APISIX installation directory to the recommended directory: `/usr/local`.\n\n## What is the difference between `plugin-metadata` and `plugin-configs` in Apache APISIX?\n\nThe differences between the two are described in the table below:\n\n| `plugin-metadata`                                                                                                | `plugin-config`                                                                                                                                     |\n| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Metadata of a Plugin shared by all configuration instances of the Plugin.                                        | Collection of configuration instances of multiple different Plugins.                                                                                |\n| Used when there are property changes that needs to be propagated across all configuration instances of a Plugin. | Used when you need to reuse a common set of configuration instances so that it can be extracted to a `plugin-config` and bound to different Routes. |\n| Takes effect on all the entities bound to the configuration instances of the Plugin.                             | Takes effect on Routes bound to the `plugin-config`.                                                                                                |\n\n## After deploying Apache APISIX, how to detect the survival of the APISIX data plane?\n\nYou can create a route named `health-info` and enable the [fault-injection](https://apisix.apache.org/docs/apisix/plugins/fault-injection/) plugin (where YOUR-TOKEN is the user's token; 127.0.0.1 is the IP address of the control plane, which can be modified by yourself):\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/health-info \\\n-H 'X-API-KEY: YOUR-TOKEN' -X PUT -d '\n{\n   \"plugins\": {\n     \"fault-injection\": {\n       \"abort\": {\n        \"http_status\": 200,\n        \"body\": \"fine\"\n       }\n     }\n   },\n   \"uri\": \"/status\"\n}'\n````\n\nVerification:\n\nAccess the `/status` of the Apache APISIX data plane to detect APISIX. If the response code is 200, it means APISIX is alive.\n\n:::note\n\nThis method only detects whether the APISIX data plane is alive or not. It does not mean that the routing and other functions of APISIX are normal. These require more routing-level detection.\n\n:::\n\n## What are the scenarios with high APISIX latency related to [etcd](https://etcd.io/) and how to fix them?\n\netcd is the data storage component of apisix, and its stability is related to the stability of APISIX.\n\nIn actual scenarios, if APISIX uses a certificate to connect to etcd through HTTPS, the following two problems of high latency for data query or writing may occur:\n\n1. Query or write data through APISIX Admin API.\n2. In the monitoring scenario, Prometheus crawls the APISIX data plane Metrics API timeout.\n\nThese problems related to higher latency seriously affect the service stability of APISIX, and the reason why such problems occur is mainly because etcd provides two modes of operation: HTTP (HTTPS) and gRPC. And APISIX uses the HTTP (HTTPS) protocol to operate etcd by default.\nIn this scenario, etcd has a bug about HTTP/2: if etcd is operated over HTTPS (HTTP is not affected), the upper limit of HTTP/2 connections is the default `250` in Golang. Therefore, when the number of APISIX data plane nodes is large, once the number of connections between all APISIX nodes and etcd exceeds this upper limit, the response of APISIX API interface will be very slow.\n\nIn Golang, the default upper limit of HTTP/2 connections is `250`, the code is as follows:\n\n```go\npackage http2\n\nimport ...\n\nconst (\n    prefaceTimeout         = 10 * time.Second\n    firstSettingsTimeout   = 2 * time.Second // should be in-flight with preface anyway\n    handlerChunkWriteSize  = 4 << 10\n    defaultMaxStreams      = 250 // TODO: make this 100 as the GFE seems to?\n    maxQueuedControlFrames = 10000\n)\n\n```\n\netcd officially maintains two main branches, `3.4` and `3.5`. In the `3.4` series, the recently released `3.4.20` version has fixed this issue. As for the `3.5` version, the official is preparing to release the `3.5.5` version a long time ago, but it has not been released as of now (2022.09.13). So, if you are using etcd version less than `3.5.5`, you can refer to the following ways to solve this problem:\n\n1. Change the communication method between APISIX and etcd from HTTPS to HTTP.\n2. Roll back the etcd to `3.4.20`.\n3. Clone the etcd source code and compile the `release-3.5` branch directly (this branch has fixed the problem of HTTP/2 connections, but the new version has not been released yet).\n\nThe way to recompile etcd is as follows:\n\n```shell\ngit checkout release-3.5\nmake GOOS=linux GOARCH=amd64\n```\n\nThe compiled binary is in the bin directory, replace it with the etcd binary of your server environment, and then restart etcd:\n\nFor more information, please refer to:\n\n- [when etcd node have many http long polling connections, it may cause etcd to respond slowly to http requests.](https://github.com/etcd-io/etcd/issues/14185)\n- [bug: when apisix starts for a while, its communication with etcd starts to time out](https://github.com/apache/apisix/issues/7078)\n- [the prometheus metrics API is tool slow](https://github.com/apache/apisix/issues/7353)\n- [Support configuring `MaxConcurrentStreams` for http2](https://github.com/etcd-io/etcd/pull/14169)\n\nAnother solution is to switch to an experimental gRPC-based configuration synchronization. This requires setting `use_grpc: true` in the configuration file `conf/config.yaml`:\n\n```yaml\n  etcd:\n    use_grpc: true\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n```\n\n## Why is the file-logger logging garbled?\n\nIf you are using the `file-logger` plugin but getting garbled logs, one possible reason is your upstream response has returned a compressed response body. You can fix this by setting the accept-encoding in the request header to not receive compressed responses using the [proxy-rewirte](https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite/) plugin:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H 'X-API-KEY: YOUR-TOKEN' -X PUT -d '\n{\n    \"methods\":[\n        \"GET\"\n    ],\n    \"uri\":\"/test/index.html\",\n    \"plugins\":{\n        \"proxy-rewrite\":{\n            \"headers\":{\n                \"set\":{\n                    \"accept-encoding\":\"gzip;q=0,deflate,sdch\"\n                }\n            }\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:80\":1\n        }\n    }\n}'\n```\n\n## How does APISIX configure ETCD with authentication?\n\nSuppose you have an ETCD cluster that enables the auth. To access this cluster, you need to configure the correct username and password for Apache APISIX in `conf/config.yaml`:\n\n```yaml\ndeployment:\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    user: etcd_user             # username for etcd\n    password: etcd_password     # password for etcd\n```\n\nFor other ETCD configurations, such as expiration times, retries, and so on, you can refer to the `etcd` section in the sample configuration `conf/config.yaml.example` file.\n\n## What is the difference between SSLs, `tls.client_cert` in upstream configurations, and `ssl_trusted_certificate` in `config.yaml`?\n\nThe `ssls` is managed through the `/apisix/admin/ssls` API. It's used for managing TLS certificates. These certificates may be used during TLS handshake (between Apache APISIX and its clients). Apache APISIX uses Server Name Indication (SNI) to differentiate between certificates of different domains.\n\nThe `tls.client_cert`, `tls.client_key`, and `tls.client_cert_id` in upstream are used for mTLS communication with the upstream.\n\nThe `ssl_trusted_certificate` in `config.yaml` configures a trusted CA certificate. It is used for verifying some certificates signed by private authorities within APISIX, to avoid APISIX rejects the certificate. Note that it is not used to trust the certificates of APISIX upstream, because APISIX does not verify the legality of the upstream certificates. Therefore, even if the upstream uses an invalid TLS certificate, it can still be accessed without configuring a root certificate.\n\n## Where can I find more answers?\n\nYou can find more answers on:\n\n- [Apache APISIX Slack Channel](/docs/general/join/#join-the-slack-channel)\n- [Ask questions on APISIX mailing list](/docs/general/join/#subscribe-to-the-mailing-list)\n- [GitHub Issues](https://github.com/apache/apisix/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and [GitHub Discussions](https://github.com/apache/apisix/discussions)\n"
  },
  {
    "path": "docs/en/latest/admin-api.md",
    "content": "---\ntitle: Admin API\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Admin API\n  - Route\n  - Plugin\n  - Upstream\ndescription: This article introduces the functions supported by the Apache APISIX Admin API, which you can use to get, create, update, and delete resources.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe Admin API lets users control their deployed Apache APISIX instance. The [architecture design](./architecture-design/apisix.md) gives an idea about how everything fits together.\n\n## Configuration\n\nWhen APISIX is started, the Admin API will listen on port `9180` by default and take the API prefixed with `/apisix/admin`.\n\nTherefore, to avoid conflicts between your designed API and `/apisix/admin`, you can modify the configuration file [`/conf/config.yaml`](https://github.com/apache/apisix/blob/master/conf/config.yaml) to modify the default listening port.\n\nAPISIX supports setting the IP access allowlist of Admin API to prevent APISIX from being illegally accessed and attacked. You can configure the IP addresses to allow access in the `deployment.admin.allow_admin` option in the `./conf/config.yaml` file.\n\nThe `X-API-KEY` shown below refers to the `deployment.admin.admin_key.key` in the `./conf/config.yaml` file, which is the access token for the Admin API.\n\n:::tip\n\nFor security reasons, please modify the default `admin_key`, and check the `allow_admin` IP access list.\n\n:::\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n    admin:\n        admin_key:\n        - name: admin\n            key: edd1c9f034335f136f87ad84b625c8f1  # using fixed API token has security risk, please update it when you deploy to production environment\n            role: admin\n        allow_admin:                    # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow\n            - 127.0.0.0/24\n        admin_listen:\n            ip: 0.0.0.0                 # Specific IP, if not set, the default value is `0.0.0.0`.\n            port: 9180                  # Specific port, which must be different from node_listen's port.\n```\n\n### Using environment variables\n\nTo configure via environment variables, you can use the `${{VAR}}` syntax. For instance:\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n    - name: admin\n      key: ${{ADMIN_KEY}}\n      role: admin\n    allow_admin:\n    - 127.0.0.0/24\n    admin_listen:\n      ip: 0.0.0.0\n      port: 9180\n```\n\nAnd then run `export ADMIN_KEY=$your_admin_key` before running `make init`.\n\nIf the configured environment variable can't be found, an error will be thrown.\n\nIf you want to use a default value when the environment variable is not set, use `${{VAR:=default_value}}` instead. For instance:\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n    - name: admin\n      key: ${{ADMIN_KEY:=edd1c9f034335f136f87ad84b625c8f1}}\n      role: admin\n    allow_admin:\n    - 127.0.0.0/24\n    admin_listen:\n      ip: 0.0.0.0\n      port: 9180\n```\n\nThis will find the environment variable `ADMIN_KEY` first, and if it does not exist, it will use `edd1c9f034335f136f87ad84b625c8f1` as the default value.\n\nYou can also specify environment variables in yaml keys. This is specifically useful in the `standalone` [mode](./deployment-modes.md#standalone) where you can specify the upstream nodes as follows:\n\n```yaml title=\"./conf/apisix.yaml\"\nroutes:\n  -\n    uri: \"/test\"\n    upstream:\n      nodes:\n        \"${{HOST_IP}}:${{PORT}}\": 1\n      type: roundrobin\n#END\n```\n\n### Force Delete\n\nBy default, the Admin API checks for references between resources and will refuse to delete resources in use.\n\nYou can make a force deletion by adding the request argument `force=true` to the delete request, for example:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```bash\n$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '{\n    \"nodes\": {\n        \"127.0.0.1:8080\": 1\n    },\n    \"type\": \"roundrobin\"\n}'\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '{\n    \"uri\": \"/*\",\n    \"upstream_id\": 1\n}'\n{\"value\":{\"priority\":0,\"upstream_id\":1,\"uri\":\"/*\",\"create_time\":1689038794,\"id\":\"1\",\"status\":1,\"update_time\":1689038916},\"key\":\"/apisix/routes/1\"}\n\n$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H \"X-API-KEY: $admin_key\" -X DELETE\n{\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n$ curl \"http://127.0.0.1:9180/apisix/admin/upstreams/1?force=anyvalue\" -H \"X-API-KEY: $admin_key\" -X DELETE\n{\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n$ curl \"http://127.0.0.1:9180/apisix/admin/upstreams/1?force=true\" -H \"X-API-KEY: $admin_key\" -X DELETE\n{\"deleted\":\"1\",\"key\":\"/apisix/upstreams/1\"}\n```\n\n## V3 new feature\n\nThe Admin API has made some breaking changes in V3 version, as well as supporting additional features.\n\n### Support new response body format\n\n1. Remove `action` field in response body;\n2. Adjust the response body structure when fetching the list of resources, the new response body structure like:\n\nReturn single resource:\n\n```json\n{\n  \"modifiedIndex\": 2685183,\n  \"value\": {\n    \"id\": \"1\",\n    ...\n  },\n  \"key\": \"/apisix/routes/1\",\n  \"createdIndex\": 2684956\n}\n```\n\nReturn multiple resources:\n\n```json\n{\n  \"list\": [\n    {\n      \"modifiedIndex\": 2685183,\n      \"value\": {\n        \"id\": \"1\",\n        ...\n      },\n      \"key\": \"/apisix/routes/1\",\n      \"createdIndex\": 2684956\n    },\n    {\n      \"modifiedIndex\": 2685163,\n      \"value\": {\n        \"id\": \"2\",\n        ...\n      },\n      \"key\": \"/apisix/routes/2\",\n      \"createdIndex\": 2685163\n    }\n  ],\n  \"total\": 2\n}\n```\n\n### Support paging query\n\nPaging query is supported when getting the resource list, paging parameters include:\n\n| parameter | Default | Valid range | Description                   |\n| --------- | ------  | ----------- | ----------------------------- |\n| page      | 1       | [1, ...]    | Number of pages.              |\n| page_size |         | [10, 500]   | Number of resources per page. |\n\nThe example is as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes?page=1&page_size=10\" \\\n-H \"X-API-KEY: $admin_key\" -X GET\n```\n\n```json\n{\n  \"total\": 1,\n  \"list\": [\n    {\n      ...\n    }\n  ]\n}\n```\n\nResources that support paging queries:\n\n- Consumer\n- Consumer Group\n- Global Rules\n- Plugin Config\n- Proto\n- Route\n- Service\n- SSL\n- Stream Route\n- Upstream\n- Secret\n\n### Support filtering query\n\nWhen getting a list of resources, it supports filtering resources based on `name`, `label`, `uri`.\n\n| parameter | parameter                                                    |\n| --------- | ------------------------------------------------------------ |\n| name      | Query resource by their `name`, which will not appear in the query results if the resource itself does not have `name`. |\n| label     | Query resource by their `label`, which will not appear in the query results if the resource itself does not have `label`. |\n| uri       | Supported on Route resources only. If the `uri` of a Route is equal to the uri of the query or if the `uris` contains the uri of the query, the Route resource appears in the query results. |\n\n:::tip\n\nWhen multiple filter parameters are enabled, use the intersection of the query results for different filter parameters.\n\n:::\n\nThe following example will return a list of routes, and all routes in the list satisfy: the `name` of the route contains the string \"test\", the `uri` contains the string \"foo\", and there is no restriction on the `label` of the route, since the label of the query is the empty string.\n\n```shell\ncurl 'http://127.0.0.1:9180/apisix/admin/routes?name=test&uri=foo&label=' \\\n-H \"X-API-KEY: $admin_key\" -X GET\n```\n\n```json\n{\n  \"total\": 1,\n  \"list\": [\n    {\n      ...\n    }\n  ]\n}\n```\n\n### Support reference filtering query\n\n:::note\n\nThis feature was introduced in APISIX 3.13.0.\n\nAPISIX supports querying routes and stream routes by `service_id` and `upstream_id`. Other resources or fields are not currently supported.\n\n:::\n\nWhen getting a list of resources, it supports a `filter` for filtering resources by filters.\n\nIt is encoded in the following manner.\n\n```text\nfilter=escape_uri(key1=value1&key2=value2)\n```\n\nThe following example filters routes using `service_id`. Applying multiple filters simultaneously will return results that match all filter conditions.\n\n```shell\ncurl 'http://127.0.0.1:9180/apisix/admin/routes?filter=service_id%3D1' \\\n-H \"X-API-KEY: $admin_key\" -X GET\n```\n\n```json\n{\n  \"total\": 1,\n  \"list\": [\n    {\n      ...\n    }\n  ]\n}\n```\n\n## Route\n\n[Routes](./terminology/route.md) match the client's request based on defined rules, loads and executes the corresponding [plugins](#plugin), and forwards the request to the specified [Upstream](#upstream).\n\n### Route API\n\nRoute resource request address: /apisix/admin/routes/{id}?ttl=0\n\n### Quick Note on ID Syntax\n\nID's as a text string must be of a length between 1 and 64 characters and they should only contain uppercase, lowercase, numbers and no special characters apart from dashes ( - ), periods ( . ) and underscores ( _ ). For integer values they simply must have a minimum character count of 1.\n\n### Request Methods\n\n| Method | Request URI                      | Request Body | Description                                                                                                                   |\n| ------ | -------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/routes             | NULL         | Fetches a list of all configured Routes.                                                                                 |\n| GET    | /apisix/admin/routes/{id}        | NULL         | Fetches specified Route by id.                                                                                                |\n| PUT    | /apisix/admin/routes/{id}        | {...}        | Creates a Route with the specified id.                                                                                            |\n| POST   | /apisix/admin/routes             | {...}        | Creates a Route and assigns a random id.                                                                                            |\n| DELETE | /apisix/admin/routes/{id}        | NULL         | Removes the Route with the specified id.                                                                                      |\n| PATCH | /apisix/admin/routes/{id}         | {...} | Standard PATCH, which modifies the specified attributes of the Route, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. Note that if an attribute is an array, it will be completely replaced. |\n| PATCH | /apisix/admin/routes/{id}/{path}  | {...} | Subpath PATCH, which specifies the Route attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### URI Request Parameters\n\n| parameter | Required | Type      | Description                                         | Example |\n| --------- | -------- | --------- | --------------------------------------------------- | ------- |\n| ttl       | False    | Auxiliary | Request expires after the specified target seconds. | ttl=1   |\n\n### Request Body Parameters\n\n| Parameter        | Required                                 | Type        | Description                                                                                                                                                                                                                                                                                    | Example                                              |\n| ---------------- | ---------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| name             | False                                    | Auxiliary   | Identifier for the Route.                                                                                                                                                                                                                                                                      | route-xxxx                                           |\n| desc             | False                                    | Auxiliary   | Description of usage scenarios.                                                                                                                                                                                                                                                                | route xxxx                                           |\n| uri              | True, can't be used with `uris`          | Match Rules | Matches the uri. For more advanced matching see [Router](./terminology/router.md).                                                                                                                                                                                                     | \"/hello\"                                             |\n| uris             | True, can't be used with `uri`           | Match Rules | Matches with any one of the multiple `uri`s specified in the form of a non-empty list.                                                                                                                                                                                                         | [\"/hello\", \"/word\"]                                  |\n| host             | False, can't be used with `hosts`        | Match Rules | Matches with domain names such as `foo.com` or PAN domain names like `*.foo.com`.                                                                                                                                                                                                              | \"foo.com\"                                            |\n| hosts            | False, can't be used with `host`         | Match Rules | Matches with any one of the multiple `host`s specified in the form of a non-empty list.                                                                                                                                                                                                        | [\"foo.com\", \"*.bar.com\"]                             |\n| remote_addr      | False, can't be used with `remote_addrs` | Match Rules | Matches with the specified IP address in standard IPv4 format (`192.168.1.101`), CIDR format (`192.168.1.0/24`), or in IPv6 format (`::1`, `fe80::1`, `fe80::1/64`).                                                                                                                           | \"192.168.1.0/24\"                                     |\n| remote_addrs     | False, can't be used with `remote_addr`  | Match Rules | Matches with any one of the multiple `remote_addr`s specified in the form of a non-empty list.                                                                                                                                                                                                 | [\"127.0.0.1\", \"192.0.0.0/8\", \"::1\"]                  |\n| methods          | False                                    | Match Rules | Matches with the specified methods. Matches all methods if empty or unspecified.                                                                                                                                                                                                               | [\"GET\", \"POST\"]                                      |\n| priority         | False                                    | Match Rules | If different Routes matches to the same `uri`, then the Route is matched based on its `priority`. A higher value corresponds to higher priority. It is set to `0` by default.                                                                                                                  | priority = 10                                        |\n| vars             | False                                    | Match Rules | Matches based on the specified variables consistent with variables in Nginx. Takes the form `[[var, operator, val], [var, operator, val], ...]]`. Note that this is case sensitive when matching a cookie name. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more details. | [[\"arg_name\", \"==\", \"json\"], [\"arg_age\", \">\", 18]]   |\n| filter_func      | False                                    | Match Rules | Matches using a user-defined function in Lua. Used in scenarios where `vars` is not sufficient. Functions accept an argument `vars` which provides access to built-in variables (including Nginx variables).                                                                                        | function(vars) return tonumber(vars.arg_userid) % 4 > 2; end |\n| plugins          | False                                    | Plugin      | Plugins that are executed during the request/response cycle. See [Plugin](terminology/plugin.md) for more.                                                                                                                                                                             |                                                      |\n| script           | False                                    | Script      | Used for writing arbitrary Lua code or directly calling existing plugins to be executed. See [Script](terminology/script.md) for more.                                                                                                                                                 |                                                      |\n| upstream         | False                                    | Upstream    | Configuration of the [Upstream](./terminology/upstream.md).                                                                                                                                                                                                                            |                                                      |\n| upstream_id      | False                                    | Upstream    | Id of the [Upstream](terminology/upstream.md) service.                                                                                                                                                                                                                                 |                                                      |\n| service_id       | False                                    | Service     | Configuration of the bound [Service](terminology/service.md).                                                                                                                                                                                                                          |                                                      |\n| plugin_config_id | False, can't be used with `script`       | Plugin      | [Plugin config](terminology/plugin-config.md) bound to the Route.                                                                                                                                                                                                                      |                                                      |\n| labels           | False                                    | Match Rules | Attributes of the Route specified as key-value pairs.                                                                                                                                                                                                                                          | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"}     |\n| timeout          | False                                    | Auxiliary   | Sets the timeout (in seconds) for connecting to, and sending and receiving messages between the Upstream and the Route. This will overwrite the `timeout` value configured in your [Upstream](#upstream).                                                                                                   | {\"connect\": 3, \"send\": 3, \"read\": 3}                 |\n| enable_websocket | False                                    | Auxiliary   | Enables a websocket. Set to `false` by default.                                                                                                                                                                                                                                                |                                                      |\n| status           | False                                    | Auxiliary   | Enables the current Route. Set to `1` (enabled) by default.                                                                                                                                                                                                                                    | `1` to enable, `0` to disable                        |\n\nExample configuration:\n\n```shell\n{\n    \"id\": \"1\",                            # id, unnecessary.\n    \"uris\": [\"/a\",\"/b\"],                  # A set of uri.\n    \"methods\": [\"GET\",\"POST\"],            # Can fill multiple methods\n    \"hosts\": [\"a.com\",\"b.com\"],           # A set of host.\n    \"plugins\": {},                        # Bound plugin\n    \"priority\": 0,                        # If different routes contain the same `uri`, determine which route is matched first based on the attribute` priority`, the default value is 0.\n    \"name\": \"route-xxx\",\n    \"desc\": \"hello world\",\n    \"remote_addrs\": [\"127.0.0.1\"],        # A set of Client IP.\n    \"vars\": [[\"http_user\", \"==\", \"ios\"]], # A list of one or more `[var, operator, val]` elements\n    \"upstream_id\": \"1\",                   # upstream id, recommended\n    \"upstream\": {},                       # upstream, not recommended\n    \"timeout\": {                          # Set the upstream timeout for connecting, sending and receiving messages of the route.\n        \"connect\": 3,\n        \"send\": 3,\n        \"read\": 3\n    },\n    \"filter_func\": \"\"                     # User-defined filtering function\n}\n```\n\n### Example API usage\n\n- Create a route\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uri\": \"/index.html\",\n        \"hosts\": [\"foo.com\", \"*.bar.com\"],\n        \"remote_addrs\": [\"127.0.0.0/8\"],\n        \"methods\": [\"PUT\", \"GET\"],\n        \"enable_websocket\": true,\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 201 Created\n    Date: Sat, 31 Aug 2019 01:17:15 GMT\n    ...\n    ```\n\n- Create a route expires after 60 seconds, then it's deleted automatically\n\n    ```shell\n    curl 'http://127.0.0.1:9180/apisix/admin/routes/2?ttl=60' \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uri\": \"/aa/index.html\",\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 201 Created\n    Date: Sat, 31 Aug 2019 01:17:15 GMT\n    ...\n    ```\n\n- Add an upstream node to the Route\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 1\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 1\n    }\n    ```\n\n- Update the weight of an upstream node to the Route\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 10\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- Delete an upstream node for the Route\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": null\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- Replace methods of the Route  --  array\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '{\n        \"methods\": [\"GET\", \"POST\"]\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, methods will not retain the original data, and the entire update is:\n\n    ```shell\n    [\"GET\", \"POST\"]\n    ```\n\n- Replace upstream nodes of the Route -- sub path\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1/upstream/nodes \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"127.0.0.1:1982\": 1\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, nodes will not retain the original data, and the entire update is:\n\n    ```shell\n    {\n        \"127.0.0.1:1982\": 1\n    }\n    ```\n\n- Replace methods of the Route -- sub path\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1/methods \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d'[\"POST\", \"DELETE\", \" PATCH\"]'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, methods will not retain the original data, and the entire update is:\n\n    ```shell\n    [\"POST\", \"DELETE\", \"PATCH\"]\n    ```\n\n- Disable route\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"status\": 0\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, status nodes will be updated to:\n\n    ```shell\n    {\n        \"status\": 0\n    }\n    ```\n\n- Enable route\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"status\": 1\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, status nodes will be updated to:\n\n    ```shell\n    {\n        \"status\": 1\n    }\n    ```\n\n### Response Parameters\n\nCurrently, the response is returned from etcd.\n\n## Service\n\nA Service is an abstraction of an API (which can also be understood as a set of Route abstractions). It usually corresponds to an upstream service abstraction.\n\nThe relationship between Routes and a Service is usually N:1.\n\n### Service API\n\nService resource request address: /apisix/admin/services/{id}\n\n### Request Methods\n\n| Method | Request URI                        | Request Body | Description                                                                                                                     |\n| ------ | ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/services             | NULL         | Fetches a list of available Services.                                                                                           |\n| GET    | /apisix/admin/services/{id}        | NULL         | Fetches specified Service by id.                                                                                                |\n| PUT    | /apisix/admin/services/{id}        | {...}        | Creates a Service with the specified id.                                                                                            |\n| POST   | /apisix/admin/services             | {...}        | Creates a Service and assigns a random id.                                                                                            |\n| DELETE | /apisix/admin/services/{id}        | NULL         | Removes the Service with the specified id.                                                                                      |\n| PATCH | /apisix/admin/services/{id}        | {...} | Standard PATCH, which modifies the specified attributes of the Service, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. Note that if an attribute is an array, it will be completely replaced. |\n| PATCH | /apisix/admin/services/{id}/{path} | {...} | Subpath PATCH, which specifies the Service attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### Request Body Parameters\n\n| Parameter        | Required | Type        | Description                                                                                                        | Example                                          |\n| ---------------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| plugins          | False    | Plugin      | Plugins that are executed during the request/response cycle. See [Plugin](terminology/plugin.md) for more. |                                                  |\n| upstream         | False    | Upstream    | Configuration of the [Upstream](./terminology/upstream.md).                                                |                                                  |\n| upstream_id      | False    | Upstream    | Id of the [Upstream](terminology/upstream.md) service.                                                     |                                                  |\n| name             | False    | Auxiliary   | Identifier for the Service.                                                                                        | service-xxxx                                     |\n| desc             | False    | Auxiliary   | Description of usage scenarios.                                                                                    | service xxxx                                     |\n| labels           | False    | Match Rules | Attributes of the Service specified as key-value pairs.                                                            | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n| enable_websocket | False    | Auxiliary   | Enables a websocket. Set to `false` by default.                                                                    |                                                  |\n| hosts            | False    | Match Rules | Matches with any one of the multiple `host`s specified in the form of a non-empty list.                            | [\"foo.com\", \"*.bar.com\"]                         |\n\nExample configuration:\n\n```shell\n{\n    \"id\": \"1\",                # id\n    \"plugins\": {},            # Bound plugin\n    \"upstream_id\": \"1\",       # upstream id, recommended\n    \"upstream\": {},           # upstream, not recommended\n    \"name\": \"service-test\",\n    \"desc\": \"hello world\",\n    \"enable_websocket\": true,\n    \"hosts\": [\"foo.com\"]\n}\n```\n\n### Example API usage\n\n- Create a service\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201  \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        },\n        \"enable_websocket\": true,\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 201 Created\n    ...\n    ```\n\n- Add an upstream node to the Service\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 1\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 1\n    }\n    ```\n\n- Update the weight of an upstream node to the Service\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201 \\\n    -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 10\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- Delete an upstream node for the Service\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201 \\\n    -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": null\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- Replace upstream nodes of the Service\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201/upstream/nodes \\\n    -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d '\n    {\n        \"127.0.0.1:1982\": 1\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, upstream nodes will not retain the original data, and the entire update is:\n\n    ```shell\n    {\n        \"127.0.0.1:1982\": 1\n    }\n    ```\n\n### Response Parameters\n\nCurrently, the response is returned from etcd.\n\n## Consumer\n\nConsumers are users of services and can only be used in conjunction with a user authentication system. A Consumer is identified by a `username` property. So, for creating a new Consumer, only the HTTP `PUT` method is supported.\n\n### Consumer API\n\nConsumer resource request address: /apisix/admin/consumers/{username}\n\n### Request Methods\n\n| Method | Request URI                        | Request Body | Description                                       |\n| ------ | ---------------------------------- | ------------ | ------------------------------------------------- |\n| GET    | /apisix/admin/consumers            | NULL         | Fetches a list of all Consumers.                  |\n| GET    | /apisix/admin/consumers/{username} | NULL         | Fetches specified Consumer by username.           |\n| PUT    | /apisix/admin/consumers            | {...}        | Create new Consumer.                              |\n| DELETE | /apisix/admin/consumers/{username} | NULL         | Removes the Consumer with the specified username. |\n\n### Request Body Parameters\n\n| Parameter   | Required | Type        | Description                                                                                                        | Example                                          |\n| ----------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| username    | True     | Name        | Name of the Consumer.                                                                                              |                                                  |\n| group_id    | False    | Name        | Group of the Consumer.                                                                                              |                                                  |\n| plugins     | False    | Plugin      | Plugins that are executed during the request/response cycle. See [Plugin](terminology/plugin.md) for more. |                                                  |\n| desc        | False    | Auxiliary   | Description of usage scenarios.                                                                                    | customer xxxx                                    |\n| labels      | False    | Match Rules | Attributes of the Consumer specified as key-value pairs.                                                           | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n\nExample Configuration:\n\n```shell\n{\n    \"plugins\": {},          # Bound plugin\n    \"username\": \"name\",     # Consumer name\n    \"desc\": \"hello world\"   # Consumer desc\n}\n```\n\nWhen bound to a Route or Service, the Authentication Plugin infers the Consumer from the request and does not require any parameters. Whereas, when it is bound to a Consumer, username, password and other information needs to be provided.\n\n### Example API usage\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        },\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    }\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\nDate: Thu, 26 Dec 2019 08:17:49 GMT\n...\n\n{\"node\":{\"value\":{\"username\":\"jack\",\"plugins\":{\"key-auth\":{\"key\":\"auth-one\"},\"limit-count\":{\"time_window\":60,\"count\":2,\"rejected_code\":503,\"key\":\"remote_addr\",\"policy\":\"local\"}}},\"createdIndex\":64,\"key\":\"\\/apisix\\/consumers\\/jack\",\"modifiedIndex\":64},\"prevNode\":{\"value\":\"{\\\"username\\\":\\\"jack\\\",\\\"plugins\\\":{\\\"key-auth\\\":{\\\"key\\\":\\\"auth-one\\\"},\\\"limit-count\\\":{\\\"time_window\\\":60,\\\"count\\\":2,\\\"rejected_code\\\":503,\\\"key\\\":\\\"remote_addr\\\",\\\"policy\\\":\\\"local\\\"}}}\",\"createdIndex\":63,\"key\":\"\\/apisix\\/consumers\\/jack\",\"modifiedIndex\":63}}\n```\n\nSince `v2.2`, we can bind multiple authentication plugins to the same consumer.\n\n### Response Parameters\n\nCurrently, the response is returned from etcd.\n\n## Credential\n\nCredential is used to hold the authentication credentials for the Consumer.\nCredentials are used when multiple credentials need to be configured for a Consumer.\n\n### Credential API\n\nCredential resource request address：/apisix/admin/consumers/{username}/credentials/{credential_id}\n\n### Request Methods\n\n| Method | Request URI                        | Request Body | Description                                    |\n| ------ |----------------------------------------------------------------|--------------|------------------------------------------------|\n| GET    | /apisix/admin/consumers/{username}/credentials                 | NUll         | Fetches list of all credentials of the Consumer |\n| GET    | /apisix/admin/consumers/{username}/credentials/{credential_id} | NUll         | Fetches the Credential by `credential_id`      |\n| PUT    | /apisix/admin/consumers/{username}/credentials/{credential_id} | {...}        | Create or update a Creddential                 |\n| DELETE | /apisix/admin/consumers/{username}/credentials/{credential_id} | NUll         | Delete the Credential                          |\n\n### Request Body Parameters\n\n| Parameter   | Required | Type        | Description                                                | Example                                         |\n| ----------- |-----| ------- |------------------------------------------------------------|-------------------------------------------------|\n| plugins     | False    | Plugin      | Auth plugins configuration.                                |                                                 |\n| name        | False    | Auxiliary   | Identifier for the Credential.                             | credential_primary                              |\n| desc        | False    | Auxiliary   | Description of usage scenarios.                            | credential xxxx                                 |\n| labels      | False    | Match Rules | Attributes of the Credential specified as key-value pairs. | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n\nExample Configuration:\n\n```shell\n{\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"auth-one\"\n      }\n    },\n    \"desc\": \"hello world\"\n}\n```\n\n### Example API usage\n\nPrerequisite: Consumer `jack` has been created.\n\nCreate the `key-auth` Credential for consumer `jack`:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/auth-one  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    }\n}'\n```\n\n```\nHTTP/1.1 200 OK\nDate: Thu, 26 Dec 2019 08:17:49 GMT\n...\n\n{\"key\":\"\\/apisix\\/consumers\\/jack\\/credentials\\/auth-one\",\"value\":{\"update_time\":1666260780,\"plugins\":{\"key-auth\":{\"key\":\"auth-one\"}},\"create_time\":1666260780}}\n```\n\n## Upstream\n\nUpstream is a virtual host abstraction that performs load balancing on a given set of service nodes according to the configured rules.\n\nAn Upstream configuration can be directly bound to a Route or a Service, but the configuration in Route has a higher priority. This behavior is consistent with priority followed by the Plugin object.\n\n### Upstream API\n\nUpstream resource request address: /apisix/admin/upstreams/{id}\n\nFor notes on ID syntax please refer to: [ID Syntax](#quick-note-on-id-syntax)\n\n### Request Methods\n\n| Method | Request URI                         | Request Body | Description                                                                                                                      |\n| ------ | ----------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/upstreams             | NULL         | Fetch a list of all configured Upstreams.                                                                                        |\n| GET    | /apisix/admin/upstreams/{id}        | NULL         | Fetches specified Upstream by id.                                                                                                |\n| PUT    | /apisix/admin/upstreams/{id}        | {...}        | Creates an Upstream with the specified id.                                                                                           |\n| POST   | /apisix/admin/upstreams             | {...}        | Creates an Upstream and assigns a random id.                                                                                           |\n| DELETE | /apisix/admin/upstreams/{id}        | NULL         | Removes the Upstream with the specified id.                                                                                      |\n| PATCH | /apisix/admin/upstreams/{id}         | {...} | Standard PATCH, which modifies the specified attributes of the existing Upstream, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. Note that if an attribute is an array, it will be completely replaced. |\n| PATCH | /apisix/admin/upstreams/{id}/{path}  | {...} | Subpath PATCH, which specifies the Upstream attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### Request Body Parameters\n\nIn addition to the equalization algorithm selections, Upstream also supports passive health check and retry for the upstream. See the table below for more details:\n\n| Parameter                   | Required                                                         | Type                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Example                                                                                                                                    |\n|-----------------------------|------------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|\n| type                        | False                                                            | Enumeration                   | Load balancing algorithm to be used, and the default value is `roundrobin`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                            |\n| nodes                       | True, can't be used with `service_name`                          | Node                          | IP addresses (with optional ports) of the Upstream nodes represented as a hash table or an array. In the hash table, the key is the IP address and the value is the weight of the node for the load balancing algorithm. For hash table case, if the key is IPv6 address with port, then the IPv6 address must be quoted with square brackets. In the array, each item is a hash table with keys `host`, `weight`, and the optional `port` and `priority` (defaults to `0`). Nodes with lower priority are used only when all nodes with a higher priority are tried and are unavailable. Empty nodes are treated as placeholders and clients trying to access this Upstream will receive a 502 response.                                                   | `192.168.1.100:80`, `[::1]:80`                                                                                                             |\n| service_name                | True, can't be used with `nodes`                                 | String                        | Service name used for [service discovery](discovery.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `a-bootiful-client`                                                                                                                        |\n| discovery_type              | True, if `service_name` is used                                  | String                        | The type of service [discovery](discovery.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `eureka`                                                                                                                                   |\n| hash_on                     | False                                                            | Auxiliary                     | Only valid if the `type` is `chash`. Supports Nginx variables (`vars`), custom headers (`header`), `cookie` and `consumer`. Defaults to `vars`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |                                                                                                                                            |\n| key                         | False                                                            | Match Rules                   | Only valid if the `type` is `chash`. Finds the corresponding node `id` according to `hash_on` and `key` values. When `hash_on` is set to `vars`, `key` is a required parameter and it supports [Nginx variables](http://nginx.org/en/docs/varindex.html). When `hash_on` is set as `header`, `key` is a required parameter, and `header name` can be customized. When `hash_on` is set to `cookie`, `key` is also a required parameter, and `cookie name` can be customized. When `hash_on` is set to `consumer`, `key` need not be set and the `key` used by the hash algorithm would be the authenticated `consumer_name`. | `uri`, `server_name`, `server_addr`, `request_uri`, `remote_port`, `remote_addr`, `query_string`, `host`, `hostname`, `arg_***`, `arg_***` |\n| checks                      | False                                                            | Health Checker                | Configures the parameters for the [health check](./tutorials/health-check.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |                                                                                                                                            |\n| retries                     | False                                                            | Integer                       | Sets the number of retries while passing the request to Upstream using the underlying Nginx mechanism. Set according to the number of available backend nodes by default. Setting this to `0` disables retry.                                                                                                                                                                                                                                                                                                                                                                                                                |                                                                                                                                            |\n| retry_timeout               | False                                                            | Integer                       | Timeout to continue with retries. Setting this to `0` disables the retry timeout.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |                                                                                                                                            |\n| timeout                     | False                                                            | Timeout                       | Sets the timeout (in seconds) for connecting to, and sending and receiving messages to and from the Upstream.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{\"connect\": 0.5,\"send\": 0.5,\"read\": 0.5}`                                                                                                 |\n| name                        | False                                                            | Auxiliary                     | Identifier for the Upstream.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |                                                                                                                                            |\n| desc                        | False                                                            | Auxiliary                     | Description of usage scenarios.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |                                                                                                                                            |\n| pass_host                   | False                                                            | Enumeration                   | Configures the `host` when the request is forwarded to the upstream. Can be one of `pass`, `node` or `rewrite`. Defaults to `pass` if not specified. `pass`- transparently passes the client's host to the Upstream. `node`- uses the host configured in the node of the Upstream. `rewrite`- Uses the value configured in `upstream_host`.                                                                                                                                                                                                                                                                                  |                                                                                                                                            |\n| upstream_host               | False                                                            | Auxiliary                     | Specifies the host of the Upstream request. This is only valid if the `pass_host` is set to `rewrite`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |                                                                                                                                            |\n| scheme                      | False                                                            | Auxiliary                     | The scheme used when communicating with the Upstream. For an L7 proxy, this value can be one of `http`, `https`, `grpc`, `grpcs`. For an L4 proxy, this value could be one of `tcp`, `udp`, `tls`. Defaults to `http`.                                                                                                                                                                                                                                                                                                                                                                                                       |                                                                                                                                            |\n| labels                      | False                                                            | Match Rules                   | Attributes of the Upstream specified as `key-value` pairs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"}                                                                                           |\n| tls.client_cert             | False, can't be used with `tls.client_cert_id`                   | HTTPS certificate             | Sets the client certificate while connecting to a TLS Upstream.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |                                                                                                                                            |\n| tls.client_key              | False, can't be used with `tls.client_cert_id`                   | HTTPS certificate private key | Sets the client private key while connecting to a TLS Upstream.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |                                                                                                                                            |\n| tls.client_cert_id          | False, can't be used with `tls.client_cert` and `tls.client_key` | SSL                           | Set the referenced [SSL](#ssl) id.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |                                                                                                                                            |\n| tls.verify                  | False, currently only kafka upstream is supported                | Boolean                       | Turn on server certificate verification, currently only kafka upstream is supported.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |                                                                                                                                            |\n| keepalive_pool.size         | False                                                            | Auxiliary                     | Sets `keepalive` directive dynamically.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |                                                                                                                                            |\n| keepalive_pool.idle_timeout | False                                                            | Auxiliary                     | Sets `keepalive_timeout` directive dynamically.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |                                                                                                                                            |\n| keepalive_pool.requests     | False                                                            | Auxiliary                     | Sets `keepalive_requests` directive dynamically.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |                                                                                                                                            |\n\nAn Upstream can be one of the following `types`:\n\n- `roundrobin`: Round robin balancing with weights.\n- `chash`: Consistent hash.\n- `ewma`: Pick the node with minimum latency. See [EWMA Chart](https://en.wikipedia.org/wiki/EWMA_chart) for more details.\n- `least_conn`: Picks the node with the lowest value of `(active_conn + 1) / weight`. Here, an active connection is a connection being used by the request and is similar to the concept in Nginx.\n- user-defined load balancer loaded via `require(\"apisix.balancer.your_balancer\")`.\n\nThe following should be considered when setting the `hash_on` value:\n\n- When set to `vars`, a `key` is required. The value of the key can be any of the [Nginx variables](http://nginx.org/en/docs/varindex.html) without the `$` prefix.\n- When set to `header`, a `key` is required. This is equal to \"http\\_`key`\".\n- When set to `cookie`, a `key` is required. This key is equal to \"cookie\\_`key`\". The cookie name is case-sensitive.\n- When set to `consumer`, the `key` is optional and the key is set to the `consumer_name` captured from the authentication Plugin.\n- When set to `vars_combinations`, the `key` is required. The value of the key can be a combination of any of the [Nginx variables](http://nginx.org/en/docs/varindex.html) like `$request_uri$remote_addr`.\n\nThe features described below requires APISIX to be run on [APISIX-Runtime](./FAQ.md#how-do-i-build-the-apisix-runtime-environment):\n\nYou can set the `scheme` to `tls`, which means \"TLS over TCP\".\n\nTo use mTLS to communicate with Upstream, you can use the `tls.client_cert/key` in the same format as SSL's `cert` and `key` fields.\n\nOr you can reference SSL object by `tls.client_cert_id` to set SSL cert and key. The SSL object can be referenced only if the `type` field is `client`, otherwise the request will be rejected by APISIX. In addition, only `cert` and `key` will be used in the SSL object.\n\nTo allow Upstream to have a separate connection pool, use `keepalive_pool`. It can be configured by modifying its child fields.\n\nExample Configuration:\n\n```shell\n{\n    \"id\": \"1\",                  # id\n    \"retries\": 1,               # retry times\n    \"timeout\": {                # Set the timeout for connecting, sending and receiving messages, each is 15 seconds.\n        \"connect\":15,\n        \"send\":15,\n        \"read\":15\n    },\n    \"nodes\": {\"host:80\": 100},  # Upstream machine address list, the format is `Address + Port`\n                                # is the same as \"nodes\": [ {\"host\": \"host\", \"port\": 80, \"weight\": 100} ],\n    \"type\":\"roundrobin\",\n    \"checks\": {},               # Health check parameters\n    \"hash_on\": \"\",\n    \"key\": \"\",\n    \"name\": \"upstream-for-test\",\n    \"desc\": \"hello world\",\n    \"scheme\": \"http\"            # The scheme used when communicating with upstream, the default is `http`\n}\n```\n\n### Example API usage\n\n#### Create an Upstream and modify the data in `nodes`\n\n1. Create upstream\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100  \\\n    -H \"X-API-KEY: $admin_key\" -i -X PUT -d '\n    {\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\": 1\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 201 Created\n    ...\n    ```\n\n2. Add a node to the Upstream\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"nodes\": {\n            \"127.0.0.1:1981\": 1\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 1\n    }\n    ```\n\n3. Update the weight of a node to the Upstream\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100 \\\n    -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d '\n    {\n        \"nodes\": {\n            \"127.0.0.1:1981\": 10\n        }\n    }'\n    ```\n\n    ```shell\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n4. Delete a node for the Upstream\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"nodes\": {\n            \"127.0.0.1:1980\": null\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After successful execution, nodes will be updated to:\n\n    ```shell\n    {\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n5. Replace the nodes of the Upstream\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100/nodes \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"127.0.0.1:1982\": 1\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    After the execution is successful, nodes will not retain the original data, and the entire update is:\n\n    ```shell\n    {\n        \"127.0.0.1:1982\": 1\n    }\n    ```\n\n#### Proxy client request to `https` Upstream service\n\n1. Create a route and configure the upstream scheme as `https`.\n\n    ```shell\n    curl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/get\",\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"scheme\": \"https\",\n            \"nodes\": {\n                \"httpbin.org:443\": 1\n            }\n        }\n    }'\n    ```\n\n    After successful execution, the scheme when requesting to communicate with the upstream will be `https`.\n\n2. Send a request to test.\n\n    ```shell\n    curl http://127.0.0.1:9080/get\n    ```\n\n    ```shell\n    {\n    \"args\": {},\n    \"headers\": {\n        \"Accept\": \"*/*\",\n        \"Host\": \"127.0.0.1\",\n        \"User-Agent\": \"curl/7.29.0\",\n        \"X-Amzn-Trace-Id\": \"Root=1-6058324a-0e898a7f04a5e95b526bb183\",\n        \"X-Forwarded-Host\": \"127.0.0.1\"\n    },\n    \"origin\": \"127.0.0.1\",\n    \"url\": \"https://127.0.0.1/get\"\n    }\n    ```\n\nThe request is successful, meaning that the proxy Upstream `https` is valid.\n\n:::note\n\nEach node can be configured with a priority. A node with low priority will only be\nused when all the nodes with higher priority have been tried or are unavailable.\n\n:::\n\nAs the default priority is 0, nodes with negative priority can be configured as a backup.\n\nFor example:\n\n```json\n{\n  \"uri\": \"/hello\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": [\n      { \"host\": \"127.0.0.1\", \"port\": 1980, \"weight\": 2000 },\n      { \"host\": \"127.0.0.1\", \"port\": 1981, \"weight\": 1, \"priority\": -1 }\n    ],\n    \"checks\": {\n      \"active\": {\n        \"http_path\": \"/status\",\n        \"healthy\": {\n          \"interval\": 1,\n          \"successes\": 1\n        },\n        \"unhealthy\": {\n          \"interval\": 1,\n          \"http_failures\": 1\n        }\n      }\n    }\n  }\n}\n```\n\nNode `127.0.0.2` will be used only after `127.0.0.1` is tried or unavailable.\nIt can therefore act as a backup for the node `127.0.0.1`.\n\n### Response Parameters\n\nCurrently, the response is returned from etcd.\n\n## SSL\n\n### SSL API\n\nSSL resource request address: /apisix/admin/ssls/{id}\n\nFor notes on ID syntax please refer to: [ID Syntax](#quick-note-on-id-syntax)\n\n### Request Methods\n\n| Method | Request URI            | Request Body | Description                                     |\n| ------ | ---------------------- | ------------ | ----------------------------------------------- |\n| GET    | /apisix/admin/ssls      | NULL         | Fetches a list of all configured SSL resources. |\n| GET    | /apisix/admin/ssls/{id} | NULL         | Fetch specified resource by id.                 |\n| PUT    | /apisix/admin/ssls/{id} | {...}        | Creates a resource with the specified id.           |\n| POST   | /apisix/admin/ssls      | {...}        | Creates a resource and assigns a random id.           |\n| DELETE | /apisix/admin/ssls/{id} | NULL         | Removes the resource with the specified id.     |\n\n### Request Body Parameters\n\n| Parameter    | Required | Type                     | Description                                                                                                    | Example                                          |\n| ------------ | -------- | ------------------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |\n| cert         | True     | Certificate              | HTTPS certificate. This field supports saving the value in Secret Manager using the [APISIX Secret](./terminology/secret.md) resource.                                                                                             |                                                  |\n| key          | True     | Private key              | HTTPS private key. This field supports saving the value in Secret Manager using the [APISIX Secret](./terminology/secret.md) resource.                                                                                             |                                                  |\n| certs        | False    | An array of certificates | Used for configuring multiple certificates for the same domain excluding the one provided in the `cert` field. This field supports saving the value in Secret Manager using the [APISIX Secret](./terminology/secret.md) resource.  |                                                  |\n| keys         | False    | An array of private keys | Private keys to pair with the `certs`. This field supports saving the value in Secret Manager using the [APISIX Secret](./terminology/secret.md) resource.                                                                   |                                                  |\n| client.ca    | False    | Certificate              | Sets the CA certificate that verifies the client. Requires OpenResty 1.19+.                                    |                                                  |\n| client.depth | False    | Certificate              | Sets the verification depth in client certificate chains. Defaults to 1. Requires OpenResty 1.19+.             |                                                  |\n| client.skip_mtls_uri_regex | False    | An array of regular expressions, in PCRE format              | Used to match URI, if matched, this request bypasses the client certificate checking, i.e. skip the MTLS.             | [\"/hello[0-9]+\", \"/foobar\"]                                                |\n| snis         | True, only if `type` is `server`     | Match Rules              | A non-empty array of HTTPS SNI                                                                                 |                                                  |\n| desc         | False    | Auxiliary                | Description of usage scenarios. | certs for production env |\n| labels       | False    | Match Rules              | Attributes of the resource specified as key-value pairs.                                                       | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n| type         | False    | Auxiliary            | Identifies the type of certificate, default  `server`.                                                                             | `client` Indicates that the certificate is a client certificate, which is used when APISIX accesses the upstream; `server` Indicates that the certificate is a server-side certificate, which is used by APISIX when verifying client requests.     |\n| status       | False    | Auxiliary                | Enables the current SSL. Set to `1` (enabled) by default.                                                      | `1` to enable, `0` to disable                    |\n| ssl_protocols | False    | An array of ssl protocols               | It is used to control the SSL/TLS protocol version used between servers and clients. See [SSL Protocol](./ssl-protocol.md) for more examples.                  |                `[\"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\"]`                                  |\n\nExample Configuration:\n\n```shell\n{\n    \"id\": \"1\",           # id\n    \"cert\": \"cert\",      # Certificate\n    \"key\": \"key\",        # Private key\n    \"snis\": [\"t.com\"]    # https SNI\n}\n```\n\nSee [Certificate](./certificate.md) for more examples.\n\n## Global Rule\n\nSets Plugins which run globally. i.e these Plugins will be run before any Route/Service level Plugins.\n\n### Global Rule API\n\nGlobal Rule resource request address: /apisix/admin/global_rules/{id}\n\n### Request Methods\n\n| Method | Request URI                            | Request Body | Description                                                                                                                         |\n| ------ | -------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/global_rules             | NULL         | Fetches a list of all Global Rules.                                                                                                 |\n| GET    | /apisix/admin/global_rules/{id}        | NULL         | Fetches specified Global Rule by id.                                                                                                |\n| PUT    | /apisix/admin/global_rules/{id}        | {...}        | Creates a Global Rule with the specified id.                                                                                        |\n| DELETE | /apisix/admin/global_rules/{id}        | NULL         | Removes the Global Rule with the specified id.                                                                                      |\n| PATCH | /apisix/admin/global_rules/{id}         | {...} | Standard PATCH, which modifies the specified attributes of the existing Global Rule, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. Note that if an attribute is an array, it will be completely replaced. |\n| PATCH | /apisix/admin/global_rules/{id}/{path}  | {...} | Subpath PATCH, which specifies the Global Rule attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### Request Body Parameters\n\n| Parameter   | Required | Description                                                                                                        | Example    |\n| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------ | ---------- |\n| plugins     | True     | Plugins that are executed during the request/response cycle. See [Plugin](terminology/plugin.md) for more. |            |\n\n## Consumer group\n\nGroup of Plugins which can be reused across Consumers.\n\n### Consumer group API\n\nConsumer group resource request address: /apisix/admin/consumer_groups/{id}\n\n### Request Methods\n\n| Method | Request URI                              | Request Body | Description                                                                                                                           |\n| ------ | ---------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/consumer_groups             | NULL         | Fetches a list of all Consumer groups.                                                                                                 |\n| GET    | /apisix/admin/consumer_groups/{id}        | NULL         | Fetches specified Consumer group by id.                                                                                                |\n| PUT    | /apisix/admin/consumer_groups/{id}        | {...}        | Creates a new Consumer group with the specified id.                                                                                    |\n| DELETE | /apisix/admin/consumer_groups/{id}        | NULL         | Removes the Consumer group with the specified id.                                                                                      |\n| PATCH | /apisix/admin/consumer_groups/{id}         | {...} | Standard PATCH, which modifies the specified attributes of the existing Consumer Group, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. Note that if an attribute is an array, it will be completely replaced. |\n| PATCH | /apisix/admin/consumer_groups/{id}/{path}  | {...} | Subpath PATCH, which specifies the Consumer Group attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### Request Body Parameters\n\n| Parameter   | Required | Description                                                                                                        | Example                                          |\n| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| plugins     | True     | Plugins that are executed during the request/response cycle. See [Plugin](terminology/plugin.md) for more. |                                                  |\n| name        | False    | Identifier for the consumer group.                                                                                 | premium-tier                            |\n| desc        | False    | Description of usage scenarios.                                                                                    | customer xxxx                                    |\n| labels      | False    | Attributes of the Consumer group specified as key-value pairs.                                                      | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n\n## Plugin config\n\nGroup of Plugins which can be reused across Routes.\n\n### Plugin Config API\n\nPlugin Config resource request address: /apisix/admin/plugin_configs/{id}\n\n### Request Methods\n\n| Method | Request URI                              | Request Body | Description                                                                                                                           |\n| ------ | ---------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/plugin_configs             | NULL         | Fetches a list of all Plugin configs.                                                                                                 |\n| GET    | /apisix/admin/plugin_configs/{id}        | NULL         | Fetches specified Plugin config by id.                                                                                                |\n| PUT    | /apisix/admin/plugin_configs/{id}        | {...}        | Creates a new Plugin config with the specified id.                                                                                    |\n| DELETE | /apisix/admin/plugin_configs/{id}        | NULL         | Removes the Plugin config with the specified id.                                                                                      |\n| PATCH | /apisix/admin/plugin_configs/{id}         | {...} | Standard PATCH, which modifies the specified attributes of the existing Plugin Config, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. Note that if an attribute is an array, it will be completely replaced. |\n| PATCH | /apisix/admin/plugin_configs/{id}/{path}  | {...} | Subpath PATCH, which specifies the Plugin Config attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### Request Body Parameters\n\n| Parameter   | Required | Description                                                                                                        | Example                                          |\n| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| plugins     | True     | Plugins that are executed during the request/response cycle. See [Plugin](terminology/plugin.md) for more. |                                                  |\n| desc        | False    | Description of usage scenarios.                                                                                    | customer xxxx                                    |\n| labels      | False    | Attributes of the Plugin config specified as key-value pairs.                                                      | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n\n## Plugin Metadata\n\n### Plugin Metadata API\n\nPlugin Metadata resource request address: /apisix/admin/plugin_metadata/{plugin_name}\n\n### Request Methods\n\n| Method | Request URI                                 | Request Body | Description                                                     |\n| ------ | ------------------------------------------- | ------------ | --------------------------------------------------------------- |\n| GET    | /apisix/admin/plugin_metadata               | NULL         | Fetches a list of all Plugin metadata.                          |\n| GET    | /apisix/admin/plugin_metadata/{plugin_name} | NULL         | Fetches the metadata of the specified Plugin by `plugin_name`.  |\n| PUT    | /apisix/admin/plugin_metadata/{plugin_name} | {...}        | Creates metadata for the Plugin specified by the `plugin_name`. |\n| DELETE | /apisix/admin/plugin_metadata/{plugin_name} | NULL         | Removes metadata for the Plugin specified by the `plugin_name`. |\n\n### Request Body Parameters\n\nA JSON object defined according to the `metadata_schema` of the Plugin ({plugin_name}).\n\nExample Configuration:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/example-plugin  \\\n-H \"X-API-KEY: $admin_key\" -i -X PUT -d '\n{\n    \"skey\": \"val\",\n    \"ikey\": 1\n}'\n```\n\n```shell\nHTTP/1.1 201 Created\nDate: Thu, 26 Dec 2019 04:19:34 GMT\nContent-Type: text/plain\n```\n\n## Plugin\n\n### Plugin API\n\nPlugin resource request address: /apisix/admin/plugins/{plugin_name}\n\n### Request Methods\n\n| Method | Request URI                         | Request Body | Description                                    |\n| ------ | ----------------------------------- | ------------ | ---------------------------------------------- |\n| GET    | /apisix/admin/plugins/list          | NULL         | Fetches a list of all Plugins.                 |\n| GET    | /apisix/admin/plugins/{plugin_name} | NULL         | Fetches the specified Plugin by `plugin_name`. |\n| GET         | /apisix/admin/plugins?all=true      | NULL         | Get all properties of all plugins. |\n| GET         | /apisix/admin/plugins?all=true&subsystem=stream| NULL | Gets properties of all Stream plugins.|\n| GET    | /apisix/admin/plugins?all=true&subsystem=http | NULL | Gets properties of all HTTP plugins. |\n| PUT    | /apisix/admin/plugins/reload       | NULL         | Reloads the plugin according to the changes made in code |\n| GET    | apisix/admin/plugins/{plugin_name}?subsystem=stream | NULL | Gets properties of a specified plugin if it is supported in Stream/L4 subsystem. |\n| GET    | apisix/admin/plugins/{plugin_name}?subsystem=http   | NULL | Gets properties of a specified plugin if it is supported in HTTP/L7 subsystem. |\n\n:::caution\n\nThe interface of getting properties of all plugins via `/apisix/admin/plugins?all=true` will be deprecated soon.\n\n:::\n\n### Request Body Parameters\n\nThe Plugin ({plugin_name}) of the data structure.\n\n### Request Arguments\n\n| Name      | Description                   | Default |\n| --------- | ----------------------------- | ------- |\n| subsystem | The subsystem of the Plugins. | http    |\n\nThe plugin can be filtered on subsystem so that the ({plugin_name}) is searched in the subsystem passed through query params.\n\n### Example API usage:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugins/list\" \\\n-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'\n```\n\n```shell\n[\"zipkin\",\"request-id\",...]\n```\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugins/key-auth?subsystem=http\" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'\n```\n\n```json\n{\"$comment\":\"this is a mark for our injected plugin schema\",\"properties\":{\"header\":{\"default\":\"apikey\",\"type\":\"string\"},\"hide_credentials\":{\"default\":false,\"type\":\"boolean\"},\"_meta\":{\"properties\":{\"filter\":{\"type\":\"array\",\"description\":\"filter determines whether the plugin needs to be executed at runtime\"},\"disable\":{\"type\":\"boolean\"},\"error_response\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"object\"}]},\"priority\":{\"type\":\"integer\",\"description\":\"priority of plugins by customized order\"}},\"type\":\"object\"},\"query\":{\"default\":\"apikey\",\"type\":\"string\"}},\"type\":\"object\"}\n```\n\n:::tip\n\nYou can use the `/apisix/admin/plugins?all=true` API to get all properties of all plugins. This API will be deprecated soon.\n\n:::\n\n## Stream Route\n\nRoute used in the [Stream Proxy](./stream-proxy.md).\n\n### Stream Route API\n\nStream Route resource request address:  /apisix/admin/stream_routes/{id}\n\n### Request Methods\n\n| Method | Request URI                      | Request Body | Description                                     |\n| ------ | -------------------------------- | ------------ | ----------------------------------------------- |\n| GET    | /apisix/admin/stream_routes      | NULL         | Fetches a list of all configured Stream Routes. |\n| GET    | /apisix/admin/stream_routes/{id} | NULL         | Fetches specified Stream Route by id.           |\n| PUT    | /apisix/admin/stream_routes/{id} | {...}        | Creates a Stream Route with the specified id.       |\n| POST   | /apisix/admin/stream_routes      | {...}        | Creates a Stream Route and assigns a random id.       |\n| DELETE | /apisix/admin/stream_routes/{id} | NULL         | Removes the Stream Route with the specified id. |\n\n### Request Body Parameters\n\n| Parameter   | Required | Type     | Description                                                         | Example                       |\n| ----------- | -------- | -------- | ------------------------------------------------------------------- | ----------------------------- |\n| name        | False    | Auxiliary | Identifier for the Stream Route.                                   | postgres-proxy                |\n| desc        | False    | Auxiliary | Description of usage scenarios.                                    | proxy endpoint for postgresql |\n| labels      | False    | Match Rules | Attributes of the Proto specified as key-value pairs.    | {\"version\":\"17\",\"service\":\"user\",\"env\":\"production\"}     |\n| upstream    | False    | Upstream | Configuration of the [Upstream](./terminology/upstream.md). |                               |\n| upstream_id | False    | Upstream | Id of the [Upstream](terminology/upstream.md) service.      |                               |\n| service_id  | False    | String   | Id of the [Service](terminology/service.md) service.        |                               |\n| remote_addr | False    | IPv4, IPv4 CIDR, IPv6  | Filters Upstream forwards by matching with client IP.               | \"127.0.0.1\" or \"127.0.0.1/32\" or \"::1\" |\n| server_addr | False    | IPv4, IPv4 CIDR, IPv6  | Filters Upstream forwards by matching with APISIX Server IP.        | \"127.0.0.1\" or \"127.0.0.1/32\" or \"::1\" |\n| server_port | False    | Integer  | Filters Upstream forwards by matching with APISIX Server port.      | 9090                          |\n| sni         | False    | Host     | Server Name Indication.                                             | \"test.com\"                    |\n| protocol.name | False    | String | Name of the protocol proxyed by xRPC framework.                     | \"redis\"                    |\n| protocol.conf | False    | Configuration | Protocol-specific configuration.                             |                    |\n\nTo learn more about filtering in stream proxies, check [this](./stream-proxy.md#more-route-match-options) document.\n\n## Secret\n\nSecret means `Secrets Management`, which could use any secret manager supported, e.g. `vault`.\n\n### Secret API\n\nSecret resource request address: /apisix/admin/secrets/{secretmanager}/{id}\n\n### Request Methods\n\n| Method | Request URI                        | Request Body | Description                                       |\n| ------ | ---------------------------------- | ------------ | ------------------------------------------------- |\n| GET    | /apisix/admin/secrets            | NULL         | Fetches a list of all secrets.                  |\n| GET    | /apisix/admin/secrets/{manager}/{id} | NULL         | Fetches specified secrets by id.           |\n| PUT    | /apisix/admin/secrets/{manager}            | {...}        | Create new secrets configuration.                              |\n| DELETE | /apisix/admin/secrets/{manager}/{id} | NULL         | Removes the secrets with the specified id. |\n| PATCH | /apisix/admin/secrets/{manager}/{id}        | {...} | Standard PATCH, which modifies the specified attributes of the existing secret, while all other attributes remain unchanged. To delete an attribute, set its value to `null`. |\n| PATCH | /apisix/admin/secrets/{manager}/{id}/{path} | {...} | Subpath PATCH, which specifies the secret attribute to update via `{path}` and completely replaces that attribute’s data, while all other attributes remain unchanged. |\n\n### Request Body Parameters\n\n#### When Secret Manager is Vault\n\n| Parameter   | Required | Type        | Description                                                                                                        | Example                                          |\n| ----------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| uri    | True     | URI        | URI of the vault server.                                                                                              |                                                  |\n| prefix    | True    | string        | key prefix\n| token     | True    | string      | vault token. |                                                  |\n| namespace | False   | string       | Vault namespace, no default value | `admin` |\n\nExample Configuration:\n\n```shell\n{\n    \"uri\": \"https://localhost/vault\",\n    \"prefix\": \"/apisix/kv\",\n    \"token\": \"343effad\"\n}\n```\n\nExample API usage:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/secrets/vault/test2 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"http://xxx/get\",\n    \"prefix\" : \"apisix\",\n    \"token\" : \"apisix\"\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n\n{\"key\":\"\\/apisix\\/secrets\\/vault\\/test2\",\"value\":{\"id\":\"vault\\/test2\",\"token\":\"apisix\",\"prefix\":\"apisix\",\"update_time\":1669625828,\"create_time\":1669625828,\"uri\":\"http:\\/\\/xxx\\/get\"}}\n```\n\n#### When Secret Manager is AWS\n\n| Parameter         | Required | Type   | Description                             |\n| ----------------- | -------- | ------ | --------------------------------------- |\n| access_key_id     | True     | string | AWS Access Key ID                       |\n| secret_access_key | True     | string | AWS Secret Access Key                   |\n| session_token     | False    | string | Temporary access credential information |\n| region            | False    | string | AWS Region                              |\n| endpoint_url      | False    | URI    | AWS Secret Manager URL                  |\n\nExample Configuration:\n\n```json\n{\n    \"endpoint_url\": \"http://127.0.0.1:4566\",\n    \"region\": \"us-east-1\",\n    \"access_key_id\": \"access\",\n    \"secret_access_key\": \"secret\",\n    \"session_token\": \"token\"\n}\n```\n\nExample API usage:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/secrets/aws/test3 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"endpoint_url\": \"http://127.0.0.1:4566\",\n    \"region\": \"us-east-1\",\n    \"access_key_id\": \"access\",\n    \"secret_access_key\": \"secret\",\n    \"session_token\": \"token\"\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n\n{\"value\":{\"create_time\":1726069970,\"endpoint_url\":\"http://127.0.0.1:4566\",\"region\":\"us-east-1\",\"access_key_id\":\"access\",\"secret_access_key\":\"secret\",\"id\":\"aws/test3\",\"update_time\":1726069970,\"session_token\":\"token\"},\"key\":\"/apisix/secrets/aws/test3\"}\n```\n\n#### When Secret Manager is GCP\n\n| Parameter                | Required | Type    | Description                                                                                                                                               | Example                                                                                          |\n| ------------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |\n| auth_config              | True     | object  | Either `auth_config` or `auth_file` must be provided.                                                                                                     |                                                                                                  |\n| auth_config.client_email | True     | string  | Email address of the Google Cloud service account.                                                                                                        |                                                                                                  |\n| auth_config.private_key  | True     | string  | Private key of the Google Cloud service account.                                                                                                          |                                                                                                  |\n| auth_config.project_id   | True     | string  | Project ID in the Google Cloud service account.                                                                                                           |                                                                                                  |\n| auth_config.token_uri    | False    | string  | Token URI of the Google Cloud service account.                                                                                                            | [https://oauth2.googleapis.com/token](https://oauth2.googleapis.com/token)                       |\n| auth_config.entries_uri  | False    | string  | The API access endpoint for the Google Secrets Manager.                                                                                                   | [https://secretmanager.googleapis.com/v1](https://secretmanager.googleapis.com/v1)               |\n| auth_config.scope        | False    | string  | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) | [https://www.googleapis.com/auth/cloud-platform](https://www.googleapis.com/auth/cloud-platform) |\n| auth_file                | True     | string  | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided.                                  |                                                                                                  |\n| ssl_verify               | False    | boolean | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).         | true                                                                                             |\n\nExample Configuration:\n\n```json\n{\n    \"auth_config\" : {\n        \"client_email\": \"email@apisix.iam.gserviceaccount.com\",\n        \"private_key\": \"private_key\",\n        \"project_id\": \"apisix-project\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"entries_uri\": \"https://secretmanager.googleapis.com/v1\",\n        \"scope\": [\"https://www.googleapis.com/auth/cloud-platform\"]\n    }\n}\n```\n\nExample API usage:\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/secrets/gcp/test4 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"auth_config\" : {\n        \"client_email\": \"email@apisix.iam.gserviceaccount.com\",\n        \"private_key\": \"private_key\",\n        \"project_id\": \"apisix-project\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"entries_uri\": \"https://secretmanager.googleapis.com/v1\",\n        \"scope\": [\"https://www.googleapis.com/auth/cloud-platform\"]\n    }\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n\n{\"value\":{\"id\":\"gcp/test4\",\"ssl_verify\":true,\"auth_config\":{\"token_uri\":\"https://oauth2.googleapis.com/token\",\"scope\":[\"https://www.googleapis.com/auth/cloud-platform\"],\"entries_uri\":\"https://secretmanager.googleapis.com/v1\",\"client_email\":\"email@apisix.iam.gserviceaccount.com\",\"private_key\":\"private_key\",\"project_id\":\"apisix-project\"},\"create_time\":1726070161,\"update_time\":1726070161},\"key\":\"/apisix/secrets/gcp/test4\"}\n```\n\n### Response Parameters\n\nCurrently, the response is returned from etcd.\n\n## Proto\n\nProto is used to store protocol buffers so that APISIX can communicate in gRPC.\n\nSee [grpc-transcode plugin](./plugins/grpc-transcode.md#enabling-the-plugin) doc for more examples.\n\n### Proto API\n\nProto resource request address: /apisix/admin/protos/{id}\n\n### Request Methods\n\n| Method | Request URI                      | Request Body | Description                                     |\n| ------ | -------------------------------- | ------------ | ----------------------------------------------- |\n| GET    | /apisix/admin/protos      | NULL         | List all Protos.  |\n| GET    | /apisix/admin/protos/{id} | NULL         | Get a Proto by id.     |\n| PUT    | /apisix/admin/protos/{id} | {...}        | Create or update a Proto with the given id.        |\n| POST   | /apisix/admin/protos      | {...}        | Create a Proto with a random id.         |\n| DELETE | /apisix/admin/protos/{id} | NULL         | Delete Proto by id.                 |\n\n### Request Body Parameters\n\n| Parameter | Required | Type      | Description                          | Example                       |\n|-----------|----------|-----------|--------------------------------------| ----------------------------- |\n| content   | True     | String    | Content of `.proto` or `.pb` files   | See [here](./plugins/grpc-transcode.md#enabling-the-plugin)         |\n| name      | False    | Auxiliary | Identifier for the Protobuf definition. | user-proto                    |\n| desc      | False    | Auxiliary | Description of usage scenarios.      | protobuf for user service     |\n| labels    | False    | Match Rules | Attributes of the Proto specified as key-value pairs. | {\"version\":\"v2\",\"service\":\"user\",\"env\":\"production\"}     |\n\n## Schema validation\n\nCheck the validity of a configuration against its entity schema. This allows you to test your input before submitting a request to the entity endpoints of the Admin API.\n\nNote that this only performs the schema validation checks, checking that the input configuration is well-formed. Requests to the entity endpoint using the given configuration may still fail due to other reasons, such as invalid foreign key relationships or uniqueness check failures against the contents of the data store.\n\n### Schema validation\n\nSchema validation request address: /apisix/admin/schema/validate/{resource}\n\n### Request Methods\n\n| Method | Request URI                      | Request Body | Description                                     |\n| ------ | -------------------------------- | ------------ | ----------------------------------------------- |\n| POST   | /apisix/admin/schema/validate/{resource}      | {..resource conf..}        | Validate the resource configuration against corresponding schema.         |\n\n### Request Body Parameters\n\n* 200: validate ok.\n* 400: validate failed, with error as response body in JSON format.\n\nExample:\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/schema/validate/routes \\\n    -H \"X-API-KEY: $admin_key\" -X POST -i -d '{\n    \"uri\": 1980,\n    \"upstream\": {\n        \"scheme\": \"https\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"nghttp2.org\": 1\n        }\n    }\n}'\nHTTP/1.1 400 Bad Request\nDate: Mon, 21 Aug 2023 07:37:13 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.4.0\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nAccess-Control-Expose-Headers: *\nAccess-Control-Max-Age: 3600\n\n{\"error_msg\":\"property \\\"uri\\\" validation failed: wrong type: expected string, got number\"}\n```\n"
  },
  {
    "path": "docs/en/latest/apisix-variable.md",
    "content": "---\ntitle: APISIX variable\nkeywords:\n - Apache APISIX\n - API Gateway\n - APISIX variable\ndescription: This article describes the variables supported by Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nBesides [NGINX variable](http://nginx.org/en/docs/varindex.html), APISIX also provides\nadditional variables.\n\n## List of variables\n\n|   Variable Name     |  Origin    | Description                                                                         | Example        |\n|-------------------- | ---------- | ----------------------------------------------------------------------------------- | -------------  |\n| balancer_ip         | core       | The IP of picked upstream server.                                                   | 192.168.1.2    |\n| balancer_port       | core       | The port of picked upstream server.                                                 | 80             |\n| consumer_name       | core       | Username of Consumer.                                                               |                |\n| consumer_group_id   | core       | Group ID of Consumer.                                                               |                |\n| graphql_name        | core       | The [operation name](https://graphql.org/learn/queries/#operation-name) of GraphQL. | HeroComparison |\n| graphql_operation   | core       | The operation type of GraphQL.                                                      | mutation       |\n| graphql_root_fields | core       | The top level fields of GraphQL.                                                    | [\"hero\"]       |\n| mqtt_client_id      | mqtt-proxy | The client id in MQTT protocol.                                                     |                |\n| route_id            | core       | Id of Route.                                                                        |                |\n| route_name          | core       | Name of Route.                                                                      |                |\n| service_id          | core       | Id of Service.                                                                      |                |\n| service_name        | core       | Name of Service.                                                                    |                |\n| redis_cmd_line      | Redis      | The content of Redis command.                                                       |                |\n| resp_body           | core       | In the logger plugin, if some of the plugins support logging of response body, for example by configuring `include_resp_body: true`, then this variable can be used in the log format. |                |\n| rpc_time            | xRPC       | Time spent at the rpc request level.                                                |                |\n\nYou can also register your own [variable](./plugin-develop.md#register-custom-variable).\n"
  },
  {
    "path": "docs/en/latest/architecture-design/apisix.md",
    "content": "---\ntitle: Architecture\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - APISIX architecture\ndescription: Architecture of Apache APISIX—the Cloud Native API Gateway.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nAPISIX is built on top of Nginx and [ngx_lua](https://github.com/openresty/lua-nginx-module) leveraging the power offered by LuaJIT. See [Why Apache APISIX chose Nginx and Lua to build API Gateway?](https://apisix.apache.org/blog/2021/08/25/why-apache-apisix-chose-nginx-and-lua/).\n\n![flow-software-architecture](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/flow-software-architecture.png)\n\nAPISIX has two main parts:\n\n1. APISIX core, Lua plugin, multi-language Plugin runtime, and the WASM plugin runtime.\n2. Built-in Plugins that adds features for observability, security, traffic control, etc.\n\nThe APISIX core handles the important functions like matching Routes, load balancing, service discovery, configuration management, and provides a management API. It also includes APISIX Plugin runtime supporting Lua and multilingual Plugins (Go, Java , Python, JavaScript, etc) including the experimental WASM Plugin runtime.\n\nAPISIX also has a set of [built-in Plugins](https://apisix.apache.org/docs/apisix/plugins/batch-requests) that adds features like authentication, security, observability, etc. They are written in Lua.\n\n## Request handling process\n\nThe diagram below shows how APISIX handles an incoming request and applies corresponding Plugins:\n\n![flow-load-plugin](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/flow-load-plugin.png)\n\n## Plugin hierarchy\n\nThe chart below shows the order in which different types of Plugin are applied to a request:\n\n![flow-plugin-internal](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/flow-plugin-internal.png)\n"
  },
  {
    "path": "docs/en/latest/aws.md",
    "content": "---\ntitle: Running APISIX in AWS with AWS CDK\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n[APISIX](https://github.com/apache/apisix) is a cloud-native microservices API gateway, delivering the ultimate performance, security, open source and scalable platform for all your APIs and microservices.\n\n## Architecture\n\nThis reference architecture walks you through building **APISIX** as a serverless container API Gateway on top of AWS Fargate with AWS CDK.\n\n![Apache APISIX Serverless Architecture](../../assets/images/aws-fargate-cdk.png)\n\n## Generate an AWS CDK project with `projen`\n\n```bash\n$ mkdir apisix-aws\n$ cd $_\n$ npx projen new awscdk-app-ts\n```\n\nupdate the `.projenrc.js` with the following content:\n\n```js\nconst { AwsCdkTypeScriptApp } = require('projen');\n\nconst project = new AwsCdkTypeScriptApp({\n  cdkVersion: \"1.70.0\",\n  name: \"apisix-aws\",\n  cdkDependencies: [\n    '@aws-cdk/aws-ec2',\n    '@aws-cdk/aws-ecs',\n    '@aws-cdk/aws-ecs-patterns',\n  ]\n});\n\nproject.synth();\n```\n\nupdate the project:\n\n```ts\n$ npx projen\n```\n\n## update `src/main.ts`\n\n```ts\nimport * as cdk from '@aws-cdk/core';\nimport { Vpc, Port } from '@aws-cdk/aws-ec2';\nimport { Cluster, ContainerImage, TaskDefinition, Compatibility } from '@aws-cdk/aws-ecs';\nimport { ApplicationLoadBalancedFargateService, NetworkLoadBalancedFargateService } from '@aws-cdk/aws-ecs-patterns';\n\nexport class ApiSixStack extends cdk.Stack {\n  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {\n    super(scope, id, props);\n\n    const vpc = Vpc.fromLookup(this, 'VPC', {\n      isDefault: true\n    })\n\n    const cluster = new Cluster(this, 'Cluster', {\n      vpc\n    })\n\n    /**\n     * ApiSix service\n     */\n    const taskDefinition = new TaskDefinition(this, 'TaskApiSix', {\n      compatibility: Compatibility.FARGATE,\n      memoryMiB: '512',\n      cpu: '256'\n    })\n\n    taskDefinition\n      .addContainer('apisix', {\n        image: ContainerImage.fromRegistry('iresty/apisix'),\n      })\n      .addPortMappings({\n        containerPort: 9080\n      })\n\n    taskDefinition\n      .addContainer('etcd', {\n        image: ContainerImage.fromRegistry('gcr.azk8s.cn/etcd-development/etcd:v3.3.12'),\n        // image: ContainerImage.fromRegistry('gcr.io/etcd-development/etcd:v3.3.12'),\n      })\n      .addPortMappings({\n        containerPort: 2379\n      })\n\n    const svc = new ApplicationLoadBalancedFargateService(this, 'ApiSixService', {\n      cluster,\n      taskDefinition,\n    })\n\n    svc.targetGroup.setAttribute('deregistration_delay.timeout_seconds', '30')\n    svc.targetGroup.configureHealthCheck({\n      interval: cdk.Duration.seconds(5),\n      healthyHttpCodes: '404',\n      healthyThresholdCount: 2,\n      unhealthyThresholdCount: 3,\n      timeout: cdk.Duration.seconds(4)\n    })\n\n    /**\n     * PHP service\n     */\n    const taskDefinitionPHP = new TaskDefinition(this, 'TaskPHP', {\n      compatibility: Compatibility.FARGATE,\n      memoryMiB: '512',\n      cpu: '256'\n    })\n\n    taskDefinitionPHP\n      .addContainer('php', {\n        image: ContainerImage.fromRegistry('abiosoft/caddy:php'),\n      })\n      .addPortMappings({\n        containerPort: 2015\n      })\n\n    const svcPHP = new NetworkLoadBalancedFargateService(this, 'PhpService', {\n      cluster,\n      taskDefinition: taskDefinitionPHP,\n      assignPublicIp: true,\n    })\n\n    // allow Fargate task behind NLB to accept all traffic\n    svcPHP.service.connections.allowFromAnyIpv4(Port.tcp(2015))\n    svcPHP.targetGroup.setAttribute('deregistration_delay.timeout_seconds', '30')\n    svcPHP.loadBalancer.setAttribute('load_balancing.cross_zone.enabled', 'true')\n\n    new cdk.CfnOutput(this, 'ApiSixDashboardURL', {\n      value: `http://${svc.loadBalancer.loadBalancerDnsName}/apisix/dashboard/`\n    })\n  }\n}\n\nconst devEnv = {\n  account: process.env.CDK_DEFAULT_ACCOUNT,\n  region: process.env.CDK_DEFAULT_REGION,\n};\n\nconst app = new cdk.App();\n\nnew ApiSixStack(app, 'apisix-stack-dev', { env: devEnv });\n\napp.synth();\n```\n\n## Deploy the APISIX Stack with AWS CDK\n\n```bash\n$ cdk diff\n$ cdk deploy\n```\n\nOn deployment complete, some outputs will be returned:\n\n```bash\nOutputs:\napiSix.PhpServiceLoadBalancerDNS5E5BAB1B = apiSi-PhpSe-FOL2MM4TW7G8-09029e095ab36fcc.elb.us-west-2.amazonaws.com\napiSix.ApiSixDashboardURL = http://apiSi-ApiSi-1TM103DN35GRY-1477666967.us-west-2.elb.amazonaws.com/apisix/dashboard/\napiSix.ApiSixServiceLoadBalancerDNSD4E5B8CB = apiSi-ApiSi-1TM103DN35GRY-1477666967.us-west-2.elb.amazonaws.com\napiSix.ApiSixServiceServiceURLF6EC7872 = http://apiSi-ApiSi-1TM103DN35GRY-1477666967.us-west-2.elb.amazonaws.com\n```\n\nOpen the `apiSix.ApiSixDashboardURL` from your browser and you will see the login prompt.\n\n### Configure the upstream nodes\n\nAll upstream nodes are running as **AWS Fargate** tasks and registered to the **NLB(Network Load Balancer)** exposing multiple static IP addresses. We can query the IP addresses by **nslookup** the **apiSix.PhpServiceLoadBalancerDNS5E5BAB1B** like this:\n\n```bash\n$ nslookup apiSi-PhpSe-FOL2MM4TW7G8-09029e095ab36fcc.elb.us-west-2.amazonaws.com\nServer:         192.168.31.1\nAddress:        192.168.31.1#53\n\nNon-authoritative answer:\nName:   apiSi-PhpSe-FOL2MM4TW7G8-09029e095ab36fcc.elb.us-west-2.amazonaws.com\nAddress: 44.224.124.213\nName:   apiSi-PhpSe-FOL2MM4TW7G8-09029e095ab36fcc.elb.us-west-2.amazonaws.com\nAddress: 18.236.43.167\nName:   apiSi-PhpSe-FOL2MM4TW7G8-09029e095ab36fcc.elb.us-west-2.amazonaws.com\nAddress: 35.164.164.178\nName:   apiSi-PhpSe-FOL2MM4TW7G8-09029e095ab36fcc.elb.us-west-2.amazonaws.com\nAddress: 44.226.102.63\n```\n\nConfigure the IP addresses returned as your upstream nodes in your **APISIX** dashboard followed by the **Services** and **Routes** configuration. Let's say we have a `/index.php` as the URI for the first route for our first **Service** from the **Upstream** IP addresses.\n\n![upstream with AWS NLB IP addresses](../../assets/images/aws-nlb-ip-addr.png)\n![service with created upstream](../../assets/images/aws-define-service.png)\n![define route with service and uri](../../assets/images/aws-define-route.png)\n\n## Validation\n\nOK. Let's test the `/index.php` on `{apiSix.ApiSixServiceServiceURL}/index.php`\n\n![Testing Apache APISIX on AWS Fargate](../../assets/images/aws-caddy-php-welcome-page.png)\n\nNow we have been successfully running **APISIX** in AWS Fargate as serverless container API Gateway service.\n\n## Clean up\n\n```bash\n$ cdk destroy\n```\n\n## Running APISIX in AWS China Regions\n\nupdate `src/main.ts`\n\n```js\n  taskDefinition\n    .addContainer('etcd', {\n      image: ContainerImage.fromRegistry('gcr.azk8s.cn/etcd-development/etcd:v3.3.12'),\n      // image: ContainerImage.fromRegistry('gcr.io/etcd-development/etcd:v3.3.12'),\n    })\n    .addPortMappings({\n      containerPort: 2379\n    })\n```\n\n_(read [here](https://github.com/iresty/docker-apisix/blob/9a731f698171f4838e9bc0f1c05d6dda130ca89b/example/docker-compose.yml#L18-L19) for more reference)_\n\nRun `cdk deploy` and specify your preferred AWS region in China.\n\n```bash\n# let's say we have another AWS_PROFILE for China regions called 'cn'\n# make sure you have aws configure --profile=cn properly.\n#\n# deploy to NingXia region\n$ cdk deploy --profile cn -c region=cn-northwest-1\n# deploy to Beijing region\n$ cdk deploy --profile cn -c region=cn-north-1\n```\n\nIn the following case, we got the `Outputs` returned for **AWS Ningxia region(cn-northwest-1)**:\n\n```bash\nOutputs:\napiSix.PhpServiceLoadBalancerDNS5E5BAB1B = apiSi-PhpSe-1760FFS3K7TXH-562fa1f7f642ec24.elb.cn-northwest-1.amazonaws.com.cn\napiSix.ApiSixDashboardURL = http://apiSi-ApiSi-123HOROQKWZKA-1268325233.cn-northwest-1.elb.amazonaws.com.cn/apisix/dashboard/\napiSix.ApiSixServiceLoadBalancerDNSD4E5B8CB = apiSi-ApiSi-123HOROQKWZKA-1268325233.cn-northwest-1.elb.amazonaws.com.cn\napiSix.ApiSixServiceServiceURLF6EC7872 = http://apiSi-ApiSi-123HOROQKWZKA-1268325233.cn-northwest-1.elb.amazonaws.com.cn\n```\n\nOpen the `apiSix.ApiSixDashboardURL` URL and log in to configure your **APISIX** in AWS China region.\n\n_TBD_\n\n## Decouple APISIX and etcd3 on AWS\n\nFor high availability and state consistency consideration, you might be interested to decouple the **etcd3** as a separate cluster from **APISIX** not only for performance but also high availability and fault tolerance yet with highly reliable state consistency.\n\n_TBD_\n"
  },
  {
    "path": "docs/en/latest/batch-processor.md",
    "content": "---\ntitle: Batch Processor\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThe batch processor can be used to aggregate entries(logs/any data) and process them in a batch.\nWhen the batch_max_size is set to 1 the processor will execute each entry immediately. Setting the batch max size more\nthan 1 will start aggregating the entries until it reaches the max size or the timeout expires.\n\n## Configurations\n\nThe only mandatory parameter to create a batch processor is a function. The function will be executed when the batch reaches the max size\nor when the buffer duration exceeds.\n\n| Name             | Type    | Requirement | Default | Valid   | Description                                                  |\n| ---------------- | ------- | ----------- | ------- | ------- | ------------------------------------------------------------ |\n| name             | string  | optional    | logger's name | [\"http logger\",...] | A unique identifier used to identify the batch processor, which defaults to the name of the logger plug-in that calls the batch processor, such as plug-in \"http logger\" 's `name` is \"http logger. |\n| batch_max_size   | integer | optional    | 1000    | [1,...] | Sets the maximum number of logs sent in each batch. When the number of logs reaches the set maximum, all logs will be automatically pushed to the HTTP/HTTPS service. |\n| inactive_timeout | integer | optional    | 5       | [1,...] | The maximum time to refresh the buffer (in seconds). When the maximum refresh time is reached, all logs will be automatically pushed to the HTTP/HTTPS service regardless of whether the number of logs in the buffer reaches the maximum number set. |\n| buffer_duration  | integer | optional    | 60      | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed. |\n| max_retry_count  | integer | optional    | 0       | [0,...] | Maximum number of retries before removing the entry from the processing pipeline when an error occurs. |\n| retry_delay      | integer | optional    | 1       | [0,...] | Number of seconds the process execution should be delayed if the execution fails. |\n\nThe following code shows an example of how to use batch processor in your plugin:\n\n```lua\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\n...\n\nlocal plugin_name = \"xxx-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\nlocal schema = {...}\nlocal _M = {\n    ...\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n}\n\n...\n\n\nfunction _M.log(conf, ctx)\n    local entry = {...} -- data to log\n\n    if batch_processor_manager:add_entry(conf, entry) then\n        return\n    end\n    -- create a new processor if not found\n\n    -- entries is an array table of entry, which can be processed in batch\n    local func = function(entries)\n        -- serialize to json array core.json.encode(entries)\n        -- process/send data\n        return true\n        -- return false, err_msg, first_fail if failed\n        -- first_fail(optional) indicates first_fail-1 entries have been successfully processed\n        -- and during processing of entries[first_fail], the error occurred. So the batch processor\n        -- only retries for the entries having index >= first_fail as per the retry policy.\n    end\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func)\nend\n```\n\nThe batch processor's configuration will be set inside the plugin's configuration.\nFor example:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"http-logger\": {\n                \"uri\": \"http://mockbin.org/bin/:ID\",\n                \"batch_max_size\": 10,\n                \"max_retry_count\": 1\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\nIf your plugin only uses one global batch processor,\nyou can also use the processor directly:\n\n```lua\nlocal entry = {...} -- data to log\nif log_buffer then\n    log_buffer:push(entry)\n    return\nend\n\nlocal config_bat = {\n    name = config.name,\n    retry_delay = config.retry_delay,\n    ...\n}\n\nlocal err\n-- entries is an array table of entry, which can be processed in batch\nlocal func = function(entries)\n    ...\n    return true\n    -- return false, err_msg, first_fail if failed\nend\nlog_buffer, err = batch_processor:new(func, config_bat)\n\nif not log_buffer then\n    core.log.warn(\"error when creating the batch processor: \", err)\n    return\nend\n\nlog_buffer:push(entry)\n```\n\nNote: Please make sure the batch max size (entry count) is within the limits of the function execution.\nThe timer to flush the batch runs based on the `inactive_timeout` configuration. Thus, for optimal usage,\nkeep the `inactive_timeout` smaller than the `buffer_duration`.\n"
  },
  {
    "path": "docs/en/latest/benchmark.md",
    "content": "---\ntitle: Benchmark\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### Benchmark Environments\n\nn1-highcpu-8 (8 vCPUs, 7.2 GB memory) on Google Cloud\n\nBut we **only** used 4 cores to run APISIX, and left 4 cores for system and [wrk](https://github.com/wg/wrk),\nwhich is the HTTP benchmarking tool.\n\n### Benchmark Test for reverse proxy\n\nOnly used APISIX as the reverse proxy server, with no logging, limit rate, or other plugins enabled,\nand the response size was 1KB.\n\n#### QPS\n\nThe x-axis means the size of CPU core, and the y-axis is QPS.\n\n![benchmark-1](../../assets/images/benchmark-1.jpg)\n\n#### Latency\n\nNote the y-axis latency in **microsecond(μs)** not millisecond.\n\n![latency-1](../../assets/images/latency-1.jpg)\n\n#### Flame Graph\n\nThe result of Flame Graph:\n![flamegraph-1](../../assets/images/flamegraph-1.jpg)\n\nAnd if you want to run the benchmark test in your machine, you should run another Nginx to listen 80 port.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1,\n            \"127.0.0.2:80\": 1\n        }\n    }\n}'\n```\n\nthen run wrk:\n\n```shell\nwrk -d 60 --latency http://127.0.0.1:9080/hello\n```\n\n### Benchmark Test for reverse proxy, enabled 2 plugins\n\nOnly used APISIX as the reverse proxy server, enabled the limit rate and prometheus plugins,\nand the response size was 1KB.\n\n#### QPS\n\nThe x-axis means the size of CPU core, and the y-axis is QPS.\n\n![benchmark-2](../../assets/images/benchmark-2.jpg)\n\n#### Latency\n\nNote the y-axis latency in **microsecond(μs)** not millisecond.\n\n![latency-2](../../assets/images/latency-2.jpg)\n\n#### Flame Graph\n\nThe result of Flame Graph:\n![flamegraph-2](../../assets/images/flamegraph-2.jpg)\n\nAnd if you want to run the benchmark test in your machine, you should run another Nginx to listen 80 port.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 999999999,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        },\n        \"prometheus\":{}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1,\n            \"127.0.0.2:80\": 1\n        }\n    }\n}'\n```\n\nthen run wrk:\n\n```shell\nwrk -d 60 --latency http://127.0.0.1:9080/hello\n```\n\nFor more reference on how to run the benchmark test, you can see this [PR](https://github.com/apache/apisix/pull/6136) and this [script](https://gist.github.com/membphis/137db97a4bf64d3653aa42f3e016bd01).\n\n:::tip\n\nIf you want to run the benchmark with a large number of connections, You may have to update the [**keepalive**](https://github.com/apache/apisix/blob/master/conf/config.yaml.example#L241) config by adding the configuration to [`config.yaml`](https://github.com/apache/apisix/blob/master/conf/config.yaml) and reload APISIX. Connections exceeding this number will become short connections. You can run the following command to test the benchmark with a large number of connections:\n\n```bash\nwrk -t200 -c5000 -d30s http://127.0.0.1:9080/hello\n```\n\nFor more details, you can refer to [Module ngx_http_upstream_module](http://nginx.org/en/docs/http/ngx_http_upstream_module.html).\n\n:::\n"
  },
  {
    "path": "docs/en/latest/build-apisix-dev-environment-devcontainers.md",
    "content": "---\nid: build-apisix-dev-environment-devcontainers\ntitle: Build development environment with Dev Containers\ndescription: This paper introduces how to quickly start the APISIX API Gateway development environment using Dev Containers.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nPreviously, building and developing APISIX on Linux or macOS required developers to install its runtime environment and toolchain themselves, and developers might not be familiar with them.\n\nAs it needs to support multiple operating systems and CPU ISAs, the process has inherent complexities in how to find and install dependencies and toolchains.\n\n:::note\n\nThe tutorial can be used as an alternative to a [bare-metal environment](building-apisix.md) or a [macOS container development environment](build-apisix-dev-environment-on-mac.md).\n\nIt only requires that you have an environment running Docker or a similar alternative (the docker/docker compose command is required), and no other dependent components need to be installed on your host machine.\n\n:::\n\n## Supported systems and CPU ISA\n\n- Linux\n  - AMD64\n  - ARM64\n- Windows (with WSL2 supported)\n  - AMD64\n- macOS\n  - ARM64\n  - AMD64\n\n## Quick Setup of Apache APISIX Development Environment\n\n### Implementation Idea\n\nWe use Dev Containers to build development environment, and when we open an APISIX project using the IDE, we have access to the container-driven runtime environment.\n\nThere the etcd is ready and we can start APISIX directly.\n\n### Steps\n\n:::note\n\nThe following uses Visual Studio Code, which has built-in integration with Dev Containers.\n\nIn theory you could also use any other editor or IDE that integrates with Dev Containers.\n\n:::\n\nFirst, clone the APISIX source code, open project in Visual Studio Code.\n\n```shell\ngit clone https://github.com/apache/apisix.git\ncd apisix\ncode . # VSCode needs to be in the PATH environment variable, you can also open the project directory manually in the UI.\n```\n\nNext, switch to Dev Containers. Open the VSCode Command Palette, and execute `Dev Containers: Reopen in Container`.\n\n![VSCode Command open in container](../../assets/images/build-devcontainers-vscode-command.png)\n\nVSCode will open the Dev Containers project in a new window, where it will build the runtime and install the toolchain according to the Dockerfile before starting the connection and finally installing the APISIX dependencies.\n\n:::note\n\nThis process requires a reliable network connection, and it will access Docker Hub, GitHub, and some other sites. You will need to ensure the network connection yourself, otherwise the container build may fail.\n\n:::\n\nWait some minutes, depending on the internet connection or computer performance, it may take from a few minutes to tens of minutes, you can click on the Progress Bar in the bottom right corner to view a live log where you will be able to check unusual stuck.\n\nIf you encounter any problems, you can search or ask questions in [GitHub Issues](https://github.com/apache/apisix/issues) or [GitHub Discussions](https://github.com/apache/apisix/discussions), and community members will respond as promptly as possible.\n\n![VSCode dev containers building progress bar](../../assets/images/build-devcontainers-vscode-progressbar.png)\n\nWhen the process in the terminal is complete, the development environment is ready, and even etcd is ready.\n\nStart APISIX with the following command:\n\n```shell\nmake run\n```\n\nNow you can start writing code and test cases, and testing tools are available:\n\n```shell\nexport TEST_NGINX_BINARY=openresty\n\n# run all tests\nmake test\n\n# or run a specify test case file\nFLUSH_ETCD=1 prove -Itest-nginx/lib -I. -r t/admin/api.t\n```\n\n## FAQ\n\n### Where's the code? When I delete the container, are the changes lost?\n\nIt will be on your host, which is where you cloned the APISIX source code, and the container uses the volume to mount the code into the container. Containers contain only the runtime environment, not the source code, so no changes will be lost whether you close or delete the container.\n\nAnd, the `git` is already installed in the container, so you can commit a change directly there.\n"
  },
  {
    "path": "docs/en/latest/build-apisix-dev-environment-on-mac.md",
    "content": "---\nid: build-apisix-dev-environment-on-mac\ntitle: Build development environment on Mac\ndescription: This paper introduces how to use Docker to quickly build the development environment of API gateway Apache APISIX on Mac.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nIf you want to quickly build and develop APISIX on your Mac platform, you can refer to this tutorial.\n\n:::note\n\nThis tutorial is suitable for situations where you need to quickly start development on the Mac platform, if you want to go further and have a better development experience, the better choice is the Linux-based virtual machine, or directly use this kind of system as your development environment.\n\nYou can see the specific supported systems [here](install-dependencies.md#install).\n\n:::\n\n## Quick Setup of Apache APISIX Development Environment\n\n### Implementation Idea\n\nWe use Docker to build the test environment of Apache APISIX. When the container starts, we can mount the source code of Apache APISIX into the container, and then we can build and run test cases in the container.\n\n### Implementation Steps\n\nFirst, clone the APISIX source code, build an image that can run test cases, and compile the Apache APISIX.\n\n```shell\ngit clone https://github.com/apache/apisix.git\ncd apisix\ndocker build -t apisix-dev-env -f example/build-dev-image.dockerfile .\n```\n\nNext, start Etcd:\n\n```shell\ndocker run -d --name etcd-apisix --net=host pachyderm/etcd:v3.5.2\n```\n\nMount the APISIX directory and start the development environment container:\n\n```shell\ndocker run -d --name apisix-dev-env --net=host -v $(pwd):/apisix:rw apisix-dev-env:latest\n```\n\nFinally, enter the container, build the Apache APISIX runtime, and configure the test environment:\n\n```shell\ndocker exec -it apisix-dev-env make deps\ndocker exec -it apisix-dev-env ln -s /usr/bin/openresty /usr/bin/nginx\n```\n\n### Run and Stop APISIX\n\n```shell\ndocker exec -it apisix-dev-env make run\ndocker exec -it apisix-dev-env make stop\n```\n\n:::note\n\nIf you encounter an error message like `nginx: [emerg] bind() to unix:/apisix/logs/worker_events.sock failed (95: Operation not supported)` while running `make run`, please use this solution.\n\nChange the `File Sharing` settings of your Docker-Desktop:\n\n![Docker-Desktop File Sharing Setting](../../assets/images/update-docker-desktop-file-sharing.png)\n\nChanging to either `gRPC FUSE` or `osxfs` can resolve this issue.\n\n:::\n\n### Run Specific Test Cases\n\n```shell\ndocker exec -it apisix-dev-env prove t/admin/routes.t\n```\n"
  },
  {
    "path": "docs/en/latest/building-apisix.md",
    "content": "---\nid: building-apisix\ntitle: Building APISIX from source\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Code Contribution\n  - Building APISIX\ndescription: Guide for building and running APISIX locally for development.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nIf you are looking to setup a development environment or contribute to APISIX, this guide is for you.\n\nIf you are looking to quickly get started with APISIX, check out the other [installation methods](./installation-guide.md).\n\n:::note\n\nTo build an APISIX docker image from source code, see [build image from source code](https://apisix.apache.org/docs/docker/build/#build-an-image-from-customizedpatched-source-code).\n\nTo build and package APISIX for a specific platform, see [apisix-build-tools](https://github.com/api7/apisix-build-tools) instead.\n\n:::\n\n## Building APISIX from source\n\nFirst of all, we need to specify the branch to be built:\n\n```shell\nAPISIX_BRANCH='release/3.14'\n```\n\nThen, you can run the following command to clone the APISIX source code from Github:\n\n```shell\ngit clone --depth 1 --branch ${APISIX_BRANCH} https://github.com/apache/apisix.git apisix-${APISIX_BRANCH}\n```\n\nAlternatively, you can also download the source package from the [Downloads](https://apisix.apache.org/downloads/) page. Note that source packages here are not distributed with test cases.\n\nBefore installation, install [OpenResty](https://openresty.org/en/installation.html).\n\nNext, navigate to the directory, install dependencies, and build APISIX.\n\n```shell\ncd apisix-${APISIX_BRANCH}\nmake deps\nmake install\n```\n\nThis will install the runtime-dependent Lua libraries and `apisix-runtime` the `apisix` CLI tool.\n\n:::note\n\nIf you get an error message like `Could not find header file for LDAP/PCRE/openssl` while running `make deps`, use this solution.\n\n`luarocks` supports custom compile-time dependencies (See: [Config file format](https://github.com/luarocks/luarocks/wiki/Config-file-format)). You can use a third-party tool to install the missing packages and add its installation directory to the `luarocks`' variables table. This method works on macOS, Ubuntu, CentOS, and other similar operating systems.\n\nThe solution below is for macOS but it works similarly for other operating systems:\n\n1. Install `openldap` by running:\n\n   ```shell\n   brew install openldap\n   ```\n\n2. Locate the installation directory by running:\n\n   ```shell\n   brew --prefix openldap\n   ```\n\n3. Add this path to the project configuration file by any of the two methods shown below:\n   1. You can use the `luarocks config` command to set `LDAP_DIR`:\n\n      ```shell\n      luarocks config variables.LDAP_DIR /opt/homebrew/cellar/openldap/2.6.1\n      ```\n\n   2. You can also change the default configuration file of `luarocks`. Open the file `~/.luaorcks/config-5.1.lua` and add the following:\n\n      ```shell\n      variables = { LDAP_DIR = \"/opt/homebrew/cellar/openldap/2.6.1\", LDAP_INCDIR = \"/opt/homebrew/cellar/openldap/2.6.1/include\", }\n      ```\n\n      `/opt/homebrew/cellar/openldap/` is default path `openldap` is installed on Apple Silicon macOS machines. For Intel machines, the default path is  `/usr/local/opt/openldap/`.\n\n:::\n\nTo uninstall the APISIX runtime, run:\n\n```shell\nmake uninstall\nmake undeps\n```\n\n:::danger\n\nThis operation will remove the files completely.\n\n:::\n\n## Installing etcd\n\nAPISIX uses [etcd](https://github.com/etcd-io/etcd) to save and synchronize configuration. Before running APISIX, you need to install etcd on your machine. Installation methods based on your operating system are mentioned below.\n\n<Tabs\n  groupId=\"os\"\n  defaultValue=\"linux\"\n  values={[\n    {label: 'Linux', value: 'linux'},\n    {label: 'macOS', value: 'mac'},\n  ]}>\n<TabItem value=\"linux\">\n\n```shell\nETCD_VERSION='3.4.18'\nwget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz\ntar -xvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz && \\\n  cd etcd-v${ETCD_VERSION}-linux-amd64 && \\\n  sudo cp -a etcd etcdctl /usr/bin/\nnohup etcd >/tmp/etcd.log 2>&1 &\n```\n\n</TabItem>\n\n<TabItem value=\"mac\">\n\n```shell\nbrew install etcd\nbrew services start etcd\n```\n\n</TabItem>\n</Tabs>\n\n## Running and managing APISIX server\n\nTo initialize the configuration file, within the APISIX directory, run:\n\n```shell\napisix init\n```\n\n:::tip\n\nYou can run `apisix help` to see a list of available commands.\n\n:::\n\nYou can then test the created configuration file by running:\n\n```shell\napisix test\n```\n\nFinally, you can run the command below to start APISIX:\n\n```shell\napisix start\n```\n\nTo stop APISIX, you can use either the `quit` or the `stop` subcommand.\n\n`apisix quit` will gracefully shutdown APISIX. It will ensure that all received requests are completed before stopping.\n\n```shell\napisix quit\n```\n\nWhere as, the `apisix stop` command does a force shutdown and discards all pending requests.\n\n```shell\napisix stop\n```\n\n## Building runtime for APISIX\n\nSome features of APISIX requires additional Nginx modules to be introduced into OpenResty.\n\nTo use these features, you need to build a custom distribution of OpenResty (apisix-runtime). See [apisix-build-tools](https://github.com/api7/apisix-build-tools) for setting up your build environment and building it.\n\n## Running tests\n\nThe steps below show how to run the test cases for APISIX:\n\n1. Install [cpanminus](https://metacpan.org/pod/App::cpanminus#INSTALLATION), the package manager for Perl.\n2. Install the [test-nginx](https://github.com/openresty/test-nginx) dependencies with `cpanm`:\n\n   ```shell\n   sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)\n   ```\n\n3. Clone the test-nginx source code locally:\n\n   ```shell\n   git clone https://github.com/openresty/test-nginx.git\n   ```\n\n4. Append the current directory to Perl's module directory by running:\n\n   ```shell\n   export PERL5LIB=.:$PERL5LIB\n   ```\n\n   You can specify the Nginx binary path by running:\n\n   ```shell\n   TEST_NGINX_BINARY=/usr/local/bin/openresty prove -Itest-nginx/lib -r t\n   ```\n\n5. Run the tests by running:\n\n   ```shell\n   make test\n   ```\n\n:::note\n\nSome tests rely on external services and system configuration modification. See [ci/linux_openresty_common_runner.sh](https://github.com/apache/apisix/blob/master/ci/linux_openresty_common_runner.sh) for a complete test environment build.\n\n:::\n\n### Troubleshooting\n\nThese are some common troubleshooting steps for running APISIX test cases.\n\n#### Configuring Nginx path\n\nFor the error `Error unknown directive \"lua_package_path\" in /API_ASPIX/apisix/t/servroot/conf/nginx.conf`, ensure that OpenResty is set to the default Nginx and export the path as follows:\n\n- Linux default installation path:\n\n  ```shell\n  export PATH=/usr/local/openresty/nginx/sbin:$PATH\n  ```\n\n#### Running a specific test case\n\nTo run a specific test case, use the command below:\n\n```shell\nprove -Itest-nginx/lib -r t/plugin/openid-connect.t\n```\n\nSee [testing framework](./internal/testing-framework.md) for more details.\n"
  },
  {
    "path": "docs/en/latest/certificate.md",
    "content": "---\ntitle: Certificate\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n`APISIX` supports to load multiple SSL certificates by TLS extension Server Name Indication (SNI).\n\n### Single SNI\n\nIt is most common for an SSL certificate to contain only one domain. We can create an `ssl` object. Here is a simple case, creates a `ssl` object and `route` object.\n\n* `cert`: PEM-encoded public certificate of the SSL key pair.\n* `key`: PEM-encoded private key of the SSL key pair.\n* `snis`: Hostname(s) to associate with this certificate as SNIs. To set this attribute this certificate must have a valid private key associated with it.\n\nThe following is an example of configuring an SSL certificate with a single SNI in APISIX.\n\nCreate an SSL object with the certificate and key valid for the SNI:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat t/certs/apisix.crt)\"'\",\n     \"key\": \"'\"$(cat t/certs/apisix.key)\"'\",\n     \"snis\": [\"test.com\"]\n}'\n```\n\nCreate a Router object:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/get\",\n    \"hosts\": [\"test.com\"],\n    \"methods\": [\"GET\"],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n\nSend a request to verify:\n\n```shell\ncurl --resolve 'test.com:9443:127.0.0.1' https://test.com:9443/get -k -vvv\n\n* Added test.com:9443:127.0.0.1 to DNS cache\n* About to connect() to test.com port 9443 (#0)\n*   Trying 127.0.0.1...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*   subject: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*   start date: Jun 24 22:18:05 2019 GMT\n*   expire date: May 31 22:18:05 2119 GMT\n*   issuer: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*   SSL certificate verify result: self-signed certificate (18), continuing anyway.\n> GET /get HTTP/2\n> Host: test.com:9443\n> user-agent: curl/7.81.0\n> accept: */*\n```\n\n### wildcard SNI\n\nAn SSL certificate could also be valid for a wildcard domain like `*.test.com`, which means it is valid for any domain of that pattern, including `www.test.com` and `mail.test.com`.\n\nThe following is an example of configuring an SSL certificate with a wildcard SNI in APISIX.\n\nCreate an SSL object with the certificate and key valid for the SNI:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n -H \"X-API-KEY: $admin_key\" -X PUT -d '\n {\n      \"cert\" : \"'\"$(cat t/certs/apisix.crt)\"'\",\n      \"key\": \"'\"$(cat t/certs/apisix.key)\"'\",\n      \"snis\": [\"*.test.com\"]\n }'\n```\n\nCreate a Router object:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/get\",\n    \"hosts\": [\"*.test.com\"],\n    \"methods\": [\"GET\"],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n\nSend a request to verify:\n\n```shell\ncurl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/get -k -vvv\n\n* Added www.test.com:9443:127.0.0.1 to DNS cache\n* Hostname www.test.com was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* Connected to www.test.com (127.0.0.1) port 9443 (#0)\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*  start date: Jun 24 22:18:05 2019 GMT\n*  expire date: May 31 22:18:05 2119 GMT\n*  issuer: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*  SSL certificate verify result: self signed certificate (18), continuing anyway.\n> GET /get HTTP/2\n> Host: www.test.com:9443\n> user-agent: curl/7.74.0\n> accept: */*\n```\n\n### multiple domain\n\nIf your SSL certificate may contain more than one domain, like `www.test.com` and `mail.test.com`, then you can add them into the `snis` array. For example:\n\n```json\n{\n    \"snis\": [\"www.test.com\", \"mail.test.com\"]\n}\n```\n\n### multiple certificates for a single domain\n\nIf you want to configure multiple certificate for a single domain, for\ninstance, supporting both the\n[ECC](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)\nand RSA key-exchange algorithm, then just configure the extra certificates (the\nfirst certificate and private key should be still put in `cert` and `key`) and\nprivate keys by `certs` and `keys`.\n\n* `certs`: PEM-encoded certificate array.\n* `keys`: PEM-encoded private key array.\n\n`APISIX` will pair certificate and private key with the same indice as a SSL key\npair. So the length of `certs` and `keys` must be same.\n\n### set up multiple CA certificates\n\nAPISIX currently uses CA certificates in several places, such as [Protect Admin API](./mtls.md#protect-admin-api), [etcd with mTLS](./mtls.md#etcd-with-mtls), and [Deployment Modes](./deployment-modes.md).\n\nIn these places, `ssl_trusted_certificate` or `trusted_ca_cert` will be used to set up the CA certificate, but these configurations will eventually be translated into [lua_ssl_trusted_certificate](https://github.com/openresty/lua-nginx-module#lua_ssl_trusted_certificate) directive in OpenResty.\n\nIf you need to set up different CA certificates in different places, then you can package these CA certificates into a CA bundle file and point to this file when you need to set up CAs. This will avoid the problem that the generated `lua_ssl_trusted_certificate` has multiple locations and overwrites each other.\n\nThe following is a complete example to show how to set up multiple CA certificates in APISIX.\n\nSuppose we let client and APISIX Admin API, APISIX and ETCD communicate with each other using mTLS protocol, and currently there are two CA certificates, `foo_ca.crt` and `bar_ca.crt`, and use each of these two CA certificates to issue client and server certificate pairs, `foo_ca.crt` and its issued certificate pair are used to protect Admin API, and `bar_ca.crt` and its issued certificate pair are used to protect ETCD.\n\nThe following table details the configurations involved in this example and what they do:\n\n| Configuration    | Type     | Description                                                                                                                                                                  |\n| -------------    | -------  | -------------------------------------------------------------------------------------------------------------------------------------------------------------                |\n| foo_ca.crt       | CA cert  | Issues the secondary certificate required for the client to communicate with the APISIX Admin API over mTLS.                                                                 |\n| foo_client.crt   | cert     | A certificate issued by `foo_ca.crt` and used by the client to prove its identity when accessing the APISIX Admin API.                                                       |\n| foo_client.key   | key      | Issued by `foo_ca.crt`, used by the client, the key file required to access the APISIX Admin API.                                                                            |\n| foo_server.crt   | cert     | Issued by `foo_ca.crt`, used by APISIX, corresponding to the `admin_api_mtls.admin_ssl_cert` configuration entry.                                                     |\n| foo_server.key   | key      | Issued by `foo_ca.crt`, used by APISIX, corresponding to the `admin_api_mtls.admin_ssl_cert_key` configuration entry.                                                 |\n| admin.apisix.dev | doname   | Common Name used in issuing `foo_server.crt` certificate, through which the client accesses APISIX Admin API                                                                 |\n| bar_ca.crt       | CA cert  | Issues the secondary certificate required for APISIX to communicate with ETCD over mTLS.                                                                                     |\n| bar_etcd.crt     | cert     | Issued by `bar_ca.crt` and used by ETCD, corresponding to the `-cert-file` option in the ETCD startup command.                                                               |\n| bar_etcd.key     | key      | Issued by `bar_ca.crt` and used by ETCD, corresponding to the `--key-file` option in the ETCD startup command.                                                               |\n| bar_apisix.crt   | cert     | Issued by `bar_ca.crt`, used by APISIX, corresponding to the `etcd.tls.cert` configuration entry.                                                                            |\n| bar_apisix.key   | key      | Issued by `bar_ca.crt`, used by APISIX, corresponding to the `etcd.tls.key` configuration entry.                                                                             |\n| etcd.cluster.dev | key      | Common Name used in issuing `bar_etcd.crt` certificate, which is used as SNI when APISIX communicates with ETCD over mTLS. corresponds to `etcd.tls.sni` configuration item. |\n| apisix.ca-bundle | CA bundle | Merged from `foo_ca.crt` and `bar_ca.crt`, replacing `foo_ca.crt` and `bar_ca.crt`.                                                                                         |\n\n1. Create CA bundle files\n\n```shell\ncat /path/to/foo_ca.crt /path/to/bar_ca.crt > apisix.ca-bundle\n```\n\n2. Start the ETCD cluster and enable client authentication\n\nStart by writing a `goreman` configuration named `Procfile-single-enable-mtls`, the content as:\n\n```text\n# Use goreman to run `go get github.com/mattn/goreman`\netcd1: etcd --name infra1 --listen-client-urls https://127.0.0.1:12379 --advertise-client-urls https://127.0.0.1:12379 --listen-peer-urls http://127.0.0.1:12380 --initial-advertise-peer-urls http://127.0.0.1:12380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle\netcd2: etcd --name infra2 --listen-client-urls https://127.0.0.1:22379 --advertise-client-urls https://127.0.0.1:22379 --listen-peer-urls http://127.0.0.1:22380 --initial-advertise-peer-urls http://127.0.0.1:22380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle\netcd3: etcd --name infra3 --listen-client-urls https://127.0.0.1:32379 --advertise-client-urls https://127.0.0.1:32379 --listen-peer-urls http://127.0.0.1:32380 --initial-advertise-peer-urls http://127.0.0.1:32380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle\n```\n\nUse `goreman` to start the ETCD cluster:\n\n```shell\ngoreman -f Procfile-single-enable-mtls start > goreman.log 2>&1 &\n```\n\n3. Update `config.yaml`\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key\n      - name: admin\n        key: edd1c9f034335f136f87ad84b625c8f1\n        role: admin\n    admin_listen:\n      ip: 127.0.0.1\n      port: 9180\n    https_admin: true\n    admin_api_mtls:\n      admin_ssl_ca_cert: /path/to/apisix.ca-bundle\n      admin_ssl_cert: /path/to/foo_server.crt\n      admin_ssl_cert_key: /path/to/foo_server.key\n\napisix:\n  ssl:\n    ssl_trusted_certificate: /path/to/apisix.ca-bundle\n\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:12379\"\n      - \"https://127.0.0.1:22379\"\n      - \"https://127.0.0.1:32379\"\n    tls:\n      cert: /path/to/bar_apisix.crt\n      key: /path/to/bar_apisix.key\n      sni: etcd.cluster.dev\n```\n\n4. Test APISIX Admin API\n\nStart APISIX, if APISIX starts successfully and there is no abnormal output in `logs/error.log`, it means that mTLS communication between APISIX and ETCD is normal.\n\nUse curl to simulate a client, communicate with APISIX Admin API with mTLS, and create a route:\n\n```shell\ncurl -vvv \\\n    --resolve 'admin.apisix.dev:9180:127.0.0.1' https://admin.apisix.dev:9180/apisix/admin/routes/1 \\\n    --cert /path/to/foo_client.crt \\\n    --key /path/to/foo_client.key \\\n    --cacert /path/to/apisix.ca-bundle \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/get\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n\nA successful mTLS communication between curl and the APISIX Admin API is indicated if the following SSL handshake process is output:\n\n```shell\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n```\n\n5. Verify APISIX proxy\n\n```shell\ncurl http://127.0.0.1:9080/get -i\n\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 298\nConnection: keep-alive\nDate: Tue, 26 Jul 2022 16:31:00 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/2.14.1\n\n...\n```\n\nAPISIX proxied the request to the `/get` path of the upstream `httpbin.org` and returned `HTTP/1.1 200 OK`. The whole process is working fine using CA bundle instead of CA certificate.\n"
  },
  {
    "path": "docs/en/latest/config.json",
    "content": "{\n  \"version\": \"3.15.0\",\n  \"sidebar\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"Getting Started\",\n      \"items\": [\n        \"getting-started/README\",\n        \"getting-started/configure-routes\",\n        \"getting-started/load-balancing\",\n        \"getting-started/key-authentication\",\n        \"getting-started/rate-limiting\"\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"installation-guide\"\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"architecture-design/apisix\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"Tutorials\",\n      \"items\": [\n        \"tutorials/expose-api\",\n        \"tutorials/protect-api\",\n        {\n          \"type\": \"category\",\n          \"label\": \"Observability\",\n          \"items\": [\n            \"tutorials/observe-your-api\",\n            \"tutorials/health-check\",\n            \"tutorials/monitor-api-health-check\"\n          ]\n        },\n        \"tutorials/manage-api-consumers\",\n        \"tutorials/cache-api-responses\",\n        \"tutorials/add-multiple-api-versions\",\n        \"tutorials/client-to-apisix-mtls\",\n        \"tutorials/websocket-authentication\",\n        \"tutorials/keycloak-oidc\"\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"Terminology\",\n      \"items\": [\n        \"terminology/api-gateway\",\n        \"terminology/consumer\",\n        \"terminology/consumer-group\",\n        \"terminology/credential\",\n        \"terminology/global-rule\",\n        \"terminology/plugin\",\n        \"terminology/plugin-config\",\n        \"terminology/plugin-metadata\",\n        \"terminology/route\",\n        \"terminology/router\",\n        \"terminology/script\",\n        \"terminology/service\",\n        \"terminology/upstream\",\n        \"terminology/secret\"\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"Plugins\",\n      \"items\": [\n        {\n          \"type\": \"category\",\n          \"label\": \"AI\",\n          \"items\": [\n            \"plugins/ai-proxy\",\n            \"plugins/ai-proxy-multi\",\n            \"plugins/ai-rate-limiting\",\n            \"plugins/ai-prompt-guard\",\n            \"plugins/ai-aws-content-moderation\",\n            \"plugins/ai-aliyun-content-moderation\",\n            \"plugins/ai-prompt-decorator\",\n            \"plugins/ai-prompt-template\",\n            \"plugins/ai-rag\",\n            \"plugins/ai-request-rewrite\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"General\",\n          \"items\": [\n            \"plugins/batch-requests\",\n            \"plugins/redirect\",\n            \"plugins/echo\",\n            \"plugins/gzip\",\n            \"plugins/brotli\",\n            \"plugins/real-ip\",\n            \"plugins/server-info\",\n            \"plugins/ext-plugin-pre-req\",\n            \"plugins/ext-plugin-post-req\",\n            \"plugins/ext-plugin-post-resp\",\n            \"plugins/inspect\",\n            \"plugins/ocsp-stapling\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Transformation\",\n          \"items\": [\n            \"plugins/response-rewrite\",\n            \"plugins/proxy-rewrite\",\n            \"plugins/grpc-transcode\",\n            \"plugins/grpc-web\",\n            \"plugins/fault-injection\",\n            \"plugins/mocking\",\n            \"plugins/degraphql\",\n            \"plugins/body-transformer\",\n            \"plugins/attach-consumer-label\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Authentication\",\n          \"items\": [\n            \"plugins/key-auth\",\n            \"plugins/jwt-auth\",\n            \"plugins/jwe-decrypt\",\n            \"plugins/basic-auth\",\n            \"plugins/authz-keycloak\",\n            \"plugins/authz-casdoor\",\n            \"plugins/wolf-rbac\",\n            \"plugins/openid-connect\",\n            \"plugins/cas-auth\",\n            \"plugins/hmac-auth\",\n            \"plugins/authz-casbin\",\n            \"plugins/ldap-auth\",\n            \"plugins/opa\",\n            \"plugins/forward-auth\",\n            \"plugins/multi-auth\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Security\",\n          \"items\": [\n            \"plugins/cors\",\n            \"plugins/uri-blocker\",\n            \"plugins/ip-restriction\",\n            \"plugins/ua-restriction\",\n            \"plugins/referer-restriction\",\n            \"plugins/consumer-restriction\",\n            \"plugins/csrf\",\n            \"plugins/public-api\",\n            \"plugins/gm\",\n            \"plugins/chaitin-waf\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Traffic\",\n          \"items\": [\n            \"plugins/limit-req\",\n            \"plugins/limit-conn\",\n            \"plugins/limit-count\",\n            \"plugins/proxy-cache\",\n            \"plugins/request-validation\",\n            \"plugins/proxy-mirror\",\n            \"plugins/api-breaker\",\n            \"plugins/traffic-split\",\n            \"plugins/request-id\",\n            \"plugins/proxy-control\",\n            \"plugins/client-control\",\n            \"plugins/workflow\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Observability\",\n          \"items\": [\n            {\n              \"type\": \"category\",\n              \"label\": \"Tracers\",\n              \"items\": [\n                \"plugins/zipkin\",\n                \"plugins/skywalking\",\n                \"plugins/opentelemetry\"\n              ]\n            },\n            {\n              \"type\": \"category\",\n              \"label\": \"Metrics\",\n              \"items\": [\n                \"plugins/prometheus\",\n                \"plugins/node-status\",\n                \"plugins/datadog\"\n              ]\n            },\n            {\n              \"type\": \"category\",\n              \"label\": \"Loggers\",\n              \"items\": [\n                \"plugins/http-logger\",\n                \"plugins/skywalking-logger\",\n                \"plugins/tcp-logger\",\n                \"plugins/kafka-logger\",\n                \"plugins/rocketmq-logger\",\n                \"plugins/udp-logger\",\n                \"plugins/clickhouse-logger\",\n                \"plugins/syslog\",\n                \"plugins/log-rotate\",\n                \"plugins/error-log-logger\",\n                \"plugins/sls-logger\",\n                \"plugins/google-cloud-logging\",\n                \"plugins/splunk-hec-logging\",\n                \"plugins/file-logger\",\n                \"plugins/loggly\",\n                \"plugins/elasticsearch-logger\",\n                \"plugins/tencent-cloud-cls\",\n                \"plugins/loki-logger\",\n                \"plugins/lago\"\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Serverless\",\n          \"items\": [\n            \"plugins/serverless\",\n            \"plugins/azure-functions\",\n            \"plugins/openwhisk\",\n            \"plugins/aws-lambda\",\n            \"plugins/openfunction\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Other protocols\",\n          \"items\": [\n            \"plugins/dubbo-proxy\",\n            \"plugins/mqtt-proxy\",\n            \"plugins/kafka-proxy\",\n            \"plugins/http-dubbo\"\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"API\",\n      \"items\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"admin-api\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"control-api\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"status-api\"\n        }\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"dashboard\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"Development\",\n      \"items\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"build-apisix-dev-environment-devcontainers\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"building-apisix\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"build-apisix-dev-environment-on-mac\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"support-fips-in-apisix\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"external-plugin\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"wasm\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"CODE_STYLE\",\n          \"href\": \"https://github.com/apache/apisix/blob/master/CODE_STYLE.md\"\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"internal\",\n          \"items\": [\n            \"internal/plugin-runner\",\n            \"internal/testing-framework\"\n          ]\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"plugin-develop\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"debug-mode\"\n        }\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"deployment-modes\"\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"FAQ\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"Others\",\n      \"items\": [\n        {\n          \"type\": \"category\",\n          \"label\": \"Discovery\",\n          \"items\": [\n            \"discovery\",\n            \"discovery/dns\",\n            \"discovery/consul\",\n            \"discovery/consul_kv\",\n            \"discovery/nacos\",\n            \"discovery/eureka\",\n            \"discovery/control-plane-service-discovery\",\n            \"discovery/kubernetes\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"PubSub\",\n          \"items\": [\n            \"pubsub\",\n            \"pubsub/kafka\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"xRPC\",\n          \"items\": [\n            \"xrpc/redis\",\n            \"xrpc\"\n          ]\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"router-radixtree\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"stream-proxy\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"grpc-proxy\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"customize-nginx-configuration\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"certificate\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"batch-processor\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"benchmark\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"install-dependencies\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"apisix-variable\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"aws\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"mtls\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"debug-function\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"profile\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"ssl-protocol\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"http3\"\n        }\n      ]\n    },\n    {\n      \"type\": \"link\",\n      \"label\": \"CHANGELOG\",\n      \"href\": \"https://github.com/apache/apisix/blob/master/CHANGELOG.md\"\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"upgrade-guide-from-2.15.x-to-3.0.0\"\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/en/latest/control-api.md",
    "content": "---\ntitle: Control API\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nIn Apache APISIX, the control API is used to:\n\n* Expose the internal state of APISIX.\n* Control the behavior of a single, isolated APISIX data plane.\n\nTo change the default endpoint (`127.0.0.1:9090`) of the Control API server, change the `ip` and `port` in the `control` section in your configuration file (`conf/config.yaml`):\n\n```yaml\napisix:\n  ...\n  enable_control: true\n  control:\n    ip: \"127.0.0.1\"\n    port: 9090\n```\n\nTo enable parameter matching in plugin's control API, add `router: 'radixtree_uri_with_parameter'` to the control section.\n\n**Note**: Never configure the control API server to listen to public traffic.\n\n## Control API Added via Plugins\n\n[Plugins](./terminology/plugin.md) can be enabled to add its control API.\n\nSome Plugins have their own control APIs. See the documentation of the specific Plugin to learn more.\n\n## Plugin Independent Control API\n\nThe supported APIs are listed below.\n\n### GET /v1/schema\n\nIntroduced in [v2.2](https://github.com/apache/apisix/releases/tag/2.2).\n\nReturns the JSON schema used by the APISIX instance:\n\n```json\n{\n    \"main\": {\n        \"route\": {\n            \"properties\": {...}\n        },\n        \"upstream\": {\n            \"properties\": {...}\n        },\n        ...\n    },\n    \"plugins\": {\n        \"example-plugin\": {\n            \"consumer_schema\": {...},\n            \"metadata_schema\": {...},\n            \"schema\": {...},\n            \"type\": ...,\n            \"priority\": 0,\n            \"version\": 0.1\n        },\n        ...\n    },\n    \"stream-plugins\": {\n        \"mqtt-proxy\": {\n            ...\n        },\n        ...\n    }\n}\n```\n\n**Note**: Only the enabled `plugins` are returned and they may lack fields like `consumer_schema` or `type` depending on how they were defined.\n\n### GET /v1/healthcheck\n\nIntroduced in [v2.3](https://github.com/apache/apisix/releases/tag/2.3).\n\nReturns a [health check](./tutorials/health-check.md) of the APISIX instance.\n\n```json\n[\n  {\n    \"nodes\": [\n      {\n        \"ip\": \"52.86.68.46\",\n        \"counter\": {\n          \"http_failure\": 0,\n          \"success\": 0,\n          \"timeout_failure\": 0,\n          \"tcp_failure\": 0\n        },\n        \"port\": 80,\n        \"status\": \"healthy\"\n      },\n      {\n        \"ip\": \"100.24.156.8\",\n        \"counter\": {\n          \"http_failure\": 5,\n          \"success\": 0,\n          \"timeout_failure\": 0,\n          \"tcp_failure\": 0\n        },\n        \"port\": 80,\n        \"status\": \"unhealthy\"\n      }\n    ],\n    \"name\": \"/apisix/routes/1\",\n    \"type\": \"http\"\n  }\n]\n\n```\n\nEach of the returned objects contain the following fields:\n\n* name: resource id, where the health checker is reporting from.\n* type: health check type: `[\"http\", \"https\", \"tcp\"]`.\n* nodes: target nodes of the health checker.\n* nodes[i].ip: ip address.\n* nodes[i].port: port number.\n* nodes[i].status: health check result: `[\"healthy\", \"unhealthy\", \"mostly_healthy\", \"mostly_unhealthy\"]`.\n* nodes[i].counter.success: success health check count.\n* nodes[i].counter.http_failure: http failures count.\n* nodes[i].counter.tcp_failure: tcp connect/read/write failures count.\n* nodes[i].counter.timeout_failure: timeout count.\n\nYou can also use `/v1/healthcheck/$src_type/$src_id` to get the health status of specific nodes.\n\nFor example, `GET /v1/healthcheck/upstreams/1` returns:\n\n```json\n{\n  \"nodes\": [\n    {\n      \"ip\": \"52.86.68.46\",\n      \"counter\": {\n        \"http_failure\": 0,\n        \"success\": 2,\n        \"timeout_failure\": 0,\n        \"tcp_failure\": 0\n      },\n      \"port\": 80,\n      \"status\": \"healthy\"\n    },\n    {\n      \"ip\": \"100.24.156.8\",\n      \"counter\": {\n        \"http_failure\": 5,\n        \"success\": 0,\n        \"timeout_failure\": 0,\n        \"tcp_failure\": 0\n      },\n      \"port\": 80,\n      \"status\": \"unhealthy\"\n    }\n  ],\n  \"type\": \"http\"\n  \"name\": \"/apisix/routes/1\"\n}\n\n```\n\n:::note\n\nOnly when one upstream is satisfied by the conditions below,\nits status is shown in the result list:\n\n* The upstream is configured with a health checker\n* The upstream has served requests in any worker process\n\n:::\n\nIf you use browser to access the control API URL, then you will get the HTML output:\n\n![Health Check Status Page](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/health_check_status_page.png)\n\n### POST /v1/gc\n\nIntroduced in [v2.8](https://github.com/apache/apisix/releases/tag/2.8).\n\nTriggers a full garbage collection in the HTTP subsystem.\n\n**Note**: When stream proxy is enabled, APISIX runs another Lua VM for the stream subsystem. Full garbage collection is not triggered in this VM.\n\n### GET /v1/routes\n\nIntroduced in [v2.10.0](https://github.com/apache/apisix/releases/tag/2.10.0).\n\nReturns all configured [Routes](./terminology/route.md):\n\n```json\n[\n  {\n    \"value\": {\n      \"priority\": 0,\n      \"uris\": [\n        \"/hello\"\n      ],\n      \"id\": \"1\",\n      \"upstream\": {\n        \"scheme\": \"http\",\n        \"pass_host\": \"pass\",\n        \"nodes\": [\n          {\n            \"port\": 1980,\n            \"host\": \"127.0.0.1\",\n            \"weight\": 1\n          }\n        ],\n        \"type\": \"roundrobin\",\n        \"hash_on\": \"vars\"\n      },\n      \"status\": 1\n    },\n    \"clean_handlers\": {},\n    \"has_domain\": false,\n    \"orig_modifiedIndex\": 1631193445,\n    \"modifiedIndex\": 1631193445,\n    \"key\": \"/routes/1\"\n  }\n]\n```\n\n### GET /v1/route/{route_id}\n\nIntroduced in [v2.10.0](https://github.com/apache/apisix/releases/tag/2.10.0).\n\nReturns the Route with the specified `route_id`:\n\n```json\n{\n  \"value\": {\n    \"priority\": 0,\n    \"uris\": [\n      \"/hello\"\n    ],\n    \"id\": \"1\",\n    \"upstream\": {\n      \"scheme\": \"http\",\n      \"pass_host\": \"pass\",\n      \"nodes\": [\n        {\n          \"port\": 1980,\n          \"host\": \"127.0.0.1\",\n          \"weight\": 1\n        }\n      ],\n      \"type\": \"roundrobin\",\n      \"hash_on\": \"vars\"\n    },\n    \"status\": 1\n  },\n  \"clean_handlers\": {},\n  \"has_domain\": false,\n  \"orig_modifiedIndex\": 1631193445,\n  \"modifiedIndex\": 1631193445,\n  \"key\": \"/routes/1\"\n}\n```\n\n### GET /v1/services\n\nIntroduced in [v2.11.0](https://github.com/apache/apisix/releases/tag/2.11.0).\n\nReturns all the Services:\n\n```json\n[\n  {\n    \"has_domain\": false,\n    \"clean_handlers\": {},\n    \"modifiedIndex\": 671,\n    \"key\": \"/apisix/services/200\",\n    \"createdIndex\": 671,\n    \"value\": {\n      \"upstream\": {\n          \"scheme\": \"http\",\n          \"hash_on\": \"vars\",\n          \"pass_host\": \"pass\",\n          \"type\": \"roundrobin\",\n          \"nodes\": [\n            {\n              \"port\": 1980,\n              \"weight\": 1,\n              \"host\": \"127.0.0.1\"\n            }\n          ]\n      },\n      \"create_time\": 1634552648,\n      \"id\": \"200\",\n      \"plugins\": {\n        \"limit-count\": {\n          \"key\": \"remote_addr\",\n          \"time_window\": 60,\n          \"redis_timeout\": 1000,\n          \"allow_degradation\": false,\n          \"show_limit_quota_header\": true,\n          \"policy\": \"local\",\n          \"count\": 2,\n          \"rejected_code\": 503\n        }\n      },\n      \"update_time\": 1634552648\n    }\n  }\n]\n```\n\n### GET /v1/service/{service_id}\n\nIntroduced in [v2.11.0](https://github.com/apache/apisix/releases/tag/2.11.0).\n\nReturns the Service with the specified `service_id`:\n\n```json\n{\n  \"has_domain\": false,\n  \"clean_handlers\": {},\n  \"modifiedIndex\": 728,\n  \"key\": \"/apisix/services/5\",\n  \"createdIndex\": 728,\n  \"value\": {\n    \"create_time\": 1634554563,\n    \"id\": \"5\",\n    \"upstream\": {\n      \"scheme\": \"http\",\n      \"hash_on\": \"vars\",\n      \"pass_host\": \"pass\",\n      \"type\": \"roundrobin\",\n      \"nodes\": [\n        {\n          \"port\": 1980,\n          \"weight\": 1,\n          \"host\": \"127.0.0.1\"\n        }\n      ]\n    },\n    \"update_time\": 1634554563\n  }\n}\n```\n\n### GET /v1/upstreams\n\nIntroduced in [v2.11.0](https://github.com/apache/apisix/releases/tag/2.11.0).\n\nDumps all Upstreams:\n\n```json\n[\n   {\n      \"value\":{\n         \"scheme\":\"http\",\n         \"pass_host\":\"pass\",\n         \"nodes\":[\n            {\n               \"host\":\"127.0.0.1\",\n               \"port\":80,\n               \"weight\":1\n            },\n            {\n               \"host\":\"foo.com\",\n               \"port\":80,\n               \"weight\":2\n            }\n         ],\n         \"hash_on\":\"vars\",\n         \"update_time\":1634543819,\n         \"key\":\"remote_addr\",\n         \"create_time\":1634539759,\n         \"id\":\"1\",\n         \"type\":\"chash\"\n      },\n      \"has_domain\":true,\n      \"key\":\"\\/apisix\\/upstreams\\/1\",\n      \"clean_handlers\":{\n      },\n      \"createdIndex\":938,\n      \"modifiedIndex\":1225\n   }\n]\n```\n\n### GET /v1/upstream/{upstream_id}\n\nIntroduced in [v2.11.0](https://github.com/apache/apisix/releases/tag/2.11.0).\n\nDumps the Upstream with the specified `upstream_id`:\n\n```json\n{\n   \"value\":{\n      \"scheme\":\"http\",\n      \"pass_host\":\"pass\",\n      \"nodes\":[\n         {\n            \"host\":\"127.0.0.1\",\n            \"port\":80,\n            \"weight\":1\n         },\n         {\n            \"host\":\"foo.com\",\n            \"port\":80,\n            \"weight\":2\n         }\n      ],\n      \"hash_on\":\"vars\",\n      \"update_time\":1634543819,\n      \"key\":\"remote_addr\",\n      \"create_time\":1634539759,\n      \"id\":\"1\",\n      \"type\":\"chash\"\n   },\n   \"has_domain\":true,\n   \"key\":\"\\/apisix\\/upstreams\\/1\",\n   \"clean_handlers\":{\n   },\n   \"createdIndex\":938,\n   \"modifiedIndex\":1225\n}\n```\n\n### GET /v1/plugin_metadatas\n\nIntroduced in [v3.0.0](https://github.com/apache/apisix/releases/tag/3.0.0).\n\nDumps all plugin_metadatas:\n\n```json\n[\n    {\n        \"log_format\": {\n            \"upstream_response_time\": \"$upstream_response_time\"\n        },\n        \"id\": \"file-logger\"\n    },\n    {\n        \"ikey\": 1,\n        \"skey\": \"val\",\n        \"id\": \"example-plugin\"\n    }\n]\n```\n\n### GET /v1/plugin_metadata/{plugin_name}\n\nIntroduced in [v3.0.0](https://github.com/apache/apisix/releases/tag/3.0.0).\n\nDumps the metadata with the specified `plugin_name`:\n\n```json\n{\n    \"log_format\": {\n        \"upstream_response_time\": \"$upstream_response_time\"\n    },\n    \"id\": \"file-logger\"\n}\n```\n\n### PUT /v1/plugins/reload\n\nIntroduced in [v3.9.0](https://github.com/apache/apisix/releases/tag/3.9.0)\n\nTriggers a hot reload of the plugins.\n\n```shell\ncurl \"http://127.0.0.1:9090/v1/plugins/reload\" -X PUT\n```\n\n### GET /v1/discovery/{service}/dump\n\nGet memory dump of discovered service endpoints and configuration details:\n\n```json\n{\n  \"endpoints\": [\n    {\n      \"endpoints\": [\n        {\n          \"value\": \"{\\\"https\\\":[{\\\"host\\\":\\\"172.18.164.170\\\",\\\"port\\\":6443,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.171\\\",\\\"port\\\":6443,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.172\\\",\\\"port\\\":6443,\\\"weight\\\":50}]}\",\n          \"name\": \"default/kubernetes\"\n        },\n        {\n          \"value\": \"{\\\"metrics\\\":[{\\\"host\\\":\\\"172.18.164.170\\\",\\\"port\\\":2379,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.171\\\",\\\"port\\\":2379,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.172\\\",\\\"port\\\":2379,\\\"weight\\\":50}]}\",\n          \"name\": \"kube-system/etcd\"\n        },\n        {\n          \"value\": \"{\\\"http-85\\\":[{\\\"host\\\":\\\"172.64.89.2\\\",\\\"port\\\":85,\\\"weight\\\":50}]}\",\n          \"name\": \"test-ws/testing\"\n        }\n      ],\n      \"id\": \"first\"\n    }\n  ],\n  \"config\": [\n    {\n      \"default_weight\": 50,\n      \"id\": \"first\",\n      \"client\": {\n        \"token\": \"xxx\"\n      },\n      \"service\": {\n        \"host\": \"172.18.164.170\",\n        \"port\": \"6443\",\n        \"schema\": \"https\"\n      },\n      \"shared_size\": \"1m\"\n    }\n  ]\n}\n```\n\n## GET /v1/discovery/{service}/show_dump_file\n\nGet configured services details.\n\n```json\n{\n  \"services\": {\n    \"service_a\": [\n      {\n        \"host\": \"172.19.5.12\",\n        \"port\": 8000,\n        \"weight\": 120\n      },\n      {\n        \"host\": \"172.19.5.13\",\n        \"port\": 8000,\n        \"weight\": 120\n      }\n    ]\n  },\n  \"expire\": 0,\n  \"last_update\": 1615877468\n}\n```\n"
  },
  {
    "path": "docs/en/latest/customize-nginx-configuration.md",
    "content": "---\ntitle: Customize Nginx configuration\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThe Nginx configuration used by APISIX is generated via the template file `apisix/cli/ngx_tpl.lua` and the parameters in `apisix/cli/config.lua` and `conf/config.yaml`.\n\nYou can take a look at the generated Nginx configuration in `conf/nginx.conf` after running `./bin/apisix start`.\n\nIf you want to customize the Nginx configuration, please read through the `nginx_config` in `conf/config.default.example`. You can override the default value in the `conf/config.yaml`. For instance, you can inject some snippets in the `conf/nginx.conf` via configuring the `xxx_snippet` entries:\n\n```yaml\n...\n# put this in config.yaml:\nnginx_config:\n    main_configuration_snippet: |\n        daemon on;\n    http_configuration_snippet: |\n        server\n        {\n            listen 45651;\n            server_name _;\n            access_log off;\n\n            location /ysec_status {\n                req_status_show;\n                allow 127.0.0.1;\n                deny all;\n            }\n        }\n\n        chunked_transfer_encoding on;\n\n    http_server_configuration_snippet: |\n        set $my \"var\";\n    http_admin_configuration_snippet: |\n        log_format admin \"$request_time $pipe\";\n    http_end_configuration_snippet: |\n        server_names_hash_bucket_size 128;\n    stream_configuration_snippet: |\n        tcp_nodelay off;\n...\n```\n\nPay attention to the indent of `nginx_config` and sub indent of the sub entries, the incorrect indent may cause `./bin/apisix start` to fail to generate Nginx configuration in `conf/nginx.conf`.\n"
  },
  {
    "path": "docs/en/latest/dashboard.md",
    "content": "---\ntitle: Apache APISIX Dashboard\nid: dashboard\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Overview\n\n[Apache APISIX Dashboard](https://github.com/apache/apisix-dashboard) provides users with an intuitive web interface to operate and manage Apache APISIX. APISIX has a built-in Dashboard UI that is enabled by default, allowing users to easily configure routes, plugins, upstream services, and more through a graphical interface.\n\n## Configuring Dashboard\n\n### Enable or Disable Dashboard\n\nApache APISIX enables the embedded Dashboard by default. To modify this setting, please edit the `conf/config.yaml` file:\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    # Enable embedded APISIX Dashboard\n    enable_admin_ui: true\n```\n\n**Configuration Description:**\n\n- `enable_admin_ui: true` - Enable embedded Dashboard (enabled by default)\n- `enable_admin_ui: false` - Disable embedded Dashboard\n\nAfter modifying the configuration, restart Apache APISIX for changes to take effect.\n\n### Restrict IP Access\n\nApache APISIX supports setting an IP access whitelist for the Admin API to prevent unauthorized access and attacks on Apache APISIX.\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n    admin:\n        # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow\n        allow_admin:\n            - 127.0.0.0/24\n```\n\n### Admin API Key\n\nThe Dashboard interacts with Apache APISIX through the Admin API and requires a correct Admin API Key for authentication.\n\n#### Configuration\n\nConfigure the Admin API Key in `conf/config.yaml`:\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n      -\n        name: admin\n        role: admin\n        # Using a simple Admin API Key poses security risks. Please update it when deploying to production\n        key: edd1c9f034335f136f87ad84b625c8f1\n```\n\nConfiguration via environment variables is also supported:\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n      - name: admin\n        # Read from environment variable\n        key: ${{ADMIN_KEY}}\n        role: admin\n```\n\nSet the environment variable before use:\n\n```bash\nexport ADMIN_KEY=your-secure-api-key\n```\n\nRestart Apache APISIX after modifying the configuration for changes to take effect.\n\n#### Using in Dashboard\n\nAccess the Dashboard, for example at `http://127.0.0.1:9180/ui/`.\n\nWhen the Admin API Key is not configured, the settings modal will pop up:\n\n![Apache APISIX Dashboard - Need Admin Key](../../assets/images/dashboard-need-admin-key.png)\n\nIf you accidentally close the settings modal, you can click the button <img src=\"../../assets/images/dashboard-settings-btn-icon.png\" alt=\"Apache APISIX Dashboard - Settings btn icon\" width=\"25px\" /> on the right side of the navigation bar to reopen it.\n\n![Apache APISIX Dashboard - Reopen Settings Modal](../../assets/images/dashboard-reopen-settings-modal.png)\n\nNext, enter the Admin API Key configured in the previous section. The Dashboard will automatically make a request. If configured incorrectly, the Dashboard will still display `failed to check token` in the upper right corner:\n\n![Apache APISIX Dashboard - Admin Key is wrong](../../assets/images/dashboard-admin-key-is-wrong.png)\n\nIf configured correctly, the Dashboard will no longer display `failed to check token`. At this point, click `X` or the blank area to close the settings modal and use normally.\n\n![Apache APISIX Dashboard - Admin Key is correct](../../assets/images/dashboard-admin-key-is-correct.png)\n\n## FAQ\n\n### Why was Apache APISIX Dashboard refactored?\n\nApache APISIX Dashboard has evolved through multiple versions:\n\n- **Version 1.x**: A simple Web UI based on Vue.js that directly called the Admin API\n- **Version 2.x**: Adopted React + Ant Design Pro frontend architecture, introducing a Golang backend and database storage\n\nDuring the development of version 2.x, as community demand for features continued to increase, the project gradually became complex and bloated, while synchronization with the main APISIX version also faced challenges.\n\nAfter thorough discussion, the community decided to clarify the Dashboard's positioning and functional boundaries, returning to a lightweight design to ensure tight integration and version synchronization with the APISIX core.\n\nFuture Apache APISIX Dashboard will focus on:\n\n- **Simplified Architecture**: Remove unnecessary complex components and return to the Dashboard's essential functions\n- **Enhanced User Experience**: Provide an intuitive and efficient management interface\n- **Version Synchronization**: Maintain synchronized releases with Apache APISIX main versions\n- **Production Ready**: Ensure stability and reliability, suitable for production environments\n\nFor more planning information, please see: [Dashboard Roadmap](https://github.com/apache/apisix-dashboard/issues/2981)\n\n### Release Cycles\n\nThe project no longer releases independently and has deprecated the release and tag versioning approach.\n\nWhen Apache APISIX is released, the Dashboard will be built directly based on a specified Git commit hash, and the artifacts will be embedded into Apache APISIX.\n\n### Legacy Apache APISIX Dashboard\n\nApache APISIX Dashboard 3.0.1 is the last version before the refactoring that used the old release model. It should only be used with Apache APISIX 3.0, as any higher or lower versions have not been tested.\n\nIf needed, you can read the [Legacy Apache APISIX Dashboard Documentation](https://apache-apisix.netlify.app/docs/dashboard/user_guide/).\n\nIf you are a new user of Apache APISIX or Apache APISIX Dashboard, we strongly recommend that you always start with the latest version rather than any historical version.\n\n### Contributing Guide\n\nFor details, please read the [Apache APISIX Dashboard README](https://github.com/apache/apisix-dashboard/blob/master/README.md).\n"
  },
  {
    "path": "docs/en/latest/debug-function.md",
    "content": "---\ntitle: Debug Function\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## `5xx` response status code\n\nSimilar `5xx` status codes such as 500, 502, 503, etc., are the status codes in response to a server error. When a request has a `5xx` status code; it may come from `APISIX` or `Upstream`. How to identify the source of these response status codes is a very meaningful thing. It can quickly help us determine the problem. (When modifying the configuration `show_upstream_status_in_response_header` in `conf/config.yaml` to `true`, all upstream status codes will be returned, not only `5xx` status.)\n\n## How to identify the source of the `5xx` response status code\n\nIn the response header of the request, through the response header of `X-APISIX-Upstream-Status`, we can effectively identify the source of the `5xx` status code. When the `5xx` status code comes from `Upstream`, the response header `X-APISIX-Upstream-Status` can be seen in the response header, and the value of this response header is the response status code. When the `5xx` status code is derived from `APISIX`, there is no response header information of `X-APISIX-Upstream-Status` in the response header. That is, only when the status code of `5xx` is derived from Upstream will the `X-APISIX-Upstream-Status` response header appear.\n\n## Example\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n>Example 1: `502` response status code comes from `Upstream` (IP address is not available)\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nTest:\n\n```shell\n$ curl http://127.0.0.1:9080/hello -v\n......\n< HTTP/1.1 502 Bad Gateway\n< Date: Wed, 25 Nov 2020 14:40:22 GMT\n< Content-Type: text/html; charset=utf-8\n< Content-Length: 154\n< Connection: keep-alive\n< Server: APISIX/2.0\n< X-APISIX-Upstream-Status: 502\n<\n<html>\n<head><title>502 Bad Gateway</title></head>\n<body>\n<center><h1>502 Bad Gateway</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n\n```\n\nIt has a response header of `X-APISIX-Upstream-Status: 502`.\n\n>Example 2: `502` response status code comes from `APISIX`\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"fault-injection\": {\n            \"abort\": {\n                \"http_status\": 500,\n                \"body\": \"Fault Injection!\\n\"\n            }\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nTest：\n\n```shell\n$ curl http://127.0.0.1:9080/hello -v\n......\n< HTTP/1.1 500 Internal Server Error\n< Date: Wed, 25 Nov 2020 14:50:20 GMT\n< Content-Type: text/plain; charset=utf-8\n< Transfer-Encoding: chunked\n< Connection: keep-alive\n< Server: APISIX/2.0\n<\nFault Injection!\n```\n\nThere is no response header for `X-APISIX-Upstream-Status`.\n\n>Example 3: `Upstream` has multiple nodes, and all nodes are unavailable\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"nodes\": {\n        \"127.0.0.3:1\": 1,\n        \"127.0.0.2:1\": 1,\n        \"127.0.0.1:1\": 1\n    },\n    \"retries\": 2,\n    \"type\": \"roundrobin\"\n}'\n```\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream_id\": \"1\"\n}'\n```\n\nTest：\n\n```shell\n$ curl http://127.0.0.1:9080/hello -v\n< HTTP/1.1 502 Bad Gateway\n< Date: Wed, 25 Nov 2020 15:07:34 GMT\n< Content-Type: text/html; charset=utf-8\n< Content-Length: 154\n< Connection: keep-alive\n< Server: APISIX/2.0\n< X-APISIX-Upstream-Status: 502, 502, 502\n<\n<html>\n<head><title>502 Bad Gateway</title></head>\n<body>\n<center><h1>502 Bad Gateway</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\nIt has a response header of `X-APISIX-Upstream-Status: 502, 502, 502`.\n"
  },
  {
    "path": "docs/en/latest/debug-mode.md",
    "content": "---\nid: debug-mode\ntitle: Debug mode\nkeywords:\n  - API gateway\n  - Apache APISIX\n  - Debug mode\ndescription: Guide for enabling debug mode in Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nYou can use APISIX's debug mode to troubleshoot your configuration.\n\n## Basic debug mode\n\nYou can enable the basic debug mode by adding this line to your debug configuration file (`conf/debug.yaml`):\n\n```yaml title=\"conf/debug.yaml\"\nbasic:\n  enable: true\n#END\n```\n\nAPISIX loads the configurations of `debug.yaml` on startup and then checks if the file is modified on an interval of 1 second. If the file is changed, APISIX automatically applies the configuration changes.\n\n:::note\n\nFor APISIX releases prior to v2.10, basic debug mode is enabled by setting `apisix.enable_debug = true` in your configuration file (`conf/config.yaml`).\n\n:::\n\nIf you have configured two Plugins `limit-conn` and `limit-count` on the Route `/hello`, you will receive a response with the header `Apisix-Plugins: limit-conn, limit-count` when you enable the basic debug mode.\n\n```shell\ncurl http://127.0.0.1:1984/hello -i\n```\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nApisix-Plugins: limit-conn, limit-count\nX-RateLimit-Limit: 2\nX-RateLimit-Remaining: 1\nServer: openresty\n\nhello world\n```\n\n:::info IMPORTANT\n\nIf the debug information cannot be included in a response header (for example, when the Plugin is in a stream subsystem), the debug information will be logged as an error log at a `warn` level.\n\n:::\n\n## Advanced debug mode\n\nYou can configure advanced options in debug mode by modifying your debug configuration file (`conf/debug.yaml`).\n\nThe following configurations are available:\n\n| Key                             | Required | Default | Description                                                                                                           |\n|---------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------|\n| hook_conf.enable                | True     | false   | Enables/disables hook debug trace. i.e. if enabled, will print the target module function's inputs or returned value. |\n| hook_conf.name                  | True     |         | Module list name of the hook that enabled the debug trace.                                                            |\n| hook_conf.log_level             | True     | warn    | Log level for input arguments & returned values.                                                                      |\n| hook_conf.is_print_input_args   | True     | true    | When set to `true` enables printing input arguments.                                                                  |\n| hook_conf.is_print_return_value | True     | true    | When set to `true` enables printing returned values.                                                                  |\n\n:::note\n\nA checker would check every second for changes to the configuration file. It will only check a file if the file was updated based on its last modification time.\n\nYou can add an `#END` flag to indicate to the checker to only look for changes until that point.\n\n:::\n\nThe example below shows how you can configure advanced options in debug mode:\n\n```yaml title=\"conf/debug.yaml\"\nhook_conf:\n  enable: false # Enables/disables hook debug trace\n  name: hook_phase # Module list name of the hook that enabled the debug trace\n  log_level: warn # Log level for input arguments & returned values\n  is_print_input_args: true # When set to `true` enables printing input arguments\n  is_print_return_value: true # When set to `true` enables printing returned values\n\nhook_phase: # Module function list, Name: hook_phase\n  apisix: # Referenced module name\n    - http_access_phase # Function names：Array\n    - http_header_filter_phase\n    - http_body_filter_phase\n    - http_log_phase\n#END\n```\n\n### Dynamically enable advanced debug mode\n\nYou can also enable advanced debug mode only on particular requests.\n\nThe example below shows how you can enable it on requests with the header `X-APISIX-Dynamic-Debug`:\n\n```yaml title=\"conf/debug.yaml\"\nhttp_filter:\n  enable: true # Enable/disable advanced debug mode dynamically\n  enable_header_name: X-APISIX-Dynamic-Debug # Trace for the request with this header\n...\n#END\n```\n\nThis will enable the advanced debug mode only for requests like:\n\n```shell\ncurl 127.0.0.1:9090/hello --header 'X-APISIX-Dynamic-Debug: foo'\n```\n\n:::note\n\nThe `apisix.http_access_phase` module cannot be hooked for this dynamic rule as the advanced debug mode is enabled based on the request.\n\n:::\n"
  },
  {
    "path": "docs/en/latest/deployment-modes.md",
    "content": "---\ntitle: Deployment modes\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - APISIX deployment modes\ndescription: Documentation about the three deployment modes of Apache APISIX.\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nAPISIX has three different deployment modes for different production use cases. The table below summarises the deployment modes:\n\n| Deployment mode | Roles                      | Description                                                                                                         |\n|-----------------|----------------------------|---------------------------------------------------------------------------------------------------------------------|\n| traditional     | traditional                | Data plane and control plane are deployed together. `enable_admin` attribute should be disabled manually.           |\n| decoupled       | data_plane / control_plane | Data plane and control plane are deployed independently.                                                            |\n| standalone      | data_plane / traditional   | The `data_plane` mode loads configuration from a local YAML / JSON file, while the traditional mode expects configuration through Admin API.   |\n\nEach of these deployment modes are explained in detail below.\n\n## Traditional\n\nIn the traditional deployment mode, one instance of APISIX will be both the `data_plane` and the `control_plane`.\n\nAn example configuration of the traditional deployment mode is shown below:\n\n```yaml title=\"conf/config.yaml\"\napisix:\n    node_listen:\n        - port: 9080\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    admin:\n        admin_listen:\n            port: 9180\n    etcd:\n       host:\n           - http://${etcd_IP}:${etcd_Port}\n       prefix: /apisix\n       timeout: 30\n#END\n```\n\nThe instance of APISIX deployed as the traditional role will:\n\n1. Listen on port `9080` to handle user requests, controlled by `node_listen`.\n2. Listen on port `9180` to handle Admin API requests, controlled by `admin_listen`.\n\n## Decoupled\n\nIn the decoupled deployment mode the `data_plane` and `control_plane` instances of APISIX are deployed separately, i.e., one instance of APISIX is configured to be a *data plane* and the other to be a *control plane*.\n\nThe instance of APISIX deployed as the data plane will:\n\nOnce the service is started, it will handle the user requests.\n\nThe example below shows the configuration of an APISIX instance as *data plane* in the decoupled mode:\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n       config_provider: etcd\n    etcd:\n       host:\n           - https://${etcd_IP}:${etcd_Port}\n#END\n```\n\nThe instance of APISIX deployed as the control plane will:\n\n1. Listen on port `9180` and handle Admin API requests.\n\nThe example below shows the configuration of an APISIX instance as *control plane* in the decoupled mode:\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n    role: control_plane\n    role_control_plane:\n        config_provider: etcd\n    etcd:\n       host:\n           - https://${etcd_IP}:${etcd_Port}\n       prefix: /apisix\n       timeout: 30\n#END\n```\n\n## Standalone\n\nTurning on the APISIX node in Standalone mode will no longer use the default etcd as the configuration center.\n\nThis method is more suitable for two types of users:\n\n1. Kubernetes(k8s)：Declarative API that dynamically updates the routing rules with a full yaml configuration.\n2. Different configuration centers: There are many implementations of the configuration center, such as Consul, etc., using the full yaml file for intermediate conversion.\n\n### Modes\n\nNow, we have two standalone running modes, file-driven and API-driven.\n\n#### File-driven\n\nThe file-driven mode is the kind APISIX has always supported.\n\nThe routing rules in the `conf/apisix.yaml` file are loaded into memory immediately after the APISIX node service starts. At each interval (default: 1 second), APISIX checks for updates to the file. If changes are detected, it reloads the rules.\n\n*Note*: Reloading and updating routing rules are all hot memory updates. There is no replacement of working processes, since it's a hot update.\n\nThis requires us to set the APISIX role to data plane. That is, set `deployment.role` to `data_plane` and `deployment.role_data_plane.config_provider` to `yaml`.\n\nRefer to the example below:\n\n```yaml\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n```\n\nYou can also provide the configuration in JSON format by placing it in `conf/apisix.json`. Before proceeding, you should change the `deployment.role_data_plane.config_provider` to `json`.\n\nRefer to the example below:\n\n```yaml\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: json\n```\n\nThis makes it possible to disable the Admin API and discover configuration changes and reloads based on the local file system.\n\n#### API-driven\n\nThe API-drive standalone mode is designed specifically for the APISIX Ingress Controller and is primarily intended for integration with ADC. APISIX provides an official, end-to-end, stateless Ingress Controller implementation. Do not use this feature directly unless you fully understand its internal workings and behavior.\n\n##### Overview\n\nAPI-driven mode is an emerging paradigm for standalone deployment, where routing rules are stored entirely in memory rather than in a configuration file. Updates must be made through the dedicated Standalone Admin API. Each update replaces the full configuration and takes effect immediately through hot updates, without requiring a restart.\n\n##### Configuration\n\nTo enable this mode, set the APISIX role to `traditional` (to start both the API gateway and the Admin API endpoint) and use the YAML config provider. Example configuration:\n\n```yaml\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: yaml\n```\n\nThis disables the local file source of configuration in favor of the API. When APISIX starts, it uses an empty configuration until updated via the API.\n\n##### API Endpoints\n\n* `conf_version` by resource type\n\n    Use `<resource>_conf_version` to indicate the client’s current version for each resource type (e.g. routes, upstreams, services, etc.).\n\n    ```json\n    {\n      \"routes_conf_version\": 12,\n      \"upstreams_conf_version\": 102,\n      \"routes\": [],\n      \"upstreams\": []\n    }\n    ```\n\n    APISIX compares each provided `<resource>_conf_version` against its in-memory `<resource>_conf_version` for that resource type. If the provided `<resource>_conf_version` is:\n\n  - **Greater than** the current `conf_version`, APISIX will **rebuild/reset** that resource type’s data to match your payload.\n\n  - **Equal to** the current `conf_version`, APISIX treats the resource as **unchanged** and **ignores** it (no data is rebuilt).\n\n  - **Less than** the current `conf_version`, APISIX considers your update **stale** and **rejects** the request for that resource type with a **400 Bad Request**.\n\n* `modifiedIndex` by individual resource\n\n    Allow setting an index for each resource. APISIX compares this index to its modifiedIndex to determine whether to accept the update.\n\n##### Example\n\n1. get configuration\n\n```shell\ncurl -X GET http://127.0.0.1:9180/apisix/admin/configs \\\n    -H \"X-API-KEY: <apikey>\" \\\n    -H \"Accept: application/json\" ## or application/yaml\n```\n\nThis returns the current configuration in JSON or YAML format.\n\n```json\n{\n    \"consumer_groups_conf_version\": 0,\n    \"consumers_conf_version\": 0,\n    \"global_rules_conf_version\": 0,\n    \"plugin_configs_conf_version\": 0,\n    \"plugin_metadata_conf_version\": 0,\n    \"protos_conf_version\": 0,\n    \"routes_conf_version\": 0,\n    \"secrets_conf_version\": 0,\n    \"services_conf_version\": 0,\n    \"ssls_conf_version\": 0,\n    \"upstreams_conf_version\": 0\n}\n```\n\n2. full update\n\n```shell\ncurl -X PUT http://127.0.0.1:9180/apisix/admin/configs \\\n    -H \"X-API-KEY: <apikey>\" \\\n    -H \"Content-Type: application/json\" ## or application/yaml \\\n    -H \"X-Digest: example_string#1\" \\\n    -d '{}'\n```\n\n:::note\n\nThe X-Digest in the request header, which is an arbitrary string that indicates to APISIX the characteristics of the current configuration version. When the value in the new request is the same as the configuration version already loaded by APISIX, APISIX skips this update.\n\nThis allows the client to determine and exclude certain unnecessary update requests. For example, the client can calculate a hash digest of the configuration and send it to APISIX; if two update requests contain the same hash digest, APISIX will not update the configuration.\n\nThe client can determine its content. The value is transparent to APISIX and will not be parsed and used for any purpose.\n\n:::\n\n3. update based on resource type\n\nIn APISIX memory, the current configuration is:\n\n```json\n{\n    \"routes_conf_version\": 1000,\n    \"upstreams_conf_version\": 1000,\n}\n```\n\nUpdate the previous upstreams configuration by setting a higher version number, such as 1001, to replace the current version 1000:\n\n```shell\ncurl -X PUT http://127.0.0.1:9180/apisix/admin/configs \\\n  -H \"X-API-KEY: ${API_KEY}\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Digest: example_string#2\" \\\n  -d '\n{\n    \"routes_conf_version\": 1000,\n    \"upstreams_conf_version\": 1001,\n    \"routes\": [\n        {\n            \"modifiedIndex\": 1000,\n            \"id\": \"r1\",\n            \"uri\": \"/hello\",\n            \"upstream_id\": \"u1\"\n        }\n    ],\n    \"upstreams\": [\n        {\n            \"modifiedIndex\": 1001,\n            \"id\": \"u1\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1,\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        }\n    ]\n}'\n```\n\n:::note\n\nThese APIs apply the same security requirements as the Admin API, including API key, TLS/mTLS, CORS, and IP allowlist.\n\nThe API accepts input in the same format as the file-based mode, supporting both JSON and YAML. Unlike the file-based mode, the API does not rely on the `#END` suffix, as HTTP guarantees input integrity.\n\n:::\n\n### How to configure rules\n\n#### To `config_provider: yaml`\n\nAll of the rules are stored in one file which named `conf/apisix.yaml`,\nAPISIX checks if this file has any change **every second**.\nIf the file is changed & it ends with `#END`,\nAPISIX loads the rules from this file and updates its memory.\n\nHere is a mini example:\n\n```yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n```\n\n*WARNING*: APISIX will not load the rules into memory from file `conf/apisix.yaml` if there is no `#END` at the end.\n\nEnvironment variables can also be used like so:\n\n```yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"${{UPSTREAM_ADDR}}\": 1\n        type: roundrobin\n#END\n```\n\n*WARNING*: When using docker to deploy APISIX in standalone mode. New environment variables added to `apisix.yaml` while APISIX has been initialized will only take effect after a reload.\n\nMore information about using environment variables can be found [here](./admin-api.md#using-environment-variables).\n\n#### To `config_provider: json`\n\nAll of the rules are stored in one file which named `conf/apisix.json`,\nAPISIX checks if this file has any change **every second**.\nIf the file is changed,\nAPISIX loads the rules from this file and updates its memory.\n\nHere is a mini example:\n\n```json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n```\n\n*WARNING*: when using `conf/apisix.json`, the `#END` marker is not required, as APISIX can directly parse and validate the JSON structure.\n\n### How to configure Route\n\nSingle Route：\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\nMultiple Routes：\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n  -\n    uri: /hello2\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    },\n    {\n      \"uri\": \"/hello2\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1981\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure Route + Service\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nroutes:\n    -\n        uri: /hello\n        service_id: 1\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"service_id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure Route + Upstream\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nroutes:\n    -\n        uri: /hello\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure Route + Service + Upstream\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nroutes:\n    -\n        uri: /hello\n        service_id: 1\nservices:\n    -\n        id: 1\n        upstream_id: 2\nupstreams:\n    -\n        id: 2\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"service_id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream_id\": 2\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 2,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure Plugins\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\n# plugins listed here will be hot reloaded and override the boot configuration\nplugins:\n  - name: ip-restriction\n  - name: jwt-auth\n  - name: mqtt-proxy\n    stream: true # set 'stream' to true for stream plugins\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"plugins\": [\n    {\n      \"name\": \"ip-restriction\"\n    },\n    {\n      \"name\": \"jwt-auth\"\n    },\n    {\n      \"name\": \"mqtt-proxy\",\n      \"stream\": true\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure Plugin Configs\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nplugin_configs:\n    -\n        id: 1\n        plugins:\n            response-rewrite:\n                body: \"hello\\n\"\nroutes:\n    - id: 1\n      uri: /hello\n      plugin_config_id: 1\n      upstream:\n        nodes:\n          \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"plugin_configs\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugin_config_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to enable SSL\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nssls:\n    -\n        cert: |\n            -----BEGIN CERTIFICATE-----\n            MIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA0GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\n            s9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\n            tdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\n            D44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\n            NFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\n            quDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\n            bnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\n            MB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\n            DQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\n            qvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\n            rAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\n            HCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\n            geAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\n            2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\n            -----END CERTIFICATE-----\n        key: |\n            -----BEGIN PRIVATE KEY-----\n            MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\n            lZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\n            FF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\n            Exnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\n            uhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n            5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\n            cyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\n            5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\n            BctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\n            0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\n            SXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\n            gf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\n            SF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\n            yLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\n            2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\n            g0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\n            QS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\n            L/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\n            LR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\n            7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\n            t1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\n            be7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\n            V3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\n            zAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\n            IeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\n            r8yiEiskqRmy7P7MY9hDmEbG\n            -----END PRIVATE KEY-----\n        snis:\n            - \"yourdomain.com\"\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"ssls\": [\n    {\n      \"cert\": \"-----BEGIN CERTIFICATE-----\\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\\ns9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\\ntdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\\nD44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\\nNFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\\nquDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\\nbnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\\nMB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\\nDQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\\nqvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\\nrAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\\nHCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\\ngeAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\\n2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\\n-----END CERTIFICATE-----\",\n      \"key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\\n5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\\nBctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\\n0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\\nSXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\\ngf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\\nSF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\\nyLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\\n2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\\ng0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\\nQS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\\nL/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\\nLR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\\n7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\\nt1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\\nbe7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\\nV3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\\nzAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\\nIeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\\nr8yiEiskqRmy7P7MY9hDmEbG\\n-----END PRIVATE KEY-----\",\n      \"snis\": [\n        \"yourdomain.com\"\n      ]\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure global rule\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nglobal_rules:\n    -\n        id: 1\n        plugins:\n            response-rewrite:\n                body: \"hello\\n\"\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"global_rules\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure consumer\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nconsumers:\n  - username: jwt\n    plugins:\n        jwt-auth:\n            key: user-key\n            secret: my-secret-key\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"consumers\": [\n    {\n      \"username\": \"jwt\",\n      \"plugins\": {\n        \"jwt-auth\": {\n          \"key\": \"user-key\",\n          \"secret\": \"my-secret-key\"\n        }\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure plugin metadata\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nupstreams:\n  - id: 1\n    nodes:\n      \"127.0.0.1:1980\": 1\n    type: roundrobin\nroutes:\n  -\n    uri: /hello\n    upstream_id: 1\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\nplugin_metadata:\n  - id: http-logger # note the id is the plugin name\n    log_format:\n        host: \"$host\"\n        remote_addr: \"$remote_addr\"\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ],\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1,\n      \"plugins\": {\n        \"http-logger\": {\n          \"batch_max_size\": 1,\n          \"uri\": \"http://127.0.0.1:1980/log\"\n        }\n      }\n    }\n  ],\n  \"plugin_metadata\": [\n    {\n      \"id\": \"http-logger\",\n      \"log_format\": {\n        \"host\": \"$host\",\n        \"remote_addr\": \"$remote_addr\"\n      }\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure stream route\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream_id: 1\n    plugins:\n      mqtt-proxy:\n        protocol_name: \"MQTT\"\n        protocol_level: 4\nupstreams:\n  - nodes:\n      \"127.0.0.1:1995\": 1\n    type: roundrobin\n    id: 1\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"stream_routes\": [\n    {\n      \"server_addr\": \"127.0.0.1\",\n      \"server_port\": 1985,\n      \"id\": 1,\n      \"upstream_id\": 1,\n      \"plugins\": {\n        \"mqtt-proxy\": {\n          \"protocol_name\": \"MQTT\",\n          \"protocol_level\": 4\n        }\n      }\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"nodes\": {\n        \"127.0.0.1:1995\": 1\n      },\n      \"type\": \"roundrobin\",\n      \"id\": 1\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n\n### How to configure protos\n\n<Tabs>\n<TabItem value=\"yaml\" label=\"YAML\" default>\n\n```yaml\nprotos:\n  - id: helloworld\n    desc: hello world\n    content: >\n      syntax = \"proto3\";\n      package helloworld;\n\n      service Greeter {\n        rpc SayHello (HelloRequest) returns (HelloReply) {}\n      }\n      message HelloRequest {\n        string name = 1;\n      }\n      message HelloReply {\n        string message = 1;\n      }\n#END\n```\n\n</TabItem>\n\n<TabItem value=\"json\" label=\"JSON\">\n\n```json\n{\n  \"protos\": [\n    {\n      \"id\": \"helloworld\",\n      \"desc\": \"hello world\",\n      \"content\": \"syntax = \\\"proto3\\\";\\npackage helloworld;\\n\\nservice Greeter {\\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\\n}\\nmessage HelloRequest {\\n  string name = 1;\\n}\\nmessage HelloReply {\\n  string message = 1;\\n}\\n\"\n    }\n  ]\n}\n```\n\n</TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/en/latest/discovery/consul.md",
    "content": "---\ntitle: consul\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Summary\n\nAPACHE APISIX supports Consul as a service discovery\n\n## Configuration for discovery client\n\n### Configuration for Consul\n\nFirst of all, we need to add following configuration in `conf/config.yaml` :\n\n```yaml\ndiscovery:\n  consul:\n    servers:                      # make sure service name is unique in these consul servers\n      - \"http://127.0.0.1:8500\"   # `http://127.0.0.1:8500` and `http://127.0.0.1:8600` are different clusters\n      - \"http://127.0.0.1:8600\"   # `consul` service is default skip service\n    token: \"...\"                  # if your consul cluster has enabled acl access control, you need to specify the token\n    skip_services:                # if you need to skip special services\n      - \"service_a\"\n    timeout:\n      connect: 1000               # default 2000 ms\n      read: 1000                  # default 2000 ms\n      wait: 60                    # default 60 sec\n    weight: 1                     # default 1\n    fetch_interval: 5             # default 3 sec, only take effect for keepalive: false way\n    keepalive: true               # default true, use the long pull way to query consul servers\n    sort_type: \"origin\"           # default origin\n    default_service:              # you can define default service when missing hit\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1           # default 1 ms\n        weight: 1                 # default 1\n        max_fails: 1              # default 1\n    dump:                         # if you need, when registered nodes updated can dump into file\n       path: \"logs/consul.dump\"\n       expire: 2592000            # unit sec, here is 30 day\n```\n\nAnd you can config it in short by default value:\n\n```yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n```\n\nThe `keepalive` has two optional values:\n\n- `true`, default and recommend value, use the long pull way to query consul servers\n- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval\n\nThe `sort_type` has four optional values:\n\n- `origin`, not sorting\n- `host_sort`, sort by host\n- `port_sort`, sort by port\n- `combine_sort`, with the precondition that hosts are ordered, ports are also ordered.\n\n#### Dump Data\n\nWhen we need reload `apisix` online, as the `consul` module maybe loads data from CONSUL slower than load routes from ETCD, and would get the log at the moment before load successfully from consul:\n\n```\n http_access_phase(): failed to set upstream: no valid upstream node\n```\n\nSo, we import the `dump` function for `consul` module. When reload, would load the dump file before from consul; when the registered nodes in consul been updated, would dump the upstream nodes into file automatically.\n\nThe `dump` has three optional values now:\n\n- `path`, the dump file save path\n    - support relative path, eg: `logs/consul.dump`\n    - support absolute path, eg: `/tmp/consul.dump`\n    - make sure the dump file's parent path exist\n    - make sure the `apisix` has the dump file's read-write access permission,eg: add below config in `conf/config.yaml`\n\n```yaml\nnginx_config:                     # config for render the template to generate nginx.conf\n  user: root                     # specifies the execution user of the worker process.\n```\n\n- `load_on_init`, default value is `true`\n    - if `true`, just try to load the data from the dump file before loading data from  consul when starting, does not care the dump file exists or not\n    - if `false`, ignore loading data from the dump file\n    - Whether `true` or `false`, we don't need to prepare a dump file for apisix at anytime\n- `expire`, unit sec, avoiding load expired dump data when load\n    - default `0`, it is unexpired forever\n    - recommend 2592000, which is 30 days(equals 3600 \\* 24 \\* 30)\n\n### Register Http API Services\n\nNow, register nodes into consul:\n\n```shell\ncurl -X PUT 'http://127.0.0.1:8500/v1/agent/service/register' \\\n-d '{\n  \"ID\": \"service_a1\",\n  \"Name\": \"service_a\",\n  \"Tags\": [\"primary\", \"v1\"],\n  \"Address\": \"127.0.0.1\",\n  \"Port\": 8000,\n  \"Meta\": {\n    \"service_a_version\": \"4.0\"\n  },\n  \"EnableTagOverride\": false,\n  \"Weights\": {\n    \"Passing\": 10,\n    \"Warning\": 1\n  }\n}'\n\ncurl -X PUT 'http://127.0.0.1:8500/v1/agent/service/register' \\\n-d '{\n  \"ID\": \"service_a1\",\n  \"Name\": \"service_a\",\n  \"Tags\": [\"primary\", \"v1\"],\n  \"Address\": \"127.0.0.1\",\n  \"Port\": 8002,\n  \"Meta\": {\n    \"service_a_version\": \"4.0\"\n  },\n  \"EnableTagOverride\": false,\n  \"Weights\": {\n    \"Passing\": 10,\n    \"Warning\": 1\n  }\n}'\n```\n\nIn some cases, same service name might exist in different consul servers.\nTo avoid confusion, use the full consul key url path as service name in practice.\n\n### Port Handling\n\nWhen APISIX retrieves service information from Consul, it handles port values as follows:\n\n- If the service registration includes a valid port number, that port will be used.\n- If the port is `nil` (not specified) or `0`, APISIX will default to port `80` for HTTP services.\n\n### Upstream setting\n\n#### L7\n\nHere is an example of routing a request with a URL of \"/*\" to a service which named \"service_a\" and use consul discovery client in the registry :\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/*\",\n    \"upstream\": {\n        \"service_name\": \"service_a\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"consul\"\n    }\n}'\n```\n\nThe format response as below:\n\n```json\n{\n  \"key\": \"/apisix/routes/1\",\n  \"value\": {\n    \"uri\": \"/*\",\n    \"priority\": 0,\n    \"id\": \"1\",\n    \"upstream\": {\n      \"scheme\": \"http\",\n      \"type\": \"roundrobin\",\n      \"hash_on\": \"vars\",\n      \"discovery_type\": \"consul\",\n      \"service_name\": \"service_a\",\n      \"pass_host\": \"pass\"\n    },\n    \"create_time\": 1669267329,\n    \"status\": 1,\n    \"update_time\": 1669267329\n  }\n}\n```\n\nYou could find more usage in the `apisix/t/discovery/consul.t` file.\n\n#### L4\n\nConsul service discovery also supports use in L4, the configuration method is similar to L7.\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n      \"scheme\": \"tcp\",\n      \"service_name\": \"service_a\",\n      \"type\": \"roundrobin\",\n      \"discovery_type\": \"consul\"\n    }\n}'\n```\n\nYou could find more usage in the `apisix/t/discovery/stream/consul.t` file.\n\n## Debugging API\n\nIt also offers control api for debugging.\n\n### Memory Dump API\n\n```shell\nGET /v1/discovery/consul/dump\n```\n\nFor example:\n\n```shell\n# curl http://127.0.0.1:9090/v1/discovery/consul/dump | jq\n{\n  \"config\": {\n    \"fetch_interval\": 3,\n    \"timeout\": {\n      \"wait\": 60,\n      \"connect\": 6000,\n      \"read\": 6000\n    },\n    \"weight\": 1,\n    \"servers\": [\n      \"http://172.19.5.30:8500\",\n      \"http://172.19.5.31:8500\"\n    ],\n    \"keepalive\": true,\n    \"default_service\": {\n      \"host\": \"172.19.5.11\",\n      \"port\": 8899,\n      \"metadata\": {\n        \"fail_timeout\": 1,\n        \"weight\": 1,\n        \"max_fails\": 1\n      }\n    },\n    \"skip_services\": [\n      \"service_d\"\n    ]\n  },\n  \"services\": {\n    \"service_a\": [\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30513,\n        \"weight\": 1\n      },\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30514,\n        \"weight\": 1\n      }\n    ],\n    \"service_b\": [\n      {\n        \"host\": \"172.19.5.51\",\n        \"port\": 50051,\n        \"weight\": 1\n      }\n    ],\n    \"service_c\": [\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30511,\n        \"weight\": 1\n      },\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30512,\n        \"weight\": 1\n      }\n    ]\n  }\n}\n```\n\n### Show Dump File API\n\nIt offers another control api for dump file view now. Maybe would add more api for debugging in future.\n\n```shell\nGET /v1/discovery/consul/show_dump_file\n```\n\nFor example:\n\n```shell\ncurl http://127.0.0.1:9090/v1/discovery/consul/show_dump_file | jq\n{\n  \"services\": {\n    \"service_a\": [\n      {\n        \"host\": \"172.19.5.12\",\n        \"port\": 8000,\n        \"weight\": 120\n      },\n      {\n        \"host\": \"172.19.5.13\",\n        \"port\": 8000,\n        \"weight\": 120\n      }\n    ]\n  },\n  \"expire\": 0,\n  \"last_update\": 1615877468\n}\n```\n"
  },
  {
    "path": "docs/en/latest/discovery/consul_kv.md",
    "content": "---\ntitle: consul_kv\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Summary\n\nFor users that are using [nginx-upsync-module](https://github.com/weibocom/nginx-upsync-module) and Consul KV as a service discovery, like the Weibo Mobile Team, this may be needed.\n\nThanks to @fatman-x guy, who developed this module, called `consul_kv`, and its worker process data flow is below:\n![consul kv module data flow diagram](https://user-images.githubusercontent.com/548385/107141841-6ced3e00-6966-11eb-8aa4-bc790a4ad113.png)\n\n## Configuration for discovery client\n\n### Configuration for Consul KV\n\nAdd following configuration in `conf/config.yaml` :\n\n```yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    token: \"...\"                  # if your consul cluster has enabled acl access control, you need to specify the token\n    prefix: \"upstreams\"\n    skip_keys:                    # if you need to skip special keys\n      - \"upstreams/unused_api/\"\n    timeout:\n      connect: 1000               # default 2000 ms\n      read: 1000                  # default 2000 ms\n      wait: 60                    # default 60 sec\n    weight: 1                     # default 1\n    fetch_interval: 5             # default 3 sec, only take effect for keepalive: false way\n    keepalive: true               # default true, use the long pull way to query consul servers\n    default_server:               # you can define default server when missing hit\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1           # default 1 ms\n        weight: 1                 # default 1\n        max_fails: 1              # default 1\n    dump:                         # if you need, when registered nodes updated can dump into file\n       path: \"logs/consul_kv.dump\"\n       expire: 2592000      # unit sec, here is 30 day\n```\n\nAnd you can config it in short by default value:\n\n```yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n```\n\nThe `keepalive` has two optional values:\n\n- `true`, default and recommend value, use the long pull way to query consul servers\n- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval\n\n#### Dump Data\n\nWhen we need reload `apisix` online, as the `consul_kv` module maybe loads data from CONSUL slower than load routes from ETCD, and would get the log at the moment before load successfully from consul:\n\n```\n http_access_phase(): failed to set upstream: no valid upstream node\n```\n\nSo, we import the `dump` function for `consul_kv` module. When reload, would load the dump file before from consul; when the registered nodes in consul been updated, would dump the upstream nodes into file automatically.\n\nThe `dump` has three optional values now:\n\n- `path`, the dump file save path\n    - support relative path, eg: `logs/consul_kv.dump`\n    - support absolute path, eg: `/tmp/consul_kv.bin`\n    - make sure the dump file's parent path exist\n    - make sure the `apisix` has the dump file's read-write access permission,eg: `chown  www:root conf/upstream.d/`\n- `load_on_init`, default value is `true`\n    - if `true`, just try to load the data from the dump file before loading data from  consul when starting, does not care the dump file exists or not\n    - if `false`, ignore loading data from the dump file\n    - Whether `true` or `false`, we don't need to prepare a dump file for apisix at anytime\n- `expire`, unit sec, avoiding load expired dump data when load\n    - default `0`, it is unexpired forever\n    - recommend 2592000, which is 30 days(equals 3600 \\* 24 \\* 30)\n\n### Register Http API Services\n\nService register Key&Value template:\n\n```\nKey:    {Prefix}/{Service_Name}/{IP}:{Port}\nValue: {\"weight\": <Num>, \"max_fails\": <Num>, \"fail_timeout\": <Num>}\n```\n\nThe register consul key use `upstreams` as prefix by default. The http api service name called `webpages` for example, and you can also use `webpages/oneteam/hello` as service name. The api instance of node's ip and port make up new key: `<IP>:<Port>`.\n\nNow, register nodes into consul:\n\n```shell\ncurl \\\n    -X PUT \\\n    -d ' {\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}' \\\n    http://127.0.0.1:8500/v1/kv/upstreams/webpages/172.19.5.12:8000\n\ncurl \\\n    -X PUT \\\n    -d ' {\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}' \\\n    http://127.0.0.1:8500/v1/kv/upstreams/webpages/172.19.5.13:8000\n```\n\nIn some case, same keys exist in different consul servers.\nTo avoid confusion, use the full consul key url path as service name in practice.\n\n### Upstream setting\n\n#### L7\n\nHere is an example of routing a request with a URL of \"/*\" to a service which named \"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\" and use consul_kv discovery client in the registry :\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/*\",\n    \"upstream\": {\n        \"service_name\": \"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"consul_kv\"\n    }\n}'\n```\n\nThe format response as below:\n\n```json\n{\n  \"node\": {\n    \"value\": {\n      \"priority\": 0,\n      \"update_time\": 1612755230,\n      \"upstream\": {\n        \"discovery_type\": \"consul_kv\",\n        \"service_name\": \"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\",\n        \"hash_on\": \"vars\",\n        \"type\": \"roundrobin\",\n        \"pass_host\": \"pass\"\n      },\n      \"id\": \"1\",\n      \"uri\": \"/*\",\n      \"create_time\": 1612755230,\n      \"status\": 1\n    },\n    \"key\": \"/apisix/routes/1\"\n  }\n}\n```\n\nYou could find more usage in the `apisix/t/discovery/consul_kv.t` file.\n\n#### L4\n\nConsul_kv service discovery also supports use in L4, the configuration method is similar to L7.\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n      \"scheme\": \"tcp\",\n      \"service_name\": \"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\",\n      \"type\": \"roundrobin\",\n      \"discovery_type\": \"consul_kv\"\n    }\n}'\n```\n\nYou could find more usage in the `apisix/t/discovery/stream/consul_kv.t` file.\n\n## Debugging API\n\nIt also offers control api for debugging.\n\n### Memory Dump API\n\n```shell\nGET /v1/discovery/consul_kv/dump\n```\n\nFor example:\n\n```shell\n# curl http://127.0.0.1:9090/v1/discovery/consul_kv/dump | jq\n{\n  \"config\": {\n    \"fetch_interval\": 3,\n    \"timeout\": {\n      \"wait\": 60,\n      \"connect\": 6000,\n      \"read\": 6000\n    },\n    \"prefix\": \"upstreams\",\n    \"weight\": 1,\n    \"servers\": [\n      \"http://172.19.5.30:8500\",\n      \"http://172.19.5.31:8500\"\n    ],\n    \"keepalive\": true,\n    \"default_service\": {\n      \"host\": \"172.19.5.11\",\n      \"port\": 8899,\n      \"metadata\": {\n        \"fail_timeout\": 1,\n        \"weight\": 1,\n        \"max_fails\": 1\n      }\n    },\n    \"skip_keys\": [\n      \"upstreams/myapi/gateway/apisix/\"\n    ]\n  },\n  \"services\": {\n    \"http://172.19.5.31:8500/v1/kv/upstreams/webpages/\": [\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30513,\n        \"weight\": 1\n      },\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30514,\n        \"weight\": 1\n      }\n    ],\n    \"http://172.19.5.30:8500/v1/kv/upstreams/1614480/grpc/\": [\n      {\n        \"host\": \"172.19.5.51\",\n        \"port\": 50051,\n        \"weight\": 1\n      }\n    ],\n    \"http://172.19.5.30:8500/v1/kv/upstreams/webpages/\": [\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30511,\n        \"weight\": 1\n      },\n      {\n        \"host\": \"127.0.0.1\",\n        \"port\": 30512,\n        \"weight\": 1\n      }\n    ]\n  }\n}\n```\n\n### Show Dump File API\n\nIt offers another control api for dump file view now. Maybe would add more api for debugging in future.\n\n```shell\nGET /v1/discovery/consul_kv/show_dump_file\n```\n\nFor example:\n\n```shell\ncurl http://127.0.0.1:9090/v1/discovery/consul_kv/show_dump_file | jq\n{\n  \"services\": {\n    \"http://172.19.5.31:8500/v1/kv/upstreams/1614480/webpages/\": [\n      {\n        \"host\": \"172.19.5.12\",\n        \"port\": 8000,\n        \"weight\": 120\n      },\n      {\n        \"host\": \"172.19.5.13\",\n        \"port\": 8000,\n        \"weight\": 120\n      }\n    ]\n  },\n  \"expire\": 0,\n  \"last_update\": 1615877468\n}\n```\n"
  },
  {
    "path": "docs/en/latest/discovery/control-plane-service-discovery.md",
    "content": "---\ntitle: Control Plane Service Discovery\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - ZooKeeper\n  - Nacos\n  - APISIX-Seed\ndescription: This documentation describes implementing service discovery through Nacos and ZooKeeper on the API Gateway APISIX Control Plane.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis document describes how to implement service discovery with Nacos and Zookeeper on the APISIX Control Plane.\n\n## APISIX-Seed Architecture\n\nApache APISIX has supported Data Plane service discovery in the early days, and now APISIX also supports Control Plane service discovery through the [APISIX-Seed](https://github.com/api7/apisix-seed) project. The following figure shows the APISIX-Seed architecture diagram.\n\n![control-plane-service-discovery](../../../assets/images/control-plane-service-discovery.png)\n\nThe specific information represented by the figures in the figure is as follows:\n\n1. Register an upstream with APISIX and specify the service discovery type. APISIX-Seed will watch APISIX resource changes in etcd, filter discovery types, and obtain service names.\n2. APISIX-Seed subscribes the specified service name to the service registry to obtain changes to the corresponding service.\n3. After the client registers the service with the service registry, APISIX-Seed will obtain the new service information and write the updated service node into etcd;\n4. When the corresponding resources in etcd change, APISIX worker will refresh the latest service node information to memory.\n\n:::note\n\nIt should be noted that after the introduction of APISIX-Seed, if the service of the registry changes frequently, the data in etcd will also change frequently. So, it is best to set the `--auto-compaction` option when starting etcd to compress the history periodically to avoid etcd eventually exhausting its storage space. Please refer to [revisions](https://etcd.io/docs/v3.5/learning/api/#revisions).\n\n:::\n\n## Why APISIX-Seed\n\n- Network topology becomes simpler\n\n  APISIX does not need to maintain a network connection with each registry, and only needs to pay attention to the configuration information in etcd. This will greatly simplify the network topology.\n\n- Total data volume about upstream service becomes smaller\n\n  Due to the characteristics of the registry, APISIX may store the full amount of registry service data in the worker, such as consul_kv. By introducing APISIX-Seed, each process of APISIX will not need to additionally cache upstream service-related information.\n\n- Easier to manage\n\n  Service discovery configuration needs to be configured once per APISIX instance. By introducing APISIX-Seed, Apache APISIX will be in different to the configuration changes of the service registry.\n\n## Supported service registry\n\nZooKeeper and Nacos are currently supported, and more service registries will be supported in the future. For more information, please refer to: [APISIX Seed](https://github.com/api7/apisix-seed#apisix-seed-for-apache-apisix).\n\n- If you want to enable control plane ZooKeeper service discovery, please refer to: [ZooKeeper Deployment Tutorial](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md).\n\n- If you want to enable control plane Nacos service discovery, please refer to: [Nacos Deployment Tutorial](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/nacos.md).\n"
  },
  {
    "path": "docs/en/latest/discovery/dns.md",
    "content": "---\ntitle: DNS\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## service discovery via DNS\n\nSome service discovery system, like Consul, support exposing service information\nvia DNS. Therefore we can use this way to discover service directly. Both L4 and L7 are supported.\n\nFirst of all, we need to configure the address of DNS servers:\n\n```yaml\n# add this to config.yaml\ndiscovery:\n   dns:\n     servers:\n       - \"127.0.0.1:8600\"          # use the real address of your dns server\n```\n\nUnlike configuring the domain in the Upstream's `nodes` field, service discovery via\nDNS will return all records. For example, with upstream configuration:\n\n```json\n{\n    \"id\": 1,\n    \"discovery_type\": \"dns\",\n    \"service_name\": \"test.consul.service\",\n    \"type\": \"roundrobin\"\n}\n```\n\nand `test.consul.service` be resolved as `1.1.1.1` and `1.1.1.2`, this result will be the same as:\n\n```json\n{\n    \"id\": 1,\n    \"type\": \"roundrobin\",\n    \"nodes\": [\n        {\"host\": \"1.1.1.1\", \"weight\": 1},\n        {\"host\": \"1.1.1.2\", \"weight\": 1}\n    ]\n}\n```\n\nNote that all the IPs from `test.consul.service` share the same weight.\n\nThe resolved records will be cached according to their TTL.\nFor service whose record is not in the cache, we will query it in the order of `SRV -> A -> AAAA -> CNAME` by default.\nWhen we refresh the cache record, we will try from the last previously successful type.\nWe can also customize the order by modifying the configuration file.\n\n```yaml\n# add this to config.yaml\ndiscovery:\n   dns:\n     servers:\n       - \"127.0.0.1:8600\"          # use the real address of your dns server\n     order:                        # order in which to try different dns record types when resolving\n       - last                      # \"last\" will try the last previously successful type for a hostname.\n       - SRV\n       - A\n       - AAAA\n       - CNAME\n```\n\nIf you want to specify the port for the upstream server, you can add it to the `service_name`:\n\n```json\n{\n    \"id\": 1,\n    \"discovery_type\": \"dns\",\n    \"service_name\": \"test.consul.service:1980\",\n    \"type\": \"roundrobin\"\n}\n```\n\nAnother way to do it is via the SRV record, see below.\n\n### SRV record\n\nBy using SRV record you can specify the port and the weight of a service.\n\nAssumed you have the SRV record like this:\n\n```\n; under the section of blah.service\nA       300 IN      A     1.1.1.1\nB       300 IN      A     1.1.1.2\nB       300 IN      A     1.1.1.3\n\n; name  TTL         type    priority    weight  port\nsrv     86400 IN    SRV     10          60      1980 A\nsrv     86400 IN    SRV     20          20      1981 B\n```\n\nUpstream configuration like:\n\n```json\n{\n    \"id\": 1,\n    \"discovery_type\": \"dns\",\n    \"service_name\": \"srv.blah.service\",\n    \"type\": \"roundrobin\"\n}\n```\n\nis the same as:\n\n```json\n{\n    \"id\": 1,\n    \"type\": \"roundrobin\",\n    \"nodes\": [\n        {\"host\": \"1.1.1.1\", \"port\": 1980, \"weight\": 60, \"priority\": -10},\n        {\"host\": \"1.1.1.2\", \"port\": 1981, \"weight\": 10, \"priority\": -20},\n        {\"host\": \"1.1.1.3\", \"port\": 1981, \"weight\": 10, \"priority\": -20}\n    ]\n}\n```\n\nNote that two records of domain B split the weight evenly.\nFor SRV record, nodes with lower priority are chosen first, so the final priority is negative.\n\nAs for 0 weight SRV record, the [RFC 2782](https://www.ietf.org/rfc/rfc2782.txt) says:\n\n> Domain administrators SHOULD use Weight 0 when there isn't any server\nselection to do, to make the RR easier to read for humans (less\nnoisy).  In the presence of records containing weights greater\nthan 0, records with weight 0 should have a very small chance of\nbeing selected.\n\nWe treat weight 0 record has a weight of 1 so the node \"have a very small chance of\nbeing selected\", which is also the common way to treat this type of record.\n\nFor SRV record which has port 0, we will fallback to use the upstream protocol's default port.\nYou can also specify the port in the \"service_name\" field directly, like \"srv.blah.service:8848\".\n"
  },
  {
    "path": "docs/en/latest/discovery/eureka.md",
    "content": "---\ntitle: eureka\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nApache APISIX supports service discovery via Eureka. For the details, please start your\nreading from [Supported discovery registries](../discovery.md#supported-discovery-registries).\n"
  },
  {
    "path": "docs/en/latest/discovery/kubernetes.md",
    "content": "---\ntitle: Kubernetes\nkeywords:\n  - Kubernetes\n  - Apache APISIX\n  - Service discovery\n  - Cluster\n  - API Gateway\ndescription: This article introduce how to perform service discovery based on Kubernetes in Apache APISIX and summarize related issues.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Summary\n\nThe [_Kubernetes_](https://kubernetes.io/) service discovery [_List-Watch_](https://kubernetes.io/docs/reference/using-api/api-concepts/) real-time changes of [_Endpoints_](https://kubernetes.io/docs/concepts/services-networking/service/) resources, then store theirs value into `ngx.shared.DICT`.\n\nDiscovery also provides a node query interface in accordance with the [_APISIX Discovery Specification_](../discovery.md).\n\n## How To Use\n\nKubernetes service discovery both support single-cluster and multi-cluster modes, applicable to the case where the service is distributed in single or multiple Kubernetes clusters.\n\n### Single-Cluster Mode Configuration\n\nA detailed configuration for single-cluster mode Kubernetes service discovery is as follows:\n\n```yaml\ndiscovery:\n  kubernetes:\n    service:\n      # apiserver schema, options [http, https]\n      schema: https #default https\n\n      # apiserver host, options [ipv4, ipv6, domain, environment variable]\n      host: ${KUBERNETES_SERVICE_HOST} #default ${KUBERNETES_SERVICE_HOST}\n\n      # apiserver port, options [port number, environment variable]\n      port: ${KUBERNETES_SERVICE_PORT}  #default ${KUBERNETES_SERVICE_PORT}\n\n    client:\n      # serviceaccount token or token_file\n      token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n\n      #token: |-\n       # eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEif\n       # 6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEifeyJhbGciOiJSUzI1NiIsImtpZCI\n\n    default_weight: 50 # weight assigned to each discovered endpoint. default 50, minimum 0\n\n    # kubernetes discovery support namespace_selector\n    # you can use one of [equal, not_equal, match, not_match] filter namespace\n    namespace_selector:\n      # only save endpoints with namespace equal default\n      equal: default\n\n      # only save endpoints with namespace not equal default\n      #not_equal: default\n\n      # only save endpoints with namespace match one of [default, ^my-[a-z]+$]\n      #match:\n       #- default\n       #- ^my-[a-z]+$\n\n      # only save endpoints with namespace not match one of [default, ^my-[a-z]+$ ]\n      #not_match:\n       #- default\n       #- ^my-[a-z]+$\n\n    # kubernetes discovery support label_selector\n    # for the expression of label_selector, please refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\n    label_selector: |-\n      first=\"a\",second=\"b\"\n\n    # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint\n    shared_size: 1m #default 1m\n\n    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints\n    watch_endpoint_slices: false #default false\n```\n\nIf the Kubernetes service discovery runs inside a pod, you can use minimal configuration:\n\n```yaml\ndiscovery:\n  kubernetes: { }\n```\n\nIf the Kubernetes service discovery runs outside a pod, you need to create or select a specified [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/), then get its token value, and use following configuration:\n\n```yaml\ndiscovery:\n  kubernetes:\n    service:\n      schema: https\n      host: # enter apiserver host value here\n      port: # enter apiserver port value here\n    client:\n      token: # enter serviceaccount token value here\n      #token_file: # enter file path here\n```\n\n### Single-Cluster Mode Query Interface\n\nThe Kubernetes service discovery provides a query interface in accordance with the [_APISIX Discovery Specification_](../discovery.md).\n\n**function:**\n nodes(service_name)\n\n**description:**\n  nodes() function attempts to look up the ngx.shared.DICT for nodes corresponding to service_name, \\\n  service_name should match pattern: _[namespace]/[name]:[portName]_\n\n  + namespace: The namespace where the Kubernetes endpoints is located\n\n  + name: The name of the Kubernetes endpoints\n\n  + portName: The `ports.name` value in the Kubernetes endpoints, if there is no `ports.name`, use `targetPort`, `port` instead. If `ports.name` exists, then port number cannot be used.\n\n**return value:**\n  if the Kubernetes endpoints value is as follows:\n\n  ```yaml\n  apiVersion: v1\n  kind: Endpoints\n  metadata:\n    name: plat-dev\n    namespace: default\n  subsets:\n    - addresses:\n        - ip: \"10.5.10.109\"\n        - ip: \"10.5.10.110\"\n      ports:\n        - port: 3306\n          name: port\n  ```\n\n  a nodes(\"default/plat-dev:port\") call will get follow result:\n\n  ```\n   {\n       {\n           host=\"10.5.10.109\",\n           port= 3306,\n           weight= 50,\n       },\n       {\n           host=\"10.5.10.110\",\n           port= 3306,\n           weight= 50,\n       },\n   }\n  ```\n\n### Multi-Cluster Mode Configuration\n\nA detailed configuration for multi-cluster mode Kubernetes service discovery is as follows:\n\n```yaml\ndiscovery:\n  kubernetes:\n  - id: release  # a custom name refer to the cluster, pattern ^[a-z0-9]{1,8}\n    service:\n      # apiserver schema, options [http, https]\n      schema: https #default https\n\n      # apiserver host, options [ipv4, ipv6, domain, environment variable]\n      host: \"1.cluster.com\"\n\n      # apiserver port, options [port number, environment variable]\n      port: \"6443\"\n\n    client:\n      # serviceaccount token or token_file\n      token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n\n      #token: |-\n       # eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEif\n       # 6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEifeyJhbGciOiJSUzI1NiIsImtpZCI\n\n    default_weight: 50 # weight assigned to each discovered endpoint. default 50, minimum 0\n\n    # kubernetes discovery support namespace_selector\n    # you can use one of [equal, not_equal, match, not_match] filter namespace\n    namespace_selector:\n      # only save endpoints with namespace equal default\n      equal: default\n\n      # only save endpoints with namespace not equal default\n      #not_equal: default\n\n      # only save endpoints with namespace match one of [default, ^my-[a-z]+$]\n      #match:\n       #- default\n       #- ^my-[a-z]+$\n\n      # only save endpoints with namespace not match one of [default, ^my-[a-z]+$]\n      #not_match:\n       #- default\n       #- ^my-[a-z]+$\n\n    # kubernetes discovery support label_selector\n    # for the expression of label_selector, please refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\n    label_selector: |-\n      first=\"a\",second=\"b\"\n\n    # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint\n    shared_size: 1m #default 1m\n\n    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints\n    watch_endpoint_slices: false #default false\n```\n\nMulti-Kubernetes service discovery does not fill default values for service and client fields, you need to fill them according to the cluster configuration.\n\n### Multi-Cluster Mode Query Interface\n\nThe Kubernetes service discovery provides a query interface in accordance with the [_APISIX Discovery Specification_](../discovery.md).\n\n**function:**\nnodes(service_name)\n\n**description:**\nnodes() function attempts to look up the ngx.shared.DICT for nodes corresponding to service_name, \\\nservice_name should match pattern: _[id]/[namespace]/[name]:[portName]_\n\n+ id: value defined in service discovery configuration\n\n+ namespace: The namespace where the Kubernetes endpoints is located\n\n+ name: The name of the Kubernetes endpoints\n\n+ portName: The `ports.name` value in the Kubernetes endpoints, if there is no `ports.name`, use `targetPort`, `port` instead. If `ports.name` exists, then port number cannot be used.\n\n**return value:**\nif the Kubernetes endpoints value is as follows:\n\n  ```yaml\n  apiVersion: v1\n  kind: Endpoints\n  metadata:\n    name: plat-dev\n    namespace: default\n  subsets:\n    - addresses:\n        - ip: \"10.5.10.109\"\n        - ip: \"10.5.10.110\"\n      ports:\n        - port: 3306\n          name: port\n  ```\n\na nodes(\"release/default/plat-dev:port\") call will get follow result:\n\n  ```\n   {\n       {\n           host=\"10.5.10.109\",\n           port= 3306,\n           weight= 50,\n       },\n       {\n           host=\"10.5.10.110\",\n           port= 3306,\n           weight= 50,\n       },\n   }\n  ```\n\n## Q&A\n\n**Q: Why only support configuration token to access _Kubernetes APIServer_?**\n\nA: Usually, we will use three ways to complete the authentication of _Kubernetes APIServer_:\n\n+ mTLS\n+ Token\n+ Basic authentication\n\nBecause lua-resty-http does not currently support mTLS, and basic authentication is not recommended, so currently only the token authentication method is implemented.\n\n**Q: APISIX inherits Nginx's multiple process model, does it mean that each nginx worker process will [_List-Watch_](https://kubernetes.io/docs/reference/using-api/api-concepts/) kubernetes endpoints resources?**\n\nA: The Kubernetes service discovery only uses privileged processes to [_List-Watch_](https://kubernetes.io/docs/reference/using-api/api-concepts/) Kubernetes endpoints resources, then store theirs value into `ngx.shared.DICT`, worker processes get results by querying `ngx.shared.DICT`.\n\n**Q: What permissions do [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) require?**\n\nA: ServiceAccount requires the permissions of cluster-level [ get, list, watch ] endpoints and endpointslices resources, the declarative definition is as follows:\n\n```yaml\nkind: ServiceAccount\napiVersion: v1\nmetadata:\n name: apisix-test\n namespace: default\n---\n\nkind: ClusterRole\napiVersion: rbac.authorization.k8s.io/v1\nmetadata:\n  name: apisix-test\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ endpoints]\n    verbs: [ get,list,watch ]\n  - apiGroups: [ \"discovery.k8s.io\" ]\n    resources: [ endpointslices ]\n    verbs: [ get,list,watch ]\n---\n\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: apisix-test\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: ClusterRole\n name: apisix-test\nsubjects:\n - kind: ServiceAccount\n   name: apisix-test\n   namespace: default\n```\n\n**Q: How to get [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) token value?**\n\nA: Assume your [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) located in namespace apisix and name is Kubernetes-discovery, you can use the following steps to get token value.\n\n 1. Get secret name. You can execute the following command, the output of the first column is the secret name we want:\n\n ```shell\n kubectl -n apisix get secrets | grep kubernetes-discovery\n ```\n\n 2. Get token value. Assume secret resources name is kubernetes-discovery-token-c64cv, you can execute the following command, the output is the service account token value we want:\n\n ```shell\n kubectl -n apisix get secret kubernetes-discovery-token-c64cv -o jsonpath={.data.token} | base64 -d\n ```\n\n## Debugging API\n\nIt also offers control api for debugging.\n\n### Memory Dump API\n\nTo query/list the nodes discoverd by kubernetes discovery, you can query the /v1/discovery/kubernetes/dump control API endpoint like so:\n\n```shell\nGET /v1/discovery/kubernetes/dump\n```\n\nWhich will yield the following response:\n\n```\n{\n  \"endpoints\": [\n    {\n      \"endpoints\": [\n        {\n          \"value\": \"{\\\"https\\\":[{\\\"host\\\":\\\"172.18.164.170\\\",\\\"port\\\":6443,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.171\\\",\\\"port\\\":6443,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.172\\\",\\\"port\\\":6443,\\\"weight\\\":50}]}\",\n          \"name\": \"default/kubernetes\"\n        },\n        {\n          \"value\": \"{\\\"metrics\\\":[{\\\"host\\\":\\\"172.18.164.170\\\",\\\"port\\\":2379,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.171\\\",\\\"port\\\":2379,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.172\\\",\\\"port\\\":2379,\\\"weight\\\":50}]}\",\n          \"name\": \"kube-system/etcd\"\n        },\n        {\n          \"value\": \"{\\\"http-85\\\":[{\\\"host\\\":\\\"172.64.89.2\\\",\\\"port\\\":85,\\\"weight\\\":50}]}\",\n          \"name\": \"test-ws/testing\"\n        }\n      ],\n      \"id\": \"first\"\n    }\n  ],\n  \"config\": [\n    {\n      \"default_weight\": 50,\n      \"id\": \"first\",\n      \"client\": {\n        \"token\": \"xxx\"\n      },\n      \"service\": {\n        \"host\": \"172.18.164.170\",\n        \"port\": \"6443\",\n        \"schema\": \"https\"\n      },\n      \"shared_size\": \"1m\"\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/en/latest/discovery/nacos.md",
    "content": "---\ntitle: nacos\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Service discovery via Nacos\n\nThe performance of this module needs to be improved:\n\n1. send the request parallelly.\n\n### Configuration for Nacos\n\nAdd following configuration in `conf/config.yaml` :\n\n```yaml\ndiscovery:\n  nacos:\n    host:\n      - \"http://${username}:${password}@${host1}:${port1}\"\n    prefix: \"/nacos/v1/\"\n    fetch_interval: 30    # default 30 sec\n    # `weight` is the `default_weight` that will be attached to each discovered node that\n    # doesn't have a weight explicitly provided in nacos results\n    weight: 100           # default 100\n    timeout:\n      connect: 2000       # default 2000 ms\n      send: 2000          # default 2000 ms\n      read: 5000          # default 5000 ms\n```\n\nAnd you can config it in short by default value:\n\n```yaml\ndiscovery:\n  nacos:\n    host:\n      - \"http://192.168.33.1:8848\"\n```\n\n### Upstream setting\n\n#### L7\n\nHere is an example of routing a request with an URI of \"/nacos/*\" to a service which named \"http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS\" and use nacos discovery client in the registry:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacos/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\"\n    }\n}'\n```\n\nThe formatted response as below:\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/1\",\n    \"value\": {\n      \"id\": \"1\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\"\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacos\\/*\"\n    }\n  }\n}\n```\n\n#### L4\n\nNacos service discovery also supports use in L4, the configuration method is similar to L7.\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n        \"scheme\": \"tcp\",\n        \"discovery_type\": \"nacos\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n### discovery_args\n\n| Name         | Type   | Requirement | Default | Valid | Description                                                  |\n| ------------ | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ |\n| namespace_id | string | optional    | public     |       | This parameter is used to specify the namespace of the corresponding service |\n| group_name   | string | optional    | DEFAULT_GROUP       |       | This parameter is used to specify the group of the corresponding service |\n\n#### Specify the namespace\n\nExample of routing a request with an URI of \"/nacosWithNamespaceId/*\" to a service with name, namespaceId \"http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS&namespaceId=test_ns\" and use nacos discovery client in the registry:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacosWithNamespaceId/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\"\n        }\n    }\n}'\n```\n\nThe formatted response as below:\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/2\",\n    \"value\": {\n      \"id\": \"2\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\"\n        }\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacosWithNamespaceId\\/*\"\n    }\n  }\n}\n```\n\n#### Specify the group\n\nExample of routing a request with an URI of \"/nacosWithGroupName/*\" to a service with name, groupName \"http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS&groupName=test_group\" and use nacos discovery client in the registry:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/3 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacosWithGroupName/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"group_name\": \"test_group\"\n        }\n    }\n}'\n```\n\nThe formatted response as below:\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/3\",\n    \"value\": {\n      \"id\": \"3\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"group_name\": \"test_group\"\n        }\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacosWithGroupName\\/*\"\n    }\n  }\n}\n```\n\n#### Specify the namespace and group\n\nExample of routing a request with an URI of \"/nacosWithNamespaceIdAndGroupName/*\" to a service with name, namespaceId, groupName \"http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS&namespaceId=test_ns&groupName=test_group\" and use nacos discovery client in the registry:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/4 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacosWithNamespaceIdAndGroupName/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\",\n          \"group_name\": \"test_group\"\n        }\n    }\n}'\n```\n\nThe formatted response as below:\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/4\",\n    \"value\": {\n      \"id\": \"4\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\",\n          \"group_name\": \"test_group\"\n        }\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacosWithNamespaceIdAndGroupName\\/*\"\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/en/latest/discovery.md",
    "content": "---\ntitle: Integration service discovery registry\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Summary\n\nWhen system traffic changes, the number of servers of the upstream service also increases or decreases, or the server needs to be replaced due to its hardware failure. If the gateway maintains upstream service information through configuration, the maintenance costs in the microservices architecture pattern are unpredictable. Furthermore, due to the untimely update of these information, will also bring a certain impact for the business, and the impact of human error operation can not be ignored. So it is very necessary for the gateway to automatically get the latest list of service instances through the service registry。As shown in the figure below：\n\n![discovery through service registry](../../assets/images/discovery.png)\n\n1. When the service starts, it will report some of its information, such as the service name, IP, port and other information to the registry. The services communicate with the registry using a mechanism such as a heartbeat, and if the registry and the service are unable to communicate for a long time, the instance will be cancel.When the service goes offline, the registry will delete the instance information.\n2. The gateway gets service instance information from the registry in near-real time.\n3. When the user requests the service through the gateway, the gateway selects one instance from the registry for proxy.\n\n## How to extend the discovery client?\n\n### Basic steps\n\nIt is very easy for APISIX to extend the discovery client, the basic steps are as follows\n\n1. Add the implementation of registry client in the 'apisix/discovery/' directory;\n\n2. Implement the `_M.init_worker()` function for initialization and the `_M.nodes(service_name)` function for obtaining the list of service instance nodes;\n\n3. If you need the discovery module to export the debugging information online, implement the `_M.dump_data()` function;\n\n4. Convert the registry data into data in APISIX;\n\n### the example of Eureka\n\n#### Implementation of Eureka client\n\nFirst, create a directory `eureka` under `apisix/discovery`;\n\nAfter that, add [`init.lua`](https://github.com/apache/apisix/blob/master/apisix/discovery/init.lua) in the `apisix/discovery/eureka` directory;\n\nThen implement the `_M.init_worker()` function for initialization and the `_M.nodes(service_name)` function for obtaining the list of service instance nodes in `init.lua`:\n\n  ```lua\n  local _M = {\n      version = 1.0,\n  }\n\n\n  function _M.nodes(service_name)\n      ... ...\n  end\n\n\n  function _M.init_worker()\n      ... ...\n  end\n\n\n  function _M.dump_data()\n      return {config = your_config, services = your_services, other = ... }\n  end\n\n\n  return _M\n  ```\n\nFinally, provide the schema for YAML configuration in the `schema.lua` under `apisix/discovery/eureka`.\n\n#### How convert Eureka's instance data to APISIX's node?\n\nHere's an example of Eureka's data：\n\n```json\n{\n  \"applications\": {\n      \"application\": [\n          {\n              \"name\": \"USER-SERVICE\",                 # service name\n              \"instance\": [\n                  {\n                      \"instanceId\": \"192.168.1.100:8761\",\n                      \"hostName\": \"192.168.1.100\",\n                      \"app\": \"USER-SERVICE\",          # service name\n                      \"ipAddr\": \"192.168.1.100\",      # IP address\n                      \"status\": \"UP\",\n                      \"overriddenStatus\": \"UNKNOWN\",\n                      \"port\": {\n                          \"$\": 8761,\n                          \"@enabled\": \"true\"\n                      },\n                      \"securePort\": {\n                          \"$\": 443,\n                          \"@enabled\": \"false\"\n                      },\n                      \"metadata\": {\n                          \"management.port\": \"8761\",\n                          \"weight\": 100               # Setting by 'eureka.instance.metadata-map.weight' of the spring boot application\n                      },\n                      \"homePageUrl\": \"http://192.168.1.100:8761/\",\n                      \"statusPageUrl\": \"http://192.168.1.100:8761/actuator/info\",\n                      \"healthCheckUrl\": \"http://192.168.1.100:8761/actuator/health\",\n                      ... ...\n                  }\n              ]\n          }\n      ]\n  }\n}\n```\n\nDeal with the Eureka's instance data need the following steps :\n\n1. select the UP instance. When the value of `overriddenStatus` is \"UP\" or the value of `overriddenStatus` is \"UNKNOWN\" and the value of `status` is \"UP\".\n2. Host. The `ipAddr` is the IP address of instance; and must be IPv4 or IPv6.\n3. Port. If the value of `port[\"@enabled\"]` is equal to \"true\", using the value of `port[\"\\$\"]`, If the value of `securePort[\"@enabled\"]` is equal to \"true\", using the value of `securePort[\"\\$\"]`.\n4. Weight. `local weight = metadata.weight or local_conf.eureka.weight or 100`\n\nThe result of this example is as follows:\n\n```json\n[\n  {\n    \"host\" : \"192.168.1.100\",\n    \"port\" : 8761,\n    \"weight\" : 100,\n    \"metadata\" : {\n      \"management.port\": \"8761\"\n    }\n  }\n]\n```\n\n## Configuration for discovery client\n\n### Initial service discovery\n\nAdd the following configuration to `conf/config.yaml` to add different service discovery clients for dynamic selection during use:\n\n```yaml\ndiscovery:\n  eureka:\n      ...\n```\n\nThis name should be consistent with the file name of the implementation registry in the `apisix/discovery/` directory.\n\nThe supported discovery client: Eureka.\n\n### Configuration for Eureka\n\nAdd following configuration in `conf/config.yaml` ：\n\n```yaml\ndiscovery:\n  eureka:\n    host:                            # it's possible to define multiple eureka hosts addresses of the same eureka cluster.\n      - \"http://${username}:${password}@${eureka_host1}:${eureka_port1}\"\n      - \"http://${username}:${password}@${eureka_host2}:${eureka_port2}\"\n    prefix: \"/eureka/\"\n    fetch_interval: 30               # 30s\n    weight: 100                      # default weight for node\n    timeout:\n      connect: 2000                  # 2000ms\n      send: 2000                     # 2000ms\n      read: 5000                     # 5000ms\n```\n\n## Upstream setting\n\n### L7\n\nHere is an example of routing a request with a URL of \"/user/*\" to a service which named \"user-service\" and use eureka discovery client in the registry :\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/user/*\",\n    \"upstream\": {\n        \"service_name\": \"USER-SERVICE\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"eureka\"\n    }\n}'\n\nHTTP/1.1 201 Created\nDate: Sat, 31 Aug 2019 01:17:15 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\n{\"node\":{\"value\":{\"uri\":\"\\/user\\/*\",\"upstream\": {\"service_name\": \"USER-SERVICE\", \"type\": \"roundrobin\", \"discovery_type\": \"eureka\"}},\"createdIndex\":61925,\"key\":\"\\/apisix\\/routes\\/1\",\"modifiedIndex\":61925}}\n```\n\nBecause the upstream interface URL may have conflict, usually in the gateway by prefix to distinguish:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/a/*\",\n    \"plugins\": {\n        \"proxy-rewrite\" : {\n            \"regex_uri\": [\"^/a/(.*)\", \"/${1}\"]\n        }\n    },\n    \"upstream\": {\n        \"service_name\": \"A-SERVICE\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"eureka\"\n    }\n}'\n\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/b/*\",\n    \"plugins\": {\n        \"proxy-rewrite\" : {\n            \"regex_uri\": [\"^/b/(.*)\", \"/${1}\"]\n        }\n    },\n    \"upstream\": {\n        \"service_name\": \"B-SERVICE\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"eureka\"\n    }\n}'\n```\n\nSuppose both A-SERVICE and B-SERVICE provide a `/test` API. The above configuration allows access to A-SERVICE's `/test` API through `/a/test` and B-SERVICE's `/test` API through `/b/test`.\n\n**Notice**：When configuring `upstream.service_name`,  `upstream.nodes` will no longer take effect, but will be replaced by 'nodes' obtained from the registry.\n\n### L4\n\nEureka service discovery also supports use in L4, the configuration method is similar to L7.\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n        \"scheme\": \"tcp\",\n        \"discovery_type\": \"eureka\",\n        \"service_name\": \"APISIX-EUREKA\",\n        \"type\": \"roundrobin\"\n    }\n}'\nHTTP/1.1 200 OK\nDate: Fri, 30 Dec 2022 03:52:19 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.0.0\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nAccess-Control-Expose-Headers: *\nAccess-Control-Max-Age: 3600\nX-API-VERSION: v3\n\n{\"key\":\"\\/apisix\\/stream_routes\\/1\",\"value\":{\"remote_addr\":\"127.0.0.1\",\"upstream\":{\"hash_on\":\"vars\",\"type\":\"roundrobin\",\"discovery_type\":\"eureka\",\"scheme\":\"tcp\",\"pass_host\":\"pass\",\"service_name\":\"APISIX-EUREKA\"},\"id\":\"1\",\"create_time\":1672106762,\"update_time\":1672372339}}\n```\n\n## Embedded control api for debugging\n\nSometimes we need the discovery client to export online data snapshot in memory when running for debugging, and if you implement the `_M. dump_data()` function:\n\n```lua\nfunction _M.dump_data()\n    return {config = local_conf.discovery.eureka, services = applications}\nend\n```\n\nThen you can call its control api as below:\n\n```shell\nGET /v1/discovery/{discovery_type}/dump\n```\n\neg:\n\n```shell\ncurl http://127.0.0.1:9090/v1/discovery/eureka/dump\n```\n"
  },
  {
    "path": "docs/en/latest/examples/plugins-hmac-auth-generate-signature.md",
    "content": "---\ntitle: HMAC Generate Signature Examples\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Python 3\n\n```python\nimport base64\nimport hashlib\nimport hmac\n\nsecret = bytes('the shared secret key here', 'utf-8')\nmessage = bytes('this is signature string', 'utf-8')\n\n\nhash = hmac.new(secret, message, hashlib.sha256)\n\n# to lowercase hexits\nhash.hexdigest()\n\n# to lowercase base64\nbase64.b64encode(hash.digest())\n```\n\n## Java\n\n```java\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport javax.xml.bind.DatatypeConverter;\n\nclass Main {\n  public static void main(String[] args) {\n   try {\n     String secret = \"the shared secret key here\";\n     String message = \"this is signature string\";\n\n     Mac hasher = Mac.getInstance(\"HmacSHA256\");\n     hasher.init(new SecretKeySpec(secret.getBytes(), \"HmacSHA256\"));\n\n     byte[] hash = hasher.doFinal(message.getBytes());\n\n     // to lowercase hexits\n     DatatypeConverter.printHexBinary(hash);\n\n     // to base64\n     DatatypeConverter.printBase64Binary(hash);\n   }\n   catch (NoSuchAlgorithmException e) {}\n   catch (InvalidKeyException e) {}\n  }\n}\n```\n\n## Go\n\n```go\npackage main\n\nimport (\n    \"crypto/hmac\"\n    \"crypto/sha256\"\n    \"encoding/base64\"\n    \"encoding/hex\"\n)\n\nfunc main() {\n    secret := []byte(\"the shared secret key here\")\n    message := []byte(\"this is signature string\")\n\n    hash := hmac.New(sha256.New, secret)\n    hash.Write(message)\n\n    // to lowercase hexits\n    hex.EncodeToString(hash.Sum(nil))\n\n    // to base64\n    base64.StdEncoding.EncodeToString(hash.Sum(nil))\n}\n```\n\n## Ruby\n\n```ruby\nrequire 'base64'\nrequire 'openssl'\n\nsecret = 'the shared secret key here'\nmessage = 'this is signature string'\n\n# to lowercase hexits\nOpenSSL::HMAC.hexdigest('sha256', secret, message)\n\n# to base64\nBase64.encode64(OpenSSL::HMAC.digest('sha256', secret, message))\n```\n\n## NodeJs\n\n```js\nvar crypto = require('crypto');\n\nvar secret = 'the shared secret key here';\nvar message = 'this is signature string';\n\nvar hash = crypto.createHmac('sha256', secret).update(message);\n\n// to lowercase hexits\nhash.digest('hex');\n\n// to base64\nhash.digest('base64');\n```\n\n## JavaScript ES6\n\n```js\nconst secret = 'the shared secret key here';\nconst message = 'this is signature string';\n\nconst getUtf8Bytes = str =>\n  new Uint8Array(\n    [...unescape(encodeURIComponent(str))].map(c => c.charCodeAt(0))\n  );\n\nconst secretBytes = getUtf8Bytes(secret);\nconst messageBytes = getUtf8Bytes(message);\n\nconst cryptoKey = await crypto.subtle.importKey(\n  'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' },\n  true, ['sign']\n);\nconst sig = await crypto.subtle.sign('HMAC', cryptoKey, messageBytes);\n\n// to lowercase hexits\n[...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, '0')).join('');\n\n// to base64\nbtoa(String.fromCharCode(...new Uint8Array(sig)));\n```\n\n## PHP\n\n```php\n<?php\n\n$secret = 'the shared secret key here';\n$message = 'this is signature string';\n\n// to lowercase hexits\nhash_hmac('sha256', $message, $secret);\n\n// to base64\nbase64_encode(hash_hmac('sha256', $message, $secret, true));\n```\n\n## Lua\n\n```lua\nlocal hmac = require(\"resty.hmac\")\nlocal secret = 'the shared secret key here'\nlocal message = 'this is signature string'\nlocal digest = hmac:new(secret, hmac.ALGOS.SHA256):final(message)\n\n--to lowercase hexits\nngx.say(digest)\n\n--to base64\nngx.say(ngx.encode_base64(digest))\n```\n\n## Shell\n\n```bash\nSECRET=\"the shared secret key here\"\nMESSAGE=\"this is signature string\"\n\n# to lowercase hexits\necho -e $MESSAGE | openssl dgst -sha256 -hmac $SECRET\n\n# to base64\necho -e $MESSAGE | openssl dgst -sha256 -hmac $SECRET -binary | base64\n```\n"
  },
  {
    "path": "docs/en/latest/external-plugin.md",
    "content": "---\ntitle: External Plugin\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## What are external plugin and plugin runner\n\nAPISIX supports writing plugins in Lua. This type of plugin will be executed\ninside APISIX. Sometimes you want to develop plugins in other languages, so APISIX\nprovides sidecars that load your plugins and run them when the requests hit\nAPISIX. These sidecars are called plugin runners and your plugins are called\nexternal plugins.\n\n## How does it work\n\n![external-plugin](../../assets/images/external-plugin.png)\n\nWhen you configure a plugin runner in APISIX, APISIX will run the plugin runner\nas a subprocess. The process will belong to the same user of the APISIX\nprocess. When we restart or reload APISIX, the plugin runner will be restarted too.\n\nOnce you have configured `ext-plugin-*` plugins for a given route, the requests\nwhich hit the route will trigger RPC call from APISIX to the plugin runner via\nunix socket.\n\nThe plugin runner will handle the RPC call, create a fake request at its side,\nrun external plugins and return the result back to APISIX.\n\nThe target external plugins and the execution order are configured in the `ext-plugin-*`\nplugins. Like other plugins, they can be enabled and reconfigured on the fly.\n\n## How is it implemented\n\nIf you are interested in the implementation of Plugin Runner, please refer to [The Implementation of Plugin Runner](./internal/plugin-runner.md).\n\n## Supported plugin runners\n\n- Java: https://github.com/apache/apisix-java-plugin-runner\n- Go: https://github.com/apache/apisix-go-plugin-runner\n- Python: https://github.com/apache/apisix-python-plugin-runner\n- JavaScript: https://github.com/zenozeng/apisix-javascript-plugin-runner\n\n## Configuration for plugin runner in APISIX\n\nTo run the plugin runner in the prod, add the section below to `config.yaml`:\n\n```yaml\next-plugin:\n  cmd: [\"blah\"] # replace it to the real runner executable according to the runner you choice\n```\n\nThen APISIX will manage the runner as its subprocess.\n\nNote: APISIX can't manage the runner on the Mac in `v2.6`.\n\nDuring development, we want to run the runner separately so that we can restart it without\nrestarting APISIX first.\n\nBy specifying the environment variable `APISIX_LISTEN_ADDRESS`, we can force the runner to\nlisten to a fixed address.\nFor instance:\n\n```bash\nAPISIX_LISTEN_ADDRESS=unix:/tmp/x.sock ./the_runner\n```\n\nwill force the runner to listen to `/tmp/x.sock`.\n\nThen you need to configure APISIX to send RPC to the fixed address:\n\n```yaml\next-plugin:\n  # cmd: [\"blah\"] # don't configure the executable!\n  path_for_test: \"/tmp/x.sock\" # without 'unix:' prefix\n```\n\nIn the prod environment, `path_for_test` should not be used and the unix socket\npath will be generated dynamically.\n\n## FAQ\n\n### When managing by APISIX, the runner can't access my environment variable\n\nSince `v2.7`, APISIX can pass environment variables to the runner.\n\nHowever, Nginx will hide all environment variables by default. So you need to\ndeclare your variable first in the `conf/config.yaml`:\n\n```yaml\nnginx_config:\n  envs:\n    - MY_ENV_VAR\n```\n\n### APISIX terminates my runner with SIGKILL but not SIGTERM!\n\nSince `v2.7`, APISIX will stop the runner with SIGTERM when it is running on\nOpenResty 1.19+.\n\nHowever, APISIX needs to wait for the runner to quit so that we can ensure the resource\nfor the process group is freed.\n\nTherefore, we send SIGTERM first. And then after 1 second, if the runner is still\nrunning, we will send SIGKILL.\n"
  },
  {
    "path": "docs/en/latest/getting-started/README.md",
    "content": "---\ntitle: Get APISIX\ndescription: This tutorial uses a script to quickly install Apache APISIX in your local environment and verify it through the Admin API.\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/\" />\n</head>\n\n> The Getting Started tutorials are contributed by [API7.ai](https://api7.ai/).\n\nDeveloped and donated by API7.ai, Apache APISIX is an open source, dynamic, scalable, and high-performance cloud native API gateway for all your APIs and microservices. It is a [top-level project](https://projects.apache.org/project.html?apisix) of the Apache Software Foundation.\n\nYou can use APISIX API Gateway as a traffic entrance to process all business data. It offers features including dynamic routing, dynamic upstream, dynamic certificates, A/B testing, canary release, blue-green deployment, limit rate, defense against malicious attacks, metrics, monitoring alarms, service observability, service governance, and more.\n\nThis tutorial uses a script to quickly install [Apache APISIX](https://api7.ai/apisix) in your local environment and verifies the installation through the Admin API.\n\n## Prerequisite(s)\n\nThe quickstart script relies on several components:\n\n* [Docker](https://docs.docker.com/get-docker/) is used to install the containerized **etcd** and **APISIX**.\n* [curl](https://curl.se/) is used to send requests to APISIX for validation.\n\n## Get APISIX\n\n:::caution\n\nTo provide a better experience in this tutorial, the authorization of Admin API is switched off by default. Please turn on the authorization of Admin API in the production environment.\n\n:::\nAPISIX can be easily installed and started with the quickstart script:\n\n```shell\ncurl -sL https://run.api7.ai/apisix/quickstart | sh\n```\n\nThe script should start two Docker containers, _apisix-quickstart_ and _etcd_. APISIX uses etcd to save and synchronize configurations. Both the etcd and the APISIX use [**host**](https://docs.docker.com/network/host/) Docker network mode. That is, the APISIX can be accessed from local.\n\nYou will see the following message once APISIX is ready:\n\n```text\n✔ APISIX is ready!\n```\n\n## Validate\n\nOnce APISIX is running, you can use curl to interact with it. Send a simple HTTP request to validate if APISIX is working properly:\n\n```shell\ncurl \"http://127.0.0.1:9080\" --head | grep Server\n```\n\nIf everything is ok, you will get the following response:\n\n```text\nServer: APISIX/Version\n```\n\n`Version` refers to the version of APISIX that you have installed. For example, `APISIX/3.3.0`.\n\nYou now have APISIX installed and running successfully!​\n\nAPISIX includes a built-in Dashboard UI, accessible at http://127.0.0.1:9180/ui. For more guidance, please read [Apache APISIX Dashboard](../dashboard.md).\n\n## Next Steps\n\nThe following tutorial is based on the working APISIX, please keep everything running and move on to the next step.\n\n* [Configure Routes](configure-routes.md)\n* [Load Balancing](load-balancing.md)\n* [Rate Limiting](rate-limiting.md)\n* [Key Authentication](key-authentication.md)\n"
  },
  {
    "path": "docs/en/latest/getting-started/configure-routes.md",
    "content": "---\ntitle: Configure Routes\nslug: /getting-started/configure-routes\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/configure-routes\" />\n</head>\n\n> The Getting Started tutorials are contributed by [API7.ai](https://api7.ai/).\n\nApache APISIX provides flexible gateway management capabilities based on _routes_, where routing paths and targets are defined for requests.\n\nThis tutorial guides you on how to create a route and validate it. You will complete the following steps:\n\n1. Create a route with a sample _upstream_ that points to [httpbin.org](http://httpbin.org).\n2. Use _cURL_ to send a test request to see how APISIX proxies and forwards the request.\n\n## What is a Route\n\nA route is a routing path to upstream targets. In [Apache APISIX](https://api7.ai/apisix), routes are responsible for matching client's requests based on defined rules, loading and executing the corresponding plugins, as well as forwarding requests to the specified upstream services.\n\nIn APISIX, a simple route can be set up with a path-matching URI and a corresponding upstream address.\n\n## What is an Upstream\n\nAn upstream is a set of target nodes with the same work. It defines a virtual host abstraction that performs load balancing on a given set of service nodes according to the configured rules.\n\n## Prerequisite(s)\n\n1. Complete [Get APISIX](./README.md) to install APISIX.\n\n## Create a Route\n\nIn this section, you will create a route that forwards client requests to [httpbin.org](http://httpbin.org), a public HTTP request and response service.\n\nThe following command creates a route, which should forward all requests sent to `http://127.0.0.1:9080/ip` to [httpbin.org/ip](http://httpbin.org/ip):\n\n[//]: <TODO: Add the link to the authorization of Admin API>\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"getting-started-ip\",\n  \"uri\": \"/ip\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\nYou will receive an `HTTP/1.1 201 Created` response if the route was created successfully.\n\n## Validate\n\n```shell\ncurl \"http://127.0.0.1:9080/ip\"\n```\n\nThe expected response is similar to the following:\n\n```text\n{\n  \"origin\": \"183.94.122.205\"\n}\n```\n\n## What's Next\n\nThis tutorial creates a route with only one target node. In the next tutorial, you will learn how to configure load balancing with multiple target nodes.\n"
  },
  {
    "path": "docs/en/latest/getting-started/key-authentication.md",
    "content": "---\ntitle: Key Authentication\nslug: /getting-started/key-authentication\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/key-authentication\" />\n</head>\n\n> The Getting Started tutorials are contributed by [API7.ai](https://api7.ai/).\n\nAn API gateway's primary role is to connect API consumers and providers. For security reasons, it should authenticate and authorize consumers before access to internal resources.\n\n![Key Authentication](https://static.apiseven.com/uploads/2023/02/08/8mRaK3v1_consumer.png)\n\nAPISIX has a flexible plugin extension system and a number of existing plugins for user authentication and authorization. For example:\n\n- [Key Authentication](https://apisix.apache.org/docs/apisix/plugins/key-auth/)\n- [Basic Authentication](https://apisix.apache.org/docs/apisix/plugins/basic-auth/)\n- [JSON Web Token (JWT) Authentication](https://apisix.apache.org/docs/apisix/plugins/jwt-auth/)\n- [Keycloak](https://apisix.apache.org/docs/apisix/plugins/authz-keycloak/)\n- [Casdoor](https://apisix.apache.org/docs/apisix/plugins/authz-casdoor/)\n- [Wolf RBAC](https://apisix.apache.org/docs/apisix/plugins/wolf-rbac/)\n- [OpenID Connect](https://apisix.apache.org/docs/apisix/plugins/openid-connect/)\n- [Central Authentication Service (CAS)](https://apisix.apache.org/docs/apisix/plugins/cas-auth/)\n- [HMAC](https://apisix.apache.org/docs/apisix/plugins/hmac-auth/)\n- [Casbin](https://apisix.apache.org/docs/apisix/plugins/authz-casbin/)\n- [LDAP](https://apisix.apache.org/docs/apisix/plugins/ldap-auth/)\n- [Open Policy Agent (OPA)](https://apisix.apache.org/docs/apisix/plugins/opa/)\n- [Forward Authentication](https://apisix.apache.org/docs/apisix/plugins/forward-auth/)\n- [Multiple Authentications](https://apisix.apache.org/docs/apisix/plugins/multi-auth/)\n\nIn this tutorial, you will create a _consumer_ with _key authentication_, and learn how to enable and disable key authentication.\n\n## What is a Consumer\n\nA Consumer is an application or a developer who consumes the API.\n\nIn APISIX, a Consumer requires a unique _username_ and an authentication _plugin_ from the list above to be created.\n\n## What is Key Authentication\n\nKey authentication is a relatively simple but widely used authentication approach. The idea is as follows:\n\n1. Administrator adds an authentication key (API key) to the Route.\n2. API consumers add the key to the query string or headers for authentication when sending requests.\n\n## Enable Key Authentication\n\n### Prerequisite(s)\n\n1. Complete [Get APISIX](./README.md) to install APISIX.\n2. Complete [Configure Routes](./configure-routes.md#what-is-a-route).\n\n### Create a Consumer\n\nLet's create a consumer named `tom` and enable the `key-auth` plugin with an API key `secret-key`. All requests sent with the key `secret-key` should be authenticated as `tom`.\n\n:::caution\n\nPlease use a complex key in the Production environment.\n\n:::\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT -d '\n{\n  \"username\": \"tom\",\n  \"plugins\": {\n    \"key-auth\": {\n      \"key\": \"secret-key\"\n    }\n  }\n}'\n```\n\nYou will receive an `HTTP/1.1 201 Created` response if the consumer was created successfully.\n\n### Enable Authentication\n\nInheriting the route `getting-started-ip` from [Configure Routes](./configure-routes.md), we only need to use the `PATCH` method to add the `key-auth` plugin to the route:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n  \"plugins\": {\n    \"key-auth\": {}\n  }\n}'\n```\n\nYou will receive an `HTTP/1.1 201 Created` response if the plugin was added successfully.\n\n### Validate\n\nLet's validate the authentication in the following scenarios:\n\n#### 1. Send a request without any key\n\nSend a request without the `apikey` header.\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\"\n```\n\nSince you enabled the key authentication, you will receive an unauthorized response with `HTTP/1.1 401 Unauthorized`.\n\n```text\nHTTP/1.1 401 Unauthorized\nDate: Wed, 08 Feb 2023 09:38:36 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.1.0\n```\n\n#### 2. Send a request with a wrong key\n\nSend a request with a wrong key (e.g. `wrong-key`) in the `apikey` header.\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\" -H 'apikey: wrong-key'\n```\n\nSince the key is incorrect, you will receive an unauthorized response with `HTTP/1.1 401 Unauthorized`.\n\n```text\nHTTP/1.1 401 Unauthorized\nDate: Wed, 08 Feb 2023 09:38:27 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.1.0\n```\n\n#### 3. Send a request with the correct key\n\nSend a request with the correct key (`secret-key`) in the `apikey` header.\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\" -H 'apikey: secret-key'\n```\n\nYou will receive an `HTTP/1.1 200 OK` response.\n\n```text\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 44\nConnection: keep-alive\nDate: Thu, 09 Feb 2023 03:27:57 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/3.1.0\n```\n\n### Disable Authentication\n\nDisable the key authentication plugin by setting the `_meta.disable` parameter to `true`.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n  \"plugins\": {\n    \"key-auth\": {\n      \"_meta\": {\n        \"disable\": true\n      }\n    }\n  }\n}'\n```\n\nYou can send a request without any key to validate:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\"\n```\n\nBecause you have disabled the key authentication plugin, you will receive an `HTTP/1.1 200 OK` response.\n\n## What's Next\n\nYou have learned how to configure key authentication for a route. In the next tutorial, you will learn how to configure rate limiting.\n"
  },
  {
    "path": "docs/en/latest/getting-started/load-balancing.md",
    "content": "---\ntitle: Load Balancing\nslug: /getting-started/load-balancing\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/load-balancing\" />\n</head>\n\n> The Getting Started tutorials are contributed by [API7.ai](https://api7.ai/).\n\nLoad balancing manages traffic between clients and servers. It is a mechanism used to decide which server handles a specific request, allowing for improved performance, scalability, and reliability. Load balancing is a key consideration in designing systems that need to handle a large volume of traffic.\n\nApache APISIX supports weighted round-robin load balancing, in which incoming traffic are distributed across a set of servers in a cyclical pattern, with each server taking a turn in a predefined order.\n\nIn this tutorial, you will create a route with two upstream services and enable round-robin load balancing to distribute traffic between the two services.\n\n## Prerequisite(s)\n\n1. Complete [Get APISIX](./README.md) to install APISIX.\n2. Understand APISIX [Route and Upstream](./configure-routes.md#what-is-a-route).\n\n## Enable Load Balancing\n\nLet's create a route with two upstream services. All requests sent to the `/headers` endpoint will be forwarded to [httpbin.org](https://httpbin.org/headers) and [mock.api7.ai](https://mock.api7.ai/headers), which should echo back the requester's headers.\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"getting-started-headers\",\n  \"uri\": \"/headers\",\n  \"upstream\" : {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:443\": 1,\n      \"mock.api7.ai:443\": 1\n    },\n    \"pass_host\": \"node\",\n    \"scheme\": \"https\"\n  }\n}'\n```\n\nYou will receive an `HTTP/1.1 201 Created` response if the route was created successfully.\n\n:::info\n\n1. The `pass_host` field is set to `node` to pass the host header to the upstream.\n2. The `scheme` field is set to `https` to enable TLS when sending requests to the upstream.\n\n:::\n\n## Validate\n\nThe two services respond with different data.\n\nFrom `httpbin.org`:\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/7.58.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-63e34b15-19f666602f22591b525e1e80\",\n    \"X-Forwarded-Host\": \"localhost\"\n  }\n}\n```\n\nFrom `mock.api7.ai`:\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"mock.api7.ai\",\n    \"user-agent\": \"curl/7.58.0\",\n    \"content-type\": \"application/json\",\n    \"x-application-owner\": \"API7.ai\"\n  }\n}\n```\n\nLet's generate 100 requests to test the load-balancing effect:\n\n```shell\nhc=$(seq 100 | xargs -I {} curl \"http://127.0.0.1:9080/headers\" -sL | grep \"httpbin\" | wc -l); echo httpbin.org: $hc, mock.api7.ai: $((100 - $hc))\n```\n\nThe result shows the requests were distributed over the two services almost equally:\n\n```text\nhttpbin.org: 51, mock.api7.ai: 49\n```\n\n## What's Next\n\nYou have learned how to configure load balancing. In the next tutorial, you will learn how to configure key authentication.\n"
  },
  {
    "path": "docs/en/latest/getting-started/rate-limiting.md",
    "content": "---\ntitle: Rate Limiting\nslug: /getting-started/rate-limiting\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/rate-limiting\" />\n</head>\n\n> The Getting Started tutorials are contributed by [API7.ai](https://api7.ai/).\n\nAPISIX is a unified control point, managing the ingress and egress of APIs and microservices traffic. In addition to the legitimate client requests, these requests may also include unwanted traffic generated by web crawlers as well as cyber attacks, such as DDoS.\n\nAPISIX offers rate limiting capabilities to protect APIs and microservices by limiting the number of requests sent to upstream services in a given period of time. The count of requests is done efficiently in memory with low latency and high performance.\n\n<br />\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.apiseven.com/uploads/2023/02/20/l9G9Kq41_rate-limiting.png\" alt=\"Routes Diagram\" />\n</div>\n<br />\n\nIn this tutorial, you will enable the `limit-count` plugin to set a rate limiting constraint on the incoming traffic.\n\n## Prerequisite(s)\n\n1. Complete the [Get APISIX](./README.md) step to install APISIX first.\n2. Complete the [Configure Routes](./configure-routes.md#what-is-a-route) step.\n\n## Enable Rate Limiting\n\nThe following route `getting-started-ip` is inherited from [Configure Routes](./configure-routes.md). You only need to use the `PATCH` method to add the `limit-count` plugin to the route:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n  \"plugins\": {\n    \"limit-count\": {\n        \"count\": 2,\n        \"time_window\": 10,\n        \"rejected_code\": 503\n     }\n  }\n}'\n```\n\nYou will receive an `HTTP/1.1 201 Created` response if the plugin was added successfully. The above configuration limits the incoming requests to a maximum of 2 requests within 10 seconds.\n\n### Validate\n\nLet's generate 100 simultaneous requests to see the rate limiting plugin in effect.\n\n```shell\ncount=$(seq 100 | xargs -I {} curl \"http://127.0.0.1:9080/ip\" -I -sL | grep \"503\" | wc -l); echo \\\"200\\\": $((100 - $count)), \\\"503\\\": $count\n```\n\nThe results are as expected: out of the 100 requests, 2 requests were sent successfully (status code `200`) while the others were rejected (status code `503`).\n\n```text\n\"200\": 2, \"503\": 98\n```\n\n## Disable Rate Limiting\n\nDisable rate limiting by setting the `_meta.disable` parameter to `true`:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"_meta\": {\n                \"disable\": true\n            }\n        }\n    }\n}'\n```\n\n### Validate\n\nLet's generate 100 requests again to validate if it is disabled:\n\n```shell\ncount=$(seq 100 | xargs -i curl \"http://127.0.0.1:9080/ip\" -I -sL | grep \"503\" | wc -l); echo \\\"200\\\": $((100 - $count)), \\\"503\\\": $count\n```\n\nThe results below show that all of the requests were sent successfully:\n\n```text\n\"200\": 100, \"503\": 0\n```\n\n## More\n\n[//]: <TODO: Add the link to matching rules configuration>\n[//]: <TODO: Add the link to cluster-level rate limiting>\n[//]: <TODO: Add the link to APISIX variables>\nYou can use the APISIX variables to configure fined matching rules of rate limiting, such as `$host` and `$uri`. In addition, APISIX also supports rate limiting at the cluster level using Redis.\n\n## What's Next\n\nCongratulations! You have learned how to configure rate limiting and completed the Getting Started tutorials.\n\nYou can continue to explore other documentations to customize APISIX and meet your production needs.\n"
  },
  {
    "path": "docs/en/latest/grpc-proxy.md",
    "content": "---\ntitle: gRPC Proxy\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nproxying gRPC traffic:\ngRPC client -> APISIX -> gRPC/gRPCS server\n\n## Parameters\n\n* `scheme`: the `scheme` of the route's upstream must be `grpc` or `grpcs`.\n* `uri`: format likes /service/method, Example：/helloworld.Greeter/SayHello\n\n### Example\n\n#### create proxying gRPC route\n\nHere's an example, to proxying gRPC service by specified route:\n\n* attention: the `scheme` of the route's upstream must be `grpc` or `grpcs`.\n* attention: APISIX use TLS‑encrypted HTTP/2 to expose gRPC service, so need to [config SSL certificate](certificate.md)\n* attention: APISIX also support to expose gRPC service with plaintext HTTP/2, which does not rely on TLS, usually used to proxy gRPC service in intranet environment\n* the grpc server example：[grpc_server_example](https://github.com/api7/grpc_server_example)\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/helloworld.Greeter/SayHello\",\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\n#### testing HTTP/2 with TLS‑encrypted\n\nInvoking the route created before：\n\n```shell\n$ grpcurl -insecure -import-path /pathtoprotos  -proto helloworld.proto  -d '{\"name\":\"apisix\"}' 127.0.0.1:9443 helloworld.Greeter.SayHello\n{\n  \"message\": \"Hello apisix\"\n}\n```\n\n> grpcurl is a CLI tool, similar to curl, that acts as a gRPC client and lets you interact with a gRPC server. For installation, please check out the official [documentation](https://github.com/fullstorydev/grpcurl#installation).\n\nThis means that the proxying is working.\n\n#### testing HTTP/2 with plaintext\n\nBy default, the APISIX only listens to `9443` for TLS‑encrypted HTTP/2. You can support HTTP/2 with plaintext via the `node_listen` section under `apisix` in `conf/config.yaml`:\n\n```yaml\napisix:\n    node_listen:\n        - port: 9080\n        - port: 9081\n    enable_http2: true\n```\n\nInvoking the route created before：\n\n```shell\n$ grpcurl -plaintext -import-path /pathtoprotos  -proto helloworld.proto  -d '{\"name\":\"apisix\"}' 127.0.0.1:9081 helloworld.Greeter.SayHello\n{\n  \"message\": \"Hello apisix\"\n}\n```\n\nThis means that the proxying is working.\n\n### gRPCS\n\nIf your gRPC service encrypts with TLS by itself (so called `gPRCS`, gPRC + TLS), you need to change the `scheme` to `grpcs`. The example above runs gRPCS service on port 50052, to proxy gRPC request, we need to use the configuration below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/helloworld.Greeter/SayHello\",\n    \"upstream\": {\n        \"scheme\": \"grpcs\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50052\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/http3.md",
    "content": "---\ntitle: HTTP/3 Protocol\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n[HTTP/3](https://en.wikipedia.org/wiki/HTTP/3) is the third major version of the Hypertext Transfer Protocol (HTTP). Unlike its predecessors which rely on TCP, HTTP/3 is based on [QUIC (Quick UDP Internet Connections) protocol](https://en.wikipedia.org/wiki/QUIC). It brings several benefits that collectively result in reduced latency and improved performance:\n\n* enabling seamless transition between different network connections, such as switching from Wi-Fi to mobile data.\n* eliminating head-of-line blocking, so that a lost packet does not block all streams.\n* negotiating TLS versions at the same time as the TLS handshakes, allowing for faster connections.\n* providing encryption by default, ensuring that all data transmitted over an HTTP/3 connection is protected and confidential.\n* providing zero round-trip time (0-RTT) when communicating with servers that clients already established connections to.\n\nAPISIX currently supports HTTP/3 connections between downstream clients and APISIX. HTTP/3 connections with upstream services are not yet supported, and contributions are welcomed.\n\n:::caution\n\nThis feature is currently experimental and not recommended for production use.\n\n:::\n\nThis document will show you how to configure APISIX to enable HTTP/3 connections between client and APISIX and document a few known issues.\n\n## Usage\n\n### Enable HTTP/3 in APISIX\n\nEnable HTTP/3 on port `9443` (or a different port) by adding the following configurations to APISIX's `config.yaml` configuration file:\n\n```yaml title=\"config.yaml\"\napisix:\n  ssl:\n    listen:\n      - port: 9443\n        enable_http3: true\n    ssl_protocols: TLSv1.3\n```\n\n:::info\n\nIf you are deploying APISIX using Docker, make sure to allow UDP in the HTTP3 port, such as `-p 9443:9443/udp`.\n\n:::\n\nThen reload APISIX for configuration changes to take effect:\n\n```shell\napisix reload\n```\n\n### Generate Certificates and Keys\n\nHTTP/3 requires TLS. You can leverage the purchased certificates or self-generate them, whichever applicable.\n\nTo self-generate, first generate the certificate authority (CA) key and certificate:\n\n```shell\nopenssl genrsa -out ca.key 2048 && \\\n  openssl req -new -sha256 -key ca.key -out ca.csr -subj \"/CN=ROOTCA\" && \\\n  openssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.crt\n```\n\nNext, generate the key and certificate with a common name for APISIX, and sign with the CA certificate:\n\n```shell\nopenssl genrsa -out server.key 2048 && \\\n  openssl req -new -sha256 -key server.key -out server.csr -subj \"/CN=test.com\" && \\\n  openssl x509 -req -days 36500 -sha256 -extensions v3_req \\\n  -CA ca.crt -CAkey ca.key -CAserial ca.srl -CAcreateserial \\\n  -in server.csr -out server.crt\n```\n\n### Configure HTTPS\n\nOptionally load the content stored in `server.crt` and `server.key` into shell variables:\n\n```shell\nserver_cert=$(cat server.crt)\nserver_key=$(cat server.key)\n```\n\nCreate an SSL certificate object to save the server certificate and its key:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/ssls\" -X PUT -d '\n{\n  \"id\": \"quickstart-tls-client-ssl\",\n  \"sni\": \"test.com\",\n  \"cert\": \"'\"${server_cert}\"'\",\n  \"key\": \"'\"${server_key}\"'\"\n}'\n```\n\n### Create a Route\n\nCreate a sample route to `httpbin.org`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\":\"httpbin-route\",\n  \"uri\":\"/get\",\n  \"upstream\": {\n    \"type\":\"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n### Verify HTTP/3 Connections\n\nInstall [static-curl](https://github.com/stunnel/static-curl) or any other curl executable that has HTTP/3 support.\n\nSend a request to the route:\n\n```shell\ncurl -kv --http3-only \\\n  -H \"Host: test.com\" \\\n  --resolve \"test.com:9443:127.0.0.1\" \"https://test.com:9443/get\"\n```\n\nYou should receive an `HTTP/3 200` response similar to the following:\n\n```text\n* Added test.com:9443:127.0.0.1 to DNS cache\n* Hostname test.com was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* QUIC cipher selection: TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256\n* Skipped certificate verification\n* Connected to test.com (127.0.0.1) port 9443\n* using HTTP/3\n* [HTTP/3] [0] OPENED stream for https://test.com:9443/get\n* [HTTP/3] [0] [:method: GET]\n* [HTTP/3] [0] [:scheme: https]\n* [HTTP/3] [0] [:authority: test.com]\n* [HTTP/3] [0] [:path: /get]\n* [HTTP/3] [0] [user-agent: curl/8.7.1]\n* [HTTP/3] [0] [accept: */*]\n> GET /get HTTP/3\n> Host: test.com\n> User-Agent: curl/8.7.1\n> Accept: */*\n>\n* Request completely sent off\n< HTTP/3 200\n...\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"0\",\n    \"Host\": \"test.com\",\n    \"User-Agent\": \"curl/8.7.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6656013a-27da6b6a34d98e3e79baaf5b\",\n    \"X-Forwarded-Host\": \"test.com\"\n  },\n  \"origin\": \"172.19.0.1, 123.40.79.456\",\n  \"url\": \"http://test.com/get\"\n}\n* Connection #0 to host test.com left intact\n```\n\n## Known Issues\n\n- For APISIX-3.9, test cases of Tongsuo will fail because the Tongsuo does not support QUIC TLS.\n- APISIX-3.9 is based on NGINX-1.25.3 with  vulnerabilities in HTTP/3 (CVE-2024-24989, CVE-2024-24990).\n"
  },
  {
    "path": "docs/en/latest/install-dependencies.md",
    "content": "---\ntitle: Install Dependencies\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Note\n\n- Since v2.0 Apache APISIX would not support the v2 protocol storage to etcd anymore, and the minimum etcd version supported is v3.4.0. What's more, etcd v3 uses gRPC as the messaging protocol, while Apache APISIX uses HTTP(S) to communicate with etcd cluster, so be sure the [etcd gRPC gateway](https://etcd.io/docs/v3.4.0/dev-guide/api_grpc_gateway/) is enabled.\n\n- Now by default Apache APISIX uses HTTP protocol to talk with etcd cluster, which is insecure. Please configure certificate and corresponding private key for your etcd cluster, and use \"https\" scheme explicitly in the etcd endpoints list in your Apache APISIX configuration, if you want to keep the data secure and integral. See the etcd section in `conf/config.yaml.example` for more details.\n\n- If it is OpenResty 1.19, APISIX will use OpenResty's built-in LuaJIT to run `bin/apisix`; otherwise it will use Lua 5.1. If you encounter `luajit: lj_asm_x86.h:2819: asm_loop_ fixup: Assertion '((intptr_t)target & 15) == 0' failed`, this is a problem with the low version of OpenResty's built-in LuaJIT under certain compilation conditions.\n\n- On some platforms, installing LuaRocks via the package manager will cause Lua to be upgraded to Lua 5.3, so we recommend installing LuaRocks via source code. if you install OpenResty and its OpenSSL develop library (openresty-openssl111-devel for rpm and openresty-openssl111-dev for deb) via the official repository, then [we provide a script for automatic installation](https://github.com/apache/apisix/blob/master/utils/linux-install-luarocks.sh). If you compile OpenResty yourself, you can refer to the above script and change the path in it. If you don't specify the OpenSSL library path when you compile, you don't need to configure the OpenSSL variables in LuaRocks, because the system's OpenSSL is used by default. If the OpenSSL library is specified at compile time, then you need to ensure that LuaRocks' OpenSSL configuration is consistent with OpenResty's.\n\n- OpenResty is a dependency of APISIX. If it is your first time to deploy APISIX and you don't need to use OpenResty to deploy other services, you can stop and disable OpenResty after installation since it will not affect the normal work of APISIX. Please operate carefully according to your service. For example in Ubuntu: `systemctl stop openresty && systemctl disable openresty`.\n\n## Install\n\nRun the following command to install Apache APISIX's dependencies on a supported operating system.\n\nSupported OS versions: Debian 11/12, Ubuntu 20.04/22.04/24.04, etc.\n\nNote that in the case of Arch Linux, we use `openresty` from the AUR, thus requiring a AUR helper. For now `yay` and `pacaur` are supported.\n\n```\ncurl https://raw.githubusercontent.com/apache/apisix/master/utils/install-dependencies.sh -sL | bash -\n```\n\nIf you have cloned the Apache APISIX project, execute in the Apache APISIX root directory:\n\n```\nbash utils/install-dependencies.sh\n```\n"
  },
  {
    "path": "docs/en/latest/installation-guide.md",
    "content": "---\ntitle: Installation\nkeywords:\n  - APISIX\n  - Installation\ndescription: This document walks you through the different Apache APISIX installation methods.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nThis guide walks you through how you can install and run Apache APISIX in your environment.\n\nRefer to the [Getting Started](./getting-started/README.md) guide for a quick walk-through on running Apache APISIX.\n\n## Installing APISIX\n\nAPISIX can be installed by the different methods listed below:\n\n<Tabs\n  groupId=\"install-method\"\n  defaultValue=\"docker\"\n  values={[\n    {label: 'Docker', value: 'docker'},\n    {label: 'Helm', value: 'helm'},\n    {label: 'RPM', value: 'rpm'},\n    {label: 'DEB', value: 'deb'},\n    {label: 'Source Code', value: 'source code'},\n  ]}>\n<TabItem value=\"docker\">\n\nFirst clone the [apisix-docker](https://github.com/apache/apisix-docker) repository:\n\n```shell\ngit clone https://github.com/apache/apisix-docker.git\ncd apisix-docker/example\n```\n\nNow, you can use `docker-compose` to start APISIX.\n\n<Tabs\n  groupId=\"cpu-arch\"\n  defaultValue=\"x86\"\n  values={[\n    {label: 'x86', value: 'x86'},\n    {label: 'ARM/M1', value: 'arm'},\n  ]}>\n<TabItem value=\"x86\">\n\n```shell\ndocker-compose -p docker-apisix up -d\n```\n\n</TabItem>\n\n<TabItem value=\"arm\">\n\n```shell\ndocker-compose -p docker-apisix -f docker-compose-arm64.yml up -d\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n\n<TabItem value=\"helm\">\n\nTo install APISIX via Helm, run:\n\n```shell\nhelm repo add apisix https://charts.apiseven.com\nhelm repo update\nhelm install apisix apisix/apisix --create-namespace  --namespace apisix\n```\n\nYou can find other Helm charts on the [apisix-helm-chart](https://github.com/apache/apisix-helm-chart) repository.\n\n</TabItem>\n\n<TabItem value=\"rpm\">\n\nThis installation method is suitable for Redhat 8 and compatible systems. If you choose this method to install APISIX, you need to install etcd first. For the specific installation method, please refer to [Installing etcd](#installing-etcd).\n\n### Installation via RPM repository\n\n```shell\nsudo yum-config-manager --add-repo https://repos.apiseven.com/packages/redhat/apache-apisix.repo\n```\n\nThen, to install APISIX, run:\n\n```shell\nsudo yum install apisix\n```\n\n:::tip\n\nYou can also install a specific version of APISIX by specifying it:\n\n```shell\nsudo yum install apisix-3.8.0\n```\n\n:::\n\n### Installation via RPM offline package\n\nFirst, download APISIX RPM offline package to an `apisix` folder:\n\n```shell\nsudo mkdir -p apisix\nsudo yum install -y https://repos.apiseven.com/packages/redhat/8/x86_64/apisix-3.13.0-0.ubi8.6.x86_64.rpm\nsudo yum clean all && yum makecache\nsudo yum install -y --downloadonly --downloaddir=./apisix apisix\n```\n\nThen copy the `apisix` folder to the target host and run:\n\n```shell\nsudo yum install ./apisix/*.rpm\n```\n\n### Managing APISIX server\n\nOnce APISIX is installed, you can initialize the configuration file and etcd by running:\n\n```shell\napisix init\n```\n\nTo start APISIX server, run:\n\n```shell\napisix start\n```\n\n:::tip\n\nRun `apisix help` to get a list of all available operations.\n\n:::\n\n</TabItem>\n\n<TabItem value=\"deb\">\n\n### Installation via DEB repository\n\nCurrently the only DEB repository supported by APISIX is Debian 11 (Bullseye) and supports both amd64 and arm64 architectures.\n\n```shell\n# amd64\nwget -O - http://repos.apiseven.com/pubkey.gpg | sudo apt-key add -\necho \"deb http://repos.apiseven.com/packages/debian bullseye main\" | sudo tee /etc/apt/sources.list.d/apisix.list\n\n# arm64\nwget -O - http://repos.apiseven.com/pubkey.gpg | sudo apt-key add -\necho \"deb http://repos.apiseven.com/packages/arm64/debian bullseye main\" | sudo tee /etc/apt/sources.list.d/apisix.list\n```\n\nThen, to install APISIX, run:\n\n```shell\nsudo apt update\nsudo apt install -y apisix=3.8.0-0\n```\n\n### Managing APISIX server\n\nOnce APISIX is installed, you can initialize the configuration file and etcd by running:\n\n```shell\nsudo apisix init\n```\n\nTo start APISIX server, run:\n\n```shell\nsudo apisix start\n```\n\n:::tip\n\nRun `apisix help` to get a list of all available operations.\n\n:::\n\n</TabItem>\n\n<TabItem value=\"source code\">\n\nIf you want to build APISIX from source, please refer to [Building APISIX from source](./building-apisix.md).\n\n</TabItem>\n</Tabs>\n\n## Installing etcd\n\nAPISIX uses [etcd](https://github.com/etcd-io/etcd) to save and synchronize configuration. Before installing APISIX, you need to install etcd on your machine.\n\nIt would be installed automatically if you choose the Docker or Helm install method while installing APISIX. If you choose a different method or you need to install it manually, follow the steps shown below:\n\n<Tabs\n  groupId=\"os\"\n  defaultValue=\"linux\"\n  values={[\n    {label: 'Linux', value: 'linux'},\n    {label: 'macOS', value: 'mac'},\n  ]}>\n<TabItem value=\"linux\">\n\n```shell\nETCD_VERSION='3.5.4'\nwget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz\ntar -xvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz && \\\n  cd etcd-v${ETCD_VERSION}-linux-amd64 && \\\n  sudo cp -a etcd etcdctl /usr/bin/\nnohup etcd >/tmp/etcd.log 2>&1 &\n```\n\n</TabItem>\n\n<TabItem value=\"mac\">\n\n```shell\nbrew install etcd\nbrew services start etcd\n```\n\n</TabItem>\n</Tabs>\n\n## Next steps\n\n### Configuring APISIX\n\nYou can configure your APISIX deployment in two ways:\n\n1. By directly changing your configuration file (`conf/config.yaml`).\n2. By using the `--config` or the `-c` flag to pass the path to your configuration file while starting APISIX.\n\n   ```shell\n   apisix start -c <path to config file>\n   ```\n\nAPISIX will use the configurations added in this configuration file and will fall back to the default configuration if anything is not configured. The default configurations can be found in `apisix/cli/config.lua` and should not be modified.\n\nFor example, to configure the default listening port to be `8000` without changing other configurations, your configuration file could look like this:\n\n```yaml title=\"conf/config.yaml\"\napisix:\n  node_listen: 8000\n```\n\nNow, if you decide you want to change the etcd address to `http://foo:2379`, you can add it to your configuration file. This will not change other configurations.\n\n```yaml title=\"conf/config.yaml\"\napisix:\n  node_listen: 8000\n\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://foo:2379\"\n```\n\n:::warning\n\nThe `conf/nginx.conf` file is automatically generated and should not be modified.\n\n:::\n\n### APISIX deployment modes\n\nAPISIX has three different deployment modes for different use cases. To learn more and configure deployment modes, see the [documentation](./deployment-modes.md).\n\n### Updating Admin API key\n\nIt is recommended to modify the Admin API key to ensure security.\n\nYou can update your configuration file as shown below:\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n      - name: \"admin\"\n        key: newsupersecurekey\n        role: admin\n```\n\nNow, to access the Admin API, you can use the new key:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes?api_key=newsupersecurekey -i\n```\n\n### Adding APISIX systemd unit file\n\nIf you installed APISIX via RPM, the APISIX unit file will already be configured and you can start APISIX by:\n\n```shell\nsystemctl start apisix\nsystemctl stop apisix\n```\n\nIf you installed APISIX through other methods, you can create `/usr/lib/systemd/system/apisix.service` and add the [configuration from the template](https://github.com/api7/apisix-build-tools/blob/master/usr/lib/systemd/system/apisix.service).\n\nSee the [Getting Started](./getting-started/README.md) guide for a quick walk-through of using APISIX.\n"
  },
  {
    "path": "docs/en/latest/internal/plugin-runner.md",
    "content": "---\ntitle: The Implementation of Plugin Runner\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Prerequirement\n\nEach request which runs the extern plugin will trigger an RPC to Plugin Runner over a connection on Unix socket. The data of RPC are serialized with [Flatbuffers](https://github.com/google/flatbuffers).\n\nTherefore, the Plugin Runner needs to:\n\n1. handle a connection on Unix socket\n2. support Flatbuffers\n3. use the proto & generated code in https://github.com/api7/ext-plugin-proto/\n\n## Listening to the Path\n\nAPISIX will pass the path of Unix socket as an environment variable `APISIX_LISTEN_ADDRESS` to the Plugin Runner. So the runner needs to read the value and listen to that address during starting.\n\n## Register Plugins\n\nThe Plugin Runner should be able to load plugins written in the particular language.\n\n## Handle RPC\n\nThere are two kinds of RPC: PrepareConf & HTTPReqCall\n\n### Handle PrepareConf\n\nAs people can configure the extern plugin on the side of APISIX, we need a way to sync the plugin configuration to the Plugin Runner.\n\nWhen there is a configuration that needs to sync to the Plugin Runner, we will send it via the PrepareConf RPC call. The Plugin Runner should be able to handle the call and store the configuration in a cache, then returns a unique conf token that represents the configuration.\n\nIn the previous design, an idempotent key is sent with the configuration. This field is deprecated and the Plugin Runner can safely ignore it.\n\nRequests run plugins with particular configuration will bear a particular conf token in the RPC call, and the Plugin Runner is expected to look up actual configuration via the token.\n\nWhen the configuration is modified, APISIX will send a new PrepareConf to the Plugin Runner. Currently, there is no way to notify the Plugin Runner that a configuration is removed. Therefore, we introduce another environment variable `APISIX_CONF_EXPIRE_TIME` as the conf cache expire time. The Plugin Runner should be able to cache the conf slightly longer than `APISIX_CONF_EXPIRE_TIME`, and APISIX will send another PrepareConf to refresh the cache if the configuration is still existing after `APISIX_CONF_EXPIRE_TIME` seconds.\n\n### Handle HTTPReqCall\n\nEach request which runs the extern plugin will trigger the HTTPReqCall. The HTTPReqCall is almost a serialized version of HTTP request, plus a conf token. The Plugin Runner is expected to tell APISIX what to update by the response of HTTPReqCall RPC call.\n\nSometimes the plugin in the Plugin Runner needs to know some information that is not part of the HTTPReqCall request, such as the request start time and the route ID in APISIX. Hence the Plugin Runner needs to reply to an `ExtraInfo` message as the response on the connection which sends the HTTPReqCall request. APISIX will read the `ExtraInfo` message and return the asked information.\n\nCurrently, the information below is passed by `ExtraInfo`:\n\n* variable value\n* request body\n\nThe flow of HTTPReqCall procession is:\n\n```\nAPISIX sends HTTPReqCall\nPlugin Runner looks up the plugin configuration by the token in HTTPReqCall\n(optional) loop:\n    Plugin Runner asks for ExtraInfo\n    APISIX replies the ExtraInfo\nPlugin Runner replies HTTPReqCall\n```\n"
  },
  {
    "path": "docs/en/latest/internal/testing-framework.md",
    "content": "---\ntitle: Introducing APISIX's testing framework\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nAPISIX uses a testing framework based on test-nginx: https://github.com/openresty/test-nginx.\nFor details, you can check the [documentation](https://metacpan.org/pod/Test::Nginx) of this project.\n\nIf you want to test the CLI behavior of APISIX (`./bin/apisix`),\nyou need to write a shell script in the t/cli directory to test it. You can refer to the existing test scripts for more details.\n\nIf you want to test the others, you need to write test code based on the framework.\n\nHere, we briefly describe how to do simple testing based on this framework.\n\n## Test file\n\nyou need to write test cases in the t/ directory, in a corresponding `.t` file. Note that a single test file should not exceed `800` lines, and if it is too long, it needs to be divided by a suffix. For example:\n\n```\nt/\n├── admin\n│ ├── consumers.t\n│ ├── consumers2.t\n```\n\nBoth `consumers.t` and `consumers2.t` contain tests for consumers in the Admin API.\n\nSome of the test files start with this paragraph:\n\n```\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (! $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (! $block->no_error_log && ! $block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n```\n\nIt means that all tests in this test file that do not define `request` are set to `GET /t`. The same is true for error_log.\n\n## Preparing the configuration\n\nWhen testing a behavior, we need to prepare the configuration.\n\nIf the configuration is from etcd:\nWe can set up specific configurations through the Admin API.\n\n```\n=== TEST 7: refer to empty nodes upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream_id\": \"1\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n```\n\nThen trigger it in a later test:\n\n```\n=== TEST 8: hit empty nodes upstream\n--- request\nGET /index.html\n--- error_code: 503\n--- error_log\nno valid upstream node\n```\n\n## Preparing the upstream\n\nTo test the code, we need to provide a mock upstream.\n\nFor HTTP request, the upstream code is put in `t/lib/server.lua`. HTTP request with\na given `path` will trigger the method in the same name. For example, a call to `/server_port`\nwill call the `_M.server_port`.\n\nFor TCP request, a dummy upstream is used:\n\n```\nlocal sock = ngx.req.socket()\nlocal data = sock:receive(\"1\")\nngx.say(\"hello world\")\n```\n\nIf you want to custom the TCP upstream logic, you can use:\n\n```\n--- stream_upstream_code\nlocal sock = ngx.req.socket()\nlocal data = sock:receive(\"1\")\nngx.sleep(0.2)\nngx.say(\"hello world\")\n```\n\n## Send request\n\nWe can initiate a request with `request` and set the request headers with `more_headers`.\n\nFor example.\n\n```\n--- request\nPUT /hello?xx=y&xx=z&&y=&&z\nbody part of the request\n--- more_headers\nX-Req: foo\nX-Req: bar\nX-Resp: cat\n```\n\nLua code can be used to send multiple requests.\n\nOne request after another:\n\n```\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n        }\n    }\n```\n\nSending multiple requests concurrently:\n\n```\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?var=2&var2=\"\n\n\n            local t = {}\n            local ports_count = {}\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri..i, {method = \"GET\"})\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    ports_count[res.body] = (ports_count[res.body] or 0) + 1\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n        }\n    }\n```\n\n## Send TCP request\n\nWe can use `stream_request` to send a TCP request, for example:\n\n```\n--- stream_request\nhello\n```\n\nTo send a TLS over TCP request, we can combine `stream_tls_request` with `stream_sni`:\n\n```\n--- stream_tls_request\nmmm\n--- stream_sni: xx.com\n```\n\n## Assertions\n\nThe following assertions are commonly used.\n\nCheck status (if not set, the framework will check if the request has 200 status code).\n\n```\n--- error_code: 405\n```\n\nCheck response headers.\n\n```\n--- response_headers\nX-Resp: foo\nX-Req: foo, bar\n```\n\nCheck response body.\n\n```\n--- response_body\n[{\"count\":12, \"port\": \"1982\"}]\n```\n\nCheck the TCP response.\n\nWhen the request is sent via `stream_request`:\n\n```\n--- stream_response\nreceive stream response error: connection reset by peer\n```\n\nWhen the request is sent via `stream_tls_request`:\n\n```\n--- response_body\nreceive stream response error: connection reset by peer\n```\n\nChecking the error log (via grep error log with regular expression).\n\n```\n--- grep_error_log eval\nqr/hash_on: header|chash_key: \"custom-one\"/\n--- grep_error_log_out\nhash_on: header\nchash_key: \"custom-one\"\nhash_on: header\nchash_key: \"custom-one\"\nhash_on: header\nchash_key: \"custom-one\"\nhash_on: header\nchash_key: \"custom-one\"\n```\n\nThe default log level is `info`, but you can get the debug level log with `--- log_level: debug`.\n\n## Upstream\n\nThe test framework listens to multiple ports when it is started.\n\n* 1980/1981/1982/5044: HTTP upstream port. We provide a mock upstream server for testing. See below for more information.\n* 1983: HTTPS upstream port\n* 1984: APISIX HTTP port. Can be used to verify HTTP related gateway logic, such as concurrent access to an API.\n* 1985: APISIX TCP port. Can be used to verify TCP related gateway logic, such as concurrent access to an API.\n* 1994: APISIX HTTPS port. Can be used to verify HTTPS related gateway logic, such as testing certificate matching logic.\n* 1995: TCP upstream port\n* 2005: APISIX TLS over TCP port. Can be used to verify TLS over TCP related gateway logic, such as concurrent access to an API.\n\nThe methods in `t/lib/server.lua` are executed when accessing the upstream port. `_M.go` is the entry point for this file.\nWhen the request accesses the upstream `/xxx`, the `_M.xxx` method is executed. For example, a request for `/hello` will execute `_M.hello`.\nThis allows us to write methods inside `t/lib/server.lua` to emulate specific upstream logic, such as sending special responses.\n\nNote that before adding new methods to `t/lib/server.lua`, make sure that you can reuse existing methods.\n\n## Run the test\n\nAssume your current work directory is the root of the apisix source code.\n\n1. Git clone the latest [test-nginx](https://github.com/openresty/test-nginx) to `../test-nginx`.\n2. Run the test: `prove -I. -I../test-nginx/inc -I../test-nginx/lib -r t/path/to/file.t`.\n\n## Tips\n\n### Debugging test cases\n\nThe Nginx configuration and logs generated by the test cases are located in the t/servroot directory. The Nginx configuration template for testing is located in t/APISIX.pm.\n\n### Running only some test cases\n\nThree notes can be used to control which parts of the tests are executed.\n\nFIRST & LAST:\n\n```\n=== TEST 1: vars rule with ! (set)\n--- FIRST\n--- config\n...\n--- response_body\npassed\n\n\n\n=== TEST 2: vars rule with ! (hit)\n--- request\nGET /hello?name=jack&age=17\n--- LAST\n--- error_code: 403\n--- response_body\nFault Injection!\n```\n\nONLY:\n\n```\n=== TEST 1: list empty resources\n--- ONLY\n--- config\n...\n--- response_body\n{\"count\":0,\"node\":{\"dir\":true,\"key\":\"/apisix/upstreams\",\"nodes\":[]}}\n```\n\n### Executing Shell Commands\n\nIt is possible to execute shell commands while writing tests in test-nginx for APISIX. We expose this feature via `exec` code block. The `stdout` of the executed process can be captured via `response_body` code block and `stderr` (if any) can be captured by filtering error.log through `grep_error_log`. Here is an example:\n\n```\n=== TEST 1: check exec stdout\n--- exec\necho hello world\n--- response_body\nhello world\n\n\n=== TEST 2: when exec returns an error\n--- exec\nechxo hello world\n--- grep_error_log eval\nqr/failed to execute the script [ -~]*/\n--- grep_error_log_out\nfailed to execute the script with status: 127, reason: exit, stderr: /bin/sh: 1: echxo: not found\n```\n"
  },
  {
    "path": "docs/en/latest/mtls.md",
    "content": "---\ntitle: Mutual TLS Authentication\nkeywords:\n  - Apache APISIX\n  - Mutual TLS\n  - mTLS\ndescription: This document describes how you can secure communication to and within APISIX with mTLS.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Protect Admin API\n\n### Why use it\n\nMutual TLS authentication provides a better way to prevent unauthorized access to APISIX.\n\nThe clients will provide their certificates to the server and the server will check whether the cert is signed by the supplied CA and decide whether to serve the request.\n\n### How to configure\n\n1. Generate self-signed key pairs, including ca, server, client key pairs.\n\n2. Modify configuration items in `conf/config.yaml`:\n\n```yaml title=\"conf/config.yaml\"\n  admin_listen:\n    ip: 127.0.0.1\n    port: 9180\n  https_admin: true\n\n  admin_api_mtls:\n    admin_ssl_ca_cert: \"/data/certs/mtls_ca.crt\"              # Path of your self-signed ca cert.\n    admin_ssl_cert: \"/data/certs/mtls_server.crt\"             # Path of your self-signed server side cert.\n    admin_ssl_cert_key: \"/data/certs/mtls_server.key\"         # Path of your self-signed server side key.\n```\n\n3. Run command:\n\n```shell\napisix init\napisix reload\n```\n\n### How client calls\n\nPlease replace the following certificate paths and domain name with your real ones.\n\n* Note: The same CA certificate as the server needs to be used *\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl --cacert /data/certs/mtls_ca.crt --key /data/certs/mtls_client.key --cert /data/certs/mtls_client.crt  https://admin.apisix.dev:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\"\n```\n\n## etcd with mTLS\n\n### How to configure\n\nYou need to configure `etcd.tls` for APISIX to work on an etcd cluster with mTLS enabled as shown below:\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    tls:\n      cert: /data/certs/etcd_client.pem       # path of certificate used by the etcd client\n      key: /data/certs/etcd_client.key        # path of key used by the etcd client\n```\n\nIf APISIX does not trust the CA certificate that used by etcd server, we need to set up the CA certificate.\n\n```yaml title=\"conf/config.yaml\"\napisix:\n  ssl:\n    ssl_trusted_certificate: /path/to/certs/ca-certificates.crt       # path of CA certificate used by the etcd server\n```\n\n## Protect Route\n\n### Why use it\n\nUsing mTLS is a way to verify clients cryptographically. It is useful and important in cases where you want to have encrypted and secure traffic in both directions.\n\n* Note: the mTLS protection only happens in HTTPS. If your route can also be accessed via HTTP, you should add additional protection in HTTP or disable the access via HTTP.*\n\n### How to configure\n\nWe provide a [tutorial](./tutorials/client-to-apisix-mtls.md) that explains in detail how to configure mTLS between the client and APISIX.\n\nWhen configuring `ssl`, use parameter `client.ca` and `client.depth` to configure the root CA that signing client certificates and the max length of certificate chain. Please refer to [Admin API](./admin-api.md#ssl) for details.\n\nHere is an example shell script to create SSL with mTLS (id is `1`, changes admin API url if needed):\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\": \"'\"$(cat t/certs/mtls_server.crt)\"'\",\n    \"key\": \"'\"$(cat t/certs/mtls_server.key)\"'\",\n    \"snis\": [\n        \"admin.apisix.dev\"\n    ],\n    \"client\": {\n        \"ca\": \"'\"$(cat t/certs/mtls_ca.crt)\"'\",\n        \"depth\": 10\n    }\n}'\n```\n\nSend a request to verify:\n\n```bash\ncurl --resolve 'mtls.test.com:<APISIX_HTTPS_PORT>:<APISIX_URL>' \"https://<APISIX_URL>:<APISIX_HTTPS_PORT>/hello\" -k --cert ./client.pem --key ./client.key\n\n* Added admin.apisix.dev:9443:127.0.0.1 to DNS cache\n* Hostname admin.apisix.dev was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* Connected to admin.apisix.dev (127.0.0.1) port 9443 (#0)\n* ALPN: offers h2\n* ALPN: offers http/1.1\n*  CAfile: t/certs/mtls_ca.crt\n*  CApath: none\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, Client hello (1):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Server hello (2):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Unknown (8):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Request CERT (13):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Certificate (11):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, CERT verify (15):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Finished (20):\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, Certificate (11):\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, CERT verify (15):\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384\n* ALPN: server accepted h2\n* Server certificate:\n*  subject: C=cn; ST=GuangDong; L=ZhuHai; CN=admin.apisix.dev; OU=ops\n*  start date: Dec  1 10:17:24 2022 GMT\n*  expire date: Aug 18 10:17:24 2042 GMT\n*  subjectAltName: host \"admin.apisix.dev\" matched cert's \"admin.apisix.dev\"\n*  issuer: C=cn; ST=GuangDong; L=ZhuHai; CN=ca.apisix.dev; OU=ops\n*  SSL certificate verify ok.\n* Using HTTP2, server supports multiplexing\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* h2h3 [:method: GET]\n* h2h3 [:path: /hello]\n* h2h3 [:scheme: https]\n* h2h3 [:authority: admin.apisix.dev:9443]\n* h2h3 [user-agent: curl/7.87.0]\n* h2h3 [accept: */*]\n* Using Stream ID: 1 (easy handle 0x13000bc00)\n> GET /hello HTTP/2\n> Host: admin.apisix.dev:9443\n> user-agent: curl/7.87.0\n> accept: */*\n```\n\nPlease make sure that the SNI fits the certificate domain.\n\n## mTLS Between APISIX and Upstream\n\n### Why use it\n\nSometimes the upstream requires mTLS. In this situation, the APISIX acts as the client, it needs to provide client certificate to communicate with upstream.\n\n### How to configure\n\nWhen configuring `upstreams`, we could use parameter `tls.client_cert` and `tls.client_key` to configure the client certificate APISIX used to communicate with upstreams. Please refer to [Admin API](./admin-api.md#upstream) for details.\n\nThis feature requires APISIX to run on [APISIX-Runtime](./FAQ.md#how-do-i-build-the-apisix-runtime-environment).\n\nHere is a similar shell script to patch a existed upstream with mTLS (changes admin API url if needed):\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/upstreams/1 \\\n-H \"X-API-KEY: $admin_key\" -X PATCH -d '\n{\n    \"tls\": {\n        \"client_cert\": \"'\"$(cat t/certs/mtls_client.crt)\"'\",\n        \"client_key\": \"'\"$(cat t/certs/mtls_client.key)\"'\"\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugin-develop.md",
    "content": "---\ntitle: Plugin Develop\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis documentation is about developing plugin in Lua. For other languages,\nsee [external plugin](./external-plugin.md).\n\n## Where to put your plugins\n\nUse the `extra_lua_path` parameter in `conf/config.yaml` file to load your custom plugin code (or use `extra_lua_cpath` for compiled `.so` or `.dll` file).\n\nFor example, you can create a directory `/path/to/example`:\n\n```yaml\napisix:\n    ...\n    extra_lua_path: \"/path/to/example/?.lua\"\n```\n\nThe structure of the `example` directory should look like this:\n\n```\n├── example\n│   └── apisix\n│       ├── plugins\n│       │   └── 3rd-party.lua\n│       └── stream\n│           └── plugins\n│               └── 3rd-party.lua\n```\n\n:::note\n\nThe directory (`/path/to/example`) must contain the `/apisix/plugins` subdirectory.\n\n:::\n\n## Enable the plugin\n\nTo enable your custom plugin, add the plugin list to `conf/config.yaml` and append your plugin name. For instance:\n\n```yaml\nplugins: # See `conf/config.yaml.example` for an example\n  - ... # Add existing plugins\n  - your-plugin # Add your custom plugin name (name is the plugin name defined in the code)\n```\n\n:::warning\n\nIn particular, most APISIX plugins are enabled by default when the plugins field configuration is not defined (The default enabled plugins can be found in [apisix/cli/config.lua](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua)).\n\nOnce the plugins configuration is defined in `conf/config.yaml`, the new plugins list will replace the default configuration instead of merging. Therefore, when defining the `plugins` field, make sure to include the built-in plugins that are being used. To maintain consistency with the default behavior, you can include all the default enabled plugins defined in `apisix/cli/config.lua`.\n\n:::\n\n## Writing plugins\n\nThe [`example-plugin`](https://github.com/apache/apisix/blob/master/apisix/plugins/example-plugin.lua) plugin in this repo provides an example.\n\n### Naming and priority\n\nSpecify the plugin name (the name is the unique identifier of the plugin and cannot be duplicate) and priority in the code.\n\n```lua\nlocal plugin_name = \"example-plugin\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 0,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n}\n```\n\nNote: The priority of the new plugin cannot be same to any existing ones, you can use the `/v1/schema` method of [control API](./control-api.md#get-v1schema) to view the priority of all plugins. In addition, plugins with higher priority value will be executed first in a given phase (see the definition of `phase` in [choose-phase-to-run](#choose-phase-to-run)). For example, the priority of example-plugin is 0 and the priority of ip-restriction is 3000. Therefore, the ip-restriction plugin will be executed first, then the example-plugin plugin. It's recommended to use priority 1 ~ 99 for your plugin unless you want it to run before some builtin plugins.\n\nNote: the order of the plugins is not related to the order of execution.\n\n### Schema and check\n\nWrite [JSON Schema](https://json-schema.org) descriptions and check functions. Similarly, take the example-plugin plugin as an example to see its\nconfiguration data:\n\n```json\n{\n  \"example-plugin\": {\n    \"i\": 1,\n    \"s\": \"s\",\n    \"t\": [1]\n  }\n}\n```\n\nLet's look at its schema description :\n\n```lua\nlocal schema = {\n    type = \"object\",\n    properties = {\n        i = {type = \"number\", minimum = 0},\n        s = {type = \"string\"},\n        t = {type = \"array\", minItems = 1},\n        ip = {type = \"string\"},\n        port = {type = \"integer\"},\n    },\n    required = {\"i\"},\n}\n```\n\nThe schema defines a non-negative number `i`, a string `s`, a non-empty array of `t`, and `ip` / `port`. Only `i` is required.\n\nAt the same time, we need to implement the __check_schema(conf, schema_type)__ method to complete the specification verification.\n\n```lua\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n```\n\n:::note\n\nNote: the project has provided the public method \"__core.schema.check__\", which can be used directly to complete JSON\nverification.\n\n:::\n\nThe input parameter **schema_type** is used to distinguish between different schemas types. For example, many plugins need to use some [metadata](./terminology/plugin-metadata.md), so they define the plugin's `metadata_schema`.\n\n```lua title=\"example-plugin.lua\"\n-- schema definition for metadata\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        ikey = {type = \"number\", minimum = 0},\n        skey = {type = \"string\"},\n    },\n    required = {\"ikey\", \"skey\"},\n}\n\nfunction _M.check_schema(conf, schema_type)\n    --- check schema for metadata\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n```\n\nAnother example, the [key-auth](https://github.com/apache/apisix/blob/master/apisix/plugins/key-auth.lua) plugin needs to provide a `consumer_schema` to check the configuration of the `plugins` attribute of the `consumer` resource in order to be used with the [Consumer](./admin-api.md#consumer) resource.\n\n```lua title=\"key-auth.lua\"\n\nlocal consumer_schema = {\n    type = \"object\",\n    properties = {\n        key = {type = \"string\"},\n    },\n    required = {\"key\"},\n}\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_CONSUMER then\n        return core.schema.check(consumer_schema, conf)\n    else\n        return core.schema.check(schema, conf)\n    end\nend\n```\n\n### Choose phase to run\n\nDetermine which [phase](./terminology/plugin.md#plugins-execution-lifecycle) to run, generally access or rewrite. If you don't know the [OpenResty lifecycle](https://github.com/openresty/lua-nginx-module/blob/master/README.markdown#directives), it's\nrecommended to learn about it in advance. For example `key-auth` is an authentication plugin, thus the authentication should be completed\nbefore forwarding the request to any upstream service. Therefore, the plugin must be executed in the rewrite phases.\nSimilarly, if you want to modify or process the response body or headers you can do that in the `body_filter` or in the `header_filter` phases respectively.\n\nThe following code snippet shows how to implement any logic relevant to the plugin in the OpenResty log phase.\n\n```lua\nfunction _M.log(conf, ctx)\n-- Implement logic here\nend\n```\n\n**Note : we can't invoke `ngx.exit`, `ngx.redirect` or `core.respond.exit` in rewrite phase and access phase. if need to exit, just return the status and body, the plugin engine will make the exit happen with the returned status and body. [example](https://github.com/apache/apisix/blob/35269581e21473e1a27b11cceca6f773cad0192a/apisix/plugins/limit-count.lua#L177)**\n\n### extra phase\n\nBesides OpenResty's phases, we also provide extra phases to satisfy specific purpose:\n\n* `delayed_body_filter`\n\n```lua\nfunction _M.delayed_body_filter(conf, ctx)\n    -- delayed_body_filter is called after body_filter\n    -- it is used by the tracing plugins to end the span right after body_filter\nend\n```\n\n### Implement the logic\n\nWrite the logic of the plugin in the corresponding phase. There are two parameters `conf` and `ctx` in the phase method, take the `limit-conn` plugin configuration as an example.\n\n#### conf parameter\n\nThe `conf` parameter is the relevant configuration information of the plugin, you can use `core.log.warn(core.json.encode(conf))` to output it to `error.log` for viewing, as shown below:\n\n```lua\nfunction _M.access(conf, ctx)\n    core.log.warn(core.json.encode(conf))\n    ......\nend\n```\n\nconf:\n\n```json\n{\n  \"rejected_code\": 503,\n  \"burst\": 0,\n  \"default_conn_delay\": 0.1,\n  \"conn\": 1,\n  \"key\": \"remote_addr\"\n}\n```\n\n#### ctx parameter\n\nThe `ctx` parameter caches data information related to the request. You can use `core.log.warn(core.json.encode(ctx, true))` to output it to `error.log` for viewing, as shown below :\n\n```lua\nfunction _M.access(conf, ctx)\n    core.log.warn(core.json.encode(ctx, true))\n    ......\nend\n```\n\n### Others\n\nIf your plugin has a new code directory of its own, and you need to redistribute it with the APISIX source code, you will need to modify the `Makefile` to create directory, such as:\n\n```\n$(INSTALL) -d $(INST_LUADIR)/apisix/plugins/skywalking\n$(INSTALL) apisix/plugins/skywalking/*.lua $(INST_LUADIR)/apisix/plugins/skywalking/\n```\n\nThere are other fields in the `_M` which affect the plugin's behavior.\n\n```lua\nlocal _M = {\n    ...\n    type = 'auth',\n    run_policy = 'prefer_route',\n}\n```\n\n`run_policy` field can be used to control the behavior of the plugin execution.\nWhen this field set to `prefer_route`, and the plugin has been configured both\nin the global and at the route level, only the route level one will take effect.\n\n`type` field is required to be set to `auth` if your plugin needs to work with consumer.\n\n## Load plugin and replace plugin\n\nUsing `require \"apisix.plugins.3rd-party\"` will load your plugin, just like `require \"apisix.plugins.jwt-auth\"` will load the `jwt-auth` plugin.\n\nSometimes you may want to override a method instead of a whole file. In this case, you can configure `lua_module_hook` in `conf/config.yaml`\nto introduce your hook.\n\nAssume that your configuration is as follows:\n\n```yaml\napisix:\n    ...\n    extra_lua_path: \"/path/to/example/?.lua\"\n    lua_module_hook: \"my_hook\"\n```\n\nThe `example/my_hook.lua` will be loaded when APISIX starts, and you can use this hook to replace a method in APISIX.\nThe example of [my_hook.lua](https://github.com/apache/apisix/blob/master/example/my_hook.lua) can be found under the `example` directory of this project.\n\n## Check external dependencies\n\nIf you have dependencies on external libraries, check the dependent items. If your plugin needs to use shared memory, it\nneeds to declare via [customizing Nginx configuration](./customize-nginx-configuration.md), for example :\n\n```yaml\n# put this in config.yaml:\nnginx_config:\n    http_configuration_snippet: |\n        # for openid-connect plugin\n        lua_shared_dict discovery             1m; # cache for discovery metadata documents\n        lua_shared_dict jwks                  1m; # cache for JWKs\n        lua_shared_dict introspection        10m; # cache for JWT verification results\n```\n\nThe plugin itself provides the init method. It is convenient for plugins to perform some initialization after\nthe plugin is loaded. If you need to clean up the initialization, you can put it in the corresponding destroy method.\n\nNote : if the dependency of some plugin needs to be initialized when Nginx start, you may need to add logic to the initialization\nmethod \"http_init\" in the file `apisix/init.lua`, and you may need to add some processing on generated part of Nginx\nconfiguration file in `apisix/cli/ngx_tpl.lua` file. But it is easy to have an impact on the overall situation according to the\nexisting plugin mechanism, **we do not recommend this unless you have a complete grasp of the code**.\n\n## Encrypted storage fields\n\nSome plugins require parameters to be stored encrypted, such as the `password` parameter of the `basic-auth` plugin. This plugin needs to specify in the `schema` which parameters need to be stored encrypted.\n\n```lua\nencrypt_fields = {\"password\"}\n```\n\nIf it is a nested parameter, such as the `clickhouse.password` parameter of the `error-log-logger` plugin, it needs to be separated by `.`:\n\n```lua\nencrypt_fields = {\"clickhouse.password\"}\n```\n\nCurrently not supported yet:\n\n1. more than two levels of nesting\n2. fields in arrays\n\nParameters can be stored encrypted by specifying `encrypt_fields = {\"password\"}` in the `schema`. APISIX will provide the following functionality.\n\n- When adding and updating resources, APISIX automatically encrypts the parameters declared in `encrypt_fields` and stores them in etcd\n- When fetching resources and when running the plugin, APISIX automatically decrypts the parameters declared in `encrypt_fields`\n\nBy default, APISIX has `data_encryption` enabled with [two default keys](https://github.com/apache/apisix/blob/85563f016c35834763376894e45908b2fb582d87/apisix/cli/config.lua#L75), you can modify them in `config.yaml`.\n\n```yaml\napisix:\n    data_encryption:\n        enable: true\n        keyring:\n            - ...\n```\n\nAPISIX will try to decrypt the data with keys in the order of the keys in the keyring (only for parameters declared in `encrypt_fields`). If the decryption fails, the next key will be tried until the decryption succeeds.\n\nIf none of the keys in `keyring` can decrypt the data, the original data is used.\n\n## Register public API\n\nA plugin can register API which exposes to the public. Take batch-requests plugin as an example, this plugin registers `POST /apisix/batch-requests` to allow developers to group multiple API requests into a single HTTP request/response cycle:\n\n```lua\nfunction batch_requests()\n    -- ...\nend\n\nfunction _M.api()\n    -- ...\n    return {\n        {\n            methods = {\"POST\"},\n            uri = \"/apisix/batch-requests\",\n            handler = batch_requests,\n        }\n    }\nend\n```\n\nNote that the public API will not be exposed by default, you will need to use the [public-api plugin](plugins/public-api.md) to expose it.\n\n## Register control API\n\nIf you only want to expose the API to the localhost or intranet, you can expose it via [Control API](./control-api.md).\n\nTake a look at example-plugin plugin:\n\n```lua\nlocal function hello()\n    local args = ngx.req.get_uri_args()\n    if args[\"json\"] then\n        return 200, {msg = \"world\"}\n    else\n        return 200, \"world\\n\"\n    end\nend\n\n\nfunction _M.control_api()\n    return {\n        {\n            methods = {\"GET\"},\n            uris = {\"/v1/plugin/example-plugin/hello\"},\n            handler = hello,\n        }\n    }\nend\n```\n\nIf you don't change the default control API configuration, the plugin will be expose `GET /v1/plugin/example-plugin/hello` which can only be accessed via `127.0.0.1`. Test with the following command:\n\n```shell\ncurl -i -X GET \"http://127.0.0.1:9090/v1/plugin/example-plugin/hello\"\n```\n\n[Read more about control API introduction](./control-api.md)\n\n## Register custom variables\n\nWe can use variables in many places of APISIX. For example, customizing log format in http-logger, using it as the key of `limit-*` plugins. In some situations, the builtin variables are not enough. Therefore, APISIX allows developers to register their variables globally, and use them as normal builtin variables.\n\nFor instance, let's register a variable called `a6_labels_zone` to fetch the value of the `zone` label in a route:\n\n```\nlocal core = require \"apisix.core\"\n\ncore.ctx.register_var(\"a6_labels_zone\", function(ctx)\n    local route = ctx.matched_route and ctx.matched_route.value\n    if route and route.labels then\n        return route.labels.zone\n    end\n    return nil\nend)\n```\n\nAfter that, any get operation to `$a6_labels_zone` will call the registered getter to fetch the value.\n\nNote that the custom variables can't be used in features that depend on the Nginx directive, like `access_log_format`.\n\n## Write test cases\n\nFor functions, write and improve the test cases of various dimensions, do a comprehensive test for your plugin! The\ntest cases of plugins are all in the \"__t/plugin__\" directory. You can go ahead to find out. APISIX uses\n[**test-nginx**](https://github.com/openresty/test-nginx) as the test framework. A test case (.t file) is usually\ndivided into prologue and data parts by \\__DATA\\__. Here we will briefly introduce the data part, that is, the part\nof the real test case. For example, the key-auth plugin:\n\n```perl\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.key-auth\")\n            local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\n[error]\n```\n\nA test case consists of three parts :\n\n- __Program code__ : configuration content of Nginx location\n- __Input__ : http request information\n- __Output check__ : status, header, body, error log check\n\nWhen we request __/t__, which config in the configuration file, the Nginx will call \"__content_by_lua_block__\" instruction to\ncomplete the Lua script, and finally return. The assertion of the use case is response_body return \"done\",\n\"__no_error_log__\" means to check the \"__error.log__\" of Nginx. There must be no ERROR level record. The log files for the unit test\nare located in the following folder: 't/servroot/logs'.\n\nThe above test case represents a simple scenario. Most scenarios will require multiple steps to validate. To do this, create multiple tests `=== TEST 1`, `=== TEST 2`, and so on. These tests will be executed sequentially, allowing you to break down scenarios into a sequence of atomic steps.\n\nAdditionally, there are some convenience testing endpoints which can be found [here](https://github.com/apache/apisix/blob/master/t/lib/server.lua#L36). For example, see [proxy-rewrite](https://github.com/apache/apisix/blob/master/t/plugin/proxy-rewrite.t). In test 42, the upstream `uri` is made to redirect `/test?new_uri=hello` to `/hello` (which always returns `hello world`). In test 43, the response body is confirmed to equal `hello world`, meaning the proxy-rewrite configuration added with test 42 worked correctly.\n\nRefer the following [document](building-apisix.md) to setup the testing framework.\n\n### Attach the test-nginx execution process:\n\nAccording to the path we configured in the makefile and some configuration items at the front of each __.t__ file, the\nframework will assemble into a complete nginx.conf file. \"__t/servroot__\" is the working directory of Nginx and start the\nNginx instance. according to the information provided by the test case, initiate the http request and check that the\nreturn items of HTTP include HTTP status, HTTP response header, HTTP response body and so on.\n\n## Additional Resource(s)\n\n- Key Concepts - [Plugins](https://apisix.apache.org/docs/apisix/terminology/plugin/)\n- [Apache APISIX Extensions Guide](https://apisix.apache.org/blog/2021/10/29/extension-guide/)\n- [Create a Custom Plugin in Lua](https://docs.api7.ai/apisix/how-to-guide/custom-plugins/create-plugin-in-lua)\n- [example-plugin code](https://github.com/apache/apisix/blob/master/apisix/plugins/example-plugin.lua)\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-aliyun-content-moderation.md",
    "content": "---\ntitle: ai-aliyun-content-moderation\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-aliyun-content-moderation\ndescription: This document contains information about the Apache APISIX ai-aliyun-content-moderation Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ai-aliyun-content-moderation` plugin integrates with Aliyun's content moderation service to check both request and response content for inappropriate material when working with LLMs. It supports both real-time streaming checks and final packet moderation.\n\nThis plugin must be used in routes that utilize the ai-proxy or ai-proxy-multi plugins.\n\n## Plugin Attributes\n\n| **Field**                   | **Required** | **Type**  | **Description**                                                                                                                                                             |\n| ---------------------------- | ------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| endpoint                     | Yes          | String    | Aliyun service endpoint URL                                                                                                                                                 |\n| region_id                    | Yes          | String    | Aliyun region identifier                                                                                                                                                    |\n| access_key_id                | Yes          | String    | Aliyun access key ID                                                                                                                                                        |\n| access_key_secret            | Yes          | String    | Aliyun access key secret                                                                                                                                                    |\n| check_request                | No           | Boolean   | Enable request content moderation. Default: `true`                                                                                                                          |\n| check_response               | No           | Boolean   | Enable response content moderation. Default: `false`                                                                                                                        |\n| stream_check_mode            | No           | String    | Streaming moderation mode. Default: `\"final_packet\"`. Valid values: `[\"realtime\", \"final_packet\"]`                                                                           |\n| stream_check_cache_size      | No           | Integer   | Max characters per moderation batch in realtime mode. Default: `128`. Must be `>= 1`.                                                                                        |\n| stream_check_interval        | No           | Number    | Seconds between batch checks in realtime mode. Default: `3`. Must be `>= 0.1`.                                                                                              |\n| request_check_service        | No           | String    | Aliyun service for request moderation. Default: `\"llm_query_moderation\"`                                                                                                    |\n| request_check_length_limit   | No           | Number    | Max characters per request moderation chunk. Default: `2000`.                                                                                                               |\n| response_check_service       | No           | String    | Aliyun service for response moderation. Default: `\"llm_response_moderation\"`                                                                                               |\n| response_check_length_limit  | No           | Number    | Max characters per response moderation chunk. Default: `5000`.                                                                                                              |\n| risk_level_bar               | No           | String    | Threshold for content rejection. Default: `\"high\"`. Valid values: `[\"none\", \"low\", \"medium\", \"high\", \"max\"]`                                                                |\n| deny_code                    | No           | Number    | HTTP status code for rejected content. Default: `200`.                                                                                                                      |\n| deny_message                 | No           | String    | Custom message for rejected content. Default: `-`.                                                                                                                           |\n| timeout                      | No           | Integer   | Request timeout in milliseconds. Default: `10000`. Must be `>= 1`.                                                                                                          |\n| ssl_verify                   | No           | Boolean   | Enable SSL certificate verification. Default: `true`.                                                                                                                       |\n\n## Example usage\n\nFirst initialise these shell variables:\n\n```shell\nADMIN_API_KEY=edd1c9f034335f136f87ad84b625c8f1\nALIYUN_ACCESS_KEY_ID=your-aliyun-access-key-id\nALIYUN_ACCESS_KEY_SECRET=your-aliyun-access-key-secret\nALIYUN_REGION=cn-hangzhou\nALIYUN_ENDPOINT=https://green.cn-hangzhou.aliyuncs.com\nOPENAI_KEY=your-openai-api-key\n```\n\nCreate a route with the `ai-aliyun-content-moderation` and `ai-proxy` plugin like so:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_KEY\"'\"\n          }\n        },\n        \"override\": {\n          \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n        }\n      },\n      \"ai-aliyun-content-moderation\": {\n        \"endpoint\": \"'\"$ALIYUN_ENDPOINT\"'\",\n        \"region_id\": \"'\"$ALIYUN_REGION\"'\",\n        \"access_key_id\": \"'\"$ALIYUN_ACCESS_KEY_ID\"'\",\n        \"access_key_secret\": \"'\"$ALIYUN_ACCESS_KEY_SECRET\"'\",\n        \"risk_level_bar\": \"high\",\n        \"check_request\": true,\n        \"check_response\": true,\n        \"deny_code\": 400,\n        \"deny_message\": \"Your request violates content policy\"\n      }\n    }\n  }'\n```\n\nThe `ai-proxy` plugin is used here as it simplifies access to LLMs. However, you may configure the LLM in the upstream configuration as well.\n\nNow send a request:\n\n```shell\ncurl http://127.0.0.1:9080/v1/chat/completions -i \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"gpt-3.5-turbo\",\n    \"messages\": [\n      {\"role\": \"user\", \"content\": \"I want to kill you\"}\n    ],\n    \"stream\": false\n  }'\n```\n\nThen the request will be blocked with error like this:\n\n```text\nHTTP/1.1 400 Bad Request\nContent-Type: application/json\n\n{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion\",\"model\":\"gpt-3.5-turbo\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"Your request violates content policy\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":0,\"completion_tokens\":0,\"total_tokens\":0}}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-aws-content-moderation.md",
    "content": "---\ntitle: ai-aws-content-moderation\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-aws-content-moderation\ndescription: This document contains information about the Apache APISIX ai-aws-content-moderation Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ai-aws-content-moderation` plugin processes the request body to check for toxicity and rejects the request if it exceeds the configured threshold.\n\n**_This plugin must be used in routes that proxy requests to LLMs only._**\n\n**_As of now, the plugin only supports the integration with [AWS Comprehend](https://aws.amazon.com/comprehend/) for content moderation. PRs for introducing support for other service providers are welcomed._**\n\n## Plugin Attributes\n\n| **Field**                    | **Required** | **Type** | **Description**                                                                                                                                                                                                                                         |\n| ---------------------------- | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| comprehend.access_key_id     | Yes          | String   | AWS access key ID                                                                                                                                                                                                                                       |\n| comprehend.secret_access_key | Yes          | String   | AWS secret access key                                                                                                                                                                                                                                   |\n| comprehend.region            | Yes          | String   | AWS region                                                                                                                                                                                                                                              |\n| comprehend.endpoint          | No           | String   | AWS Comprehend service endpoint. Must match the pattern `^https?://`                                                                                                                                                                                    |\n| comprehend.ssl_verify        | No           | String   | Enables SSL certificate verification.                                                                                                                                                                                                                   |\n| moderation_categories        | No           | Object   | Key-value pairs of moderation category and their score. In each pair, the key should be one of the `PROFANITY`, `HATE_SPEECH`, `INSULT`, `HARASSMENT_OR_ABUSE`, `SEXUAL`, or `VIOLENCE_OR_THREAT`; and the value should be between 0 and 1 (inclusive). |\n| moderation_threshold         | No           | Number   | The degree to which content is harmful, offensive, or inappropriate. A higher value indicates more toxic content allowed. Range: 0 - 1. Default: 0.5                                                                                                    |\n\n## Example usage\n\nFirst initialise these shell variables:\n\n```shell\nADMIN_API_KEY=edd1c9f034335f136f87ad84b625c8f1\nACCESS_KEY_ID=aws-comprehend-access-key-id-here\nSECRET_ACCESS_KEY=aws-comprehend-secret-access-key-here\nOPENAI_KEY=open-ai-key-here\n```\n\nCreate a route with the `ai-aws-content-moderation` and `ai-proxy` plugin like so:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"ai-aws-content-moderation\": {\n        \"comprehend\": {\n          \"access_key_id\": \"'\"$ACCESS_KEY_ID\"'\",\n          \"secret_access_key\": \"'\"$SECRET_ACCESS_KEY\"'\",\n          \"region\": \"us-east-1\"\n        },\n        \"moderation_categories\": {\n          \"PROFANITY\": 0.5\n        }\n      },\n      \"ai-proxy\": {\n        \"auth\": {\n          \"header\": {\n            \"api-key\": \"'\"$OPENAI_KEY\"'\"\n          }\n        },\n        \"model\": {\n          \"provider\": \"openai\",\n          \"name\": \"gpt-4\",\n          \"options\": {\n            \"max_tokens\": 512,\n            \"temperature\": 1.0\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nThe `ai-proxy` plugin is used here as it simplifies access to LLMs. However, you may configure the LLM in the upstream configuration as well.\n\nNow send a request:\n\n```shell\ncurl http://127.0.0.1:9080/post -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"<very profane message here>\"\n    }\n  ]\n}'\n```\n\nThen the request will be blocked with error like this:\n\n```text\nHTTP/1.1 400 Bad Request\nDate: Thu, 03 Oct 2024 11:53:15 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.10.0\n\nrequest body exceeds PROFANITY threshold\n```\n\nSend a request with compliant content in the request body:\n\n```shell\ncurl http://127.0.0.1:9080/post -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are a mathematician\"\n    },\n    { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n  ]\n}'\n```\n\nThis request will be proxied normally to the configured LLM.\n\n```text\nHTTP/1.1 200 OK\nDate: Thu, 03 Oct 2024 11:53:00 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.10.0\n\n{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"content\":\"1+1 equals 2.\",\"role\":\"assistant\"}}],\"created\":1727956380,\"id\":\"chatcmpl-AEEg8Pe5BAW5Sw3C1gdwXnuyulIkY\",\"model\":\"gpt-4o-2024-05-13\",\"object\":\"chat.completion\",\"system_fingerprint\":\"fp_67802d9a6d\",\"usage\":{\"completion_tokens\":7,\"prompt_tokens\":23,\"total_tokens\":30}}\n```\n\nYou can also configure filters on other moderation categories like so:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"ai-aws-content-moderation\": {\n        \"comprehend\": {\n          \"access_key_id\": \"'\"$ACCESS_KEY_ID\"'\",\n          \"secret_access_key\": \"'\"$SECRET_ACCESS_KEY\"'\",\n          \"region\": \"us-east-1\"\n        },\n        \"moderation_categories\": {\n          \"PROFANITY\": 0.5,\n          \"HARASSMENT_OR_ABUSE\": 0.7,\n          \"SEXUAL\": 0.2\n        }\n      },\n      \"ai-proxy\": {\n        \"auth\": {\n          \"header\": {\n            \"api-key\": \"'\"$OPENAI_KEY\"'\"\n          }\n        },\n        \"model\": {\n          \"provider\": \"openai\",\n          \"name\": \"gpt-4\",\n          \"options\": {\n            \"max_tokens\": 512,\n            \"temperature\": 1.0\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nIf none of the `moderation_categories` are configured, request bodies will be moderated on the basis of overall toxicity.\nThe default `moderation_threshold` is 0.5, it can be configured like so.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n  \"uri\": \"/post\",\n  \"plugins\": {\n    \"ai-aws-content-moderation\": {\n      \"provider\": {\n        \"comprehend\": {\n          \"access_key_id\": \"'\"$ACCESS_KEY_ID\"'\",\n          \"secret_access_key\": \"'\"$SECRET_ACCESS_KEY\"'\",\n          \"region\": \"us-east-1\"\n        }\n      },\n      \"moderation_threshold\": 0.7,\n      \"llm_provider\": \"openai\"\n    },\n    \"ai-proxy\": {\n      \"auth\": {\n        \"header\": {\n          \"api-key\": \"'\"$OPENAI_KEY\"'\"\n        }\n      },\n      \"model\": {\n        \"provider\": \"openai\",\n        \"name\": \"gpt-4\",\n        \"options\": {\n          \"max_tokens\": 512,\n          \"temperature\": 1.0\n        }\n      }\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-prompt-decorator.md",
    "content": "---\ntitle: ai-prompt-decorator\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-prompt-decorator\ndescription: This document contains information about the Apache APISIX ai-prompt-decorator Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ai-prompt-decorator` plugin simplifies access to LLM providers, such as OpenAI and Anthropic, and their models by appending or prepending prompts into the request.\n\n## Plugin Attributes\n\n| **Field**         | **Required**    | **Type** | **Description**                                     |\n| ----------------- | --------------- | -------- | --------------------------------------------------- |\n| `prepend`         | Conditionally\\* | Array    | An array of prompt objects to be prepended          |\n| `prepend.role`    | Yes             | String   | Role of the message (`system`, `user`, `assistant`) |\n| `prepend.content` | Yes             | String   | Content of the message. Minimum length: 1           |\n| `append`          | Conditionally\\* | Array    | An array of prompt objects to be appended           |\n| `append.role`     | Yes             | String   | Role of the message (`system`, `user`, `assistant`) |\n| `append.content`  | Yes             | String   | Content of the message. Minimum length: 1           |\n\n\\* **Conditionally Required**: At least one of `prepend` or `append` must be provided.\n\n## Example usage\n\nCreate a route with the `ai-prompt-decorator` plugin like so:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-prompt-decorator\": {\n        \"prepend\":[\n          {\n            \"role\": \"system\",\n            \"content\": \"I have exams tomorrow so explain conceptually and briefly\"\n          }\n        ],\n        \"append\":[\n          {\n            \"role\": \"system\",\n            \"content\": \"End the response with an analogy.\"\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"api.openai.com:443\": 1\n      },\n      \"pass_host\": \"node\",\n      \"scheme\": \"https\"\n    }\n  }'\n```\n\nNow send a request:\n\n```shell\ncurl http://127.0.0.1:9080/v1/chat/completions -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"model\": \"gpt-4\",\n  \"messages\": [{ \"role\": \"user\", \"content\": \"What is TLS Handshake?\" }]\n}' -H \"Authorization: Bearer <your token here>\"\n```\n\nThen the request body will be modified to something like this:\n\n```json\n{\n  \"model\": \"gpt-4\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"I have exams tomorrow so explain conceptually and briefly\"\n    },\n    { \"role\": \"user\", \"content\": \"What is TLS Handshake?\" },\n    {\n      \"role\": \"system\",\n      \"content\": \"End the response with an analogy.\"\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-prompt-guard.md",
    "content": "---\ntitle: ai-prompt-guard\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-prompt-guard\ndescription: This document contains information about the Apache APISIX ai-prompt-guard Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ai-prompt-guard` plugin safeguards your AI endpoints by inspecting and validating incoming prompt messages. It checks the content of requests against user-defined allowed and denied patterns to ensure that only approved inputs are processed. Based on its configuration, the plugin can either examine just the latest message or the entire conversation history, and it can be set to check prompts from all roles or only from end users.\n\nWhen both **allow** and **deny** patterns are configured, the plugin first ensures that at least one allowed pattern is matched. If none match, the request is rejected with a _\"Request doesn't match allow patterns\"_ error. If an allowed pattern is found, it then checks for any occurrences of denied patterns—rejecting the request with a _\"Request contains prohibited content\"_ error if any are detected.\n\n## Plugin Attributes\n\n| **Field**                      | **Required** | **Type**  | **Description**                                                                                                                                                      |\n| ------------------------------ | ------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| match_all_roles                | No           | boolean   | If set to `true`, the plugin will check prompt messages from all roles. Otherwise, it only validates when its role is `\"user\"`. Default is `false`. |\n| match_all_conversation_history | No           | boolean   | When enabled, all messages in the conversation history are concatenated and checked. If `false`, only the content of the last message is examined. Default is `false`. |\n| allow_patterns                 | No           | array     | A list of regex patterns. When provided, the prompt must match **at least one** pattern to be considered valid.                                                      |\n| deny_patterns                  | No           | array     | A list of regex patterns. If any of these patterns match the prompt content, the request is rejected.                                                                  |\n\n## Example usage\n\nCreate a route with the `ai-prompt-guard` plugin like so:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-prompt-guard\": {\n        \"match_all_roles\": true,\n          \"allow_patterns\": [\n            \"goodword\"\n          ],\n        \"deny_patterns\": [\n          \"badword\"\n        ]\n     }\n  },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"api.openai.com:443\": 1\n      },\n      \"pass_host\": \"node\",\n      \"scheme\": \"https\"\n    }\n  }'\n```\n\nNow send a request:\n\n```shell\ncurl http://127.0.0.1:9080/v1/chat/completions -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"model\": \"gpt-4\",\n  \"messages\": [{ \"role\": \"user\", \"content\": \"badword request\" }]\n}' -H \"Authorization: Bearer <your token here>\"\n```\n\nThe request will fail with 400 error and following response.\n\n```bash\n{\"message\":\"Request doesn't match allow patterns\"}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-prompt-template.md",
    "content": "---\ntitle: ai-prompt-template\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-prompt-template\ndescription: The ai-prompt-template plugin supports pre-configuring prompt templates that only accept user inputs in designated template variables, in a fill-in-the-blank fashion.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-prompt-template\" />\n</head>\n\n## Description\n\nThe `ai-prompt-template` Plugin simplifies access to LLM providers, such as OpenAI and Anthropic, and their models. It pre-configures prompt templates that only accept user inputs in designated template variables, in a \"fill in the blank\" fashion.\n\n## Plugin Attributes\n\n| **Field** | **Required** | **Type** | **Description** |\n| :--- | :--- | :--- | :--- |\n| `templates` | Yes | Array | An array of template objects. |\n| `templates.name` | Yes | String | Name of the template. When requesting the route, the request should include the template name that corresponds to the configured template. |\n| `templates.template` | Yes | Object | Template specification. |\n| `templates.template.model` | Yes | String | Name of the AI Model, such as `gpt-4` or `gpt-3.5`. See your LLM provider API documentation for more available models. |\n| `templates.template.messages` | Yes | Array | Template message specification. |\n| `templates.template.messages.role` | Yes | String | Role of the message, such as `system`, `user`, or `assistant`. |\n| `templates.template.messages.content` | Yes | String | Content of the message (prompt). |\n\n## Examples\n\nThe following examples will be using OpenAI as the Upstream service provider. Before proceeding, create an [OpenAI account](https://openai.com) and an [API key](https://openai.com/blog/openai-api). You can optionally save the key to an environment variable as such:\n\n```shell\nexport OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>\n```\n\nIf you are working with other LLM providers, please refer to the provider's documentation to obtain an API key.\n\n### Configure a Template for Open Questions in Custom Complexity\n\nThe following example demonstrates how to use the `ai-prompt-template` Plugin to configure a template which can be used to answer open questions and accepts user-specified response complexity.\n\nCreate a Route to the chat completion endpoint with pre-configured prompt templates as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\": {\n          \"model\": \"gpt-4\"\n        }\n      },\n      \"ai-prompt-template\": {\n        \"templates\": [\n          {\n            \"name\": \"QnA with complexity\",\n            \"template\": {\n              \"model\": \"gpt-4\",\n              \"messages\": [\n                {\n                  \"role\": \"system\",\n                  \"content\": \"Answer in {{complexity}}.\"\n                },\n                {\n                  \"role\": \"user\",\n                  \"content\": \"Explain {{prompt}}.\"\n                }\n              ]\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a sample question and desired answer complexity in the request body.\n\nNow send a request:\n\n```shell\ncurl \"http://127.0.0.1:9080/v1/chat/completions\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template_name\": \"QnA with complexity\",\n    \"complexity\": \"brief\",\n    \"prompt\": \"quick sort\"\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": {\n        \"content\": \"Quick sort is a highly efficient sorting algorithm that uses a divide-and-conquer approach to arrange elements in a list or array in order. Here’s a brief explanation:\\n\\n1. **Choose a Pivot**: Select an element from the list as a 'pivot'. Common methods include choosing the first element, the last element, the middle element, or a random element.\\n\\n2. **Partitioning**: Rearrange the elements in the list such that all elements less than the pivot are moved before it, and all elements greater than the pivot are moved after it. The pivot is now in its final position.\\n\\n3. **Recursively Apply**: Recursively apply the same process to the sub-lists of elements to the left and right of the pivot.\\n\\nThe base case of the recursion is lists of size zero or one, which are already sorted.\\n\\nQuick sort has an average-case time complexity of O(n log n), making it suitable for large datasets. However, its worst-case time complexity is O(n^2), which occurs when the smallest or largest element is always chosen as the pivot. This can be mitigated by using good pivot selection strategies or randomization.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  \"created\": 1723194057,\n  \"id\": \"chatcmpl-9uFmTYN4tfwaXZjyOQwcp0t5law4x\",\n  \"model\": \"gpt-4o-2024-05-13\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": {\n    \"completion_tokens\": 234,\n    \"prompt_tokens\": 18,\n    \"total_tokens\": 252\n  }\n}\n```\n\n### Configure Multiple Templates\n\nThe following example demonstrates how you can configure multiple templates on the same Route. When requesting the Route, users will be able to pass custom inputs to different templates by specifying the template name.\n\nThe example continues with the [last example](#configure-a-template-for-open-questions-in-custom-complexity). Update the Plugin with another template:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-prompt-template\": {\n        \"templates\": [\n          {\n            \"name\": \"QnA with complexity\",\n            \"template\": {\n              \"model\": \"gpt-4\",\n              \"messages\": [\n                {\n                  \"role\": \"system\",\n                  \"content\": \"Answer in {{complexity}}.\"\n                },\n                {\n                  \"role\": \"user\",\n                  \"content\": \"Explain {{prompt}}.\"\n                }\n              ]\n            }\n          },\n          {\n            \"name\": \"echo\",\n            \"template\": {\n              \"model\": \"gpt-4\",\n              \"messages\": [\n                {\n                  \"role\": \"system\",\n                  \"content\": \"You are an echo bot. You must repeat exactly what the user says without any changes or additional text.\"\n                },\n                {\n                  \"role\": \"user\",\n                  \"content\": \"Echo {{prompt}}.\"\n                }\n              ]\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nYou should now be able to use both templates through the same Route.\n\nSend a POST request to the Route and use the first template:\n\n```shell\ncurl \"http://127.0.0.1:9080/v1/chat/completions\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template_name\": \"QnA with complexity\",\n    \"complexity\": \"brief\",\n    \"prompt\": \"quick sort\"\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": {\n        \"content\": \"Quick sort is a highly efficient sorting algorithm that uses a divide-and-conquer approach to arrange elements in a list or array in order. Here’s a brief explanation:\\n\\n1. **Choose a Pivot**: Select an element from the list as a 'pivot'. Common methods include choosing the first element, the last element, the middle element, or a random element.\\n\\n2. **Partitioning**: Rearrange the elements in the list such that all elements less than the pivot are moved before it, and all elements greater than the pivot are moved after it. The pivot is now in its final position.\\n\\n3. **Recursively Apply**: Recursively apply the same process to the sub-lists of elements to the left and right of the pivot.\\n\\nThe base case of the recursion is lists of size zero or one, which are already sorted.\\n\\nQuick sort has an average-case time complexity of O(n log n), making it suitable for large datasets. However, its worst-case time complexity is O(n^2), which occurs when the smallest or largest element is always chosen as the pivot. This can be mitigated by using good pivot selection strategies or randomization.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  ...\n}\n```\n\nSend a POST request to the Route and use the second template:\n\n```shell\ncurl \"http://127.0.0.1:9080/v1/chat/completions\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template_name\": \"echo\",\n    \"prompt\": \"hello APISIX\"\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": {\n        \"content\": \"hello APISIX\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  ...\n}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-proxy-multi.md",
    "content": "---\ntitle: ai-proxy-multi\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-proxy-multi\n  - AI\n  - LLM\ndescription: The ai-proxy-multi Plugin extends the capabilities of ai-proxy with load balancing, retries, fallbacks, and health chekcs, simplifying the integration with OpenAI, DeepSeek, Azure, AIMLAPI, Anthropic, OpenRouter, Gemini, Vertex AI, and other OpenAI-compatible APIs.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-proxy-multi\" />\n</head>\n\n## Description\n\nThe `ai-proxy-multi` Plugin simplifies access to LLM and embedding models by transforming Plugin configurations into the designated request format for OpenAI, DeepSeek, Azure, AIMLAPI, Anthropic, OpenRouter, Gemini, Vertex AI, and other OpenAI-compatible APIs. It extends the capabilities of [`ai-proxy`](./ai-proxy.md) with load balancing, retries, fallbacks, and health checks.\n\nIn addition, the Plugin also supports logging LLM request information in the access log, such as token usage, model, time to the first response, and more.\n\n## Request Format\n\n| Name               | Type   | Required | Description                                         |\n| ------------------ | ------ | -------- | --------------------------------------------------- |\n| `messages`         | Array  | True      | An array of message objects.                        |\n| `messages.role`    | String | True      | Role of the message (`system`, `user`, `assistant`).|\n| `messages.content` | String | True      | Content of the message.                             |\n\n## Attributes\n\n| Name                               | Type            | Required | Default                           | Valid Values | Description |\n|------------------------------------|----------------|----------|-----------------------------------|--------------|-------------|\n| fallback_strategy                  | string or array         | False    |  | string: \"instance_health_and_rate_limiting\", \"http_429\", \"http_5xx\"<br />array: [\"rate_limiting\", \"http_429\", \"http_5xx\"] | Fallback strategy. When set, the Plugin will check whether the specified instance’s token has been exhausted when a request is forwarded. If so, forward the request to the next instance regardless of the instance priority. When not set, the Plugin will not forward the request to low priority instances when token of the high priority instance is exhausted. |\n| balancer                           | object         | False    |                                   |              | Load balancing configurations. |\n| balancer.algorithm                 | string         | False    | roundrobin                     | [roundrobin, chash] | Load balancing algorithm. When set to `roundrobin`, weighted round robin algorithm is used. When set to `chash`, consistent hashing algorithm is used. |\n| balancer.hash_on                   | string         | False    |                                   | [vars, headers, cookie, consumer, vars_combinations] | Used when `type` is `chash`. Support hashing on [NGINX variables](https://nginx.org/en/docs/varindex.html), headers, cookie, consumer, or a combination of [NGINX variables](https://nginx.org/en/docs/varindex.html). |\n| balancer.key                       | string         | False    |                                   |              | Used when `type` is `chash`. When `hash_on` is set to `header` or `cookie`, `key` is required. When `hash_on` is set to `consumer`, `key` is not required as the consumer name will be used as the key automatically. |\n| instances                          | array[object]  | True     |                                   |              | LLM instance configurations. |\n| instances.name                     | string         | True     |                                   |              | Name of the LLM service instance. |\n| instances.provider                 | string         | True     |                                   | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, openai-compatible] | LLM service provider. When set to `openai`, the Plugin will proxy the request to `api.openai.com`. When set to `deepseek`, the Plugin will proxy the request to `api.deepseek.com`. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `api.aimlapi.com` by default. When set to `anthropic`, the Plugin will proxy the request to `api.anthropic.com` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `openrouter.ai` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `generativelanguage.googleapis.com` by default. When set to `vertex-ai`, the Plugin will proxy the request to `aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |\n| instances.provider_conf            | object         | False     |                                   |              | Configuration for the specific provider. Required when `provider` is set to `vertex-ai` and `override` is not configured. |\n| instances.provider_conf.project_id | string         | True     |                                   |              | Google Cloud Project ID. |\n| instances.provider_conf.region     | string         | True     |                                   |              | Google Cloud Region. |\n| instances.priority                  | integer        | False    | 0                               |              | Priority of the LLM instance in load balancing. `priority` takes precedence over `weight`. |\n| instances.weight                    | string         | True     | 0                               | greater or equal to 0 | Weight of the LLM instance in load balancing. |\n| instances.auth                      | object         | True     |                                   |              | Authentication configurations. |\n| instances.auth.header               | object         | False    |                                   |              | Authentication headers. At least one of the `header` and `query` should be configured. |\n| instances.auth.query                | object         | False    |                                   |              | Authentication query parameters. At least one of the `header` and `query` should be configured. |\n| instances.auth.gcp                  | object         | False    |                                   |              | Configuration for Google Cloud Platform (GCP) authentication. |\n| instances.auth.gcp.service_account_json | string     | False    |                                   |              | Content of the GCP service account JSON file. This can also be configured by setting the `GCP_SERVICE_ACCOUNT` environment variable. |\n| instances.auth.gcp.max_ttl          | integer        | False    |                                   | minimum = 1  | Maximum TTL (in seconds) for caching the GCP access token. |\n| instances.auth.gcp.expire_early_secs| integer        | False    | 60                                | minimum = 0  | Seconds to expire the access token before its actual expiration time to avoid edge cases. |\n| instances.options                   | object         | False    |                                   |              | Model configurations. In addition to `model`, you can configure additional parameters and they will be forwarded to the upstream LLM service in the request body. For instance, if you are working with OpenAI, DeepSeek, or AIMLAPI, you can configure additional parameters such as `max_tokens`, `temperature`, `top_p`, and `stream`. See your LLM provider's API documentation for more available options. |\n| instances.options.model             | string         | False    |                                   |              | Name of the LLM model, such as `gpt-4` or `gpt-3.5`. See your LLM provider's API documentation for more available models. |\n| logging                             | object         | False    |                                   |              | Logging configurations. |\n| logging.summaries                   | boolean        | False    | false                           |              | If true, log request LLM model, duration, request, and response tokens. |\n| logging.payloads                    | boolean        | False    | false                           |              | If true, log request and response payload. |\n| logging.override                    | object         | False    |                                   |              | Override setting. |\n| logging.override.endpoint           | string         | False    |                                   |              | LLM provider endpoint to replace the default endpoint with. If not configured, the Plugin uses the default OpenAI endpoint `https://api.openai.com/v1/chat/completions`. |\n| checks                              | object         | False    |                                   |              | Health check configurations. Note that at the moment, OpenAI, DeepSeek, and AIMLAPI do not provide an official health check endpoint. Other LLM services that you can configure under `openai-compatible` provider may have available health check endpoints. |\n| checks.active                       | object         | True     |                                   |              | Active health check configurations. |\n| checks.active.type                  | string         | False    | http                            | [http, https, tcp] | Type of health check connection. |\n| checks.active.timeout               | number         | False    | 1                               |              | Health check timeout in seconds. |\n| checks.active.concurrency           | integer        | False    | 10                              |              | Number of upstream nodes to be checked at the same time. |\n| checks.active.host                  | string         | False    |                                   |              | HTTP host. |\n| checks.active.port                  | integer        | False    |                                   | between 1 and 65535 inclusive | HTTP port. |\n| checks.active.http_path             | string         | False    | /                               |              | Path for HTTP probing requests. |\n| checks.active.https_verify_certificate | boolean   | False    | true                            |              | If true, verify the node's TLS certificate. |\n| timeout                             | integer        | False    | 30000                           | greater than or equal to 1 | Request timeout in milliseconds when requesting the LLM service. |\n| keepalive                           | boolean        | False    | true                            |              | If true, keep the connection alive when requesting the LLM service. |\n| keepalive_timeout                   | integer        | False    | 60000                           | greater than or equal to 1000 | Request timeout in milliseconds when requesting the LLM service. |\n| keepalive_pool                      | integer        | False    | 30                              |              | Keepalive pool size for when connecting with the LLM service. |\n| ssl_verify                          | boolean        | False    | true                            |              | If true, verify the LLM service's certificate. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `ai-proxy-multi` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Load Balance between Instances\n\nThe following example demonstrates how you can configure two models for load balancing, forwarding 80% of the traffic to one instance and 20% to the other.\n\nFor demonstration and easier differentiation, you will be configuring one OpenAI instance and one DeepSeek instance as the upstream LLM services.\n\nCreate a Route as such and update with your LLM providers, models, API keys, and endpoints if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 8,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 2,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend 10 POST requests to the Route with a system prompt and a sample user question in the request body, to see the number of requests forwarded to OpenAI and DeepSeek:\n\n```shell\nopenai_count=0\ndeepseek_count=0\n\nfor i in {1..10}; do\n  model=$(curl -s \"http://127.0.0.1:9080/anything\" -X POST \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\n      \"messages\": [\n        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n      ]\n    }' | jq -r '.model')\n\n  if [[ \"$model\" == *\"gpt-4\"* ]]; then\n    ((openai_count++))\n  elif [[ \"$model\" == \"deepseek-chat\" ]]; then\n    ((deepseek_count++))\n  fi\ndone\n\necho \"OpenAI responses: $openai_count\"\necho \"DeepSeek responses: $deepseek_count\"\n```\n\nYou should see a response similar to the following:\n\n```text\nOpenAI responses: 8\nDeepSeek responses: 2\n```\n\n### Configure Instance Priority and Rate Limiting\n\nThe following example demonstrates how you can configure two models with different priorities and apply rate limiting on the instance with a higher priority. In the case where `fallback_strategy` is set to `[\"rate_limiting\"]`, the Plugin should continue to forward requests to the low priority instance once the high priority instance's rate limiting quota is fully consumed.\n\nCreate a Route as such and update with your LLM providers, models, API keys, and endpoints if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"fallback_strategy: [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"priority\": 1,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"priority\": 0,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of `10`, the next request within the 60-second window is expected to be forwarded to the other instance.\n\nWithin the same 60-second window, send another POST request to the route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newton law\" }\n    ]\n  }'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\n### Load Balance and Rate Limit by Consumers\n\nThe following example demonstrates how you can configure two models for load balancing and apply rate limiting by consumer.\n\nCreate a Consumer `johndoe` and a rate limiting quota of 10 tokens in a 60-second window on `openai-instance` instance:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nConfigure `key-auth` credential for `johndoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate another Consumer `janedoe` and a rate limiting quota of 10 tokens in a 60-second window on `deepseek-instance` instance:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nConfigure `key-auth` credential for `janedoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/janedoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route as such and update with your LLM providers, models, API keys, and endpoints if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"key-auth\": {},\n      \"ai-proxy-multi\": {\n        \"fallback_strategy: [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route without any consumer key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive an `HTTP/1.1 401 Unauthorized` response.\n\nSend a POST request to the Route with `johndoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of the `openai` instance for `johndoe`, the next request within the 60-second window from `johndoe` is expected to be forwarded to the `deepseek` instance.\n\nWithin the same 60-second window, send another POST request to the Route with `johndoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\nSend a POST request to the Route with `janedoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"The sum of 1 and 1 is 2. This is a basic arithmetic operation where you combine two units to get a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 14,\n    \"completion_tokens\": 31,\n    \"total_tokens\": 45,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 14\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of the `deepseek` instance for `janedoe`, the next request within the 60-second window from `janedoe` is expected to be forwarded to the `openai` instance.\n\nWithin the same 60-second window, send another POST request to the Route with `janedoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure, here are Newton's three laws of motion:\\n\\n1) Newton's First Law, also known as the Law of Inertia, states that an object at rest will stay at rest, and an object in motion will stay in motion, unless acted on by an external force. In simple words, this law suggests that an object will keep doing whatever it is doing until something causes it to do otherwise. \\n\\n2) Newton's Second Law states that the force acting on an object is equal to the mass of that object times its acceleration (F=ma). This means that force is directly proportional to mass and acceleration. The heavier the object and the faster it accelerates, the greater the force.\\n\\n3) Newton's Third Law, also known as the law of action and reaction, states that for every action, there is an equal and opposite reaction. Essentially, any force exerted onto a body will create a force of equal magnitude but in the opposite direction on the object that exerted the first force.\\n\\nRemember, these laws become less accurate when considering speeds near the speed of light (where Einstein's theory of relativity becomes more appropriate) or objects very small or very large. However, for everyday situations, they provide a good model of how things move.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\nThis shows `ai-proxy-multi` load balance the traffic with respect to the rate limiting rules in `ai-rate-limiting` by consumers.\n\n### Restrict Maximum Number of Completion Tokens\n\nThe following example demonstrates how you can restrict the number of `completion_tokens` used when generating the chat completion.\n\nFor demonstration and easier differentiation, you will be configuring one OpenAI instance and one DeepSeek instance as the upstream LLM services.\n\nCreate a Route as such and update with your LLM providers, models, API keys, and endpoints if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\",\n              \"max_tokens\": 50\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\",\n              \"max_tokens\": 100\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons law\" }\n    ]\n  }'\n```\n\nIf the request is proxied to OpenAI, you should see a response similar to the following, where the content is truncated per 50 `max_tokens` threshold:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Newton's Laws of Motion are three physical laws that form the bedrock for classical mechanics. They describe the relationship between a body and the forces acting upon it, and the body's motion in response to those forces. \\n\\n1. Newton's First Law\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 20,\n    \"completion_tokens\": 50,\n    \"total_tokens\": 70,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nIf the request is proxied to DeepSeek, you should see a response similar to the following, where the content is truncated per 100 `max_tokens` threshold:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Newton's Laws of Motion are three fundamental principles that form the foundation of classical mechanics. They describe the relationship between a body and the forces acting upon it, and the body's motion in response to those forces. Here's a brief explanation of each law:\\n\\n1. **Newton's First Law (Law of Inertia):**\\n   - **Statement:** An object will remain at rest or in uniform motion in a straight line unless acted upon by an external force.\\n   - **Explanation:** This law\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 10,\n    \"completion_tokens\": 100,\n    \"total_tokens\": 110,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 10\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\n### Proxy to Embedding Models\n\nThe following example demonstrates how you can configure the `ai-proxy-multi` Plugin to proxy requests and load balance between embedding models.\n\nCreate a Route as such and update with your LLM providers, embedding models, API keys, and endpoints:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"text-embedding-3-small\"\n            },\n            \"override\": {\n              \"endpoint\": \"https://api.openai.com/v1/embeddings\"\n            }\n          },\n          {\n            \"name\": \"az-openai-instance\",\n            \"provider\": \"openai-compatible\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$AZ_OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"text-embedding-3-small\"\n            },\n            \"override\": {\n              \"endpoint\": \"https://ai-plugin-developer.openai.azure.com/openai/deployments/text-embedding-3-small/embeddings?api-version=2023-05-15\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with an input string:\n\n```shell\ncurl \"http://127.0.0.1:9080/embeddings\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": \"hello world\"\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  \"object\": \"list\",\n  \"data\": [\n    {\n      \"object\": \"embedding\",\n      \"index\": 0,\n      \"embedding\": [\n        -0.0067144386,\n        -0.039197803,\n        0.034177095,\n        0.028763203,\n        -0.024785956,\n        -0.04201061,\n        ...\n      ],\n    }\n  ],\n  \"model\": \"text-embedding-3-small\",\n  \"usage\": {\n    \"prompt_tokens\": 2,\n    \"total_tokens\": 2\n  }\n}\n```\n\n### Enable Active Health Checks\n\nThe following example demonstrates how you can configure the `ai-proxy-multi` Plugin to proxy requests and load balance between models, and enable active health check to improve service availability. You can enable health check on one or multiple instances.\n\nCreate a Route as such and update the LLM providers, embedding models, API keys, and health check related configurations:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"llm-instance-1\",\n            \"provider\": \"openai-compatible\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$YOUR_LLM_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"'\"$YOUR_LLM_MODEL\"'\"\n            }\n          },\n          {\n            \"name\": \"llm-instance-2\",\n            \"provider\": \"openai-compatible\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$YOUR_LLM_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"'\"$YOUR_LLM_MODEL\"'\"\n            },\n            \"checks\": {\n              \"active\": {\n                \"type\": \"https\",\n                \"host\": \"yourhost.com\",\n                \"http_path\": \"/your/probe/path\",\n                \"healthy\": {\n                  \"interval\": 2,\n                  \"successes\": 1\n                },\n                \"unhealthy\": {\n                  \"interval\": 1,\n                  \"http_failures\": 3\n                }\n              }\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nFor verification, the behaviours should be consistent with the verification in [active health checks](../tutorials/health-check.md).\n\n### Include LLM Information in Access Log\n\nThe following example demonstrates how you can log LLM request related information in the gateway's access log to improve analytics and audit. The following variables are available:\n\n* `request_llm_model`: LLM model name specified in the request.\n* `apisix_upstream_response_time`: Time taken for APISIX to send the request to the upstream service and receive the full response\n* `request_type`: Type of request, where the value could be `traditional_http`, `ai_chat`, or `ai_stream`.\n* `llm_time_to_first_token`: Duration from request sending to the first token received from the LLM service, in milliseconds.\n* `llm_model`: LLM model.\n* `llm_prompt_tokens`: Number of tokens in the prompt.\n* `llm_completion_tokens`: Number of chat completion tokens in the prompt.\n\nUpdate the access log format in your configuration file to include additional LLM related variables:\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host \\\"$request_line\\\" $status $body_bytes_sent $request_time \\\"$http_referer\\\" \\\"$http_user_agent\\\" $upstream_addr $upstream_status $apisix_upstream_response_time \\\"$upstream_scheme://$upstream_host$upstream_uri\\\" \\\"$apisix_request_id\\\" \\\"$request_type\\\" \\\"$llm_time_to_first_token\\\" \\\"$llm_model\\\" \\\"$request_llm_model\\\"  \\\"$llm_prompt_tokens\\\" \\\"$llm_completion_tokens\\\"\"\n```\n\nReload APISIX for configuration changes to take effect.\n\nNext, create a Route with the `ai-proxy-multi` Plugin and send a request. For instance, if the request is forwarded to OpenAI and you receive the following response:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null,\n        \"annotations\": []\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    ...\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nIn the gateway's access log, you should see a log entry similar to the following:\n\n```text\n192.168.215.1 - - [21/Mar/2025:04:28:03 +0000] api.openai.com \"POST /anything HTTP/1.1\" 200 804 2.858 \"-\" \"curl/8.6.0\" - - - 5765 \"http://api.openai.com\" \"5c5e0b95f8d303cb81e4dc456a4b12d9\" \"ai_chat\" \"2858\" \"gpt-4\" \"gpt-4\" \"23\" \"8\"\n```\n\nThe access log entry shows the request type is `ai_chat`, Apisix upstream response time is `5765` milliseconds, time to first token is `2858` milliseconds, Requested LLM model is `gpt-4`. LLM model is `gpt-4`, prompt token usage is `23`, and completion token usage is `8`.\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-proxy.md",
    "content": "---\ntitle: ai-proxy\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-proxy\n  - AI\n  - LLM\ndescription: The ai-proxy Plugin simplifies access to LLM and embedding models providers by converting Plugin configurations into the required request format for OpenAI, DeepSeek, Azure, AIMLAPI, Anthropic, OpenRouter, Gemini, Vertex AI, and other OpenAI-compatible APIs.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-proxy\" />\n</head>\n\n## Description\n\nThe `ai-proxy` Plugin simplifies access to LLM and embedding models by transforming Plugin configurations into the designated request format. It supports the integration with OpenAI, DeepSeek, Azure, AIMLAPI, Anthropic, OpenRouter, Gemini, Vertex AI, and other OpenAI-compatible APIs.\n\nIn addition, the Plugin also supports logging LLM request information in the access log, such as token usage, model, time to the first response, and more.\n\n## Request Format\n\n| Name               | Type   | Required | Description                                         |\n| ------------------ | ------ | -------- | --------------------------------------------------- |\n| `messages`         | Array  | True      | An array of message objects.                        |\n| `messages.role`    | String | True      | Role of the message (`system`, `user`, `assistant`).|\n| `messages.content` | String | True      | Content of the message.                             |\n\n## Attributes\n\n| Name               | Type    | Required | Default | Valid values                              | Description |\n|--------------------|--------|----------|---------|------------------------------------------|-------------|\n| provider          | string  | True     |         | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, openai-compatible] | LLM service provider. When set to `openai`, the Plugin will proxy the request to `https://api.openai.com/chat/completions`. When set to `deepseek`, the Plugin will proxy the request to `https://api.deepseek.com/chat/completions`. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://api.aimlapi.com/v1/chat/completions` by default. When set to `anthropic`, the Plugin will proxy the request to `https://api.anthropic.com/v1/chat/completions` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://openrouter.ai/api/v1/chat/completions` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` by default. When set to `vertex-ai`, the Plugin will proxy the request to `https://aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |\n| provider_conf      | object  | False    |         |                                          | Configuration for the specific provider. Required when `provider` is set to `vertex-ai` and `override` is not configured. |\n| provider_conf.project_id | string | True |       |                                          | Google Cloud Project ID.  |\n| provider_conf.region | string | True   |         |                                          | Google Cloud Region.  |\n| auth             | object  | True     |         |                                          | Authentication configurations. |\n| auth.header      | object  | False    |         |                                          | Authentication headers. At least one of `header` or `query` must be configured. |\n| auth.query       | object  | False    |         |                                          | Authentication query parameters. At least one of `header` or `query` must be configured. |\n| auth.gcp         | object  | False    |         |                                          | Configuration for Google Cloud Platform (GCP) authentication. |\n| auth.gcp.service_account_json | string | False |  |                                          | Content of the GCP service account JSON file. This can also be configured by setting the `GCP_SERVICE_ACCOUNT` environment variable. |\n| auth.gcp.max_ttl | integer | False    |         | minimum = 1                              | Maximum TTL (in seconds) for caching the GCP access token. |\n| auth.gcp.expire_early_secs | integer | False | 60 | minimum = 0                              | Seconds to expire the access token before its actual expiration time to avoid edge cases. |\n| options         | object  | False    |         |                                          | Model configurations. In addition to `model`, you can configure additional parameters and they will be forwarded to the upstream LLM service in the request body. For instance, if you are working with OpenAI, you can configure additional parameters such as `temperature`, `top_p`, and `stream`. See your LLM provider's API documentation for more available options.  |\n| options.model   | string  | False    |         |                                          | Name of the LLM model, such as `gpt-4` or `gpt-3.5`. Refer to the LLM provider's API documentation for available models. |\n| override        | object  | False    |         |                                          | Override setting. |\n| override.endpoint | string | False    |         |                                          | Custom LLM provider endpoint, required when `provider` is `openai-compatible`. |\n| logging        | object  | False    |         |                                          | Logging configurations. |\n| logging.summaries | boolean | False | false |                                          | If true, logs request LLM model, duration, request, and response tokens. |\n| logging.payloads  | boolean | False | false |                                          | If true, logs request and response payload. |\n| timeout        | integer | False    | 30000    | ≥ 1                                      | Request timeout in milliseconds when requesting the LLM service. |\n| keepalive      | boolean | False    | true   |                                          | If true, keeps the connection alive when requesting the LLM service. |\n| keepalive_timeout | integer | False | 60000  | ≥ 1000                                   | Keepalive timeout in milliseconds when connecting to the LLM service. |\n| keepalive_pool | integer | False    | 30       |                                          | Keepalive pool size for the LLM service connection. |\n| ssl_verify     | boolean | False    | true   |                                          | If true, verifies the LLM service's certificate. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `ai-proxy` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Proxy to OpenAI\n\nThe following example demonstrates how you can configure the API key, model, and other parameters in the `ai-proxy` Plugin and configure the Plugin on a Route to proxy user prompts to OpenAI.\n\nObtain the OpenAI [API key](https://openai.com/blog/openai-api) and save it to an environment variable:\n\n```shell\nexport OPENAI_API_KEY=<your-api-key>\n```\n\nCreate a Route and configure the `ai-proxy` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\":{\n          \"model\": \"gpt-4\"\n        }\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Host: api.openai.com\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n### Proxy to DeepSeek\n\nThe following example demonstrates how you can configure the `ai-proxy` Plugin to proxy requests to DeekSeek.\n\nObtain the DeekSeek API key and save it to an environment variable:\n\n```shell\nexport DEEPSEEK_API_KEY=<your-api-key>\n```\n\nCreate a Route and configure the `ai-proxy` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"deepseek\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n          }\n        },\n        \"options\": {\n          \"model\": \"deepseek-chat\"\n        }\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a sample question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      {\n        \"role\": \"system\",\n        \"content\": \"You are an AI assistant that helps people find information.\"\n      },\n      {\n        \"role\": \"user\",\n        \"content\": \"Write me a 50-word introduction for Apache APISIX.\"\n      }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Apache APISIX is a dynamic, real-time, high-performance API gateway and cloud-native platform. It provides rich traffic management features like load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability, and more. Designed for microservices and serverless architectures, APISIX ensures scalability, security, and seamless integration with modern DevOps workflows.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n### Proxy to Azure OpenAI\n\nThe following example demonstrates how you can configure the `ai-proxy` Plugin to proxy requests to other LLM services, such as Azure OpenAI.\n\nObtain the Azure OpenAI API key and save it to an environment variable:\n\n```shell\nexport AZ_OPENAI_API_KEY=<your-api-key>\n```\n\nCreate a Route and configure the `ai-proxy` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai-compatible\",\n        \"auth\": {\n          \"header\": {\n            \"api-key\": \"'\"$AZ_OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\":{\n          \"model\": \"gpt-4\"\n        },\n        \"override\": {\n          \"endpoint\": \"https://api7-auzre-openai.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-15-preview\"\n        }\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a sample question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      {\n        \"role\": \"system\",\n        \"content\": \"You are an AI assistant that helps people find information.\"\n      },\n      {\n        \"role\": \"user\",\n        \"content\": \"Write me a 50-word introduction for Apache APISIX.\"\n      }\n    ],\n    \"max_tokens\": 800,\n    \"temperature\": 0.7,\n    \"frequency_penalty\": 0,\n    \"presence_penalty\": 0,\n    \"top_p\": 0.95,\n    \"stop\": null\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  \"choices\": [\n    {\n      ...,\n      \"message\": {\n        \"content\": \"Apache APISIX is a modern, cloud-native API gateway built to handle high-performance and low-latency use cases. It offers a wide range of features, including load balancing, rate limiting, authentication, and dynamic routing, making it an ideal choice for microservices and cloud-native architectures.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  ...\n}\n```\n\n### Proxy to Embedding Models\n\nThe following example demonstrates how you can configure the `ai-proxy` Plugin to proxy requests to embedding models. This example will use the OpenAI embedding model endpoint.\n\nObtain the OpenAI [API key](https://openai.com/blog/openai-api) and save it to an environment variable:\n\n```shell\nexport OPENAI_API_KEY=<your-api-key>\n```\n\nCreate a Route and configure the `ai-proxy` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/embeddings\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\":{\n          \"model\": \"text-embedding-3-small\",\n          \"encoding_format\": \"float\"\n        },\n        \"override\": {\n          \"endpoint\": \"https://api.openai.com/v1/embeddings\"\n        }\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with an input string:\n\n```shell\ncurl \"http://127.0.0.1:9080/embeddings\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": \"hello world\"\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  \"object\": \"list\",\n  \"data\": [\n    {\n      \"object\": \"embedding\",\n      \"index\": 0,\n      \"embedding\": [\n        -0.0067144386,\n        -0.039197803,\n        0.034177095,\n        0.028763203,\n        -0.024785956,\n        -0.04201061,\n        ...\n      ],\n    }\n  ],\n  \"model\": \"text-embedding-3-small\",\n  \"usage\": {\n    \"prompt_tokens\": 2,\n    \"total_tokens\": 2\n  }\n}\n```\n\n### Include LLM Information in Access Log\n\nThe following example demonstrates how you can log LLM request related information in the gateway's access log to improve analytics and audit. The following variables are available:\n\n* `request_llm_model`: LLM model name specified in the request.\n* `apisix_upstream_response_time`: Time taken for APISIX to send the request to the upstream service and receive the full response\n* `request_type`: Type of request, where the value could be `traditional_http`, `ai_chat`, or `ai_stream`.\n* `llm_time_to_first_token`: Duration from request sending to the first token received from the LLM service, in milliseconds.\n* `llm_model`: LLM model.\n* `llm_prompt_tokens`: Number of tokens in the prompt.\n* `llm_completion_tokens`: Number of chat completion tokens in the prompt.\n\nUpdate the access log format in your configuration file to include additional LLM related variables:\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host \\\"$request_line\\\" $status $body_bytes_sent $request_time \\\"$http_referer\\\" \\\"$http_user_agent\\\" $upstream_addr $upstream_status $apisix_upstream_response_time \\\"$upstream_scheme://$upstream_host$upstream_uri\\\" \\\"$apisix_request_id\\\" \\\"$request_type\\\" \\\"$llm_time_to_first_token\\\" \\\"$llm_model\\\" \\\"$request_llm_model\\\"  \\\"$llm_prompt_tokens\\\" \\\"$llm_completion_tokens\\\"\"\n```\n\nReload APISIX for configuration changes to take effect.\n\nNow if you create a Route and send a request following the [Proxy to OpenAI example](#proxy-to-openai), you should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null,\n        \"annotations\": []\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    ...\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nIn the gateway's access log, you should see a log entry similar to the following:\n\n```text\n192.168.215.1 - - [21/Mar/2025:04:28:03 +0000] api.openai.com \"POST /anything HTTP/1.1\" 200 804 2.858 \"-\" \"curl/8.6.0\" - - - 5765 \"http://api.openai.com\" \"5c5e0b95f8d303cb81e4dc456a4b12d9\" \"ai_chat\" \"2858\" \"gpt-4\" \"gpt-4\" \"23\" \"8\"\n```\n\nThe access log entry shows the request type is `ai_chat`, Apisix upstream response time is `5765` milliseconds, time to first token is `2858` milliseconds, Requested LLM model is `gpt-4`. LLM model is `gpt-4`, prompt token usage is `23`, and completion token usage is `8`.\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-rag.md",
    "content": "---\ntitle: ai-rag\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-rag\n  - AI\n  - LLM\ndescription: The ai-rag Plugin enhances LLM outputs with Retrieval-Augmented Generation (RAG), efficiently retrieving relevant documents to improve accuracy and contextual relevance in responses.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-rag\" />\n</head>\n\n## Description\n\nThe `ai-rag` Plugin provides Retrieval-Augmented Generation (RAG) capabilities with LLMs. It facilitates the efficient retrieval of relevant documents or information from external data sources, which are used to enhance the LLM responses, thereby improving the accuracy and contextual relevance of the generated outputs.\n\nThe Plugin supports using [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) and [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) services for generating embeddings and performing vector search.\n\n**_As of now only [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) and [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) services are supported for generating embeddings and performing vector search respectively. PRs for introducing support for other service providers are welcomed._**\n\n## Attributes\n\n| Name                                      |   Required   |   Type   |   Description                                                                                                                             |\n| ----------------------------------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| embeddings_provider                             | True          | object   | Configurations of the embedding models provider.                                                                                           |\n| embeddings_provider.azure_openai                | True          | object   | Configurations of [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) as the embedding models provider. |\n| embeddings_provider.azure_openai.endpoint       | True          | string   | Azure OpenAI embedding model endpoint.                                                                                  |\n| embeddings_provider.azure_openai.api_key        | True          | string   | Azure OpenAI API key.                                                                                                                    |\n| vector_search_provider                          | True          | object   | Configuration for the vector search provider.                                                                                              |\n| vector_search_provider.azure_ai_search          | True          | object   | Configuration for Azure AI Search.                                                                                                         |\n| vector_search_provider.azure_ai_search.endpoint | True          | string   | Azure AI Search endpoint.                                                                                                                  |\n| vector_search_provider.azure_ai_search.api_key  | True          | string   | Azure AI Search API key.                                                                                                                  |\n\n## Request Body Format\n\nThe following fields must be present in the request body.\n\n|   Field              |   Type   |    Description                                                                                                                   |\n| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| ai_rag               | object   | Request body RAG specifications.                                                                              |\n| ai_rag.embeddings    | object   | Request parameters required to generate embeddings. Contents will depend on the API specification of the configured provider.   |\n| ai_rag.vector_search | object   | Request parameters required to perform vector search. Contents will depend on the API specification of the configured provider. |\n\n- Parameters of `ai_rag.embeddings`\n\n  - Azure OpenAI\n\n  |   Name          |   Required   |   Type   |   Description                                                                                                              |\n  | --------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------- |\n  | input           | True          | string   | Input text used to compute embeddings, encoded as a string.                                                                |\n  | user            | False           | string   | A unique identifier representing your end-user, which can help in monitoring and detecting abuse.                          |\n  | encoding_format | False           | string   | The format to return the embeddings in. Can be either `float` or `base64`. Defaults to `float`.                            |\n  | dimensions      | False           | integer  | The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models. |\n\nFor other parameters please refer to the [Azure OpenAI embeddings documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings).\n\n- Parameters of `ai_rag.vector_search`\n\n  - Azure AI Search\n\n  |   Field   |   Required   |   Type   |   Description                |\n  | --------- | ------------ | -------- | ---------------------------- |\n  | fields    | True          | String   | Fields for the vector search. |\n\n  For other parameters please refer the [Azure AI Search documentation](https://learn.microsoft.com/en-us/rest/api/searchservice/documents/search-post).\n\nExample request body:\n\n```json\n{\n  \"ai_rag\": {\n    \"vector_search\": { \"fields\": \"contentVector\" },\n    \"embeddings\": {\n      \"input\": \"which service is good for devops\",\n      \"dimensions\": 1024\n    }\n  }\n}\n```\n\n## Example\n\nTo follow along the example, create an [Azure account](https://portal.azure.com) and complete the following steps:\n\n* In [Azure AI Foundry](https://oai.azure.com/portal), deploy a generative chat model, such as `gpt-4o`, and an embedding model, such as `text-embedding-3-large`. Obtain the API key and model endpoints.\n* Follow [Azure's example](https://github.com/Azure/azure-search-vector-samples/blob/main/demo-python/code/basic-vector-workflow/azure-search-vector-python-sample.ipynb) to prepare for a vector search in [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) using Python. The example will create a search index called `vectest` with the desired schema and upload the [sample data](https://github.com/Azure/azure-search-vector-samples/blob/main/data/text-sample.json) which contains 108 descriptions of various Azure services, for embeddings `titleVector` and `contentVector` to be generated based on `title` and `content`. Complete all the setups before performing vector searches in Python.\n* In [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search), [obtain the Azure vector search API key and the search service endpoint](https://learn.microsoft.com/en-us/azure/search/search-get-started-vector?tabs=api-key#retrieve-resource-information).\n\nSave the API keys and endpoints to environment variables:\n\n```shell\n# replace with your values\n\nAZ_OPENAI_DOMAIN=https://ai-plugin-developer.openai.azure.com\nAZ_OPENAI_API_KEY=9m7VYroxITMDEqKKEnpOknn1rV7QNQT7DrIBApcwMLYJQQJ99ALACYeBjFXJ3w3AAABACOGXGcd\nAZ_CHAT_ENDPOINT=${AZ_OPENAI_DOMAIN}/openai/deployments/gpt-4o/chat/completions?api-version=2024-02-15-preview\nAZ_EMBEDDING_MODEL=text-embedding-3-large\nAZ_EMBEDDINGS_ENDPOINT=${AZ_OPENAI_DOMAIN}/openai/deployments/${AZ_EMBEDDING_MODEL}/embeddings?api-version=2023-05-15\n\nAZ_AI_SEARCH_SVC_DOMAIN=https://ai-plugin-developer.search.windows.net\nAZ_AI_SEARCH_KEY=IFZBp3fKVdq7loEVe9LdwMvVdZrad9A4lPH90AzSeC06SlR\nAZ_AI_SEARCH_INDEX=vectest\nAZ_AI_SEARCH_ENDPOINT=${AZ_AI_SEARCH_SVC_DOMAIN}/indexes/${AZ_AI_SEARCH_INDEX}/docs/search?api-version=2024-07-01\n```\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Integrate with Azure for RAG-Enhaned Responses\n\nThe following example demonstrates how you can use the [`ai-proxy`](./ai-proxy.md) Plugin to proxy requests to Azure OpenAI LLM and use the `ai-rag` Plugin to generate embeddings and perform vector search to enhance LLM responses.\n\nCreate a Route as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n  \"id\": \"ai-rag-route\",\n  \"uri\": \"/rag\",\n  \"plugins\": {\n    \"ai-rag\": {\n      \"embeddings_provider\": {\n        \"azure_openai\": {\n          \"endpoint\": \"'\"$AZ_EMBEDDINGS_ENDPOINT\"'\",\n          \"api_key\": \"'\"$AZ_OPENAI_API_KEY\"'\"\n        }\n      },\n      \"vector_search_provider\": {\n        \"azure_ai_search\": {\n          \"endpoint\": \"'\"$AZ_AI_SEARCH_ENDPOINT\"'\",\n          \"api_key\": \"'\"$AZ_AI_SEARCH_KEY\"'\"\n        }\n      }\n    },\n    \"ai-proxy\": {\n      \"provider\": \"openai\",\n      \"auth\": {\n        \"header\": {\n          \"api-key\": \"'\"$AZ_OPENAI_API_KEY\"'\"\n        }\n      },\n      \"model\": \"gpt-4o\",\n      \"override\": {\n        \"endpoint\": \"'\"$AZ_CHAT_ENDPOINT\"'\"\n      }\n    }\n  }\n}'\n```\n\nSend a POST request to the Route with the vector fields name, embedding model dimensions, and an input prompt in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/rag\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"ai_rag\":{\n      \"vector_search\":{\n        \"fields\":\"contentVector\"\n      },\n      \"embeddings\":{\n        \"input\":\"Which Azure services are good for DevOps?\",\n        \"dimensions\":1024\n      }\n    }\n  }'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"choices\": [\n    {\n      \"content_filter_results\": {\n        ...\n      },\n      \"finish_reason\": \"length\",\n      \"index\": 0,\n      \"logprobs\": null,\n      \"message\": {\n        \"content\": \"Here is a list of Azure services categorized along with a brief description of each based on the provided JSON data:\\n\\n### Developer Tools\\n- **Azure DevOps**: A suite of services that help you plan, build, and deploy applications, including Azure Boards, Azure Repos, Azure Pipelines, Azure Test Plans, and Azure Artifacts.\\n- **Azure DevTest Labs**: A fully managed service to create, manage, and share development and test environments in Azure, supporting custom templates, cost management, and integration with Azure DevOps.\\n\\n### Containers\\n- **Azure Kubernetes Service (AKS)**: A managed container orchestration service based on Kubernetes, simplifying deployment and management of containerized applications with features like automatic upgrades and scaling.\\n- **Azure Container Instances**: A serverless container runtime to run and scale containerized applications without managing the underlying infrastructure.\\n- **Azure Container Registry**: A fully managed Docker registry service to store and manage container images and artifacts.\\n\\n### Web\\n- **Azure App Service**: A fully managed platform for building, deploying, and scaling web apps, mobile app backends, and RESTful APIs with support for multiple programming languages.\\n- **Azure SignalR Service**: A fully managed real-time messaging service to build and scale real-time web applications.\\n- **Azure Static Web Apps**: A serverless hosting service for modern web applications using static front-end technologies and serverless APIs.\\n\\n### Compute\\n- **Azure Virtual Machines**: Infrastructure-as-a-Service (IaaS) offering for deploying and managing virtual machines in the cloud.\\n- **Azure Functions**: A serverless compute service to run event-driven code without managing infrastructure.\\n- **Azure Batch**: A job scheduling service to run large-scale parallel and high-performance computing (HPC) applications.\\n- **Azure Service Fabric**: A platform to build, deploy, and manage scalable and reliable microservices and container-based applications.\\n- **Azure Quantum**: A quantum computing service to build and run quantum applications.\\n- **Azure Stack Edge**: A managed edge computing appliance to run Azure services and AI workloads on-premises or at the edge.\\n\\n### Security\\n- **Azure Bastion**: A fully managed service providing secure and scalable remote access to virtual machines.\\n- **Azure Security Center**: A unified security management service to protect workloads across Azure and on-premises infrastructure.\\n- **Azure DDoS Protection**: A cloud-based service to protect applications and resources from distributed denial-of-service (DDoS) attacks.\\n\\n### Databases\\n\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  \"created\": 1740625850,\n  \"id\": \"chatcmpl-B54gQdumpfioMPIybFnirr6rq9ZZS\",\n  \"model\": \"gpt-4o-2024-05-13\",\n  \"object\": \"chat.completion\",\n  \"prompt_filter_results\": [\n    {\n      \"prompt_index\": 0,\n      \"content_filter_results\": {\n        ...\n      }\n    }\n  ],\n  \"system_fingerprint\": \"fp_65792305e4\",\n  \"usage\": {\n    ...\n  }\n}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-rate-limiting.md",
    "content": "---\ntitle: ai-rate-limiting\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ai-rate-limiting\n  - AI\n  - LLM\ndescription: The ai-rate-limiting Plugin enforces token-based rate limiting for LLM service requests, preventing overuse, optimizing API consumption, and ensuring efficient resource allocation.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-rate-limiting\" />\n</head>\n\n## Description\n\nThe `ai-rate-limiting` Plugin enforces token-based rate limiting for requests sent to LLM services. It helps manage API usage by controlling the number of tokens consumed within a specified time frame, ensuring fair resource allocation and preventing excessive load on the service. It is often used with [`ai-proxy`](./ai-proxy.md) or [`ai-proxy-multi`](./ai-proxy-multi.md) plugin.\n\n## Attributes\n\n| Name                         | Type            | Required | Default  | Valid values                                             | Description |\n|------------------------------|----------------|----------|----------|---------------------------------------------------------|-------------|\n| limit                        | integer        | False    |          | >0                             | The maximum number of tokens allowed within a given time interval. At least one of `limit` and `instances.limit` should be configured. Required if `rules` is not configured. |\n| time_window                  | integer        | False    |          | >0                             | The time interval corresponding to the rate limiting `limit` in seconds. At least one of `time_window` and `instances.time_window` should be configured. Required if `rules` is not configured. |\n| rules                        | array[object]  | False    |          |                                                         | A list of rate limiting rules. Each rule is an object containing `count`, `time_window`, and `key`. If configured, this takes precedence over `limit` and `time_window`. |\n| rules.count                  | integer or string | True  |          | >0 or variable expression                              | The maximum number of tokens allowed within a given time interval. Can be a static integer or a variable expression like `$http_custom_limit`. |\n| rules.time_window            | integer or string | True  |          | >0 or variable expression                              | The time interval corresponding to the rate limiting `count` in seconds. Can be a static integer or a variable expression. |\n| rules.key                    | string         | True     |          |                                                         | The key to count requests by. If the configured key does not exist, the rule will not be executed. The `key` is interpreted as a combination of variables, for example: `$http_custom_a $http_custom_b`. |\n| rules.header_prefix          | string         | False    |          |                                                         | Prefix for rate limit headers. If configured, the response will include `X-{header_prefix}-RateLimit-Limit`, `X-{header_prefix}-RateLimit-Remaining`, and `X-{header_prefix}-RateLimit-Reset` headers. If not configured, the index of the rule in the rules array is used as the prefix. For example, headers for the first rule will be `X-1-RateLimit-Limit`, `X-1-RateLimit-Remaining`, and `X-1-RateLimit-Reset`. |\n| show_limit_quota_header      | boolean        | False    | true     |                                                         | If true, includes `X-AI-RateLimit-Limit-*`, `X-AI-RateLimit-Remaining-*`, and `X-AI-RateLimit-Reset-*` headers in the response, where `*` is the instance name. |\n| limit_strategy               | string         | False    | total_tokens | [total_tokens, prompt_tokens, completion_tokens] | Type of token to apply rate limiting. `total_tokens` is the sum of `prompt_tokens` and `completion_tokens`. |\n| instances                    | array[object]  | False    |          |                                                         | LLM instance rate limiting configurations. |\n| instances.name               | string         | True     |          |                                                         | Name of the LLM service instance. |\n| instances.limit              | integer        | True     |          | >0                             | The maximum number of tokens allowed within a given time interval for an instance. |\n| instances.time_window        | integer        | True     |          | >0                             | The time interval corresponding to the rate limiting `limit` in seconds for an instance. |\n| rejected_code                | integer        | False    | 503      |  [200, 599]                           | The HTTP status code returned when a request exceeding the quota is rejected. |\n| rejected_msg                 | string         | False    |          |                                           | The response body returned when a request exceeding the quota is rejected. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `ai-rate-limiting` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Apply Rate Limiting with `ai-proxy`\n\nThe following example demonstrates how you can use `ai-proxy` to proxy LLM traffic and use `ai-rate-limiting` to configure token-based rate limiting on the instance.\n\nCreate a Route as such and update with your LLM providers, models, API keys, and endpoints, if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\": {\n          \"model\": \"gpt-35-turbo-instruct\",\n          \"max_tokens\": 512,\n          \"temperature\": 1.0\n        }\n      },\n      \"ai-rate-limiting\": {\n        \"limit\": 300,\n        \"time_window\": 30,\n        \"limit_strategy\": \"prompt_tokens\"\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1 + 1 equals 2. This is a fundamental arithmetic operation where adding one unit to another results in a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\nIf the rate limiting quota of 300 prompt tokens has been consumed in a 30-second window, all additional requests will be rejected.\n\n### Rate Limit One Instance Among Multiple\n\nThe following example demonstrates how you can use `ai-proxy-multi` to configure two models for load balancing, forwarding 80% of the traffic to one instance and 20% to the other. Additionally, use `ai-rate-limiting` to configure token-based rate limiting on the instance that receives 80% of the traffic, such that when the configured quota is fully consumed, the additional traffic will be forwarded to the other instance.\n\nCreate a Route which applies rate limiting quota of 100 total tokens in a 30-second window on the `deepseek-instance-1` instance, and update with your LLM providers, models, API keys, and endpoints, if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance-1\",\n            \"provider\": \"deepseek\",\n            \"weight\": 8,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance-2\",\n            \"provider\": \"deepseek\",\n            \"weight\": 2,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance-1\",\n            \"limit_strategy\": \"total_tokens\",\n            \"limit\": 100,\n            \"time_window\": 30\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1 + 1 equals 2. This is a fundamental arithmetic operation where adding one unit to another results in a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\nIf `deepseek-instance-1` instance rate limiting quota of 100 tokens has been consumed in a 30-second window, the additional requests will all be forwarded to `deepseek-instance-2`, which is not rate limited.\n\n### Apply the Same Quota to All Instances\n\nThe following example demonstrates how you can apply the same rate limiting quota to all LLM upstream instances in `ai-rate-limiting`.\n\nFor demonstration and easier differentiation, you will be configuring one OpenAI instance and one DeepSeek instance as the upstream LLM services.\n\nCreate a Route which applies a rate limiting quota of 100 total tokens for all instances within a 60-second window, and update with your LLM providers, models, API keys, and endpoints, if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"limit\": 100,\n        \"time_window\": 60,\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws\" }\n    ]\n  }'\n```\n\nYou should receive a response from either LLM instance, similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure! Sir Isaac Newton formulated three laws of motion that describe the motion of objects. These laws are widely used in physics and engineering for studying and understanding how things move. Here they are:\\n\\n1. Newton's First Law - Law of Inertia: An object at rest tends to stay at rest and an object in motion tends to stay in motion with the same speed and in the same direction unless acted upon by an unbalanced force. This is also known as the principle of inertia.\\n\\n2. Newton's Second Law of Motion - Force and Acceleration: The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. This is usually formulated as F=ma where F is the force applied, m is the mass of the object and a is the acceleration produced.\\n\\n3. Newton's Third Law - Action and Reaction: For every action, there is an equal and opposite reaction. This means that any force exerted on a body will create a force of equal magnitude but in the opposite direction on the object that exerted the first force.\\n\\nIn simple terms: \\n1. If you slide a book on a table and let go, it will stop because of the friction (or force) between it and the table.\\n2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 256,\n    \"total_tokens\": 279,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of `100`, the next request within the 60-second window is expected to be forwarded to the other instance.\n\nWithin the same 60-second window, send another POST request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws\" }\n    ]\n  }'\n```\n\nYou should receive a response from the other LLM instance, similar to the following:\n\n```json\n{\n  ...\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics. Here's an explanation of each law:\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia)**\\n- **Statement**: An object will remain at rest or in uniform motion in a straight line unless acted upon by an external force.\\n- **What it means**: This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion. If no net force acts on an object, its velocity (speed and direction) will not change.\\n- **Example**: A book lying on a table will stay at rest unless you push it. Similarly, a hockey puck sliding on ice will keep moving at a constant speed unless friction or another force slows it down.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration)**\\n- **Statement**: The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 13,\n    \"completion_tokens\": 256,\n    \"total_tokens\": 269,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 13\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of `100`, the next request within the 60-second window is expected to be rejected.\n\nWithin the same 60-second window, send a third POST request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws\" }\n    ]\n  }'\n```\n\nYou should receive an `HTTP 429 Too Many Requests` response and observe the following headers:\n\n```text\nX-AI-RateLimit-Limit-openai-instance: 100\nX-AI-RateLimit-Remaining-openai-instance: 0\nX-AI-RateLimit-Reset-openai-instance: 0\nX-AI-RateLimit-Limit-deepseek-instance: 100\nX-AI-RateLimit-Remaining-deepseek-instance: 0\nX-AI-RateLimit-Reset-deepseek-instance: 0\n```\n\n### Configure Instance Priority and Rate Limiting\n\nThe following example demonstrates how you can configure two models with different priorities and apply rate limiting on the instance with a higher priority. In the case where `fallback_strategy` is set to `[\"rate_limiting\"]`, the Plugin should continue to forward requests to the low priority instance once the high priority instance's rate limiting quota is fully consumed.\n\nCreate a Route as such to set rate limiting and a higher priority on `openai-instance` instance and set the `fallback_strategy` to `[\"rate_limiting\"]`. Update with your LLM providers, models, API keys, and endpoints, if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"fallback_strategy: [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"priority\": 1,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"priority\": 0,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with a system prompt and a sample user question in the request body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of `10`, the next request within the 60-second window is expected to be forwarded to the other instance.\n\nWithin the same 60-second window, send another POST request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newton law\" }\n    ]\n  }'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\n### Load Balance and Rate Limit by Consumers\n\nThe following example demonstrates how you can configure two models for load balancing and apply rate limiting by Consumer.\n\nCreate a Consumer `johndoe` and a rate limiting quota of 10 tokens in a 60-second window on `openai-instance` instance:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nConfigure `key-auth` credential for `johndoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate another Consumer `janedoe` and a rate limiting quota of 10 tokens in a 60-second window on `deepseek-instance` instance:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\nConfigure `key-auth` credential for `janedoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/janedoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route as such and update with your LLM providers, models, API keys, and endpoints, if applicable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"key-auth\": {},\n      \"ai-proxy-multi\": {\n        \"fallback_strategy: [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route without any Consumer key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive an `HTTP/1.1 401 Unauthorized` response.\n\nSend a POST request to the Route with `johndoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of the `openai` instance for `johndoe`, the next request within the 60-second window from `johndoe` is expected to be forwarded to the `deepseek` instance.\n\nWithin the same 60-second window, send another POST request to the Route with `johndoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\nSend a POST request to the Route with `janedoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\nYou should receive a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"The sum of 1 and 1 is 2. This is a basic arithmetic operation where you combine two units to get a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 14,\n    \"completion_tokens\": 31,\n    \"total_tokens\": 45,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 14\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\nSince the `total_tokens` value exceeds the configured quota of the `deepseek` instance for `janedoe`, the next request within the 60-second window from `janedoe` is expected to be forwarded to the `openai` instance.\n\nWithin the same 60-second window, send another POST request to the Route with `janedoe`'s key:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure, here are Newton's three laws of motion:\\n\\n1) Newton's First Law, also known as the Law of Inertia, states that an object at rest will stay at rest, and an object in motion will stay in motion, unless acted on by an external force. In simple words, this law suggests that an object will keep doing whatever it is doing until something causes it to do otherwise. \\n\\n2) Newton's Second Law states that the force acting on an object is equal to the mass of that object times its acceleration (F=ma). This means that force is directly proportional to mass and acceleration. The heavier the object and the faster it accelerates, the greater the force.\\n\\n3) Newton's Third Law, also known as the law of action and reaction, states that for every action, there is an equal and opposite reaction. Essentially, any force exerted onto a body will create a force of equal magnitude but in the opposite direction on the object that exerted the first force.\\n\\nRemember, these laws become less accurate when considering speeds near the speed of light (where Einstein's theory of relativity becomes more appropriate) or objects very small or very large. However, for everyday situations, they provide a good model of how things move.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\nThis shows `ai-proxy-multi` load balance the traffic with respect to the rate limiting rules in `ai-rate-limiting` by Consumers.\n"
  },
  {
    "path": "docs/en/latest/plugins/ai-request-rewrite.md",
    "content": "---\ntitle: ai-request-rewrite\nkeywords:\n  - Apache APISIX\n  - AI Gateway\n  - Plugin\n  - ai-request-rewrite\ndescription: The ai-request-rewrite plugin intercepts client requests before they are forwarded to the upstream service. It sends a predefined prompt, along with the original request body, to a specified LLM service. The LLM processes the input and returns a modified request body, which is then used for the upstream request. This allows dynamic transformation of API requests based on AI-generated content.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ai-request-rewrite` plugin intercepts client requests before they are forwarded to the upstream service. It sends a predefined prompt, along with the original request body, to a specified LLM service. The LLM processes the input and returns a modified request body, which is then used for the upstream request. This allows dynamic transformation of API requests based on AI-generated content.\n\n## Plugin Attributes\n\n| **Field**                 | **Required** | **Type** | **Description**                                                                      |\n| ------------------------- | ------------ | -------- | ------------------------------------------------------------------------------------ |\n| prompt                    | Yes          | String   | The prompt send to LLM service.                                                      |\n| provider                  | Yes          | String   | Name of the LLM service. Available options: openai, deekseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, and openai-compatible. When `aimlapi` is selected, the plugin uses the OpenAI-compatible driver with a default endpoint of `https://api.aimlapi.com/v1/chat/completions`.   |\n| provider_conf             | No           | Object   | Configuration for the specific provider. Required when `provider` is set to `vertex-ai` and `override` is not configured. |\n| provider_conf.project_id  | Yes          | String   | Google Cloud Project ID. |\n| provider_conf.region      | Yes          | String   | Google Cloud Region. |\n| auth                      | Yes          | Object   | Authentication configuration                                                         |\n| auth.header               | No           | Object   | Authentication headers. Key must match pattern `^[a-zA-Z0-9._-]+$`.                  |\n| auth.query                | No           | Object   | Authentication query parameters. Key must match pattern `^[a-zA-Z0-9._-]+$`.         |\n| auth.gcp                  | No           | Object   | Configuration for Google Cloud Platform (GCP) authentication. |\n| auth.gcp.service_account_json | No       | String   | Content of the GCP service account JSON file. This can also be configured by setting the `GCP_SERVICE_ACCOUNT` environment variable. |\n| auth.gcp.max_ttl          | No           | Integer  | Maximum TTL (in seconds) for caching the GCP access token. Minimum: 1. |\n| auth.gcp.expire_early_secs| No           | Integer  | Seconds to expire the access token before its actual expiration time to avoid edge cases. Minimum: 0. Default: 60. |\n| options                   | No           | Object   | Key/value settings for the model                                                     |\n| options.model             | No           | String   | Model to execute. Examples: \"gpt-3.5-turbo\" for openai, \"deepseek-chat\" for deekseek, or \"qwen-turbo\" for openai-compatible or aimlapi services |\n| override.endpoint         | No           | String   | Override the default endpoint when using OpenAI-compatible services (e.g., self-hosted models or third-party LLM services). When the provider is 'openai-compatible', the endpoint field is required. |\n| timeout                   | No           | Integer  | Total timeout in milliseconds for requests to LLM service, including connect, send, and read timeouts. Range: 1 - 60000. Default: 30000|\n| keepalive                 | No           | Boolean  | Enable keepalive for requests to LLM service. Default: true                                  |\n| keepalive_timeout         | No           | Integer  | Keepalive timeout in milliseconds for requests to LLM service. Minimum: 1000. Default: 60000 |\n| keepalive_pool            | No           | Integer  | Keepalive pool size for requests to LLM service. Minimum: 1. Default: 30                     |\n| ssl_verify                | No           | Boolean  | SSL verification for requests to LLM service. Default: true                                  |\n\n## How it works\n\n![image](https://github.com/user-attachments/assets/c7288e4f-00fc-46ca-b69e-d3d74d7085ca)\n\n## Examples\n\nThe examples below demonstrate how you can configure `ai-request-rewrite` for different scenarios.\n\n:::note\n\nYou can fetch the admin_key from config.yaml and save to an environment variable with the following command:\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n\n:::\n\n### Redact sensitive information\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ai-request-rewrite\": {\n        \"prompt\": \"Given a JSON request body, identify and mask any sensitive information such as credit card numbers, social security numbers, and personal identification numbers (e.g., passport or driver'\\''s license numbers). Replace detected sensitive values with a masked format (e.g., \\\"*** **** **** 1234\\\") for credit card numbers. Ensure the JSON structure remains unchanged.\",\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer <some-token>\"\n          }\n        },\n        \"options\": {\n          \"model\": \"gpt-4\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nNow send a request:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"John Doe\",\n    \"email\": \"john.doe@example.com\",\n    \"credit_card\": \"4111 1111 1111 1111\",\n    \"ssn\": \"123-45-6789\",\n    \"address\": \"123 Main St\"\n  }'\n```\n\nThe request body send to the LLM Service is as follows:\n\n```json\n{\n  \"messages\": [\n     {\n       \"role\": \"system\",\n       \"content\": \"Given a JSON request body, identify and mask any sensitive information such as credit card numbers, social security numbers, and personal identification numbers (e.g., passport or driver's license numbers). Replace detected sensitive values with a masked format (e.g., '*** **** **** 1234') for credit card numbers). Ensure the JSON structure remains unchanged.\"\n     },\n     {\n       \"role\": \"user\",\n       \"content\": \"{\\n\\\"name\\\":\\\"John Doe\\\",\\n\\\"email\\\":\\\"john.doe@example.com\\\",\\n\\\"credit_card\\\":\\\"4111 1111 1111 1111\\\",\\n\\\"ssn\\\":\\\"123-45-6789\\\",\\n\\\"address\\\":\\\"123 Main St\\\"\\n}\"\n     }\n   ]\n}\n\n```\n\nThe LLM processes the input and returns a modified request body, which replace detected sensitive values with a masked format then used for the upstream request:\n\n```json\n{\n  \"name\": \"John Doe\",\n  \"email\": \"john.doe@example.com\",\n  \"credit_card\": \"**** **** **** 1111\",\n  \"ssn\": \"***-**-6789\",\n  \"address\": \"123 Main St\"\n}\n```\n\n### Send request to an OpenAI compatible LLM\n\nCreate a route with the `ai-request-rewrite` plugin with `provider` set to `openai-compatible` and the endpoint of the model set to `override.endpoint` like so:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ai-request-rewrite\": {\n        \"prompt\": \"Given a JSON request body, identify and mask any sensitive information such as credit card numbers, social security numbers, and personal identification numbers (e.g., passport or driver'\\''s license numbers). Replace detected sensitive values with a masked format (e.g., '*** **** **** 1234') for credit card numbers). Ensure the JSON structure remains unchanged.\",\n        \"provider\": \"openai-compatible\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer <some-token>\"\n          }\n        },\n        \"options\": {\n          \"model\": \"qwen-plus\",\n          \"max_tokens\": 1024,\n          \"temperature\": 1\n        },\n        \"override\": {\n          \"endpoint\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/api-breaker.md",
    "content": "---\ntitle: api-breaker\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - API Breaker\ndescription: This document describes the information about the Apache APISIX api-breaker Plugin, you can use it to protect Upstream services.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `api-breaker` Plugin implements circuit breaker functionality to protect Upstream services.\n\n:::note\n\nWhenever the Upstream service responds with a status code from the configured `unhealthy.http_statuses` list for the configured `unhealthy.failures` number of times, the Upstream service will be considered unhealthy.\n\nThe request is then retried in 2, 4, 8, 16 ... seconds until the `max_breaker_sec`.\n\nIn an unhealthy state, if the Upstream service responds with a status code from the configured list `healthy.http_statuses` for `healthy.successes` times, the service is considered healthy again.\n\n:::\n\n## Attributes\n\n| Name                    | Type           | Required | Default | Valid values    | Description                                                                                                                                                                                                                                  |\n|-------------------------|----------------|----------|---------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| break_response_code     | integer        | True     |         | [200, ..., 599] | HTTP error code to return when Upstream is unhealthy.                                                                                                                                                                                        |\n| break_response_body     | string         | False    |         |                 | Body of the response message to return when Upstream is unhealthy.                                                                                                                                                                           |\n| break_response_headers  | array[object]  | False    |         | [{\"key\":\"header_name\",\"value\":\"can contain Nginx $var\"}] | Headers of the response message to return when Upstream is unhealthy. Can only be configured when the `break_response_body` attribute is configured. The values can contain APISIX variables. For example, we can use `{\"key\":\"X-Client-Addr\",\"value\":\"$remote_addr:$remote_port\"}`. |\n| max_breaker_sec         | integer        | False    | 300     | >=3             | Maximum time in seconds for circuit breaking.                                                                                                                                                                                                |\n| unhealthy.http_statuses | array[integer] | False    | [500]   | [500, ..., 599] | Status codes of Upstream to be considered unhealthy.                                                                                                                                                                                         |\n| unhealthy.failures      | integer        | False    | 3       | >=1             | Number of failures within a certain period of time for the Upstream service to be considered unhealthy.                                                                                                                                                          |\n| healthy.http_statuses   | array[integer] | False    | [200]   | [200, ..., 499] | Status codes of Upstream to be considered healthy.                                                                                                                                                                                           |\n| healthy.successes       | integer        | False    | 3       | >=1             | Number of consecutive healthy requests for the Upstream service to be considered healthy.                                                                                                                                                    |\n\n## Enable Plugin\n\nThe example below shows how you can configure the Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"api-breaker\": {\n            \"break_response_code\": 502,\n            \"unhealthy\": {\n                \"http_statuses\": [500, 503],\n                \"failures\": 3\n            },\n            \"healthy\": {\n                \"http_statuses\": [200],\n                \"successes\": 1\n            }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nIn this configuration, a response code of `500` or `503` three times within a certain period of time triggers the unhealthy status of the Upstream service. A response code of `200` restores its healthy status.\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can test it out by sending a request.\n\n```shell\ncurl -i -X POST \"http://127.0.0.1:9080/hello\"\n```\n\nIf the Upstream service responds with an unhealthy response code, you will receive the configured response code (`break_response_code`).\n\n```shell\nHTTP/1.1 502 Bad Gateway\n...\n<html>\n<head><title>502 Bad Gateway</title></head>\n<body>\n<center><h1>502 Bad Gateway</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n## Delete Plugin\n\nTo remove the `api-breaker` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/attach-consumer-label.md",
    "content": "---\ntitle: attach-consumer-label\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - API Consumer\ndescription: This article describes the Apache APISIX attach-consumer-label plugin, which you can use to pass custom consumer labels to upstream services.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `attach-consumer-label` plugin attaches custom consumer-related labels, in addition to `X-Consumer-Username` and `X-Credential-Indentifier`, to authenticated requests, for upstream services to differentiate between consumers and implement additional logics.\n\n## Attributes\n\n| Name     | Type   | Required | Default | Valid values | Description                                                                                                                                                                                                                                                                                                                                                                                                     |\n|----------|--------|----------|---------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| headers  | object | True     |         |              | Key-value pairs of consumer labels to be attached to request headers, where key is the request header name, such as `X-Consumer-Role`, and the value is a reference to the custom label key, such as `$role`. Note that the value should always start with a dollar sign (`$`). If a referenced consumer value is not configured on the consumer, the corresponding header will not be attached to the request. |\n\n## Enable Plugin\n\nThe following example demonstrates how you can attach custom labels to request headers before authenticated requests are forwarded to upstream services. If the request is rejected, you should not see any consumer labels attached to request headers. If a certain label value is not configured on the consumer but referenced in the `attach-consumer-label` plugin, the corresponding header will also not be attached.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\nCreate a consumer `john` with custom labels:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"labels\": {\n      \"department\": \"devops\",\n      \"company\": \"api7\"\n    }\n  }'\n```\n\nConfigure the `key-auth` credential for the consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a route enabling the `key-auth` and `attach-consumer-label` plugins:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"attach-consumer-label-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"attach-consumer-label\": {\n        \"headers\": {\n          \"X-Consumer-Department\": \"$department\",\n          \"X-Consumer-Company\": \"$company\",\n          \"X-Consumer-Role\": \"$role\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n:::tip\n\nThe consumer label references must be prefixed by a dollar sign (`$`).\n\n:::\n\nTo verify, send a request to the route with the valid credential:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key'\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Apikey\": \"john-key\",\n    \"Host\": \"127.0.0.1\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-key-auth\",\n    \"X-Consumer-Company\": \"api7\",\n    \"X-Consumer-Department\": \"devops\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66e5107c-5bb3e24f2de5baf733aec1cc\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n## Delete plugin\n\nTo remove the Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/attach-consumer-label-route\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/get\",\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/authz-casbin.md",
    "content": "---\ntitle: authz-casbin\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Authz Casbin\n  - authz-casbin\ndescription: This document contains information about the Apache APISIX authz-casbin Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `authz-casbin` Plugin is an authorization Plugin based on [Lua Casbin](https://github.com/casbin/lua-casbin/). This Plugin supports powerful authorization scenarios based on various [access control models](https://casbin.org/docs/en/supported-models).\n\n## Attributes\n\n| Name        | Type   | Required | Description                                                                            |\n|-------------|--------|----------|----------------------------------------------------------------------------------------|\n| model_path  | string | True     | Path of the Casbin model configuration file.                                           |\n| policy_path | string | True     | Path of the Casbin policy file.                                                        |\n| model       | string | True     | Casbin model configuration in text format.                                             |\n| policy      | string | True     | Casbin policy in text format.                                                          |\n| username    | string | True     | Header in the request that will be used in the request to pass the username (subject). |\n\n:::note\n\nYou must either specify the `model_path`, `policy_path`, and the `username` attributes or specify the `model`, `policy` and the `username` attributes in the Plugin configuration for it to be valid.\n\nIf you wish to use a global Casbin configuration, you can first specify `model` and `policy` attributes in the Plugin metadata and only the `username` attribute in the Plugin configuration. All Routes will use the Plugin configuration this way.\n\n:::\n\n## Metadata\n\n| Name   | Type   | Required | Description                                |\n|--------|--------|----------|--------------------------------------------|\n| model  | string | True     | Casbin model configuration in text format. |\n| policy | string | True     | Casbin policy in text format.              |\n\n## Enable Plugin\n\nYou can enable the Plugin on a Route by either using the model/policy file paths or using the model/policy text in Plugin configuration/metadata.\n\n### By using model/policy file paths\n\nThe example below shows setting up Casbin authentication from your model/policy configuration file:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"authz-casbin\": {\n            \"model_path\": \"/path/to/model.conf\",\n            \"policy_path\": \"/path/to/policy.csv\",\n            \"username\": \"user\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n```\n\n### By using model/policy text in Plugin configuration\n\nThe example below shows setting up Casbin authentication from your model/policy text in your Plugin configuration:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"authz-casbin\": {\n            \"model\": \"[request_definition]\n            r = sub, obj, act\n\n            [policy_definition]\n            p = sub, obj, act\n\n            [role_definition]\n            g = _, _\n\n            [policy_effect]\n            e = some(where (p.eft == allow))\n\n            [matchers]\n            m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n\n            \"policy\": \"p, *, /, GET\n            p, admin, *, *\n            g, alice, admin\",\n\n            \"username\": \"user\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n```\n\n### By using model/policy text in Plugin metadata\n\nFirst, you need to send a `PUT` request to the Admin API to add the `model` and `policy` text to the Plugin metadata.\n\nAll Routes configured this way will use a single Casbin enforcer with the configured Plugin metadata. You can also update the model/policy in this way and the Plugin will automatically update to the new configuration.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/authz-casbin -H \"X-API-KEY: $admin_key\" -i -X PUT -d '\n{\n\"model\": \"[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n\n\"policy\": \"p, *, /, GET\np, admin, *, *\ng, alice, admin\"\n}'\n```\n\nOnce you have updated the Plugin metadata, you can add the Plugin to a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"authz-casbin\": {\n            \"username\": \"user\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n```\n\n:::note\n\nThe Plugin Route configuration has a higher precedence than the Plugin metadata configuration. If the model/policy configuration is present in the Plugin Route configuration, it is used instead of the metadata configuration.\n\n:::\n\n## Example usage\n\nWe define the example model as:\n\n```conf\n[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\n```\n\nAnd the example policy as:\n\n```conf\np, *, /, GET\np, admin, *, *\ng, alice, admin\n```\n\nSee [examples](https://github.com/casbin/lua-casbin/tree/master/examples) for more policy and model configurations.\n\nThe above configuration will let anyone access the homepage (`/`) using a `GET` request while only users with admin permissions can access other pages and use other request methods.\n\nSo if we make a get request to the homepage:\n\n```shell\ncurl -i http://127.0.0.1:9080/ -X GET\n```\n\nBut if an unauthorized user tries to access any other page, they will get a 403 error:\n\n```shell\ncurl -i http://127.0.0.1:9080/res -H 'user: bob' -X GET\nHTTP/1.1 403 Forbidden\n```\n\nAnd only users with admin privileges can access the endpoints:\n\n```shell\ncurl -i http://127.0.0.1:9080/res -H 'user: alice' -X GET\n```\n\n## Delete Plugin\n\nTo remove the `authz-casbin` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/authz-casdoor.md",
    "content": "---\ntitle: authz-casdoor\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Authz Casdoor\n  - authz-casdoor\ndescription: This document contains information about the Apache APISIX authz-casdoor Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `authz-casdoor` Plugin can be used to add centralized authentication with [Casdoor](https://casdoor.org/).\n\n## Attributes\n\n| Name          | Type   | Required | Description                                  |\n|---------------|--------|----------|----------------------------------------------|\n| endpoint_addr | string | True     | URL of Casdoor.                              |\n| client_id     | string | True     | Client ID in Casdoor.                        |\n| client_secret | string | True     | Client secret in Casdoor.                    |\n| callback_url  | string | True     | Callback URL used to receive state and code. |\n\nNOTE: `encrypt_fields = {\"client_secret\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\n:::info IMPORTANT\n\n`endpoint_addr` and `callback_url` should not end with '/'.\n\n:::\n\n:::info IMPORTANT\n\nThe `callback_url` must belong to the URI of your Route. See the code snippet below for an example configuration.\n\n:::\n\n## Enable Plugin\n\nYou can enable the Plugin on a specific Route as shown below:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"uri\": \"/anything/*\",\n  \"plugins\": {\n    \"authz-casdoor\": {\n        \"endpoint_addr\":\"http://localhost:8000\",\n        \"callback_url\":\"http://localhost:9080/anything/callback\",\n        \"client_id\":\"7ceb9b7fda4a9061ec1c\",\n        \"client_secret\":\"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n## Example usage\n\nOnce you have enabled the Plugin, a new user visiting this Route would first be processed by the `authz-casdoor` Plugin. They would be redirected to the login page of Casdoor.\n\nAfter successfully logging in, Casdoor will redirect this user to the `callback_url` with GET parameters `code` and `state` specified. The Plugin will also request for an access token and confirm whether the user is really logged in. This process is only done once and subsequent requests are left uninterrupted.\n\nOnce this is done, the user is redirected to the original URL they wanted to visit.\n\n## Delete Plugin\n\nTo remove the `authz-casdoor` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/anything/*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/authz-keycloak.md",
    "content": "---\ntitle: authz-keycloak\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Authz Keycloak\n  - authz-keycloak\ndescription: This document contains information about the Apache APISIX authz-keycloak Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `authz-keycloak` Plugin can be used to add authentication with [Keycloak Identity Server](https://www.keycloak.org/).\n\n:::tip\n\nAlthough this Plugin was developed to work with Keycloak, it should work with any OAuth/OIDC and UMA compliant identity providers as well.\n\n:::\n\nRefer to [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/) for more information on Keycloak.\n\n## Attributes\n\n| Name                                         | Type          | Required | Default                                       | Valid values                                                       | Description                                                                                                                                                                                                                                           |\n|----------------------------------------------|---------------|----------|-----------------------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| discovery                                    | string        | False    |                                               | https://host.domain/realms/foo/.well-known/uma2-configuration | URL to [discovery document](https://www.keycloak.org/docs/latest/authorization_services/index.html) of Keycloak Authorization Services.                                                                                                |\n| token_endpoint                               | string        | False    |                                               | https://host.domain/realms/foo/protocol/openid-connect/token  | An OAuth2-compliant token endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. If provided, overrides the value from discovery.                                                                                       |\n| resource_registration_endpoint               | string        | False    |                                               | https://host.domain/realms/foo/authz/protection/resource_set  | A UMA-compliant resource registration endpoint. If provided, overrides the value from discovery.                                                                                                                                                      |\n| client_id                                    | string        | True     |                                               |                                                                    | The identifier of the resource server to which the client is seeking access.                                                                                                                                                                         |\n| client_secret                                | string        | False    |                                               |                                                                    | The client secret, if required. You can use APISIX secret to store and reference this value. APISIX currently supports storing secrets in two ways. [Environment Variables and HashiCorp Vault](../terminology/secret.md)                                                                                                                                                                                                                         |\n| grant_type                                   | string        | False    | \"urn:ietf:params:oauth:grant-type:uma-ticket\" | [\"urn:ietf:params:oauth:grant-type:uma-ticket\"]                    |                                                                                                                                                                                                                                                       |\n| policy_enforcement_mode                      | string        | False    | \"ENFORCING\"                                   | [\"ENFORCING\", \"PERMISSIVE\"]                                        |                                                                                                                                                                                                                                                       |\n| permissions                                  | array[string] | False    |                                               |                                                                    | An array of strings, each representing a set of one or more resources and scopes the client is seeking access.                                                                                                                                        |\n| lazy_load_paths                              | boolean       | False    | false                                         |                                                                    | When set to true, dynamically resolves the request URI to resource(s) using the resource registration endpoint instead of the static permission.                                                                                                      |\n| http_method_as_scope                         | boolean       | False    | false                                         |                                                                    | When set to true, maps the HTTP request type to scope of the same name and adds to all requested permissions.                                                                                                                                         |\n| timeout                                      | integer       | False    | 3000                                          | [1000, ...]                                                        | Timeout in ms for the HTTP connection with the Identity Server.                                                                                                                                                                                       |\n| access_token_expires_in                      | integer       | False    | 300                                           | [1, ...]                                                           | Expiration time(s) of the access token.                                                                                                                                                                                                               |\n| access_token_expires_leeway                  | integer       | False    | 0                                             | [0, ...]                                                           | Expiration leeway(s) for access_token renewal. When set, the token will be renewed access_token_expires_leeway seconds before expiration. This avoids errors in cases where the access_token just expires when reaching the OAuth Resource Server.    |\n| refresh_token_expires_in                     | integer       | False    | 3600                                          | [1, ...]                                                           | The expiration time(s) of the refresh token.                                                                                                                                                                                                          |\n| refresh_token_expires_leeway                 | integer       | False    | 0                                             | [0, ...]                                                           | Expiration leeway(s) for refresh_token renewal. When set, the token will be renewed refresh_token_expires_leeway seconds before expiration. This avoids errors in cases where the refresh_token just expires when reaching the OAuth Resource Server. |\n| ssl_verify                                   | boolean       | False    | true                                          |                                                                    | When set to true, verifies if TLS certificate matches hostname.                                                                                                                                                                                       |\n| cache_ttl_seconds                            | integer       | False    | 86400 (equivalent to 24h)                     | positive integer >= 1                                              | Maximum time in seconds up to which the Plugin caches discovery documents and tokens used by the Plugin to authenticate to Keycloak.                                                                                                                  |\n| keepalive                                    | boolean       | False    | true                                          |                                                                    | When set to true, enables HTTP keep-alive to keep connections open after use. Set to `true` if you are expecting a lot of requests to Keycloak.                                                                                                       |\n| keepalive_timeout                            | integer       | False    | 60000                                         | positive integer >= 1000                                           | Idle time after which the established HTTP connections will be closed.                                                                                                                                                                                |\n| keepalive_pool                               | integer       | False    | 5                                             | positive integer >= 1                                              | Maximum number of connections in the connection pool.                                                                                                                                                                                                 |\n| access_denied_redirect_uri                   | string        | False    |                                               | [1, 2048]                                                          | URI to redirect the user to instead of returning an error message like `\"error_description\":\"not_authorized\"`.                                                                                                                                        |\n| password_grant_token_generation_incoming_uri | string        | False    |                                               | /api/token                                                         | Set this to generate token using the password grant type. The Plugin will compare incoming request URI to this value.                                                                                                                                 |\n\nNOTE: `encrypt_fields = {\"client_secret\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\n### Discovery and endpoints\n\nIt is recommended to use the `discovery` attribute as the `authz-keycloak` Plugin can discover the Keycloak API endpoints from it.\n\nIf set, the `token_endpoint` and `resource_registration_endpoint` will override the values obtained from the discovery document.\n\n### Client ID and secret\n\nThe Plugin needs the `client_id` attribute for identification and to specify the context in which to evaluate permissions when interacting with Keycloak.\n\nIf the `lazy_load_paths` attribute is set to true, then the Plugin additionally needs to obtain an access token for itself from Keycloak. In such cases, if the client access to Keycloak is confidential, you need to configure the `client_secret` attribute.\n\n### Policy enforcement mode\n\nThe `policy_enforcement_mode` attribute specifies how policies are enforced when processing authorization requests sent to the server.\n\n#### `ENFORCING` mode\n\nRequests are denied by default even when there is no policy associated with a resource.\n\nThe `policy_enforcement_mode` is set to `ENFORCING` by default.\n\n#### `PERMISSIVE` mode\n\nRequests are allowed when there is no policy associated with a given resource.\n\n### Permissions\n\nWhen handling incoming requests, the Plugin can determine the permissions to check with Keycloak statically or dynamically from the properties of the request.\n\nIf the `lazy_load_paths` attribute is set to `false`, the permissions are taken from the `permissions` attribute. Each entry in `permissions` needs to be formatted as expected by the token endpoint's `permission` parameter. See [Obtaining Permissions](https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions).\n\n:::note\n\nA valid permission can be a single resource or a resource paired with on or more scopes.\n\n:::\n\nIf the `lazy_load_paths` attribute is set to `true`, the request URI is resolved to one or more resources configured in Keycloak using the resource registration endpoint. The resolved resources are used as the permissions to check.\n\n:::note\n\nThis requires the Plugin to obtain a separate access token for itself from the token endpoint. So, make sure to set the `Service Accounts Enabled` option in the client settings in Keycloak.\n\nAlso make sure that the issued access token contains the `resource_access` claim with the `uma_protection` role to ensure that the Plugin is able to query resources through the Protection API.\n\n:::\n\n### Automatically mapping HTTP method to scope\n\nThe `http_method_as_scope` is often used together with `lazy_load_paths` but can also be used with a static permission list.\n\nIf the `http_method_as_scope` attribute is set to `true`, the Plugin maps the request's HTTP method to the scope with the same name. The scope is then added to every permission to check.\n\nIf the `lazy_load_paths` attribute is set to false, the Plugin adds the mapped scope to any of the static permissions configured in the `permissions` attribute—even if they contain on or more scopes already.\n\n### Generating a token using `password` grant\n\nTo generate a token using `password` grant, you can set the value of the `password_grant_token_generation_incoming_uri` attribute.\n\nIf the incoming URI matches the configured attribute and the request method is POST, a token is generated using the `token_endpoint`.\n\nYou also need to add `application/x-www-form-urlencoded` as `Content-Type` header and `username` and `password` as parameters.\n\nThe example below shows a request if the `password_grant_token_generation_incoming_uri` is `/api/token`:\n\n```shell\ncurl --location --request POST 'http://127.0.0.1:9080/api/token' \\\n--header 'Accept: application/json, text/plain, */*' \\\n--header 'Content-Type: application/x-www-form-urlencoded' \\\n--data-urlencode 'username=<User_Name>' \\\n--data-urlencode 'password=<Password>'\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the `authz-keycloak` Plugin on a specific Route. `${realm}` represents the realm name in Keycloak.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/get\",\n    \"plugins\": {\n        \"authz-keycloak\": {\n            \"token_endpoint\": \"http://127.0.0.1:8090/realms/${realm}/protocol/openid-connect/token\",\n            \"permissions\": [\"resource name#scope name\"],\n            \"client_id\": \"Client ID\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have enabled the Plugin on a Route you can use it.\n\nFirst, you have to get the JWT token from Keycloak:\n\n```shell\ncurl \"http://<YOUR_KEYCLOAK_HOST>/realms/<YOUR_REALM>/protocol/openid-connect/token\" \\\n  -d \"client_id=<YOUR_CLIENT_ID>\" \\\n  -d \"client_secret=<YOUR_CLIENT_SECRET>\" \\\n  -d \"username=<YOUR_USERNAME>\" \\\n  -d \"password=<YOUR_PASSWORD>\" \\\n  -d \"grant_type=password\"\n```\n\nYou should see a response similar to the following:\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoT3ludlBPY2d6Y3VWWnYtTU42bXZKMUczb0dOX2d6MFo3WFl6S2FSa1NBIn0.eyJleHAiOjE3MDMyOTAyNjAsImlhdCI6MTcwMzI4OTk2MCwianRpIjoiMjJhOGFmMzItNDM5Mi00Yzg3LThkM2UtZDkyNDVmZmNiYTNmIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjAyZWZlY2VlLTBmYTgtNDg1OS1iYmIwLTgyMGZmZDdjMWRmYSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtcXVpY2tzdGFydC1yZWFsbSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InF1aWNrc3RhcnQtdXNlciJ9.WNZQiLRleqCxw-JS-MHkqXnX_BPA9i6fyVHqF8l-L-2QxcqTAwbIp7AYKX-z90CG6EdRXOizAEkQytB32eVWXaRkLeTYCI7wIrT8XSVTJle4F88ohuBOjDfRR61yFh5k8FXXdAyRzcR7tIeE2YUFkRqw1gCT_VEsUuXPqm2wTKOmZ8fRBf4T-rP4-ZJwPkHAWc_nG21TmLOBCSulzYqoC6Lc-OvX5AHde9cfRuXx-r2HhSYs4cXtvX-ijA715MY634CQdedheoGca5yzPsJWrAlBbCruN2rdb4u5bDxKU62pJoJpmAsR7d5qYpYVA6AsANDxHLk2-W5F7I_IxqR0YQ\",\"expires_in\":300,\"refresh_expires_in\":1800,\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjN2IwYmY4NC1kYjk0LTQ5YzctYWIyZC01NmU3ZDc1MmRkNDkifQ.eyJleHAiOjE3MDMyOTE3NjAsImlhdCI6MTcwMzI4OTk2MCwianRpIjoiYzcyZjAzMzctYmZhNS00MWEzLTlhYjEtZmJlNGY0NmZjMDgxIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwic3ViIjoiMDJlZmVjZWUtMGZhOC00ODU5LWJiYjAtODIwZmZkN2MxZGZhIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYifQ.7AH7ppbVOlkYc9CoJ7kLSlDUkmFuNga28Amugn2t724\",\"token_type\":\"Bearer\",\"not-before-policy\":0,\"session_state\":\"5c23f5dd-a7fa-4e2b-9d14-62b5c626e546\",\"scope\":\"email profile\"}\n```\n\nNow you can make requests with the access token:\n\n```shell\ncurl http://127.0.0.1:9080/get -H 'Authorization: Bearer ${ACCESS_TOKEN}'\n```\n\nTo learn more about how you can integrate authorization policies into your API workflows you can checkout the unit test [authz-keycloak.t](https://github.com/apache/apisix/blob/master/t/plugin/authz-keycloak.t).\n\nRun the following Docker image and go to `http://localhost:8090` to view the associated policies for the unit tests.\n\n```bash\ndocker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -p 8090:8080 sshniro/keycloak-apisix\n```\n\nThe image below shows how the policies are configured in the Keycloak server:\n\n![Keycloak policy design](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/plugin/authz-keycloak.png)\n\n## Delete Plugin\n\nTo remove the `authz-keycloak` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/get\",\n    \"plugins\": {\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n\n## Plugin roadmap\n\n- Currently, the `authz-keycloak` Plugin requires you to define the resource name and the required scopes to enforce policies for a Route. Keycloak's official adapted (Java, Javascript) provides path matching by querying Keycloak paths dynamically and lazy loading the paths to identity resources. Upcoming releases of the Plugin will support this function.\n\n- To support reading scope and configurations from the Keycloak JSON file.\n"
  },
  {
    "path": "docs/en/latest/plugins/aws-lambda.md",
    "content": "---\ntitle: aws-lambda\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - AWS Lambda\n  - aws-lambda\ndescription: This document contains information about the Apache APISIX aws-lambda Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `aws-lambda` Plugin is used for integrating APISIX with [AWS Lambda](https://aws.amazon.com/lambda/) and [Amazon API Gateway](https://aws.amazon.com/api-gateway/) as a dynamic upstream to proxy all requests for a particular URI to the AWS Cloud.\n\nWhen enabled, the Plugin terminates the ongoing request to the configured URI and initiates a new request to the AWS Lambda Gateway URI on behalf of the client with configured authorization details, request headers, body and parameters (all three passed from the original request). It returns the response with headers, status code and the body to the client that initiated the request with APISIX.\n\nThis Plugin supports authorization via AWS API key and AWS IAM secrets. The Plugin implements [AWS Signature Version 4 signing](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html) for IAM secrets.\n\n## Attributes\n\n| Name                 | Type    | Required | Default | Valid values | Description                                                                                                                                |\n|----------------------|---------|----------|---------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------|\n| function_uri         | string  | True     |         |              | AWS API Gateway endpoint which triggers the lambda serverless function.                                                                    |\n| authorization        | object  | False    |         |              | Authorization credentials to access the cloud function.                                                                                    |\n| authorization.apikey | string  | False    |         |              | Generated API Key to authorize requests to the AWS Gateway endpoint.                                                                       |\n| authorization.iam    | object  | False    |         |              | Used for AWS IAM role based authorization performed via AWS v4 request signing. See [IAM authorization schema](#iam-authorization-schema). |\n| authorization.iam.accesskey  | string | True     |               | Generated access key ID from AWS IAM console.                                       |\n| authorization.iam.secretkey | string | True     |               | Generated access key secret from AWS IAM console.                                   |\n| authorization.iam.aws_region | string | False    | \"us-east-1\"   | AWS region where the request is being sent.                                         |\n| authorization.iam.service    | string | False    | \"execute-api\" | The service that is receiving the request. For Amazon API gateway APIs, it should be set to `execute-api`. For Lambda function, it should be set to `lambda`. |\n| timeout              | integer | False    | 3000    | [100,...]    | Proxy request timeout in milliseconds.                                                                                                     |\n| ssl_verify           | boolean | False    | true    | true/false   | When set to `true` performs SSL verification.                                                                                              |\n| keepalive            | boolean | False    | true    | true/false   | When set to `true` keeps the connection alive for reuse.                                                                                   |\n| keepalive_pool       | integer | False    | 5       | [1,...]      | Maximum number of requests that can be sent on this connection before closing it.                                                          |\n| keepalive_timeout    | integer | False    | 60000   | [1000,...]   | Time is ms for connection to remain idle without closing.                                                          |\n\n## Enable Plugin\n\nThe example below shows how you can configure the Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"aws-lambda\": {\n            \"function_uri\": \"https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com/default/test-apisix\",\n            \"authorization\": {\n                \"apikey\": \"<Generated API Key from aws console>\"\n            },\n            \"ssl_verify\":false\n        }\n    },\n    \"uri\": \"/aws\"\n}'\n```\n\nNow, any requests (HTTP/1.1, HTTPS, HTTP2) to the endpoint `/aws` will invoke the configured AWS Functions URI and the response will be proxied back to the client.\n\nIn the example below, AWS Lambda takes in name from the query and returns a message \"Hello $name\":\n\n```shell\ncurl -i -XGET localhost:9080/aws\\?name=APISIX\n```\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: application/json\nConnection: keep-alive\nDate: Sat, 27 Nov 2021 13:08:27 GMT\nx-amz-apigw-id: JdwXuEVxIAMFtKw=\nx-amzn-RequestId: 471289ab-d3b7-4819-9e1a-cb59cac611e0\nContent-Length: 16\nX-Amzn-Trace-Id: Root=1-61a22dca-600c552d1c05fec747fd6db0;Sampled=0\nServer: APISIX/2.10.2\n\n\"Hello, APISIX!\"\n```\n\nAnother example of a request where the client communicates with APISIX via HTTP/2 is shown below. Before proceeding, make sure you have configured `enable_http2: true` in your configuration file `config.yaml` for port `9081` and reloaded APISIX. See [`config.yaml.example`](https://github.com/apache/apisix/blob/master/conf/config.yaml.example) for the example configuration.\n\n```shell\ncurl -i -XGET --http2 --http2-prior-knowledge localhost:9081/aws\\?name=APISIX\n```\n\n```shell\nHTTP/2 200\ncontent-type: application/json\ncontent-length: 16\nx-amz-apigw-id: JdwulHHrIAMFoFg=\ndate: Sat, 27 Nov 2021 13:10:53 GMT\nx-amzn-trace-id: Root=1-61a22e5d-342eb64077dc9877644860dd;Sampled=0\nx-amzn-requestid: a2c2b799-ecc6-44ec-b586-38c0e3b11fe4\nserver: APISIX/2.10.2\n\n\"Hello, APISIX!\"\n```\n\nSimilarly, the function can be triggered via AWS API Gateway by using AWS IAM permissions for authorization. The Plugin includes authentication signatures in HTTP calls via AWS v4 request signing. The example below shows this method:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"aws-lambda\": {\n            \"function_uri\": \"https://ajycz5e0v9.execute-api.us-east-1.amazonaws.com/default/test-apisix\",\n            \"authorization\": {\n                \"iam\": {\n                    \"accesskey\": \"<access key>\",\n                    \"secretkey\": \"<access key secret>\"\n                }\n            },\n            \"ssl_verify\": false\n        }\n    },\n    \"uri\": \"/aws\"\n}'\n```\n\n:::note\n\nThis approach assumes that you have already an IAM user with programmatic access enabled with the required permissions (`AmazonAPIGatewayInvokeFullAccess`) to access the endpoint.\n\n:::\n\n### Configuring path forwarding\n\nThe `aws-lambda` Plugin also supports URL path forwarding while proxying requests to the AWS upstream. Extensions to the base request path gets appended to the `function_uri` specified in the Plugin configuration.\n\n:::info IMPORTANT\n\nThe `uri` configured on a Route must end with `*` for this feature to work properly. APISIX Routes are matched strictly and the `*` implies that any subpath to this URI would be matched to the same Route.\n\n:::\n\nThe example below configures this feature:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"aws-lambda\": {\n            \"function_uri\": \"https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com\",\n            \"authorization\": {\n                \"apikey\": \"<Generate API key>\"\n            },\n            \"ssl_verify\":false\n        }\n    },\n    \"uri\": \"/aws/*\"\n}'\n```\n\nNow, any requests to the path `aws/default/test-apisix` will invoke the AWS Lambda Function and the added path is forwarded:\n\n```shell\ncurl -i -XGET http://127.0.0.1:9080/aws/default/test-apisix\\?name\\=APISIX\n```\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: application/json\nConnection: keep-alive\nDate: Wed, 01 Dec 2021 14:23:27 GMT\nX-Amzn-Trace-Id: Root=1-61a7855f-0addc03e0cf54ddc683de505;Sampled=0\nx-amzn-RequestId: f5f4e197-9cdd-49f9-9b41-48f0d269885b\nContent-Length: 16\nx-amz-apigw-id: JrHG8GC4IAMFaGA=\nServer: APISIX/2.11.0\n\n\"Hello, APISIX!\"\n```\n\n## Delete Plugin\n\nTo remove the `aws-lambda` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/aws\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/azure-functions.md",
    "content": "---\ntitle: azure-functions\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Azure Functions\n  - azure-functions\ndescription: This document contains information about the Apache APISIX azure-functions Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `azure-functions` Plugin is used to integrate APISIX with [Azure Serverless Function](https://azure.microsoft.com/en-in/services/functions/) as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure Cloud.\n\nWhen enabled, the Plugin terminates the ongoing request to the configured URI and initiates a new request to Azure Functions on behalf of the client with configured authorization details, request headers, body and parameters (all three passed from the original request). It returns back the response with headers, status code and the body to the client that initiated the request with APISIX.\n\n## Attributes\n\n| Name                   | Type    | Required | Default | Valid values | Description                                                                                                                           |\n|------------------------|---------|----------|---------|--------------|---------------------------------------------------------------------------------------------------------------------------------------|\n| function_uri           | string  | True     |         |              | Azure FunctionS endpoint which triggers the serverless function. For example, `http://test-apisix.azurewebsites.net/api/HttpTrigger`. |\n| authorization          | object  | False    |         |              | Authorization credentials to access Azure Functions.                                                                                  |\n| authorization.apikey   | string  | False    |         |              | Generated API key to authorize requests.                                                                                              |\n| authorization.clientid | string  | False    |         |              | Azure AD client ID to authorize requests.                                                                                             |\n| timeout                | integer | False    | 3000    | [100,...]    | Proxy request timeout in milliseconds.                                                                                                |\n| ssl_verify             | boolean | False    | true    | true/false   | When set to `true` performs SSL verification.                                                                                         |\n| keepalive              | boolean | False    | true    | true/false   | When set to `true` keeps the connection alive for reuse.                                                                              |\n| keepalive_pool         | integer | False    | 5       | [1,...]      | Maximum number of requests that can be sent on this connection before closing it.                                                     |\n| keepalive_timeout      | integer | False    | 60000   | [1000,...]   | Time is ms for connection to remain idle without closing.                                                                             |\n\n## Metadata\n\n| Name            | Type   | Required | Default | Description                                                          |\n|-----------------|--------|----------|---------|----------------------------------------------------------------------|\n| master_apikey   | string | False    | \"\"      | API Key secret that could be used to access the Azure Functions URI. |\n| master_clientid | string | False    | \"\"      | Azure AD client ID that could be used to authorize the function URI. |\n\nMetadata can be used in the `azure-functions` Plugin for an authorization fallback. If there are no authorization details in the Plugin's attributes, the `master_apikey` and `master_clientid` configured in the metadata is used.\n\nThe relative order priority is as follows:\n\n1. Plugin looks for `x-functions-key` or `x-functions-clientid` key inside the header from the request to APISIX.\n2. If not found, the Plugin checks the configured attributes for authorization details. If present, it adds the respective header to the request sent to the Azure Functions.\n3. If authorization details are not configured in the Plugin's attributes, APISIX fetches the metadata and uses the master keys.\n\nTo add a new master API key, you can make a request to `/apisix/admin/plugin_metadata` with the required metadata as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/azure-functions -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"master_apikey\" : \"<Your Azure master access key>\"\n}'\n```\n\n## Enable Plugin\n\nYou can configure the Plugin on a specific Route as shown below assuming that you already have your Azure Functions up and running:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"azure-functions\": {\n            \"function_uri\": \"http://test-apisix.azurewebsites.net/api/HttpTrigger\",\n            \"authorization\": {\n                \"apikey\": \"<Generated API key to access the Azure-Function>\"\n            }\n        }\n    },\n    \"uri\": \"/azure\"\n}'\n```\n\nNow, any requests (HTTP/1.1, HTTPS, HTTP2) to the endpoint `/azure` will invoke the configured Azure Functions URI and the response will be proxied back to the client.\n\nIn the example below, the Azure Function takes in name from the query and returns a message \"Hello $name\":\n\n```shell\ncurl -i -XGET http://localhost:9080/azure\\?name=APISIX\n```\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nRequest-Context: appId=cid-v1:38aae829-293b-43c2-82c6-fa94aec0a071\nDate: Wed, 17 Nov 2021 14:46:55 GMT\nServer: APISIX/2.10.2\n\nHello, APISIX\n```\n\nAnother example of a request where the client communicates with APISIX via HTTP/2 is shown below. Before proceeding, make sure you have configured `enable_http2: true` in your configuration file `config.yaml` for port `9081` and reloaded APISIX. See [`config.yaml.example`](https://github.com/apache/apisix/blob/master/conf/config.yaml.example) for the example configuration.\n\n```shell\ncurl -i -XGET --http2 --http2-prior-knowledge http://localhost:9081/azure\\?name=APISIX\n```\n\n```shell\nHTTP/2 200\ncontent-type: text/plain; charset=utf-8\nrequest-context: appId=cid-v1:38aae829-293b-43c2-82c6-fa94aec0a071\ndate: Wed, 17 Nov 2021 14:54:07 GMT\nserver: APISIX/2.10.2\n\nHello, APISIX\n```\n\n### Configuring path forwarding\n\nThe `azure-functions` Plugins also supports URL path forwarding while proxying requests to the Azure Functions upstream. Extensions to the base request path gets appended to the `function_uri` specified in the Plugin configuration.\n\n:::info IMPORTANT\n\nThe `uri` configured on a Route must end with `*` for this feature to work properly. APISIX Routes are matched strictly and the `*` implies that any subpath to this URI would be matched to the same Route.\n\n:::\n\nThe example below configures this feature:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"azure-functions\": {\n            \"function_uri\": \"http://app-bisakh.azurewebsites.net/api\",\n            \"authorization\": {\n                \"apikey\": \"<Generated API key to access the Azure-Function>\"\n            }\n        }\n    },\n    \"uri\": \"/azure/*\"\n}'\n```\n\nNow, any requests to the path `azure/HttpTrigger1` will invoke the Azure Function and the added path is forwarded:\n\n```shell\ncurl -i -XGET http://127.0.0.1:9080/azure/HttpTrigger1\\?name\\=APISIX\\\n```\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Wed, 01 Dec 2021 14:19:53 GMT\nRequest-Context: appId=cid-v1:4d4b6221-07f1-4e1a-9ea0-b86a5d533a94\nServer: APISIX/2.11.0\n\nHello, APISIX\n```\n\n## Delete Plugin\n\nTo remove the `azure-functions` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/azure\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/basic-auth.md",
    "content": "---\ntitle: basic-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Basic Auth\n  - basic-auth\ndescription: The basic-auth Plugin adds basic access authentication for Consumers to authenticate themselves before being able to access Upstream resources.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/basic-auth\" />\n</head>\n\n## Description\n\nThe `basic-auth` Plugin adds [basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) for [Consumers](../terminology/consumer.md) to authenticate themselves before being able to access Upstream resources.\n\nWhen a Consumer is successfully authenticated, APISIX adds additional headers, such as `X-Consumer-Username`, `X-Credential-Indentifier`, and other Consumer custom headers if configured, to the request, before proxying it to the Upstream service. The Upstream service will be able to differentiate between consumers and implement additional logics as needed. If any of these values is not available, the corresponding header will not be added.\n\n## Attributes\n\nFor Consumer/Credentials:\n\n| Name     | Type   | Required | Description                                                                                                            |\n|----------|--------|----------|------------------------------------------------------------------------------------------------------------------------|\n| username | string | True     | Unique basic auth username for a consumer. |\n| password | string | True     | Basic auth password for the consumer.  |\n\nNOTE: `encrypt_fields = {\"password\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nFor Route:\n\n| Name             | Type    | Required | Default | Description                                                            |\n|------------------|---------|----------|---------|------------------------------------------------------------------------|\n| hide_credentials | boolean | False    | false   | If true, do not pass the authorization request header to Upstream services. |\n| anonymous_consumer | boolean | False    | false | Anonymous Consumer name. If configured, allow anonymous users to bypass the authentication. |\n| realm            | string  | False    | basic | The realm to include in the `WWW-Authenticate` header when authentication fails. |\n\n## Examples\n\nThe examples below demonstrate how you can work with the `basic-auth` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Implement Basic Authentication on Route\n\nThe following example demonstrates how to implement basic authentication on a Route.\n\nCreate a Consumer `johndoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\"\n  }'\n```\n\nCreate `basic-auth` Credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `basic-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"basic-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"basic-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n#### Verify with a Valid Key\n\nSend a request to with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Apikey\": \"john-key\",\n    \"Authorization\": \"Basic am9obmRvZTpqb2huLWtleQ==\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66e5107c-5bb3e24f2de5baf733aec1cc\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-basic-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n#### Verify with an Invalid Key\n\nSend a request with an invalid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:invalid-key\n```\n\nYou should see an `HTTP/1.1 401 Unauthorized` response with the following:\n\n```text\n{\"message\":\"Invalid user authorization\"}\n```\n\n#### Verify without a Key\n\nSend a request to without a key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should see an `HTTP/1.1 401 Unauthorized` response with the following:\n\n```text\n{\"message\":\"Missing authorization in request\"}\n```\n\n### Hide Authentication Information From Upstream\n\nThe following example demonstrates how to prevent the key from being sent to the Upstream services by configuring `hide_credentials`. In APISIX, the authentication key is forwarded to the Upstream services by default, which might lead to security risks in some circumstances and you should consider updating `hide_credentials`.\n\nCreate a Consumer `johndoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\"\n  }'\n```\n\nCreate `basic-auth` Credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\n#### Without Hiding Credentials\n\nCreate a Route with `basic-auth` and configure `hide_credentials` to `false`, which is the default configuration:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"id\": \"basic-auth-route\",\n  \"uri\": \"/anything\",\n  \"plugins\": {\n    \"basic-auth\": {\n      \"hide_credentials\": false\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\nSend a request with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Basic am9obmRvZTpqb2huLWtleQ==\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66cc2195-22bd5f401b13480e63c498c6\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-basic-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 43.228.226.23\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\nNote that the credentials are visible to the Upstream service in base64-encoded format.\n\n:::tip\n\nYou can also pass the base64-encoded credentials in the request using the `Authorization` header as such:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"Authorization: Basic am9obmRvZTpqb2huLWtleQ==\"\n```\n\n:::\n\n#### Hide Credentials\n\nUpdate the plugin's `hide_credentials` to `true`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/basic-auth-route\" -X PATCH \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"plugins\": {\n    \"basic-auth\": {\n      \"hide_credentials\": true\n    }\n  }\n}'\n```\n\nSend a request with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66cc21a7-4f6ac87946e25f325167d53a\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-basic-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 43.228.226.23\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\nNote that the credentials are no longer visible to the Upstream service.\n\n### Add Consumer Custom ID to Header\n\nThe following example demonstrates how you can attach a Consumer custom ID to authenticated request in the `Consumer-Custom-Id` header, which can be used to implement additional logics as needed.\n\nCreate a Consumer `johndoe` with a custom ID label:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\nCreate `basic-auth` Credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `basic-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"basic-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"basic-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send a request to the Route with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the `X-Consumer-Custom-Id` similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Basic am9obmRvZTpqb2huLWtleQ==\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66ea8d64-33df89052ae198a706e18c2a\",\n    \"X-Consumer-Username\": \"johndoe\",\n    \"X-Credential-Identifier\": \"cred-john-basic-auth\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\n### Rate Limit with Anonymous Consumer\n\nThe following example demonstrates how you can configure different rate limiting policies by regular and anonymous consumers, where the anonymous Consumer does not need to authenticate and has less quotas.\n\nCreate a regular Consumer `johndoe` and configure the `limit-count` Plugin to allow for a quota of 3 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate the `basic-auth` Credential for the Consumer `johndoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate an anonymous user `anonymous` and configure the `limit-count` Plugin to allow for a quota of 1 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate a Route and configure the `basic-auth` Plugin to accept anonymous Consumer `anonymous` from bypassing the authentication:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"basic-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send five consecutive requests with `john`'s key:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -u johndoe:john-key -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 5 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    3, 429:    2\n```\n\nSend five anonymous requests:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that only one request was successful:\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/batch-requests.md",
    "content": "---\ntitle: batch-requests\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Batch Requests\ndescription: This document contains information about the Apache APISIX batch-request Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nAfter enabling the batch-requests plugin, users can assemble multiple requests into one request and send them to the gateway. The gateway will parse the corresponding requests from the request body and then individually encapsulate them into separate requests. Instead of the user initiating multiple HTTP requests to the gateway, the gateway will use the HTTP pipeline method, go through several stages such as route matching, forwarding to the corresponding upstream, and then return the combined results to the client after merging.\n\n![batch-request](https://static.apiseven.com/uploads/2023/06/27/ATzEuOn4_batch-request.png)\n\nIn cases where the client needs to access multiple APIs, this will significantly improve performance.\n\n:::note\n\nThe request headers in the user’s original request (except for headers starting with “Content-”, such as “Content-Type”) will be assigned to each request in the HTTP pipeline. Therefore, to the gateway, these HTTP pipeline requests sent to itself are no different from external requests initiated directly by users. They can only access pre-configured routes and will undergo a complete authentication process, so there are no security issues.\n\nIf the request headers of the original request conflict with those configured in the plugin, the request headers configured in the plugin will take precedence (except for the real_ip_header specified in the configuration file).\n\n:::\n\n## Attributes\n\nNone.\n\n## API\n\nThis plugin adds `/apisix/batch-requests` as an endpoint.\n\n:::note\n\nYou may need to use the [public-api](public-api.md) plugin to expose this endpoint.\n\n:::\n\n## Enable Plugin\n\nYou can enable the `batch-requests` Plugin by adding it to your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - ...\n  - batch-requests\n```\n\n## Configuration\n\nBy default, the maximum body size that can be sent to `/apisix/batch-requests` can't be larger than 1 MiB. You can change this configuration of the Plugin through the endpoint `apisix/admin/plugin_metadata/batch-requests`:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/batch-requests -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"max_body_size\": 4194304\n}'\n```\n\n## Metadata\n\n| Name          | Type    | Required | Default | Valid values | Description                                |\n| ------------- | ------- | -------- | ------- | ------------ | ------------------------------------------ |\n| max_body_size | integer | True     | 1048576 | [1, ...]     | Maximum size of the request body in bytes. |\n\n## Request and response format\n\nThis plugin will create an API endpoint in APISIX to handle batch requests.\n\n### Request\n\n| Name     | Type                               | Required | Default | Description                   |\n| -------- |------------------------------------| -------- | ------- | ----------------------------- |\n| query    | object                             | False    |         | Query string for the request. |\n| headers  | object                             | False    |         | Headers for all the requests. |\n| timeout  | integer                            | False    | 30000   | Timeout in ms.                |\n| pipeline | array[[HttpRequest](#httprequest)] | True     |         | Details of the request.       |\n\n#### HttpRequest\n\n| Name       | Type    | Required | Default | Valid                                                                            | Description                                                                           |\n| ---------- | ------- | -------- | ------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| version    | string  | False    | 1.1     | [1.0, 1.1]                                                                       | HTTP version.                                                                         |\n| method     | string  | False    | GET     | [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\", \"CONNECT\", \"TRACE\"] | HTTP method.                                                                          |\n| query      | object  | False    |         |                                                                                  | Query string for the request. If set, overrides the value of the global query string. |\n| headers    | object  | False    |         |                                                                                  | Headers for the request. If set, overrides the value of the global query string.      |\n| path       | string  | True     |         |                                                                                  | Path of the HTTP request.                                                             |\n| body       | string  | False    |         |                                                                                  | Body of the HTTP request.                                                             |\n| ssl_verify | boolean | False    | false   |                                                                                  | Set to verify if the SSL certs matches the hostname.                                  |\n\n### Response\n\nThe response is an array of [HttpResponses](#httpresponse).\n\n#### HttpResponse\n\n| Name    | Type    | Description            |\n| ------- | ------- | ---------------------- |\n| status  | integer | HTTP status code.      |\n| reason  | string  | HTTP reason-phrase.    |\n| body    | string  | HTTP response body.    |\n| headers | object  | HTTP response headers. |\n\n## Specifying a custom URI\n\nYou can specify a custom URI with the [public-api](public-api.md) Plugin.\n\nYou can set the URI you want when creating the Route and change the configuration of the public-api Plugin:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/br -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/batch-requests\",\n    \"plugins\": {\n        \"public-api\": {\n            \"uri\": \"/apisix/batch-requests\"\n        }\n    }\n}'\n```\n\n## Example usage\n\nFirst, you need to setup a Route to the batch request API. We will use the [public-api](public-api.md) Plugin for this:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/br -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/batch-requests\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\nNow you can make a request to the batch request API (`/apisix/batch-requests`):\n\n```shell\ncurl --location --request POST 'http://127.0.0.1:9080/apisix/batch-requests' \\\n--header 'Content-Type: application/json' \\\n--data '{\n    \"headers\": {\n        \"Content-Type\": \"application/json\",\n        \"admin-jwt\":\"xxxx\"\n    },\n    \"timeout\": 500,\n    \"pipeline\": [\n        {\n            \"method\": \"POST\",\n            \"path\": \"/community.GiftSrv/GetGifts\",\n            \"body\": \"test\"\n        },\n        {\n            \"method\": \"POST\",\n            \"path\": \"/community.GiftSrv/GetGifts\",\n            \"body\": \"test2\"\n        }\n    ]\n}'\n```\n\nThis will give a response:\n\n```json\n[\n  {\n    \"status\": 200,\n    \"reason\": \"OK\",\n    \"body\": \"{\\\"ret\\\":500,\\\"msg\\\":\\\"error\\\",\\\"game_info\\\":null,\\\"gift\\\":[],\\\"to_gets\\\":0,\\\"get_all_msg\\\":\\\"\\\"}\",\n    \"headers\": {\n      \"Connection\": \"keep-alive\",\n      \"Date\": \"Sat, 11 Apr 2020 17:53:20 GMT\",\n      \"Content-Type\": \"application/json\",\n      \"Content-Length\": \"81\",\n      \"Server\": \"APISIX web server\"\n    }\n  },\n  {\n    \"status\": 200,\n    \"reason\": \"OK\",\n    \"body\": \"{\\\"ret\\\":500,\\\"msg\\\":\\\"error\\\",\\\"game_info\\\":null,\\\"gift\\\":[],\\\"to_gets\\\":0,\\\"get_all_msg\\\":\\\"\\\"}\",\n    \"headers\": {\n      \"Connection\": \"keep-alive\",\n      \"Date\": \"Sat, 11 Apr 2020 17:53:20 GMT\",\n      \"Content-Type\": \"application/json\",\n      \"Content-Length\": \"81\",\n      \"Server\": \"APISIX web server\"\n    }\n  }\n]\n```\n\n## Delete Plugin\n\nYou can remove `batch-requests` from your list of Plugins in your configuration file (`conf/config.yaml`).\n"
  },
  {
    "path": "docs/en/latest/plugins/body-transformer.md",
    "content": "---\ntitle: body-transformer\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - BODY TRANSFORMER\n  - body-transformer\ndescription: The body-transformer Plugin performs template-based transformations to transform the request and/or response bodies from one format to another, for example, from JSON to JSON, JSON to HTML, or XML to YAML.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/body-transformer\" />\n</head>\n\n## Description\n\nThe `body-transformer` Plugin performs template-based transformations to transform the request and/or response bodies from one format to another, for example, from JSON to JSON, JSON to HTML, or XML to YAML.\n\n## Attributes\n\n| Name          | Type    | Required | Default | Valid values | Description                                |\n| ------------- | ------- | -------- | ------- | ------------ | ------------------------------------------ |\n| `request`      | object       | False      | | | Request body transformation configuration.      |\n| `request.input_format`      | string       | False      | | [`xml`,`json`,`encoded`,`args`,`plain`,`multipart`] | Request body original media type. If unspecified, the value would be determined by the `Content-Type` header to apply the corresponding decoder. The `xml` option corresponds to `text/xml` media type. The `json` option corresponds to `application/json` media type. The `encoded` option corresponds to `application/x-www-form-urlencoded` media type. The `args` option corresponds to GET requests. The `plain` option corresponds to `text/plain` media type. The `multipart` option corresponds to `multipart/related` media type. If the media type is neither type, the value would be left unset and the transformation template will be directly applied.      |\n| `request.template`      | string       | True      | | | Request body transformation template. The template uses [lua-resty-template](https://github.com/bungle/lua-resty-template) syntax. See the [template syntax](https://github.com/bungle/lua-resty-template#template-syntax) for more details. You can also use auxiliary functions `_escape_json()` and `_escape_xml()` to escape special characters such as double quotes, `_body` to access request body, and `_ctx` to access context variables.    |\n| `request.template_is_base64`      | boolean       | False    | false | | Set to true if the template is base64 encoded.      |\n| `response`      | object       | False      | | | Response body transformation configuration.     |\n| `response.input_format`      | string       | False      | | [`xml`,`json`] | Response body original media type. If unspecified, the value would be determined by the `Content-Type` header to apply the corresponding decoder. If the media type is neither `xml` nor `json`, the value would be left unset and the transformation template will be directly applied.       |\n| `response.template`      | string       | True      | | | Response body transformation template.       |\n| `response.template_is_base64`      | boolean       | False     | false | | Set to true if the template is base64 encoded.       |\n\n## Examples\n\nThe examples below demonstrate how you can configure `body-transformer` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\nThe transformation template uses [lua-resty-template](https://github.com/bungle/lua-resty-template) syntax. See the [template syntax](https://github.com/bungle/lua-resty-template#template-syntax) to learn more.\n\nYou can also use auxiliary functions `_escape_json()` and `_escape_xml()` to escape special characters such as double quotes, `_body` to access request body, and `_ctx` to access context variables.\n\nIn all cases, you should ensure that the transformation template is a valid JSON string.\n\n### Transform between JSON and XML SOAP\n\nThe following example demonstrates how to transform the request body from JSON to XML and the response body from XML to JSON when working with a SOAP Upstream service.\n\nStart the sample SOAP service:\n\n```shell\ncd /tmp\ngit clone https://github.com/spring-guides/gs-soap-service.git\ncd gs-soap-service/complete\n./mvnw spring-boot:run\n```\n\nCreate the request and response transformation templates:\n\n```shell\nreq_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1' | awk '{$1=$1};1' | tr -d '\\r\\n'\n<?xml version=\"1.0\"?>\n<soap-env:Envelope xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <soap-env:Body>\n  <ns0:getCountryRequest xmlns:ns0=\"http://spring.io/guides/gs-producing-web-service\">\n   <ns0:name>{{_escape_xml(name)}}</ns0:name>\n  </ns0:getCountryRequest>\n </soap-env:Body>\n</soap-env:Envelope>\nEOF\n)\n\nrsp_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1' | awk '{$1=$1};1' | tr -d '\\r\\n'\n{% if Envelope.Body.Fault == nil then %}\n{\n  \"status\":\"{{_ctx.var.status}}\",\n  \"currency\":\"{{Envelope.Body.getCountryResponse.country.currency}}\",\n  \"population\":{{Envelope.Body.getCountryResponse.country.population}},\n  \"capital\":\"{{Envelope.Body.getCountryResponse.country.capital}}\",\n  \"name\":\"{{Envelope.Body.getCountryResponse.country.name}}\"\n}\n{% else %}\n{\n  \"message\":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},\n  \"code\":\"{{Envelope.Body.Fault.faultcode}}\"\n  {% if Envelope.Body.Fault.faultactor ~= nil then %}\n  , \"actor\":\"{{Envelope.Body.Fault.faultactor}}\"\n  {% end %}\n}\n{% end %}\nEOF\n)\n```\n\n`awk` and `tr` are used above to manipulate the template such that the template would be a valid JSON string.\n\nCreate a Route with `body-transformer` using the templates created previously. In the Plugin, set the request input format as JSON, the response input format as XML, and the `Content-Type` header to `text/xml` for the Upstream service to respond properly:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"methods\": [\"POST\"],\n    \"uri\": \"/ws\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"'\"$req_template\"'\",\n          \"input_format\": \"json\"\n        },\n        \"response\": {\n          \"template\": \"'\"$rsp_template\"'\",\n          \"input_format\": \"xml\"\n        }\n      },\n      \"proxy-rewrite\": {\n        \"headers\": {\n          \"set\": {\n            \"Content-Type\": \"text/xml\"\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"localhost:8080\": 1\n      }\n    }\n  }'\n```\n\n:::tip\n\nIf it is cumbersome to adjust complex text files to be valid transformation templates, you can use the base64 utility to encode the files, such as the following:\n\n```json\n\"body-transformer\": {\n  \"request\": {\n    \"template\": \"'\"$(base64 -w0 /path/to/request_template_file)\"'\"\n  },\n  \"response\": {\n    \"template\": \"'\"$(base64 -w0 /path/to/response_template_file)\"'\"\n  }\n}\n```\n\n:::\n\nSend a request with a valid JSON body:\n\n```shell\ncurl \"http://127.0.0.1:9080/ws\" -X POST -d '{\"name\": \"Spain\"}'\n```\n\nThe JSON body sent in the request will be transformed into XML before being forwarded to the Upstream SOAP service, and the response body will be transformed back from XML to JSON.\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"status\": \"200\",\n  \"currency\": \"EUR\",\n  \"population\": 46704314,\n  \"capital\": \"Madrid\",\n  \"name\": \"Spain\"\n}\n```\n\n### Modify Request Body\n\nThe following example demonstrates how to dynamically modify the request body.\n\nCreate a Route with `body-transformer`, in which the template appends the word `world` to the `name` and adds `10` to the `age` to set them as values to `foo` and `bar` respectively:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"{\\\"foo\\\":\\\"{{name .. \\\" world\\\"}}\\\",\\\"bar\\\":{{age+10}}}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"hello\",\"age\":20}' \\\n  -i\n```\n\nYou should see a response of the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"foo\\\":\\\"hello world\\\",\\\"bar\\\":30}\",\n  ...\n  \"json\": {\n    \"bar\": 30,\n    \"foo\": \"hello world\"\n  },\n  \"method\": \"POST\",\n  ...\n}\n```\n\n### Generate Request Body Using Variables\n\nThe following example demonstrates how to generate request body dynamically using the `ctx` context variables.\n\nCreate a Route with `body-transformer`, in which the template accesses the request argument using the [Nginx variable](https://nginx.org/en/docs/http/ngx_http_core_module.html) `arg_name`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"{\\\"foo\\\":\\\"{{_ctx.var.arg_name .. \\\" world\\\"}}\\\"}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route with `name` argument:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?name=hello\"\n```\n\nYou should see a response like this:\n\n```json\n{\n  \"args\": {\n    \"name\": \"hello\"\n  },\n  ...,\n  \"json\": {\n    \"foo\": \"hello world\"\n  },\n...\n}\n```\n\n### Transform Body from YAML to JSON\n\nThe following example demonstrates how to transform request body from YAML to JSON.\n\nCreate the request transformation template:\n\n```shell\nreq_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1'\n{%\n    local yaml = require(\"tinyyaml\")\n    local body = yaml.parse(_body)\n%}\n{\"foobar\":\"{{body.foobar.foo .. \" \" .. body.foobar.bar}}\"}\nEOF\n)\n```\n\nCreate a Route with `body-transformer` that uses the template:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"'\"$req_template\"'\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route with a YAML body:\n\n```shell\nbody='\nfoobar:\n  foo: hello\n  bar: world'\n\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -d \"$body\" \\\n  -H \"Content-Type: text/yaml\" \\\n  -i\n```\n\nYou should see a response similar to the following, which verifies that the YAML body was appropriately transformed to JSON:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"foobar\\\":\\\"hello world\\\"}\",\n  ...\n  \"json\": {\n    \"foobar\": \"hello world\"\n  },\n...\n}\n```\n\n### Transform Form URL Encoded Body to JSON\n\nThe following example demonstrates how to transform `form-urlencoded` body to JSON.\n\nCreate a Route with `body-transformer` which sets the `input_format` to `encoded` and configures a template that appends string `world` to the `name` input, add `10` to the `age` input:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"encoded\",\n          \"template\": \"{\\\"foo\\\":\\\"{{name .. \\\" world\\\"}}\\\",\\\"bar\\\":{{age+10}}}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route with an encoded body:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d 'name=hello&age=20'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"{\\\"foo\\\":\\\"hello world\\\",\\\"bar\\\":30}\": \"\"\n  },\n  \"headers\": {\n    ...\n  },\n  ...\n}\n```\n\n### Transform GET Request Query Parameter to Body\n\nThe following example demonstrates how to transform a GET request query parameter to request body. Note that this does not transform the HTTP method. To transform the method, see [`proxy-rewrite`](./proxy-rewrite.md).\n\nCreate a Route with `body-transformer`, which sets the `input_format` to `args` and configures a template that adds a message to the request:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"args\",\n          \"template\": \"{\\\"message\\\": \\\"hello {{name}}\\\"}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a GET request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything?name=john\"\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"message\\\": \\\"hello john\\\"}\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    ...\n  },\n  \"json\": {\n    \"message\": \"hello john\"\n  },\n  \"method\": \"GET\",\n  ...\n}\n```\n\n### Transform Plain Media Type\n\nThe following example demonstrates how to transform requests with `plain` media type.\n\nCreate a Route with `body-transformer`, which sets the `input_format` to `plain` and configures a template to remove `not` and a subsequent space from the body string:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"plain\",\n          \"template\": \"{\\\"message\\\": \\\"{* string.gsub(_body, \\\"not \\\", \\\"\\\") *}\\\"}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a POST request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -d 'not actually json' \\\n  -i\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"{\\\"message\\\": \\\"actually json\\\"}\": \"\"\n  },\n  \"headers\": {\n    ...\n  },\n  ...\n}\n```\n\n### Transform Multipart Media Type\n\nThe following example demonstrates how to transform requests with `multipart` media type.\n\nCreate a request transformation template which adds a `status` to the body based on the `age` provided in the request body:\n\n```shell\nreq_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1'\n{%\n  local core = require 'apisix.core'\n  local cjson = require 'cjson'\n\n  if tonumber(context.age) > 18 then\n      context._multipart:set_simple(\"status\", \"adult\")\n  else\n      context._multipart:set_simple(\"status\", \"minor\")\n  end\n\n  local body = context._multipart:tostring()\n%}{* body *}\nEOF\n)\n```\n\nCreate a Route with `body-transformer`, which sets the `input_format` to `multipart` and uses the previously created request template for transformation:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"multipart\",\n          \"template\": \"'\"$req_template\"'\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a multipart POST request to the Route:\n\n```shell\ncurl -X POST \\\n  -F \"name=john\" \\\n  -F \"age=10\" \\\n  \"http://127.0.0.1:9080/anything\"\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"age\": \"10\",\n    \"name\": \"john\",\n    \"status\": \"minor\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"361\",\n    \"Content-Type\": \"multipart/form-data; boundary=------------------------qtPjk4c8ZjmGOXNKzhqnOP\",\n    ...\n  },\n  ...\n}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/brotli.md",
    "content": "---\ntitle: brotli\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - brotli\ndescription: This document contains information about the Apache APISIX brotli Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `brotli` Plugin dynamically sets the behavior of [brotli in Nginx](https://github.com/google/ngx_brotli).\n\n## Prerequisites\n\nThis Plugin requires brotli shared libraries.\n\nThe example commands to build and install brotli shared libraries:\n\n``` shell\nwget https://github.com/google/brotli/archive/refs/tags/v1.1.0.zip\nunzip v1.1.0.zip\ncd brotli-1.1.0 && mkdir build && cd build\ncmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local/brotli ..\nsudo cmake --build . --config Release --target install\nsudo sh -c \"echo /usr/local/brotli/lib >> /etc/ld.so.conf.d/brotli.conf\"\nsudo ldconfig\n```\n\n:::caution\n\nIf the upstream is returning a compressed response, then the Brotli plugin won't be able to compress it.\n\n:::\n\n## Attributes\n\n| Name           | Type                 | Required | Default       | Valid values | Description                                                                             |\n|----------------|----------------------|----------|---------------|--------------|-----------------------------------------------------------------------------------------|\n| types          | array[string] or \"*\" | False    | [\"text/html\"] |              | Dynamically sets the `brotli_types` directive. Special value `\"*\"` matches any MIME type. |\n| min_length     | integer              | False    | 20            | >= 1         | Dynamically sets the `brotli_min_length` directive. |\n| comp_level     | integer              | False    | 6             | [0, 11]      | Dynamically sets the `brotli_comp_level` directive. |\n| mode           | integer              | False    | 0             | [0, 2]       | Dynamically sets the `brotli decompress mode`, more info in [RFC 7932](https://tools.ietf.org/html/rfc7932). |\n| lgwin          | integer              | False    | 19            | [0, 10-24]   | Dynamically sets the `brotli sliding window size`, `lgwin` is Base 2 logarithm of the sliding window size, set to `0` lets compressor decide over the optimal value, more info in [RFC 7932](https://tools.ietf.org/html/rfc7932). |\n| lgblock        | integer              | False    | 0             | [0, 16-24]   | Dynamically sets the `brotli input block size`, `lgblock` is Base 2 logarithm of the maximum input block size, set to `0` lets compressor decide over the optimal value, more info in [RFC 7932](https://tools.ietf.org/html/rfc7932). |\n| http_version   | number               | False    | 1.1           | 1.1, 1.0     | Like the `gzip_http_version` directive, sets the minimum HTTP version of a request required to compress a response. |\n| vary           | boolean              | False    | false         |              | Like the `gzip_vary` directive, enables or disables inserting the “Vary: Accept-Encoding” response header field. |\n\n## Enable Plugin\n\nThe example below enables the `brotli` Plugin on the specified Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/\",\n    \"plugins\": {\n        \"brotli\": {\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can make a request as shown below:\n\n```shell\ncurl http://127.0.0.1:9080/ -i -H \"Accept-Encoding: br\"\n```\n\n```\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Tue, 05 Dec 2023 03:06:49 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/3.6.0\nContent-Encoding: br\n\nWarning: Binary output can mess up your terminal. Use \"--output -\" to tell\nWarning: curl to output it to your terminal anyway, or consider \"--output\nWarning: <FILE>\" to save to a file.\n```\n\n## Delete Plugin\n\nTo remove the `brotli` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/cas-auth.md",
    "content": "---\ntitle: cas-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - CAS AUTH\n  - cas-auth\ndescription: This document contains information about the Apache APISIX cas-auth Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `cas-auth` Plugin can be used to access CAS (Central Authentication Service 2.0) IdP (Identity Provider)\nto do authentication, from the SP (service provider) perspective.\n\n## Attributes\n\n| Name      | Type | Required      | Description |\n| ----------- | ----------- | ----------- | ----------- |\n| `idp_uri`      | string       | True      | URI of IdP.       |\n| `cas_callback_uri`      | string       | True      | redirect uri used to callback the SP from IdP after login or logout.       |\n| `logout_uri`      | string       | True      | logout uri to trigger logout.       |\n\n## Enable Plugin\n\nYou can enable the Plugin on a specific Route as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/cas1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\", \"POST\"],\n    \"host\" : \"127.0.0.1\",\n    \"uri\": \"/anything/*\",\n    \"plugins\": {\n          \"cas-auth\": {\n              \"idp_uri\": \"http://127.0.0.1:8080/realms/test/protocol/cas\",\n              \"cas_callback_uri\": \"/anything/cas_callback\",\n              \"logout_uri\": \"/anything/logout\"\n          }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n\n```\n\n## Configuration description\n\nOnce you have enabled the Plugin, a new user visiting this Route would first be processed by the `cas-auth` Plugin.\nIf no login session exists, the user would be redirected to the login page of `idp_uri`.\n\nAfter successfully logging in from IdP, IdP will redirect this user to the `cas_callback_uri` with\nGET parameters CAS ticket specified. If the ticket gets verified, the login session would be created.\n\nThis process is only done once and subsequent requests are left uninterrupted.\nOnce this is done, the user is redirected to the original URL they wanted to visit.\n\nLater, the user could visit `logout_uri` to start logout process. The user would be redirected to `idp_uri` to do logout.\n\nNote that, `cas_callback_uri` and `logout_uri` should be\neither full qualified address (e.g. `http://127.0.0.1:9080/anything/logout`),\nor path only (e.g. `/anything/logout`), but it is recommended to be path only to keep consistent.\n\nThese uris need to be captured by the route where the current APISIX is located.\nFor example, if the `uri` of the current route is `/api/v1/*`, `cas_callback_uri` can be filled in as `/api/v1/cas_callback`.\n\n## Delete Plugin\n\nTo remove the `cas-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/cas1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\", \"POST\"],\n    \"uri\": \"/anything/*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/chaitin-waf.md",
    "content": "---\ntitle: chaitin-waf\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - WAF\ndescription: The chaitin-waf Plugin integrates with Chaitin WAF (SafeLine) to detect and block web threats, strengthening API security and protecting user data.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/chaitin-waf\" />\n</head>\n\n## Description\n\nThe `chaitin-waf` Plugin integrates with the Chaitin WAF (SafeLine) service to provide advanced detection and prevention of web-based threats, enhancing application security and protecting sensitive user data.\n\n## Response Headers\n\nThe Plugin can add the following response headers, depending on the configuration of `append_waf_resp_header` and `append_waf_debug_header`:\n\n| Header | Description |\n|--------|-------------|\n| `X-APISIX-CHAITIN-WAF` | Indicates whether APISIX forwarded the request to the WAF server.<br />• `yes`: Request was forwarded to the WAF server.<br />• `no`: Request was not forwarded to the WAF server.<br />• `unhealthy`: Request matches the configured rules, but no WAF service is available.<br />• `err`: An error occurred during Plugin execution. The `X-APISIX-CHAITIN-WAF-ERROR` header is also included with details.<br />• `waf-err`: Error while interacting with the WAF server. The `X-APISIX-CHAITIN-WAF-ERROR` header is also included with details.<br />• `timeout`: Request to the WAF server timed out. |\n| `X-APISIX-CHAITIN-WAF-TIME` | Round-trip time (RTT) in milliseconds for the request to the Chaitin WAF server, including both network latency and WAF server processing. |\n| `X-APISIX-CHAITIN-WAF-STATUS` | Status code returned to APISIX by the WAF server. |\n| `X-APISIX-CHAITIN-WAF-ACTION` | Action returned to APISIX by the WAF server.<br />• `pass`: Request was allowed by the WAF service.<br />• `reject`: Request was blocked by the WAF service. |\n| `X-APISIX-CHAITIN-WAF-ERROR` | Debug header. Contains WAF error message. |\n| `X-APISIX-CHAITIN-WAF-SERVER` | Debug header. Indicates which WAF server was selected. |\n\n## Attributes\n\n| Name                     | Type          | Required | Default | Valid values             | Description |\n|--------------------------|---------------|----------|---------|--------------------------|-------------|\n| mode                     | string        | false    | block   | `off`, `monitor`, `block`| Mode to determine how the Plugin behaves for matched requests. In `off` mode, WAF checks are skipped. In `monitor` mode, requests with potential threats are logged but not blocked. In `block` mode, requests with threats are blocked as determined by the WAF service. |\n| match                    | array[object] | false    |         |                          | An array of matching rules. The Plugin uses these rules to decide whether to perform a WAF check on a request. If the list is empty, all requests are processed. |\n| match.vars               | array[array]  | false    |         |                          | An array of one or more matching conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) to conditionally execute the plugin. |\n| append_waf_resp_header   | boolean       | false    | true    |                          | If true, add response headers `X-APISIX-CHAITIN-WAF`, `X-APISIX-CHAITIN-WAF-TIME`, `X-APISIX-CHAITIN-WAF-ACTION`, and `X-APISIX-CHAITIN-WAF-STATUS`. |\n| append_waf_debug_header  | boolean       | false    | false   |                          | If true, add debugging headers `X-APISIX-CHAITIN-WAF-ERROR` and `X-APISIX-CHAITIN-WAF-SERVER` to the response. Effective only when `append_waf_resp_header` is `true`. |\n| config                   | object        | false    |         |                          | Chaitin WAF service configurations. These settings override the corresponding metadata defaults when specified. |\n| config.connect_timeout   | integer       | false    | 1000    |                          | The connection timeout to the WAF service, in milliseconds. |\n| config.send_timeout      | integer       | false    | 1000    |                          | The sending timeout for transmitting data to the WAF service, in milliseconds. |\n| config.read_timeout      | integer       | false    | 1000    |                          | The reading timeout for receiving data from the WAF service, in milliseconds. |\n| config.req_body_size     | integer       | false    | 1024    |                          | The maximum allowed request body size, in KB. |\n| config.keepalive_size    | integer       | false    | 256     |                          | The maximum number of idle connections to the WAF detection service that can be maintained concurrently. |\n| config.keepalive_timeout | integer       | false    | 60000   |                          | The idle connection timeout for the WAF service, in milliseconds. |\n| config.real_client_ip    | boolean       | false    | true    |                          | If true, the client IP is obtained from the `X-Forwarded-For` header. If false, the Plugin uses the client IP from the connection. |\n\n## Plugin Metadata\n\n| Name                     | Type          | Required | Default | Valid values | Description |\n|--------------------------|---------------|----------|---------|--------------|-------------|\n| nodes                    | array[object] | True     |         |              | An array of addresses for the Chaitin WAF service. |\n| nodes.host               | string        | True     |         |              | Address of Chaitin WAF service. Supports IPv4, IPv6, Unix Socket, etc. |\n| nodes.port               | integer       | False    | 80      |              | Port of Chaitin WAF service. |\n| mode                     | string        | False    |         |    block     | Mode to determine how the Plugin behaves for matched requests. In `off` mode, WAF checks are skipped. In `monitor` mode, requests with potential threats are logged but not blocked. In `block` mode, requests with threats are blocked as determined by the WAF service. |\n| config                   | object        | False    |         |              | Chaitin WAF service configurations. |\n| config.connect_timeout   | integer       | False    | 1000    |              | The connection timeout to the WAF service, in milliseconds. |\n| config.send_timeout      | integer       | False    | 1000    |              | The sending timeout for transmitting data to the WAF service, in milliseconds. |\n| config.read_timeout      | integer       | False    | 1000    |              | The reading timeout for receiving data from the WAF service, in milliseconds. |\n| config.req_body_size     | integer       | False    | 1024    |              | The maximum allowed request body size, in KB. |\n| config.keepalive_size    | integer       | False    | 256     |              | The maximum number of idle connections to the WAF detection service that can be maintained concurrently. |\n| config.keepalive_timeout | integer       | False    | 60000   |              | The idle connection timeout for the WAF service, in milliseconds. |\n| config.real_client_ip    | boolean       | False    | true    |              | If true, the client IP is obtained from the `X-Forwarded-For` header. If false, the Plugin uses the client IP from the connection. |\n\n## Examples\n\nThe examples below demonstrate how you can configure chaitin-waf Plugin for different scenarios.\n\nBefore proceeding, make sure you have installed [Chaitin WAF (SafeLine)](https://docs.waf.chaitin.com/en/GetStarted/Deploy).\n\n:::note\nOnly `X-Forwarded-*` headers sent from addresses in the `apisix.trusted_addresses` configuration (supports IP and CIDR) will be trusted and passed to plugins or upstream. If `apisix.trusted_addresses` is not configured or the IP is not within the configured address range, all `X-Forwarded-*` headers will be overridden with trusted values.\n:::\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Block Malicious Requests on a Route\n\nThe following example demonstrates how to integrate with Chaitin WAF to protect traffic on a route, rejecting malicious requests immediately.\n\nConfigure the Chaitin WAF connection details using Plugin Metadata (update the address accordingly):\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/chaitin-waf\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"nodes\": [\n      {\n        \"host\": \"172.22.222.5\",\n        \"port\": 8000\n      }\n    ]\n  }'\n```\n\nCreate a Route and enable `chaitin-waf` on the Route to block requests identified to be malicious:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"chaitin-waf-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"chaitin-waf\": {\n        \"mode\": \"block\",\n        \"append_waf_resp_header\": true,\n        \"append_waf_debug_header\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a standard request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nSend a request with SQL injection to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -d 'a=1 and 1=1'\n```\n\nYou should see an `HTTP/1.1 403 Forbidden` response similar to the following:\n\n```text\n...\nX-APISIX-CHAITIN-WAF-STATUS: 403\nX-APISIX-CHAITIN-WAF-ACTION: reject\nX-APISIX-CHAITIN-WAF-SERVER: 172.22.222.5\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-TIME: 3\n...\n\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"276be6457d8447a4bf1f792501dfba6c\"}\n```\n\n### Monitor Requests for Malicious Intent\n\nThis example shows how to integrate with Chaitin WAF to monitor all routes with `chaitin-waf` without rejection, and to reject potentially malicious requests on a specific route.\n\nConfigure the Chaitin WAF connection details using Plugin Metadata (update the address accordingly) and configure the mode:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/chaitin-waf\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"nodes\": [\n      {\n        \"host\": \"172.22.222.5\",\n        \"port\": 8000\n      }\n    ],\n    \"mode\": \"monitor\"\n  }'\n```\n\nCreate a Route and enable `chaitin-waf` without any configuration on the Route:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"chaitin-waf-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"chaitin-waf\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a standard request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nSend a request with SQL injection to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -d 'a=1 and 1=1'\n```\n\nYou should also receive an `HTTP/1.1 200 OK` response as the request is not blocked in the `monitor` mode, but observe the following in the log entry:\n\n```text\n2025/09/09 11:44:08 [warn] 115#115: *31683 [lua] chaitin-waf.lua:385: do_access(): chaitin-waf monitor mode: request would have been rejected, event_id: 49bed20603e242f9be5ba6f1744bba4b, client: 172.20.0.1, server: _, request: \"POST /anything HTTP/1.1\", host: \"127.0.0.1:9080\"\n```\n\nIf you explicitly configure the `mode` on a route, it will take precedence over the configuration in the Plugin Metadata. For instance, if you create a Route like this:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"chaitin-waf-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"chaitin-waf\": {\n        \"mode\": \"block\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a standard request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nSend a request with SQL injection to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -d 'a=1 and 1=1'\n```\n\nYou should see an `HTTP/1.1 403 Forbidden` response similar to the following:\n\n```text\n...\nX-APISIX-CHAITIN-WAF-STATUS: 403\nX-APISIX-CHAITIN-WAF-ACTION: reject\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-TIME: 3\n...\n\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"c3eb25eaa7ae4c0d82eb8ceebf3600d0\"}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/clickhouse-logger.md",
    "content": "---\ntitle: clickhouse-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ClickHouse Logger\ndescription: This document contains information about the Apache APISIX clickhouse-logger Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `clickhouse-logger` Plugin is used to push logs to [ClickHouse](https://clickhouse.com/) database.\n\n## Attributes\n\n| Name          | Type    | Required | Default             | Valid values | Description                                                    |\n|---------------|---------|----------|---------------------|--------------|----------------------------------------------------------------|\n| endpoint_addr | Deprecated   | True     |                |              | Use `endpoint_addrs` instead. ClickHouse endpoints.            |\n| endpoint_addrs | array  | True     |                     |              | ClickHouse endpoints.                                          |\n| database      | string  | True     |                     |              | Name of the database to store the logs.                        |\n| logtable      | string  | True     |                     |              | Table name to store the logs.                                  |\n| user          | string  | True     |                     |              | ClickHouse username.                                           |\n| password      | string  | True     |                     |              | ClickHouse password.                                           |\n| timeout       | integer | False    | 3                   | [1,...]      | Time to keep the connection alive for after sending a request. |\n| name          | string  | False    | \"clickhouse logger\" |              | Unique identifier for the logger. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`.                              |\n| ssl_verify    | boolean | False    | true                | [true,false] | When set to `true`, verifies SSL.                              |\n| log_format       | object  | False    |  |              | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| include_req_body       | boolean | False    | false          | [false, true]         | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitations.                                                                                                                                                                                 |\n| include_req_body_expr  | array   | False    |                |                       | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                          |\n| max_req_body_bytes | integer | False | 524288 | >=1 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean | False    | false          | [false, true]         | When set to `true` includes the response body in the log.                                                                                                                                                                                                                                                                                        |\n| include_resp_body_expr | array   | False    |                |                       | Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                        |\n| max_resp_body_bytes | integer | False | 524288 | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nNOTE: `encrypt_fields = {\"password\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nNOTE: In addition, you can use Environment Variables or APISIX secret to store and reference plugin attributes. APISIX currently supports storing secrets in two ways - [Environment Variables and HashiCorp Vault](../terminology/secret.md).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n    \"response\": {\n        \"status\": 200,\n        \"size\": 118,\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"server\": \"APISIX/3.7.0\",\n            \"content-length\": \"12\"\n        }\n    },\n    \"client_ip\": \"127.0.0.1\",\n    \"upstream_latency\": 3,\n    \"apisix_latency\": 98.999998092651,\n    \"upstream\": \"127.0.0.1:1982\",\n    \"latency\": 101.99999809265,\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"route_id\": \"1\",\n    \"start_time\": 1704507612177,\n    \"service_id\": \"\",\n    \"request\": {\n        \"method\": \"POST\",\n        \"querystring\": {\n            \"foo\": \"unknown\"\n        },\n        \"headers\": {\n            \"host\": \"localhost\",\n            \"connection\": \"close\",\n            \"content-length\": \"18\"\n        },\n        \"size\": 110,\n        \"uri\": \"/hello?foo=unknown\",\n        \"url\": \"http://localhost:1984/hello?foo=unknown\"\n    }\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `clickhouse-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/clickhouse-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\"\n    }\n}'\n```\n\nYou can use the clickhouse docker image to create a container like so:\n\n```shell\ndocker run -d -p 8123:8123 -p 9000:9000 -p 9009:9009 --name some-clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server\n```\n\nThen create a table in your ClickHouse database to store the logs.\n\n```shell\ncurl -X POST 'http://localhost:8123/' \\\n--data-binary 'CREATE TABLE default.test (host String, client_ip String, route_id String, service_id String, `@timestamp` String, PRIMARY KEY(`@timestamp`)) ENGINE = MergeTree()' --user default:\n```\n\n## Enable Plugin\n\nIf multiple endpoints are configured, they will be written randomly.\nThe example below shows how you can enable the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"clickhouse-logger\": {\n                \"user\": \"default\",\n                \"password\": \"\",\n                \"database\": \"default\",\n                \"logtable\": \"test\",\n                \"endpoint_addrs\": [\"http://127.0.0.1:8123\"]\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your ClickHouse database:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\nNow, if you check for the rows in the table, you will get the following output:\n\n```shell\ncurl 'http://localhost:8123/?query=select%20*%20from%20default.test'\n127.0.0.1\t127.0.0.1\t1\t\t2023-05-08T19:15:53+05:30\n```\n\n## Delete Plugin\n\nTo remove the `clickhouse-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/client-control.md",
    "content": "---\ntitle: client-control\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Client Control\ndescription: This document describes the Apache APISIX client-control Plugin, you can use it to control NGINX behavior to handle a client request dynamically.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `client-control` Plugin can be used to dynamically control the behavior of NGINX to handle a client request, by setting the max size of the request body.\n\n:::info IMPORTANT\n\nThis Plugin requires APISIX to run on APISIX-Runtime. See [apisix-build-tools](https://github.com/api7/apisix-build-tools) for more info.\n\n:::\n\n## Attributes\n\n| Name          | Type    | Required | Valid values | Description                                                                                                                          |\n| ------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |\n| max_body_size | integer | False    | [0,...]      | Set the maximum limit for the client request body and dynamically adjust the size of [`client_max_body_size`](https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size), measured in bytes. If you set the `max_body_size` to 0, then the size of the client's request body will not be checked. |\n\n## Enable Plugin\n\nThe example below enables the Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"client-control\": {\n            \"max_body_size\" : 1\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nNow since you have configured the `max_body_size` to `1` above, you will get the following message when you make a request:\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html -d '123'\n```\n\n```shell\nHTTP/1.1 413 Request Entity Too Large\n...\n<html>\n<head><title>413 Request Entity Too Large</title></head>\n<body>\n<center><h1>413 Request Entity Too Large</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n## Delete Plugin\n\nTo remove the `client-control` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload, and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/consumer-restriction.md",
    "content": "---\ntitle: consumer-restriction\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Consumer restriction\ndescription: The Consumer Restriction Plugin allows users to configure access restrictions on Consumer, Route, Service, or Consumer Group.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `consumer-restriction` Plugin allows users to configure access restrictions on Consumer, Route, Service, or Consumer Group.\n\n## Attributes\n\n| Name                       | Type          | Required | Default       | Valid values                                                 | Description                                                  |\n| -------------------------- | ------------- | -------- | ------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| type                       | string        | False    | consumer_name | [\"consumer_name\", \"consumer_group_id\", \"service_id\", \"route_id\"] | Type of object to base the restriction on.                   |\n| whitelist                  | array[string] | True     |               |                                                              | List of objects to whitelist. Has a higher priority than `allowed_by_methods`. |\n| blacklist                  | array[string] | True     |               |                                                              | List of objects to blacklist. Has a higher priority than `whitelist`. |\n| rejected_code              | integer       | False    | 403           | [200,...]                                                    | HTTP status code returned when the request is rejected.      |\n| rejected_msg               | string        | False    |               |                                                              | Message returned when the request is rejected.               |\n| allowed_by_methods         | array[object] | False    |               |                                                              | List of allowed configurations for Consumer settings, including a username of the Consumer and a list of allowed HTTP methods. |\n| allowed_by_methods.user    | string        | False    |               |                                                              | A username for a Consumer.                                   |\n| allowed_by_methods.methods | array[string] | False    |               | [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\", \"CONNECT\", \"TRACE\", \"PURGE\"] | List of allowed HTTP methods for a Consumer.                 |\n\n:::note\n\nThe different values in the `type` attribute have these meanings:\n\n- `consumer_name`: Username of the Consumer to restrict access to a Route or a Service.\n- `consumer_group_id`: ID of the Consumer Group to restrict access to a Route or a Service.\n- `service_id`: ID of the Service to restrict access from a Consumer. Need to be used with an Authentication Plugin.\n- `route_id`: ID of the Route to restrict access from a Consumer.\n\n:::\n\n## Example usage\n\n### Restricting by `consumer_name`\n\nThe example below shows how you can use the `consumer-restriction` Plugin on a Route to restrict specific consumers.\n\nYou can first create two consumers `jack1` and `jack2`:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"username\": \"jack1\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\":\"jack2019\",\n            \"password\": \"123456\"\n        }\n    }\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"username\": \"jack2\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\":\"jack2020\",\n            \"password\": \"123456\"\n        }\n    }\n}'\n```\n\nNext, you can configure the Plugin to the Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {},\n        \"consumer-restriction\": {\n            \"whitelist\": [\n                \"jack1\"\n            ]\n        }\n    }\n}'\n```\n\nNow, this configuration will only allow `jack1` to access your Route:\n\n```shell\ncurl -u jack2019:123456 http://127.0.0.1:9080/index.html\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\nAnd requests from `jack2` are blocked:\n\n```shell\ncurl -u jack2020:123456 http://127.0.0.1:9080/index.html -i\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"The consumer_name is forbidden.\"}\n```\n\n### Restricting by `allowed_by_methods`\n\nThe example below configures the Plugin to a Route to restrict `jack1` to only make `POST` requests:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {},\n        \"consumer-restriction\": {\n            \"allowed_by_methods\":[{\n                \"user\": \"jack1\",\n                \"methods\": [\"POST\"]\n            }]\n        }\n    }\n}'\n```\n\nNow if `jack1` makes a `GET` request, the access is restricted:\n\n```shell\ncurl -u jack2019:123456 http://127.0.0.1:9080/index.html\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"The consumer_name is forbidden.\"}\n```\n\nTo also allow `GET` requests, you can update the Plugin configuration and it would be reloaded automatically:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {},\n        \"consumer-restriction\": {\n            \"allowed_by_methods\":[{\n                \"user\": \"jack1\",\n                \"methods\": [\"POST\",\"GET\"]\n            }]\n        }\n    }\n}'\n```\n\nNow, if a `GET` request is made:\n\n```shell\ncurl -u jack2019:123456 http://127.0.0.1:9080/index.html\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n### Restricting by `service_id`\n\nTo restrict a Consumer from accessing a Service, you also need to use an Authentication Plugin. The example below uses the [key-auth](./key-auth.md) Plugin.\n\nFirst, you can create two services:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/services/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"desc\": \"new service 001\"\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/services/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"desc\": \"new service 002\"\n}'\n```\n\nThen configure the `consumer-restriction` Plugin on the Consumer with the `key-auth` Plugin and the `service_id` to whitelist.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"new_consumer\",\n    \"plugins\": {\n    \"key-auth\": {\n        \"key\": \"auth-jack\"\n    },\n    \"consumer-restriction\": {\n           \"type\": \"service_id\",\n            \"whitelist\": [\n                \"1\"\n            ],\n            \"rejected_code\": 403\n        }\n    }\n}'\n```\n\nFinally, you can configure the `key-auth` Plugin and bind the service to the Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"service_id\": 1,\n    \"plugins\": {\n         \"key-auth\": {\n        }\n    }\n}'\n```\n\nNow, if you test the Route, you should be able to access the Service:\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'apikey: auth-jack' -i\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n```\n\nNow, if the Route is configured to the Service with `service_id` `2`:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"service_id\": 2,\n    \"plugins\": {\n         \"key-auth\": {\n        }\n    }\n}'\n```\n\nSince the Service is not in the whitelist, it cannot be accessed:\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'apikey: auth-jack' -i\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"The service_id is forbidden.\"}\n```\n\n## Delete Plugin\n\nTo remove the `consumer-restriction` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {}\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/cors.md",
    "content": "---\ntitle: cors\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - CORS\ndescription: This document contains information about the Apache APISIX cors Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `cors` Plugins lets you enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) easily.\n\n## Attributes\n\n### CORS attributes\n\n| Name                      | Type    | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                        |\n|---------------------------|---------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| allow_origins             | string  | False    | \"*\"     | Origins to allow CORS. Use the `scheme://host:port` format. For example, `https://somedomain.com:8081`. If you have multiple origins, use a `,` to list them. If `allow_credential` is set to `false`, you can enable CORS for all origins by using `*`. If `allow_credential` is set to `true`, you can forcefully allow CORS on all origins by using `**` but it will pose some security issues. |\n| allow_methods             | string  | False    | \"*\"     | Request methods to enable CORS on. For example `GET`, `POST`. Use `,` to add multiple methods. If `allow_credential` is set to `false`, you can enable CORS for all methods by using `*`. If `allow_credential` is set to `true`, you can forcefully allow CORS on all methods by using `**` but it will pose some security issues.                                                                |\n| allow_headers             | string  | False    | \"*\"     | Headers in the request allowed when accessing a cross-origin resource. Use `,` to add multiple headers. If `allow_credential` is set to `false`, you can enable CORS for all request headers by using `*`. If `allow_credential` is set to `true`, you can forcefully allow CORS on all request headers by using `**` but it will pose some security issues.                                       |\n| expose_headers            | string  | False    |         | Headers in the response allowed when accessing a cross-origin resource. Use `,` to add multiple headers. If `allow_credential` is set to `false`, you can enable CORS for all response headers by using `*`. If not specified, the plugin will not modify the `Access-Control-Expose-Headers header`. See [Access-Control-Expose-Headers - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) for more details.  |\n| max_age                   | integer | False    | 5       | Maximum time in seconds the result is cached. If the time is within this limit, the browser will check the cached result. Set to `-1` to disable caching. Note that the maximum value is browser dependent. See [Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#Directives) for more details.                                            |\n| allow_credential          | boolean | False    | false   | When set to `true`, allows requests to include credentials like cookies. According to CORS specification, if you set this to `true`, you cannot use '*' to allow all for the other attributes.                                                                                                                                                                                                     |\n| allow_origins_by_regex    | array   | False    | nil     | Regex to match origins that allow CORS. For example, `[\".*\\.test.com$\"]` can match all subdomains of `test.com`. When set to specified range, only domains in this range will be allowed, no matter what `allow_origins` is.                                                                                                                                                                   |\n| allow_origins_by_metadata | array   | False    | nil     | Origins to enable CORS referenced from `allow_origins` set in the Plugin metadata. For example, if `\"allow_origins\": {\"EXAMPLE\": \"https://example.com\"}` is set in the Plugin metadata, then `[\"EXAMPLE\"]` can be used to allow CORS on the origin `https://example.com`.                                                                                                                          |\n\n:::info IMPORTANT\n\n1. The `allow_credential` attribute is sensitive and must be used carefully. If set to `true` the default value `*` of the other attributes will be invalid and they should be specified explicitly.\n2. When using `**` you are vulnerable to security risks like CSRF. Make sure that this meets your security levels before using it.\n\n:::\n\n### Resource Timing attributes\n\n| Name                      | Type    | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                        |\n|---------------------------|---------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| timing_allow_origins             | string  | False    | nil     | Origin to allow to access the resource timing information. See [Timing-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin). Use the `scheme://host:port` format. For example, `https://somedomain.com:8081`. If you have multiple origins, use a `,` to list them. |\n| timing_allow_origins_by_regex    | array   | False    | nil     | Regex to match with origin for enabling access to the resource timing information. For example, `[\".*\\.test.com\"]` can match all subdomain of `test.com`. When set to specified range, only domains in this range will be allowed, no matter what `timing_allow_origins` is. |\n\n:::note\n\nThe Timing-Allow-Origin header is defined in the Resource Timing API, but it is related to the CORS concept.\n\nSuppose you have 2 domains, `domain-A.com` and `domain-B.com`.\nYou are on a page on `domain-A.com`, you have an XHR call to a resource on `domain-B.com` and you need its timing information.\nYou can allow the browser to show this timing information only if you have cross-origin permissions on `domain-B.com`.\nSo, you have to set the CORS headers first, then access the `domain-B.com` URL, and if you set [Timing-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin), the browser will show the requested timing information.\n\n:::\n\n## Metadata\n\n| Name          | Type   | Required | Description                                                                                                                                                                                             |\n|---------------|--------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| allow_origins | object | False    | A map with origin reference and allowed origins. The keys in the map are used in the attribute `allow_origins_by_metadata` and the value are equivalent to the `allow_origins` attribute of the Plugin. |\n\n## Enable Plugin\n\nYou can enable the Plugin on a specific Route or Service:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"cors\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nAfter enabling the Plugin, you can make a request to the server and see the CORS headers returned:\n\n```shell\ncurl http://127.0.0.1:9080/hello -v\n```\n\n```shell\n...\n< Server: APISIX web server\n< Access-Control-Allow-Origin: *\n< Access-Control-Allow-Methods: *\n< Access-Control-Allow-Headers: *\n< Access-Control-Max-Age: 5\n...\n```\n\n## Delete Plugin\n\nTo remove the `cors` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/csrf.md",
    "content": "---\ntitle: csrf\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Cross-site request forgery\n  - csrf\ndescription: The CSRF Plugin can be used to protect your API against CSRF attacks using the Double Submit Cookie method.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `csrf` Plugin can be used to protect your API against [CSRF attacks](https://en.wikipedia.org/wiki/Cross-site_request_forgery) using the [Double Submit Cookie](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Double_Submit_Cookie) method.\n\nThis Plugin considers the `GET`, `HEAD` and `OPTIONS` methods to be safe operations (`safe-methods`) and such requests are not checked for interception by an attacker. Other methods are termed as `unsafe-methods`.\n\n## Attributes\n\n| Name    | Type   | Required | Default             | Description                                                                                 |\n|---------|--------|----------|---------------------|---------------------------------------------------------------------------------------------|\n| name    | string | False    | `apisix-csrf-token` | Name of the token in the generated cookie.                                                  |\n| expires | number | False    | `7200`              | Expiration time in seconds of the CSRF cookie. Set to `0` to skip checking expiration time. |\n| key     | string | True     |                     | Secret key used to encrypt the cookie.                                                      |\n\nNOTE: `encrypt_fields = {\"key\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\n## Enable Plugin\n\nThe example below shows how you can enable the Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT-d '\n{\n  \"uri\": \"/hello\",\n  \"plugins\": {\n    \"csrf\": {\n      \"key\": \"edd1c9f034335f136f87ad84b625c8f1\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  }\n}'\n```\n\nThe Route is now protected and trying to access it with methods other than `GET` will be blocked with a 401 status code.\n\nSending a `GET` request to the `/hello` endpoint will send back a cookie with an encrypted token. The name of the token can be set through the `name` attribute of the Plugin configuration and if unset, it defaults to `apisix-csrf-token`.\n\n:::note\n\nA new cookie is returned for each request.\n\n:::\n\nFor subsequent requests with `unsafe-methods`, you need to read the encrypted token from the cookie and append the token to the request header by setting the field name to the `name` attribute in the Plugin configuration.\n\n## Example usage\n\nAfter you have configured the Plugin as shown above, trying to directly make a `POST` request to the `/hello` Route will result in an error:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello -X POST\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"error_msg\":\"no csrf token in headers\"}\n```\n\nTo get the cookie with the encrypted token, you can make a `GET` request:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 200 OK\nSet-Cookie: apisix-csrf-token=eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ==;path=/;Expires=Mon, 13-Dec-21 09:33:55 GMT\n```\n\nThis token must then be read from the cookie and added to the request header for subsequent `unsafe-methods` requests.\n\nFor example, you can use [js-cookie](https://github.com/js-cookie/js-cookie) to read the cookie and [axios](https://github.com/axios/axios) to send requests:\n\n```js\nconst token = Cookie.get('apisix-csrf-token');\n\nconst instance = axios.create({\n  headers: {'apisix-csrf-token': token}\n});\n```\n\nAlso make sure that you carry the cookie.\n\nYou can also use curl to send the request:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello -X POST -H 'apisix-csrf-token: eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ==' -b 'apisix-csrf-token=eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ=='\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n## Delete Plugin\n\nTo remove the `csrf` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"uri\": \"/hello\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:1980\": 1\n    }\n  }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/datadog.md",
    "content": "---\ntitle: datadog\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Datadog\ndescription: This document contains information about the Apache APISIX datadog Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `datadog` monitoring Plugin is for seamless integration of APISIX with [Datadog](https://www.datadoghq.com/), one of the most used monitoring and observability platform for cloud applications.\n\nWhen enabled, the Plugin supports multiple metric capture types for request and response cycles.\n\nThis Plugin, pushes its custom metrics to the [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent) server over UDP protocol and comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/).\n\nDogStatsD implements the StatsD protocol which collects the custom metrics for the Apache APISIX agent, aggregates them into a single data point, and sends it to the configured Datadog server.\n\nThis Plugin provides the ability to push metrics as a batch to the external Datadog agent, reusing the same datagram socket. It might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name           | Type    | Required | Default | Valid values | Description                                                                                                      |\n| -------------- | ------- | -------- | ------- | ------------ | ---------------------------------------------------------------------------------------------------------------- |\n| prefer_name    | boolean | False    | true    | [true,false] | When set to `false`, uses Route/Service ID instead of name (default) with metric tags.                           |\n| include_path   | boolean | False    | false   | [true,false] | When set to `true`, includes the path pattern in metric tags.                                                    |\n| include_method | boolean | False    | false   | [true,false] | When set to `true`, includes the HTTP method in metric tags.                                                     |\n| constant_tags  | array   | False    | []      |              | Static tags to embed into all metrics generated by this route. Useful for grouping metrics over certain signals. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n## Metadata\n\nYou can configure the Plugin through the Plugin metadata.\n\n| Name          | Type    | Required | Default             | Description                                                                                                                               |\n| ------------- | ------- | -------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| host          | string  | False    | \"127.0.0.1\"         | DogStatsD server host address.                                                                                                            |\n| port          | integer | False    | 8125                | DogStatsD server host port.                                                                                                               |\n| namespace     | string  | False    | \"apisix\"            | Prefix for all custom metrics sent by APISIX agent. Useful for finding entities for metrics graph. For example, `apisix.request.counter`. |\n| constant_tags | array   | False    | [ \"source:apisix\" ] | Static tags to embed into generated metrics. Useful for grouping metrics over certain signals.                                            |\n\n:::tip\n\nSee [defining tags](https://docs.datadoghq.com/getting_started/tagging/#defining-tags) to know more about how to effectively use tags.\n\n:::\n\nBy default, the Plugin expects the DogStatsD service to be available at `127.0.0.1:8125`. If you want to change this, you can update the Plugin metadata as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/datadog -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"host\": \"172.168.45.29\",\n    \"port\": 8126,\n    \"constant_tags\": [\n        \"source:apisix\",\n        \"service:custom\"\n    ],\n    \"namespace\": \"apisix\"\n}'\n```\n\nTo reset to default configuration, make a PUT request with empty body:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/datadog -H \"X-API-KEY: $admin_key\" -X PUT -d '{}'\n```\n\n## Exported metrics\n\nWhen the `datadog` Plugin is enabled, the APISIX agent exports the following metrics to the DogStatsD server for each request/response cycle:\n\n| Metric name      | StatsD type | Description                                                                                           |\n| ---------------- | ----------- | ----------------------------------------------------------------------------------------------------- |\n| Request Counter  | Counter     | Number of requests received.                                                                          |\n| Request Latency  | Histogram   | Time taken to process the request (in milliseconds).                                                  |\n| Upstream latency | Histogram   | Time taken to proxy the request to the upstream server till a response is received (in milliseconds). |\n| APISIX Latency   | Histogram   | Time taken by APISIX agent to process the request (in milliseconds).                                  |\n| Ingress Size     | Timer       | Request body size in bytes.                                                                           |\n| Egress Size      | Timer       | Response body size in bytes.                                                                          |\n\nThe metrics will be sent to the DogStatsD agent with the following tags:\n\n- `route_name`: Name specified in the Route schema definition. If not present or if the attribute `prefer_name` is set to false, falls back to the Route ID.\n- `service_name`: If a Route has been created with an abstracted Service, the Service name/ID based on the attribute `prefer_name`.\n- `consumer`: If the Route is linked to a Consumer, the username will be added as a tag.\n- `balancer_ip`: IP address of the Upstream balancer that processed the current request.\n- `response_status`: HTTP response status code. E.g. \"200\", \"404\", \"503\".\n- `response_status_class`: HTTP response status code class. E.g. \"2xx\", \"4xx\", \"5xx\".\n- `scheme`: Request scheme such as HTTP, gRPC, and gRPCs.\n- `path`: The HTTP path pattern. Only available if the attribute `include_path` is set to true.\n- `method`: The HTTP method. Only available if the attribute `include_method` is set to true.\n\n:::note\n\nIf there are no suitable values for any particular tag, the tag will be omitted.\n\n:::\n\n## Enable Plugin\n\nOnce you have your Datadog agent running, you can enable the Plugin as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"datadog\": {}\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\nNow, requests to the endpoint `/hello` will generate metrics and push it to the DogStatsD server.\n\n## Delete Plugin\n\nTo remove the `datadog` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/degraphql.md",
    "content": "---\ntitle: degraphql\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Degraphql\ndescription: This document contains information about the Apache APISIX degraphql Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `degraphql` Plugin is used to support decoding RESTful API to GraphQL.\n\n## Attributes\n\n| Name           | Type   | Required | Description                                                                                  |\n| -------------- | ------ | -------- | -------------------------------------------------------------------------------------------- |\n| query          | string | True     | The GraphQL query sent to the upstream                                                       |\n| operation_name | string | False    | The name of the operation, is only required if multiple operations are present in the query. |\n| variables      | array  | False    | The variables used in the GraphQL query                                                      |\n\n## Example usage\n\n### Start GraphQL server\n\nWe use docker to deploy a [GraphQL server demo](https://github.com/npalm/graphql-java-demo) as the backend.\n\n```bash\ndocker run -d --name grapql-demo -p 8080:8080 npalm/graphql-java-demo\n```\n\nAfter starting the server, the following endpoints are now available:\n\n- http://localhost:8080/graphiql - GraphQL IDE - GrahphiQL\n- http://localhost:8080/playground - GraphQL IDE - Prisma GraphQL Client\n- http://localhost:8080/altair - GraphQL IDE - Altair GraphQL Client\n- http://localhost:8080/ - A simple reacter\n- ws://localhost:8080/subscriptions\n\n### Enable Plugin\n\n#### Query list\n\nIf we have a GraphQL query like this:\n\n```graphql\nquery {\n  persons {\n    id\n    name\n  }\n}\n```\n\nWe can execute it on `http://localhost:8080/playground`, and get the data as below:\n\n```json\n{\n  \"data\": {\n    \"persons\": [\n      {\n        \"id\": \"7\",\n        \"name\": \"Niek\"\n      },\n      {\n        \"id\": \"8\",\n        \"name\": \"Josh\"\n      },\n      ......\n    ]\n  }\n}\n```\n\nNow we can use RESTful API to query the same data that is proxy by APISIX.\n\nFirst, we need to create a route in APISIX, and enable the degreaph plugin on the route, we need to define the GraphQL query in the plugin's config.\n\n```bash\ncurl --location --request PUT 'http://localhost:9180/apisix/admin/routes/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"uri\": \"/graphql\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    },\n    \"plugins\": {\n        \"degraphql\": {\n            \"query\": \"{\\n  persons {\\n    id\\n    name\\n  }\\n}\\n\"\n        }\n    }\n}'\n```\n\nWe convert the GraphQL query\n\n```graphql\n{\n  persons {\n    id\n    name\n  }\n}\n```\n\nto JSON string `\"{\\n  persons {\\n    id\\n    name\\n  }\\n}\\n\"`, and put it in the plugin's configuration.\n\nThen we can query the data by RESTful API:\n\n```bash\ncurl --location --request POST 'http://localhost:9080/graphql'\n```\n\nand get the result:\n\n```json\n{\n  \"data\": {\n    \"persons\": [\n      {\n        \"id\": \"7\",\n        \"name\": \"Niek\"\n      },\n      {\n        \"id\": \"8\",\n        \"name\": \"Josh\"\n      },\n      ......\n    ]\n  }\n}\n```\n\n#### Query with variables\n\nIf we have a GraphQL query like this:\n\n```graphql\nquery($name: String!, $githubAccount: String!) {\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\n    id\n    name\n    blog\n    githubAccount\n    talks {\n      id\n      title\n    }\n  }\n}\n\nvariables:\n{\n  \"name\": \"Niek\",\n  \"githubAccount\": \"npalm\"\n}\n```\n\nwe can execute it on `http://localhost:8080/playground`, and get the data as below:\n\n```json\n{\n  \"data\": {\n    \"persons\": [\n      {\n        \"id\": \"7\",\n        \"name\": \"Niek\",\n        \"blog\": \"https://040code.github.io\",\n        \"githubAccount\": \"npalm\",\n        \"talks\": [\n          {\n            \"id\": \"19\",\n            \"title\": \"GraphQL - The Next API Language\"\n          },\n          {\n            \"id\": \"20\",\n            \"title\": \"Immutable Infrastructure\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nWe convert the GraphQL query to JSON string like `\"query($name: String!, $githubAccount: String!) {\\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\"`, so we create a route like this:\n\n```bash\ncurl --location --request PUT 'http://localhost:9180/apisix/admin/routes/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"uri\": \"/graphql\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    },\n    \"plugins\": {\n        \"degraphql\": {\n            \"query\": \"query($name: String!, $githubAccount: String!) {\\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\",\n            \"variables\": [\n                \"name\",\n                \"githubAccount\"\n            ]\n        }\n    }\n}'\n```\n\nWe define the `variables` in the plugin's config, and the `variables` is an array, which contains the variables' name in the GraphQL query, so that we can pass the query variables by RESTful API.\n\nQuery the data by RESTful API that proxy by APISIX:\n\n```bash\ncurl --location --request POST 'http://localhost:9080/graphql' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"name\": \"Niek\",\n    \"githubAccount\": \"npalm\"\n}'\n```\n\nand get the result:\n\n```json\n{\n  \"data\": {\n    \"persons\": [\n      {\n        \"id\": \"7\",\n        \"name\": \"Niek\",\n        \"blog\": \"https://040code.github.io\",\n        \"githubAccount\": \"npalm\",\n        \"talks\": [\n          {\n            \"id\": \"19\",\n            \"title\": \"GraphQL - The Next API Language\"\n          },\n          {\n            \"id\": \"20\",\n            \"title\": \"Immutable Infrastructure\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nwhich is the same as the result of the GraphQL query.\n\nIt's also possible to get the same result via GET request:\n\n```bash\ncurl 'http://localhost:9080/graphql?name=Niek&githubAccount=npalm'\n```\n\n```json\n{\n  \"data\": {\n    \"persons\": [\n      {\n        \"id\": \"7\",\n        \"name\": \"Niek\",\n        \"blog\": \"https://040code.github.io\",\n        \"githubAccount\": \"npalm\",\n        \"talks\": [\n          {\n            \"id\": \"19\",\n            \"title\": \"GraphQL - The Next API Language\"\n          },\n          {\n            \"id\": \"20\",\n            \"title\": \"Immutable Infrastructure\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nIn the GET request, the variables are passed in the query string.\n\n## Delete Plugin\n\nTo remove the `degraphql` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"uri\": \"/graphql\",\n  \"plugins\": {},\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:8080\": 1\n    }\n  }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/dubbo-proxy.md",
    "content": "---\ntitle: dubbo-proxy\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Apache Dubbo\n  - dubbo-proxy\ndescription: This document contains information about the Apache APISIX dubbo-proxy Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `dubbo-proxy` Plugin allows you to proxy HTTP requests to [Apache Dubbo](https://dubbo.apache.org/en/index.html).\n\n:::info IMPORTANT\n\nIf you are using OpenResty, you need to build it with Dubbo support. See [How do I build the APISIX runtime environment](./../FAQ.md#how-do-i-build-the-apisix-runtime-environment) for details.\n\n:::\n\n## Runtime Attributes\n\n| Name            | Type   | Required | Default              | Description                     |\n| --------------- | ------ | -------- | -------------------- | ------------------------------- |\n| service_name    | string | True     |                      | Dubbo provider service name.    |\n| service_version | string | True     |                      | Dubbo provider service version. |\n| method          | string | False    | The path of the URI. | Dubbo provider service method.  |\n\n## Static Attributes\n\n| Name                     | Type   | Required | Default | Valid values | Description                                                     |\n| ------------------------ | ------ | -------- | ------- | ------------ | --------------------------------------------------------------- |\n| upstream_multiplex_count | number | True | 32      | >= 1         | Maximum number of multiplex requests in an upstream connection. |\n\n## Enable Plugin\n\nTo enable the `dubbo-proxy` Plugin, you have to add it in your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - ...\n  - dubbo-proxy\n```\n\nNow, when APISIX is reloaded, you can add it to a specific Route as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/upstreams/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"nodes\": {\n        \"127.0.0.1:20880\": 1\n    },\n    \"type\": \"roundrobin\"\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uris\": [\n        \"/hello\"\n    ],\n    \"plugins\": {\n        \"dubbo-proxy\": {\n            \"service_name\": \"org.apache.dubbo.sample.tengine.DemoService\",\n            \"service_version\": \"0.0.0\",\n            \"method\": \"tengineDubbo\"\n        }\n    },\n    \"upstream_id\": 1\n}'\n```\n\n## Example usage\n\nYou can follow the [Quick Start](https://github.com/alibaba/tengine/tree/master/modules/mod_dubbo#quick-start) guide in Tengine with the configuration above for testing.\n\nAPISIX dubbo plugin uses `hessian2` as the serialization protocol. It supports only `Map<String, Object>` as the request and response data type.\n\n### Application\n\nYour dubbo config should be configured to use `hessian2` as the serialization protocol.\n\n```yml\ndubbo:\n  ...\n  protocol:\n    ...\n    serialization: hessian2\n```\n\nYour application should implement the interface with the request and response data type as `Map<String, Object>`.\n\n```java\npublic interface DemoService {\n    Map<String, Object> sayHello(Map<String, Object> context);\n}\n```\n\n### Request and Response\n\nIf you need to pass request data, you can add the data to the HTTP request header. The plugin will convert the HTTP request header to the request data of the Dubbo service. Here is a sample HTTP request that passes `user` information:\n\n```bash\ncurl -i -X POST 'http://localhost:9080/hello' \\\n                    --header 'user: apisix'\n\n\nHTTP/1.1 200 OK\nDate: Mon, 15 Jan 2024 10:15:57 GMT\nContent-Type: text/plain; charset=utf-8\n...\nhello: apisix\n...\nServer: APISIX/3.8.0\n```\n\nIf the returned data is:\n\n```json\n{\n  \"status\": \"200\",\n  \"header1\": \"value1\",\n  \"header2\": \"value2\",\n  \"body\": \"body of the message\"\n}\n```\n\nThe converted HTTP response will be:\n\n```\nHTTP/1.1 200 OK\n...\nheader1: value1\nheader2: value2\n...\n\nbody of the message\n```\n\n## Delete Plugin\n\nTo remove the `dubbo-proxy` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uris\": [\n        \"/hello\"\n    ],\n    \"plugins\": {\n    },\n    \"upstream_id\": 1\n    }\n}'\n```\n\nTo completely disable the `dubbo-proxy` Plugin, you can remove it from your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  # - dubbo-proxy\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/echo.md",
    "content": "---\ntitle: echo\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Echo\ndescription: This document contains information about the Apache APISIX echo Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `echo` Plugin is to help users understand how they can develop an APISIX Plugin.\n\nThis Plugin addresses common functionalities in phases like init, rewrite, access, balancer, header filter, body filter and log.\n\n:::caution WARNING\n\nThe `echo` Plugin is built as an example. It has missing cases and should **not** be used in production environments.\n\n:::\n\n## Attributes\n\n| Name        | Type   | Requirement | Default | Valid | Description                               |\n| ----------- | ------ | ----------- | ------- | ----- | ----------------------------------------- |\n| before_body | string | optional    |         |       | Body to use before the filter phase.      |\n| body        | string | optional    |         |       | Body that replaces the Upstream response. |\n| after_body  | string | optional    |         |       | Body to use after the modification phase. |\n| headers     | object | optional    |         |       | New headers to use for the response.      |\n\nAt least one of `before_body`, `body`, and `after_body` must be specified.\n\n## Enable Plugin\n\nThe example below shows how you can enable the `echo` Plugin for a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"echo\": {\n            \"before_body\": \"before the body modification \"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nFirst, we configure the Plugin as mentioned above. We can then make a request as shown below:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nbefore the body modification hello world\n```\n\n## Delete Plugin\n\nTo remove the `echo` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/elasticsearch-logger.md",
    "content": "---\ntitle: elasticsearch-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Elasticsearch-logger\ndescription: The elasticsearch-logger Plugin pushes request and response logs in batches to Elasticsearch and supports the customization of log formats.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/elasticsearch-logger\" />\n</head>\n\n## Description\n\nThe `elasticsearch-logger` Plugin pushes request and response logs in batches to [Elasticsearch](https://www.elastic.co) and supports the customization of log formats. When enabled, the Plugin will serialize the request context information to [Elasticsearch Bulk format](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html#docs-bulk) and add them to the queue, before they are pushed to Elasticsearch. See [batch processor](../batch-processor.md) for more details.\n\n## Attributes\n\n| Name          | Type    | Required | Default                     | Description                                                  |\n| ------------- | ------- | -------- | --------------------------- | ------------------------------------------------------------ |\n| endpoint_addrs  | array[string] | True     |                             | Elasticsearch API endpoint addresses. If multiple endpoints are configured, they will be written randomly.            |\n| field         | object   | True     |                             | Elasticsearch `field` configuration.                          |\n| field.index   | string  | True     |                             | Elasticsearch [_index field](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html#mapping-index-field). |\n| log_format | object | False    |                             | Custom log format as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX variables](http://nginx.org/en/docs/varindex.html) can be referenced by prefixing with `$`. |\n| auth          | array   | False    |                             | Elasticsearch [authentication](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html) configuration. |\n| auth.username | string  | True     |                             | Elasticsearch [authentication](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html) username. |\n| auth.password | string  | True     |                             | Elasticsearch [authentication](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html) password. |\n| headers | object  | False     |                             | Custom headers to send with requests as key-value pairs. For example: `{\"Authorization\": \"Bearer token\", \"X-API-Key\": \"key\"}`. |\n| ssl_verify    | boolean | False    | true                        | If true, perform SSL verification. |\n| timeout       | integer | False    | 10                          | Elasticsearch send data timeout in seconds.                  |\n| include_req_body       | boolean       | False    | false   |  If true, include the request body in the log. Note that if the request body is too big to be kept in the memory, it can not be logged due to NGINX's limitations.       |\n| include_req_body_expr  | array[array]  | False    |         | An array of one or more conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the `include_req_body` is true. Request body would only be logged when the expressions configured here evaluate to true.      |\n| max_req_body_bytes | integer | False | 524288 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean       | False    | false   | If true, include the response body in the log.       |\n| include_resp_body_expr | array[array]  | False    |         | An array of one or more conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the `include_resp_body` is true. Response body would only be logged when the expressions configured here evaluate to true.     |\n| max_resp_body_bytes | integer | False | 524288 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nNOTE: `encrypt_fields = {\"auth.password\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n## Plugin Metadata\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| log_format | object | False |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `elasticsearch-logger` Plugin for different scenarios.\n\nTo follow along the examples, start an Elasticsearch instance in Docker:\n\n```shell\ndocker run -d \\\n  --name elasticsearch \\\n  --network apisix-quickstart-net \\\n  -v elasticsearch_vol:/usr/share/elasticsearch/data/ \\\n  -p 9200:9200 \\\n  -p 9300:9300 \\\n  -e ES_JAVA_OPTS=\"-Xms512m -Xmx512m\" \\\n  -e discovery.type=single-node \\\n  -e xpack.security.enabled=false \\\n  docker.elastic.co/elasticsearch/elasticsearch:7.17.1\n```\n\nStart a Kibana instance in Docker to visualize the indexed data in Elasticsearch:\n\n```shell\ndocker run -d \\\n  --name kibana \\\n  --network apisix-quickstart-net \\\n  -p 5601:5601 \\\n  -e ELASTICSEARCH_HOSTS=\"http://elasticsearch:9200\" \\\n  docker.elastic.co/kibana/kibana:7.17.1\n```\n\nIf successful, you should see the Kibana dashboard on [localhost:5601](http://localhost:5601).\n\n:::note\n\nYou can fetch the APISIX `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Log in the Default Log Format\n\nThe following example demonstrates how you can enable the `elasticsearch-logger` Plugin on a route, which logs client requests and responses to the Route and pushes logs to Elasticsearch.\n\nCreate a Route with `elasticsearch-logger` to configure the `index` field as `gateway`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"elasticsearch-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"elasticsearch-logger\": {\n        \"endpoint_addrs\": [\"http://elasticsearch:9200\"],\n        \"field\": {\n          \"index\": \"gateway\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend a request to the Route to generate a log entry:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nNavigate to the Kibana dashboard on [localhost:5601](http://localhost:5601) and under __Discover__ tab, create a new index pattern `gateway` to fetch the data from Elasticsearch. Once configured, navigate back to the __Discover__ tab and you should see a log generated, similar to the following:\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"CE-JL5QBOkdYRG7kEjTJ\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"request\": {\n      \"headers\": {\n        \"host\": \"127.0.0.1:9080\",\n        \"accept\": \"*/*\",\n        \"user-agent\": \"curl/8.6.0\"\n      },\n      \"size\": 85,\n      \"querystring\": {},\n      \"method\": \"GET\",\n      \"url\": \"http://127.0.0.1:9080/anything\",\n      \"uri\": \"/anything\"\n    },\n    \"response\": {\n      \"headers\": {\n        \"content-type\": \"application/json\",\n        \"access-control-allow-credentials\": \"true\",\n        \"server\": \"APISIX/3.11.0\",\n        \"content-length\": \"390\",\n        \"access-control-allow-origin\": \"*\",\n        \"connection\": \"close\",\n        \"date\": \"Mon, 13 Jan 2025 10:18:14 GMT\"\n      },\n      \"status\": 200,\n      \"size\": 618\n    },\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"latency\": 585.00003814697,\n    \"apisix_latency\": 18.000038146973,\n    \"upstream_latency\": 567,\n    \"upstream\": \"50.19.58.113:80\",\n    \"server\": {\n      \"hostname\": \"0b9a772e68f8\",\n      \"version\": \"3.11.0\"\n    },\n    \"service_id\": \"\",\n    \"client_ip\": \"192.168.65.1\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n### Log Request and Response Headers With Plugin Metadata\n\nThe following example demonstrates how you can customize log format using [Plugin Metadata](../terminology/plugin-metadata.md) and [NGINX variables](http://nginx.org/en/docs/varindex.html) to log specific headers from request and response.\n\nIn APISIX, [Plugin Metadata](../terminology/plugin-metadata.md) is used to configure the common metadata fields of all Plugin instances of the same plugin. It is useful when a Plugin is enabled across multiple resources and requires a universal update to their metadata fields.\n\nFirst, create a Route with `elasticsearch-logger` as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"elasticsearch-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"elasticsearch-logger\": {\n        \"endpoint_addrs\": [\"http://elasticsearch:9200\"],\n        \"field\": {\n          \"index\": \"gateway\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nNext, configure the Plugin metadata for `elasticsearch-logger`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/elasticsearch-logger\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"log_format\": {\n      \"host\": \"$host\",\n      \"@timestamp\": \"$time_iso8601\",\n      \"client_ip\": \"$remote_addr\",\n      \"env\": \"$http_env\",\n      \"resp_content_type\": \"$sent_http_Content_Type\"\n    }\n  }'\n```\n\nSend a request to the Route with the `env` header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"env: dev\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nNavigate to the Kibana dashboard on [localhost:5601](http://localhost:5601) and under __Discover__ tab, create a new index pattern `gateway` to fetch the data from Elasticsearch, if you have not done so already. Once configured, navigate back to the __Discover__ tab and you should see a log generated, similar to the following:\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"Ck-WL5QBOkdYRG7kODS0\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"client_ip\": \"192.168.65.1\",\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"@timestamp\": \"2025-01-06T10:32:36+00:00\",\n    \"host\": \"127.0.0.1\",\n    \"resp_content_type\": \"application/json\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n### Log Request Bodies Conditionally\n\nThe following example demonstrates how you can conditionally log request body.\n\nCreate a Route with `elasticsearch-logger` to only log request body if the URL query string `log_body` is `true`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"elasticsearch-logger\": {\n        \"endpoint_addrs\": [\"http://elasticsearch:9200\"],\n        \"field\": {\n          \"index\": \"gateway\"\n        },\n        \"include_req_body\": true,\n        \"include_req_body_expr\": [[\"arg_log_body\", \"==\", \"yes\"]]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    },\n  \"uri\": \"/anything\",\n  \"id\": \"elasticsearch-logger-route\"\n}'\n```\n\nSend a request to the Route with an URL query string satisfying the condition:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?log_body=yes\" -X POST -d '{\"env\": \"dev\"}'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nNavigate to the Kibana dashboard on [localhost:5601](http://localhost:5601) and under __Discover__ tab, create a new index pattern `gateway` to fetch the data from Elasticsearch, if you have not done so already. Once configured, navigate back to the __Discover__ tab and you should see a log generated, similar to the following:\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"Dk-cL5QBOkdYRG7k7DSW\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"request\": {\n      \"headers\": {\n        \"user-agent\": \"curl/8.6.0\",\n        \"accept\": \"*/*\",\n        \"content-length\": \"14\",\n        \"host\": \"127.0.0.1:9080\",\n        \"content-type\": \"application/x-www-form-urlencoded\"\n      },\n      \"size\": 182,\n      \"querystring\": {\n        \"log_body\": \"yes\"\n      },\n      \"body\": \"{\\\"env\\\": \\\"dev\\\"}\",\n      \"method\": \"POST\",\n      \"url\": \"http://127.0.0.1:9080/anything?log_body=yes\",\n      \"uri\": \"/anything?log_body=yes\"\n    },\n    \"start_time\": 1735965595203,\n    \"response\": {\n      \"headers\": {\n        \"content-type\": \"application/json\",\n        \"server\": \"APISIX/3.11.0\",\n        \"access-control-allow-credentials\": \"true\",\n        \"content-length\": \"548\",\n        \"access-control-allow-origin\": \"*\",\n        \"connection\": \"close\",\n        \"date\": \"Mon, 13 Jan 2025 11:02:32 GMT\"\n      },\n      \"status\": 200,\n      \"size\": 776\n    },\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"latency\": 703.9999961853,\n    \"apisix_latency\": 34.999996185303,\n    \"upstream_latency\": 669,\n    \"upstream\": \"34.197.122.172:80\",\n    \"server\": {\n      \"hostname\": \"0b9a772e68f8\",\n      \"version\": \"3.11.0\"\n    },\n    \"service_id\": \"\",\n    \"client_ip\": \"192.168.65.1\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\nSend a request to the Route without any URL query string:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST -d '{\"env\": \"dev\"}'\n```\n\nNavigate to the Kibana dashboard __Discover__ tab and you should see a log generated, but without the request body:\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"EU-eL5QBOkdYRG7kUDST\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"request\": {\n      \"headers\": {\n        \"content-type\": \"application/x-www-form-urlencoded\",\n        \"accept\": \"*/*\",\n        \"content-length\": \"14\",\n        \"host\": \"127.0.0.1:9080\",\n        \"user-agent\": \"curl/8.6.0\"\n      },\n      \"size\": 169,\n      \"querystring\": {},\n      \"method\": \"POST\",\n      \"url\": \"http://127.0.0.1:9080/anything\",\n      \"uri\": \"/anything\"\n    },\n    \"start_time\": 1735965686363,\n    \"response\": {\n      \"headers\": {\n        \"content-type\": \"application/json\",\n        \"access-control-allow-credentials\": \"true\",\n        \"server\": \"APISIX/3.11.0\",\n        \"content-length\": \"510\",\n        \"access-control-allow-origin\": \"*\",\n        \"connection\": \"close\",\n        \"date\": \"Mon, 13 Jan 2025 11:15:54 GMT\"\n      },\n      \"status\": 200,\n      \"size\": 738\n    },\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"latency\": 680.99999427795,\n    \"apisix_latency\": 4.9999942779541,\n    \"upstream_latency\": 676,\n    \"upstream\": \"34.197.122.172:80\",\n    \"server\": {\n      \"hostname\": \"0b9a772e68f8\",\n      \"version\": \"3.11.0\"\n    },\n    \"service_id\": \"\",\n    \"client_ip\": \"192.168.65.1\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n:::info\n\nIf you have customized the `log_format` in addition to setting `include_req_body` or `include_resp_body` to `true`, the Plugin would not include the bodies in the logs.\n\nAs a workaround, you may be able to use the NGINX variable `$request_body` in the log format, such as:\n\n```json\n{\n  \"elasticsearch-logger\": {\n    ...,\n    \"log_format\": {\"body\": \"$request_body\"}\n  }\n}\n```\n\n:::\n"
  },
  {
    "path": "docs/en/latest/plugins/error-log-logger.md",
    "content": "---\ntitle: error-log-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Error log logger\ndescription: This document contains information about the Apache APISIX error-log-logger Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `error-log-logger` Plugin is used to push APISIX's error logs (`error.log`) to TCP, [Apache SkyWalking](https://skywalking.apache.org/), Apache Kafka or ClickHouse servers. You can also set the error log level to send the logs to server.\n\nIt might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name                             | Type    | Required | Default                        | Valid values                                                                            | Description                                                                                                  |\n|----------------------------------|---------|----------|--------------------------------|-----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|\n| tcp.host                         | string  | True     |                                |                                                                                         | IP address or the hostname of the TCP server.                                                                |\n| tcp.port                         | integer | True     |                                | [0,...]                                                                                 | Target upstream port.                                                                                        |\n| tcp.tls                          | boolean | False    | false                          |                                                                                         | When set to `true` performs SSL verification.                                                                |\n| tcp.tls_server_name              | string  | False    |                                |                                                                                         | Server name for the new TLS extension SNI.                                                                   |\n| skywalking.endpoint_addr         | string  | False    | http://127.0.0.1:12900/v3/logs |                                                                                         | Apache SkyWalking HTTP endpoint.                                                                             |\n| skywalking.service_name          | string  | False    | APISIX                         |                                                                                         | Service name for the SkyWalking reporter.                                                                    |\n| skywalking.service_instance_name | String  | False    | APISIX Instance Name           |                                                                                         | Service instance name for the SkyWalking reporter. Set it to `$hostname` to directly get the local hostname. |\n| clickhouse.endpoint_addr         | String  | False    | http://127.0.0.1:8213          |                                                                                         | ClickHouse endpoint.                                                                                         |\n| clickhouse.user                  | String  | False    | default                        |                                                                                         | ClickHouse username.                                                                                         |\n| clickhouse.password              | String  | False    |                                |                                                                                         | ClickHouse password.                                                                                         |\n| clickhouse.database              | String  | False    |                                |                                                                                         | Name of the database to store the logs.                                                                      |\n| clickhouse.logtable              | String  | False    |                                |                                                                                         | Table name to store the logs.                                                                                |\n| kafka.brokers                    | array   | True     |                |                       | List of Kafka brokers (nodes).                                                                                                                                                                                                                                                                                                                   |\n| kafka.brokers.host                     | string  | True     |                |                       | The host of Kafka broker, e.g, `192.168.1.1`.                                                                                                                                                                                                                                                                                                                   |\n| kafka.brokers.port                     | integer | True     |                |   [0, 65535]                  |  The port of Kafka broker                                                                                                                                                                                                                                                                                                                  |\n| kafka.brokers.sasl_config              | object  | False    |                |                               |  The sasl config of Kafka broker                                                                                                                                                                                                                                                                                                                 |\n| kafka.brokers.sasl_config.mechanism    | string  | False    | \"PLAIN\"          | [\"PLAIN\"]           |     The mechaism of sasl config                                                                                                                                                                                                                                                                                                             |\n| kafka.brokers.sasl_config.user         | string  | True     |                  |                     |  The user of sasl_config. If sasl_config exists, it's required.                                                                                                                                                                                                                                                                                             |\n| kafka.brokers.sasl_config.password     | string  | True     |                  |                     | The password of sasl_config. If sasl_config exists, it's required.                                                                                                                                                                                                                                                                                                 |\n| kafka.kafka_topic                      | string  | True     |                |                       | Target topic to push the logs for organisation.                                                                                                                                                                                                                                                                                                  |\n| kafka.producer_type                    | string  | False    | async          | [\"async\", \"sync\"]     | Message sending mode of the producer.                                                                                                                                                                                                                                                                                                            |\n| kafka.required_acks                    | integer | False    | 1              | [0, 1, -1]            | Number of acknowledgements the leader needs to receive for the producer to consider the request complete. This controls the durability of the sent records. The attribute follows the same configuration as the Kafka `acks` attribute. See [Apache Kafka documentation](https://kafka.apache.org/documentation/#producerconfigs_acks) for more. |\n| kafka.key                              | string  | False    |                |                       | Key used for allocating partitions for messages.                                                                                                                                                                                                                                                                                                 |\n| kafka.cluster_name           | integer | False    | 1              | [0,...]               | Name of the cluster. Used when there are two or more Kafka clusters. Only works if the `producer_type` attribute is set to `async`.                                                                                                                                                                                                              |\n| kafka.meta_refresh_interval | integer | False    | 30              | [1,...]               | `refresh_interval` parameter in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) specifies the time to auto refresh the metadata, in seconds.|\n| timeout                          | integer | False    | 3                              | [1,...]                                                                                 | Timeout (in seconds) for the upstream to connect and send data.                                              |\n| keepalive                        | integer | False    | 30                             | [1,...]                                                                                 | Time in seconds to keep the connection alive after sending data.                                             |\n| level                            | string  | False    | WARN                           | [\"STDERR\", \"EMERG\", \"ALERT\", \"CRIT\", \"ERR\", \"ERROR\", \"WARN\", \"NOTICE\", \"INFO\", \"DEBUG\"] | Log level to filter the error logs. `ERR` is same as `ERROR`.                                                |\n\nNOTE: `encrypt_fields = {\"clickhouse.password\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```text\n[\"2024/01/06 16:04:30 [warn] 11786#9692271: *1 [lua] plugin.lua:205: load(): new plugins: {\"error-log-logger\":true}, context: init_worker_by_lua*\",\"\\n\",\"2024/01/06 16:04:30 [warn] 11786#9692271: *1 [lua] plugin.lua:255: load_stream(): new plugins: {\"limit-conn\":true,\"ip-restriction\":true,\"syslog\":true,\"mqtt-proxy\":true}, context: init_worker_by_lua*\",\"\\n\"]\n```\n\n## Enable Plugin\n\nTo enable the Plugin, you can add it in your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - request-id\n  - hmac-auth\n  - api-breaker\n  - error-log-logger\n```\n\nOnce you have enabled the Plugin, you can configure it through the Plugin metadata.\n\n### Configuring TCP server address\n\nYou can set the TCP server address by configuring the Plugin metadata as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"tcp\": {\n    \"host\": \"127.0.0.1\",\n    \"port\": 1999\n  },\n  \"inactive_timeout\": 1\n}'\n```\n\n### Configuring SkyWalking OAP server address\n\nYou can configure the SkyWalking OAP server address as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"skywalking\": {\n    \"endpoint_addr\":\"http://127.0.0.1:12800/v3/logs\"\n  },\n  \"inactive_timeout\": 1\n}'\n```\n\n### Configuring ClickHouse server details\n\nThe Plugin sends the error log as a string to the `data` field of a table in your ClickHouse server.\n\nYou can configure it as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"clickhouse\": {\n      \"user\": \"default\",\n      \"password\": \"a\",\n      \"database\": \"error_log\",\n      \"logtable\": \"t\",\n      \"endpoint_addr\": \"http://127.0.0.1:8123\"\n  }\n}'\n```\n\n### Configuring Kafka server\n\nThe Plugin sends the error log to Kafka, you can configure it as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n   \"kafka\":{\n      \"brokers\":[\n         {\n            \"host\":\"127.0.0.1\",\n            \"port\":9092\n         }\n      ],\n      \"kafka_topic\":\"test2\"\n   },\n   \"level\":\"ERROR\",\n   \"inactive_timeout\":1\n}'\n```\n\n## Delete Plugin\n\nTo remove the Plugin, you can remove it from your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - request-id\n  - hmac-auth\n  - api-breaker\n  # - error-log-logger\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ext-plugin-post-req.md",
    "content": "---\ntitle: ext-plugin-post-req\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - ext-plugin-post-req\ndescription: This document contains information about the Apache APISIX ext-plugin-post-req Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\n`ext-plugin-post-req` differs from the [ext-plugin-pre-req](./ext-plugin-pre-req.md) Plugin in that it runs after executing the built-in Lua Plugins and before proxying to the Upstream.\n\nYou can learn more about the configuration from the [ext-plugin-pre-req](./ext-plugin-pre-req.md) Plugin document.\n"
  },
  {
    "path": "docs/en/latest/plugins/ext-plugin-post-resp.md",
    "content": "---\ntitle: ext-plugin-post-resp\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ext-plugin-post-resp\ndescription: This document contains information about the Apache APISIX ext-plugin-post-resp Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ext-plugin-post-resp` Plugin is for running specific external Plugins in the Plugin Runner before executing the built-in Lua Plugins.\n\nThe `ext-plugin-post-resp` plugin will be executed after the request gets a response from the upstream.\n\nThis plugin uses [lua-resty-http](https://github.com/api7/lua-resty-http) library under the hood to send requests to the upstream, due to which the [proxy-control](./proxy-control.md), [proxy-mirror](./proxy-mirror.md), and [proxy-cache](./proxy-cache.md) plugins are not available to be used alongside this plugin. Also, [mTLS Between APISIX and Upstream](../mtls.md#mtls-between-apisix-and-upstream) is not yet supported.\n\nSee [External Plugin](../external-plugin.md) to learn more.\n\n:::note\n\nExecution of External Plugins will affect the response of the current request.\n\n:::\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values                                                    | Description                                                                                                            |\n|-------------------|---------|----------|---------|-----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|\n| conf              | array   | False    |         | [{\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}] | List of Plugins and their configurations to be executed on the Plugin Runner.                                          |\n| allow_degradation | boolean | False    | false   |                                                                 | Sets Plugin degradation when the Plugin Runner is not available. When set to `true`, requests are allowed to continue. |\n\n## Enable Plugin\n\nThe example below enables the `ext-plugin-post-resp` Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"ext-plugin-post-resp\": {\n            \"conf\" : [\n                {\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}\n            ]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the External Plugin as shown above, you can make a request to execute the Plugin:\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\nThis will reach the configured Plugin Runner and the `ext-plugin-A` will be executed.\n\n## Delete Plugin\n\nTo remove the `ext-plugin-post-resp` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ext-plugin-pre-req.md",
    "content": "---\ntitle: ext-plugin-pre-req\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - ext-plugin-pre-req\ndescription: This document contains information about the Apache APISIX ext-plugin-pre-req Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ext-plugin-pre-req` Plugin is for running specific external Plugins in the Plugin Runner before executing the built-in Lua Plugins.\n\nSee [External Plugin](../external-plugin.md) to learn more.\n\n:::note\n\nExecution of External Plugins will affect the behavior of the current request.\n\n:::\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values                                                    | Description                                                                                                            |\n|-------------------|---------|----------|---------|-----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|\n| conf              | array   | False    |         | [{\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}] | List of Plugins and their configurations to be executed on the Plugin Runner.                                          |\n| allow_degradation | boolean | False    | false   |                                                                 | Sets Plugin degradation when the Plugin Runner is not available. When set to `true`, requests are allowed to continue. |\n\n## Enable Plugin\n\nThe example below enables the `ext-plugin-pre-req` Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"ext-plugin-pre-req\": {\n            \"conf\" : [\n                {\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}\n            ]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the External Plugin as shown above, you can make a request to execute the Plugin:\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\nThis will reach the configured Plugin Runner and the `ext-plugin-A` will be executed.\n\n## Delete Plugin\n\nTo remove the `ext-plugin-pre-req` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/fault-injection.md",
    "content": "---\ntitle: fault-injection\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Fault Injection\n  - fault-injection\ndescription: This document contains information about the Apache APISIX fault-injection Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `fault-injection` Plugin can be used to test the resiliency of your application. This Plugin will be executed before the other configured Plugins.\n\nThe `abort` attribute will directly return the specified HTTP code to the client and skips executing the subsequent Plugins.\n\nThe `delay` attribute delays a request and executes the subsequent Plugins.\n\n## Attributes\n\n| Name              | Type    | Requirement | Default | Valid      | Description                                                                                                                                                 |\n|-------------------|---------|-------------|---------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| abort.http_status | integer | required    |         | [200, ...] | HTTP status code of the response to return to the client.                                                                                                   |\n| abort.body        | string  | optional    |         |            | Body of the response returned to the client. Nginx variables like `client addr: $remote_addr\\n` can be used in the body.                                    |\n| abort.headers     | object  | optional    |         |            | Headers of the response returned to the client. The values in the header can contain Nginx variables like `$remote_addr`. |\n| abort.percentage  | integer | optional    |         | [0, 100]   | Percentage of requests to be aborted.                                                                                                                       |\n| abort.vars        | array[] | optional    |         |            | Rules which are matched before executing fault injection. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for a list of available expressions. |\n| delay.duration    | number  | required    |         |            | Duration of the delay. Can be decimal.                                                                                                                      |\n| delay.percentage  | integer | optional    |         | [0, 100]   | Percentage of requests to be delayed.                                                                                                                       |\n| delay.vars        | array[] | optional    |         |            | Rules which are matched before executing fault injection. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for a list of available expressions.  |\n\n:::info IMPORTANT\n\nTo use the `fault-injection` Plugin one of `abort` or `delay` must be specified.\n\n:::\n\n:::tip\n\n`vars` can have expressions from [lua-resty-expr](https://github.com/api7/lua-resty-expr) and can flexibly implement AND/OR relationship between rules. For example:\n\n```json\n[\n    [\n        [ \"arg_name\",\"==\",\"jack\" ],\n        [ \"arg_age\",\"==\",18 ]\n    ],\n    [\n        [ \"arg_name2\",\"==\",\"allen\" ]\n    ]\n]\n```\n\nThis means that the relationship between the first two expressions is AND, and the relationship between them and the third expression is OR.\n\n:::\n\n## Enable Plugin\n\nYou can enable the `fault-injection` Plugin on a specific Route as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"fault-injection\": {\n           \"abort\": {\n              \"http_status\": 200,\n              \"body\": \"Fault Injection!\"\n           }\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nSimilarly, to enable a `delay` fault:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"fault-injection\": {\n           \"delay\": {\n              \"duration\": 3\n           }\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nYou can also enable the Plugin with both `abort` and `delay` which can have `vars` for matching:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"fault-injection\": {\n            \"abort\": {\n                \"http_status\": 403,\n                \"body\": \"Fault Injection!\\n\",\n                \"vars\": [\n                    [\n                        [ \"arg_name\",\"==\",\"jack\" ]\n                    ]\n                ]\n            },\n            \"delay\": {\n                \"duration\": 2,\n                \"vars\": [\n                    [\n                        [ \"http_age\",\"==\",\"18\" ]\n                    ]\n                ]\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nOnce you have enabled the Plugin as shown above, you can make a request to the configured Route:\n\n```shell\ncurl http://127.0.0.1:9080/hello -i\n```\n\n```\nHTTP/1.1 200 OK\nDate: Mon, 13 Jan 2020 13:50:04 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\nFault Injection!\n```\n\nAnd if we configure the `delay` fault:\n\n```shell\ntime curl http://127.0.0.1:9080/hello -i\n```\n\n```\nHTTP/1.1 200 OK\nContent-Type: application/octet-stream\nContent-Length: 6\nConnection: keep-alive\nServer: APISIX web server\nDate: Tue, 14 Jan 2020 14:30:54 GMT\nLast-Modified: Sat, 11 Jan 2020 12:46:21 GMT\n\nhello\n\nreal    0m3.034s\nuser    0m0.007s\nsys     0m0.010s\n```\n\n### Fault injection with criteria matching\n\nYou can enable the `fault-injection` Plugin with the `vars` attribute to set specific rules:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"fault-injection\": {\n            \"abort\": {\n                    \"http_status\": 403,\n                    \"body\": \"Fault Injection!\\n\",\n                    \"vars\": [\n                        [\n                            [ \"arg_name\",\"==\",\"jack\" ]\n                        ]\n                    ]\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nNow, we can test the Route. First, we test with a different `name` argument:\n\n```shell\ncurl \"http://127.0.0.1:9080/hello?name=allen\" -i\n```\n\nYou will get the expected response without the fault injected:\n\n```\nHTTP/1.1 200 OK\nContent-Type: application/octet-stream\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Wed, 20 Jan 2021 07:21:57 GMT\nServer: APISIX/2.2\n\nhello\n```\n\nNow if we set the `name` to match our configuration, the `fault-injection` Plugin is executed:\n\n```shell\ncurl \"http://127.0.0.1:9080/hello?name=jack\" -i\n```\n\n```\nHTTP/1.1 403 Forbidden\nDate: Wed, 20 Jan 2021 07:23:37 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/2.2\n\nFault Injection!\n```\n\n## Delete Plugin\n\nTo remove the `fault-injection` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/file-logger.md",
    "content": "---\ntitle: file-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - File Logger\ndescription: This document contains information about the Apache APISIX file-logger Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `file-logger` Plugin is used to push log streams to a specific location.\n\n:::tip\n\n- `file-logger` plugin can count request and response data for individual routes locally, which is useful for [debugging](../debug-mode.md).\n- `file-logger` plugin can get [APISIX variables](../apisix-variable.md) and [NGINX variables](http://nginx.org/en/docs/varindex.html), while `access.log` can only use NGINX variables.\n- `file-logger` plugin support hot-loaded so that we can change its configuration at any time with immediate effect.\n- `file-logger` plugin saves every data in JSON format.\n- The user can modify the functions executed by the `file-logger` during the `log phase` to collect the information they want.\n\n:::\n\n## Attributes\n\n| Name | Type   | Required | Description   |\n| ---- | ------ | -------- | ------------- |\n| path | string | True     | Log file path. |\n| log_format | object | False    | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| include_req_body       | boolean | False    | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitations. |\n| include_req_body_expr  | array   | False    | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more. |\n| max_req_body_bytes | integer | False | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean | False     | When set to `true` includes the response body in the log file.                                                                                                                                                                |\n| include_resp_body_expr | array   | False     | When the `include_resp_body` attribute is set to `true`, use this to filter based on [lua-resty-expr](https://github.com/api7/lua-resty-expr). If present, only logs the response into file if the expression evaluates to `true`. |\n| max_resp_body_bytes | integer | False | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| match        | array[array] | False   | Logs will be recorded when the rule matching is successful if the option is set. See [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) for a list of available expressions.   |\n\n### Example of default log format\n\n  ```json\n  {\n    \"service_id\": \"\",\n    \"apisix_latency\": 100.99999809265,\n    \"start_time\": 1703907485819,\n    \"latency\": 101.99999809265,\n    \"upstream_latency\": 1,\n    \"client_ip\": \"127.0.0.1\",\n    \"route_id\": \"1\",\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"request\": {\n        \"headers\": {\n            \"host\": \"127.0.0.1:1984\",\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"user-agent\": \"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\",\n            \"content-length\": \"12\"\n        },\n        \"method\": \"POST\",\n        \"size\": 194,\n        \"url\": \"http://127.0.0.1:1984/hello?log_body=no\",\n        \"uri\": \"/hello?log_body=no\",\n        \"querystring\": {\n            \"log_body\": \"no\"\n        }\n    },\n    \"response\": {\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"content-length\": \"12\",\n            \"server\": \"APISIX/3.7.0\"\n        },\n        \"status\": 200,\n        \"size\": 123\n    },\n    \"upstream\": \"127.0.0.1:1982\"\n }\n  ```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| path       | string | False    |  | Log file path used when the Plugin configuration does not specify `path`. |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/file-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"path\": \"logs/metadata-file.log\",\n  \"log_format\": {\n    \"host\": \"$host\",\n    \"@timestamp\": \"$time_iso8601\",\n    \"client_ip\": \"$remote_addr\",\n    \"request\": {\n      \"method\": \"$request_method\",\n      \"uri\": \"$request_uri\"\n    },\n    \"response\": {\n      \"status\": \"$status\"\n    }\n  }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"plugins\": {\n    \"file-logger\": {\n      \"path\": \"logs/file.log\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  },\n  \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request, it will be logged in the path you specified:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\nYou will be able to find the `file.log` file in the configured `logs` directory.\n\n## Filter logs\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"plugins\": {\n    \"file-logger\": {\n      \"path\": \"logs/file.log\",\n      \"match\": [\n        [\n          [ \"arg_name\",\"==\",\"jack\" ]\n        ]\n      ]\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  },\n  \"uri\": \"/hello\"\n}'\n```\n\nTest:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello?name=jack\n```\n\nLog records can be seen in `logs/file.log`.\n\n```shell\ncurl -i http://127.0.0.1:9080/hello?name=rose\n```\n\nLog records cannot be seen in `logs/file.log`.\n\n## Delete Plugin\n\nTo remove the `file-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"uri\": \"/hello\",\n  \"plugins\": {},\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/forward-auth.md",
    "content": "---\ntitle: forward-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Forward Authentication\n  - forward-auth\ndescription: This document contains information about the Apache APISIX forward-auth Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `forward-auth` Plugin implements a classic external authentication model. When authentication fails, you can have a custom error message or redirect the user to an authentication page.\n\nThis Plugin moves the authentication and authorization logic to a dedicated external service. APISIX forwards the user's requests to the external service, blocks the original request, and replaces the result when the external service responds with a non 2xx status code.\n\n## Attributes\n\n| Name              | Type          | Required | Default | Valid values   | Description                                                                                                                                                |\n| ----------------- | ------------- | -------- | ------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| uri               | string        | True     |         |                | URI of the authorization service.                                                                                                                          |\n| ssl_verify        | boolean       | False    | true    |                | When set to `true`, verifies the SSL certificate.                                                                                                          |\n| request_method    | string        | False    | GET     | [\"GET\",\"POST\"] | HTTP method for a client to send requests to the authorization service. When set to `POST` the request body is sent to the authorization service. (not recommended - see section on [Using data from POST body](#using-data-from-post-body-to-make-decision-on-authorization-service)) |\n| request_headers   | array[string] | False    |         |                | Client request headers to be sent to the authorization service. If not set, only the headers provided by APISIX are sent (for example, `X-Forwarded-XXX`). |\n| extra_headers   |object | False    |         |                | Extra headers to be sent to the authorization service passed in key-value format. The value can be a variable like `$request_uri`, `$post_arg.xyz` |\n| upstream_headers  | array[string] | False    |         |                | Authorization service response headers to be forwarded to the Upstream. If not set, no headers are forwarded to the Upstream service.                      |\n| client_headers    | array[string] | False    |         |                | Authorization service response headers to be sent to the client when authorization fails. If not set, no headers will be sent to the client.               |\n| timeout           | integer       | False    | 3000ms  | [1, 60000]ms   | Timeout for the authorization service HTTP call.                                                                                                           |\n| keepalive         | boolean       | False    | true    |                | When set to `true`, keeps the connection alive for multiple requests.                                                                                      |\n| keepalive_timeout | integer       | False    | 60000ms | [1000, ...]ms  | Idle time after which the connection is closed.                                                                                                            |\n| keepalive_pool    | integer       | False    | 5       | [1, ...]ms     | Connection pool limit.                                                                                                                           |\n| allow_degradation | boolean       | False    | false   |                | When set to `true`, allows authentication to be skipped when authentication server is unavailable. |\n| status_on_error   | integer       | False    | 403     | [200,...,599]  | Sets the HTTP status that is returned to the client when there is a network error to the authorization service. The default status is “403” (HTTP Forbidden). |\n\n## Data definition\n\nAPISIX will generate and send the request headers listed below to the authorization service:\n\n| Scheme            | HTTP Method        | Host             | URI             | Source IP       |\n| ----------------- | ------------------ | ---------------- | --------------- | --------------- |\n| X-Forwarded-Proto | X-Forwarded-Method | X-Forwarded-Host | X-Forwarded-Uri | X-Forwarded-For |\n\n## Example usage\n\nFirst, you need to setup your external authorization service. The example below uses Apache APISIX's [serverless](./serverless.md) Plugin to mock the service:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/auth' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/auth\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\n                \"return function (conf, ctx)\n                    local core = require(\\\"apisix.core\\\");\n                    local authorization = core.request.header(ctx, \\\"Authorization\\\");\n                    if authorization == \\\"123\\\" then\n                        core.response.exit(200);\n                    elseif authorization == \\\"321\\\" then\n                        core.response.set_header(\\\"X-User-ID\\\", \\\"i-am-user\\\");\n                        core.response.exit(200);\n                    else core.response.set_header(\\\"Location\\\", \\\"http://example.com/auth\\\");\n                        core.response.exit(403);\n                    end\n                end\"\n            ]\n        }\n    }\n}'\n```\n\nNow you can configure the `forward-auth` Plugin to a specific Route:\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n    \"uri\": \"/headers\",\n    \"plugins\": {\n        \"forward-auth\": {\n            \"uri\": \"http://127.0.0.1:9080/auth\",\n            \"request_headers\": [\"Authorization\"],\n            \"upstream_headers\": [\"X-User-ID\"],\n            \"client_headers\": [\"Location\"]\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nNow if we send the authorization details in the request header:\n\n```shell\ncurl http://127.0.0.1:9080/headers -H 'Authorization: 123'\n```\n\n```\n{\n    \"headers\": {\n        \"Authorization\": \"123\",\n        \"Next\": \"More-headers\"\n    }\n}\n```\n\nThe authorization service response can also be forwarded to the Upstream:\n\n```shell\ncurl http://127.0.0.1:9080/headers -H 'Authorization: 321'\n```\n\n```\n{\n    \"headers\": {\n        \"Authorization\": \"321\",\n        \"X-User-ID\": \"i-am-user\",\n        \"Next\": \"More-headers\"\n    }\n}\n```\n\nWhen authorization fails, the authorization service can send custom response back to the user:\n\n```shell\ncurl -i http://127.0.0.1:9080/headers\n```\n\n```\nHTTP/1.1 403 Forbidden\nLocation: http://example.com/auth\n```\n\n### Using data from POST body to make decision on Authorization service\n\n::: note\nWhen the decision is to be made on the basis of POST body, then it is recommended to use `$post_arg.*` with `extra_headers` field and make the decision on Authorization service on basis of headers rather than using POST `request_method` to pass the entire request body to Authorization service.\n:::\n\nCreate a serverless function on the `/auth` route that checks for the presence of the `tenant_id` header and confirms its value. If present, the route responds with HTTP 200.. If `tenant_id` is missing, it returns HTTP 400 with an error message.\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/auth' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/auth\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\n                \"return function(conf, ctx)\n                 local core = require(\\\"apisix.core\\\")\n                 local tenant_id = core.request.header(ctx, \\\"tenant_id\\\")\n                 if tenant_id == \\\"123\\\" then\n                     core.response.set_header(\\\"X-User-ID\\\", \\\"i-am-an-user\\\");\n                     core.response.exit(200);\n                else\n                    core.response.exit(400, \\\"tenant_id is \\\"..tenant_id .. \\\" but expected 123\\\");\n                end\n            end\"\n            ]\n        }\n    }\n}'\n```\n\nCreate a route that accepts POST requests and uses the `forward-auth` plugin to call the auth endpoint with the `tenant_id` from the request. The request is forwarded to the upstream service only if the auth check returns 200.\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n    \"uri\": \"/post\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n        \"forward-auth\": {\n            \"uri\": \"http://127.0.0.1:9080/auth\",\n            \"request_method\": \"GET\",\n            \"extra_headers\": {\"tenant_id\": \"$post_arg.tenant_id\"}\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nSend a POST request with the `tenant_id` header:\n\n```shell\ncurl -i http://127.0.0.1:9080/post -H \"Content-Type: application/json\" -X POST -d '{\n   \"tenant_id\": \"123\"\n}'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\n   \\\"tenant_id\\\": \\\"123\\\"\\n}\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"25\",\n    \"Content-Type\": \"application/json\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.13.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-687775d8-6890073173b30c2834901e8b\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": {\n    \"tenant_id\": \"123\"\n  },\n  \"origin\": \"127.0.0.1, 106.215.82.114\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\nSend a POST request with wrong the `tenant_id` header:\n\n```shell\ncurl -i http://127.0.0.1:9080/post -H \"Content-Type: application/json\" -X POST -d '{\n   \"tenant_id\": \"asdfasd\"\n}'\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` response with the following message:\n\n```shell\ntenant_id is asdfasd but expected 123\n```\n\n## Delete Plugin\n\nTo remove the `forward-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/gm.md",
    "content": "---\ntitle: GM\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - GM\ndescription: This article introduces the basic information and usage of the Apache APISIX `gm` plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n:::info\nThe function usage scenarios introduced in this article are mainly in China, so this article only has a Chinese version temporarily. You can click [here](https://apisix.apache.org/zh/docs/apisix/plugins/gm/) for more details. If you are interested in this feature, welcome to translate this document.\n:::\n"
  },
  {
    "path": "docs/en/latest/plugins/google-cloud-logging.md",
    "content": "---\ntitle: google-cloud-logging\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Google Cloud logging\ndescription: This document contains information about the Apache APISIX google-cloud-logging Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `google-cloud-logging` Plugin is used to send APISIX access logs to [Google Cloud Logging Service](https://cloud.google.com/logging/).\n\nThis plugin also allows to push logs as a batch to your Google Cloud Logging Service. It might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name                    | Required | Default                                                                                                                                                                                              | Description                                                                                                                                                        |\n|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| auth_config             | True     |                                                                                                                                                                                                      | Either `auth_config` or `auth_file` must be provided.                                                                                                              |\n| auth_config.client_email | True     |                                                                                                                                                                                                    | Email address of the Google Cloud service account.                                                                                                                   |\n| auth_config.private_key | True     |                                                                                                                                                                                                      | Private key of the Google Cloud service account.                                                                                                                   |\n| auth_config.project_id  | True     |                                                                                                                                                                                                      | Project ID in the Google Cloud service account.                                                                                                                    |\n| auth_config.token_uri   | True    | https://oauth2.googleapis.com/token                                                                                                                                                                  | Token URI of the Google Cloud service account.                                                                                                                     |\n| auth_config.entries_uri | False    | https://logging.googleapis.com/v2/entries:write                                                                                                                                                      | Google Cloud Logging Service API.                                                                                                                                  |\n| auth_config.scope       | False    | [\"https://www.googleapis.com/auth/logging.read\", \"https://www.googleapis.com/auth/logging.write\", \"https://www.googleapis.com/auth/logging.admin\", \"https://www.googleapis.com/auth/cloud-platform\"] | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#logging). |\n| auth_config.scopes      | Deprecated    | [\"https://www.googleapis.com/auth/logging.read\", \"https://www.googleapis.com/auth/logging.write\", \"https://www.googleapis.com/auth/logging.admin\", \"https://www.googleapis.com/auth/cloud-platform\"] | Access scopes of the Google Cloud service account. Use `auth_config.scope` instead.                                                                           |\n| auth_file               | True     |                                                                                                                                                                                                      | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided.                                           |\n| ssl_verify              | False    | true                                                                                                                                                                                                 | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).                  |\n| resource                | False    | {\"type\": \"global\"}                                                                                                                                                                                   | Google monitor resource. See [MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource) for more details.                   |\n| log_id                  | False    | apisix.apache.org%2Flogs                                                                                                                                                                             | Google Cloud logging ID. See [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) for details.                                          |\n| log_format              | False    |                            | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\nNOTE: `encrypt_fields = {\"auth_config.private_key\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n    \"insertId\": \"0013a6afc9c281ce2e7f413c01892bdc\",\n    \"labels\": {\n        \"source\": \"apache-apisix-google-cloud-logging\"\n    },\n    \"logName\": \"projects/apisix/logs/apisix.apache.org%2Flogs\",\n    \"httpRequest\": {\n        \"requestMethod\": \"GET\",\n        \"requestUrl\": \"http://localhost:1984/hello\",\n        \"requestSize\": 59,\n        \"responseSize\": 118,\n        \"status\": 200,\n        \"remoteIp\": \"127.0.0.1\",\n        \"serverIp\": \"127.0.0.1:1980\",\n        \"latency\": \"0.103s\"\n    },\n    \"resource\": {\n        \"type\": \"global\"\n    },\n    \"jsonPayload\": {\n        \"service_id\": \"\",\n        \"route_id\": \"1\"\n    },\n    \"timestamp\": \"2024-01-06T03:34:45.065Z\"\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `google-cloud-logging` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/google-cloud-logging -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```json\n{\"partialSuccess\":false,\"entries\":[{\"jsonPayload\":{\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"},\"resource\":{\"type\":\"global\"},\"insertId\":\"942e81f60b9157f0d46bc9f5a8f0cc40\",\"logName\":\"projects/apisix/logs/apisix.apache.org%2Flogs\",\"timestamp\":\"2023-01-09T14:47:25+08:00\",\"labels\":{\"source\":\"apache-apisix-google-cloud-logging\"}}]}\n```\n\n## Enable Plugin\n\n### Full configuration\n\nThe example below shows a complete configuration of the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"google-cloud-logging\": {\n            \"auth_config\":{\n                \"project_id\":\"apisix\",\n                \"client_email\":\"your service account email@apisix.iam.gserviceaccount.com\",\n                \"private_key\":\"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----\",\n                \"token_uri\":\"https://oauth2.googleapis.com/token\",\n                \"scope\":[\n                    \"https://www.googleapis.com/auth/logging.admin\"\n                ],\n                \"entries_uri\":\"https://logging.googleapis.com/v2/entries:write\"\n            },\n            \"resource\":{\n                \"type\":\"global\"\n            },\n            \"log_id\":\"apisix.apache.org%2Flogs\",\n            \"inactive_timeout\":10,\n            \"max_retry_count\":0,\n            \"buffer_duration\":60,\n            \"retry_delay\":1,\n            \"batch_max_size\":1\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n### Minimal configuration\n\nThe example below shows a bare minimum configuration of the Plugin on a Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"google-cloud-logging\": {\n            \"auth_config\":{\n                \"project_id\":\"apisix\",\n                \"client_email\":\"your service account email@apisix.iam.gserviceaccount.com\",\n                \"private_key\":\"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your Google Cloud Logging Service.\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\nYou can then login and view the logs in [Google Cloud Logging Service](https://console.cloud.google.com/logs/viewer).\n\n## Delete Plugin\n\nTo remove the `google-cloud-logging` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/grpc-transcode.md",
    "content": "---\ntitle: grpc-transcode\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - gRPC Transcode\n  - grpc-transcode\ndescription: This document contains information about the Apache APISIX grpc-transcode Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `grpc-transcode` Plugin converts between HTTP and gRPC requests.\n\nAPISIX takes in an HTTP request, transcodes it and forwards it to a gRPC service, gets the response and returns it back to the client in HTTP format.\n\n<!-- TODO: use an image here to explain the concept better -->\n\n## Attributes\n\n| Name      | Type                                                   | Required | Default | Description                          |\n| --------- | ------------------------------------------------------ | -------- | ------- | ------------------------------------ |\n| proto_id  | string/integer                                         | True     |         | id of the the proto content.         |\n| service   | string                                                 | True     |         | Name of the gRPC service.            |\n| method    | string                                                 | True     |         | Method name of the gRPC service.     |\n| deadline  | number                                                 | False    | 0       | Deadline for the gRPC service in ms. |\n| pb_option | array[string([pb_option_def](#options-for-pb_option))] | False    |         | protobuf options.                    |\n| show_status_in_body  | boolean                                     | False    | false   | Whether to display the parsed `grpc-status-details-bin` in the response body |\n| status_detail_type | string                                        | False    |         | The message type corresponding to the [details](https://github.com/googleapis/googleapis/blob/b7cb84f5d42e6dba0fdcc2d8689313f6a8c9d7b9/google/rpc/status.proto#L46) part of `grpc-status-details-bin`, if not specified, this part will not be decoded  |\n\n### Options for pb_option\n\n| Type            | Valid values                                                                              |\n|-----------------|-------------------------------------------------------------------------------------------|\n| enum as result  | `enum_as_name`, `enum_as_value`                                                           |\n| int64 as result | `int64_as_number`, `int64_as_string`, `int64_as_hexstring`                                |\n| default values  | `auto_default_values`, `no_default_values`, `use_default_values`, `use_default_metatable` |\n| hooks           | `enable_hooks`, `disable_hooks`                                                           |\n\n## Enable Plugin\n\nBefore enabling the Plugin, you have to add the content of your `.proto` or `.pb` files to APISIX.\n\nYou can use the `/admin/protos/id` endpoint and add the contents of the file to the `content` field:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"syntax = \\\"proto3\\\";\n    package helloworld;\n    service Greeter {\n        rpc SayHello (HelloRequest) returns (HelloReply) {}\n    }\n    message HelloRequest {\n        string name = 1;\n    }\n    message HelloReply {\n        string message = 1;\n    }\"\n}'\n```\n\nIf your proto file contains imports, or if you want to combine multiple proto files, you can generate a `.pb` file and use it in APISIX.\n\nFor example, if we have a file called `proto/helloworld.proto` which imports another proto file:\n\n```proto\nsyntax = \"proto3\";\n\npackage helloworld;\nimport \"proto/import.proto\";\n...\n```\n\nWe first generate a `.pb` file from the proto files:\n\n```shell\nprotoc --include_imports --descriptor_set_out=proto.pb proto/helloworld.proto\n```\n\nThe output binary file, `proto.pb` will contain both `helloworld.proto` and `import.proto`.\n\nWe can now use the content of `proto.pb` in the `content` field of the API request.\n\nAs the content of the proto is binary, we encode it in `base64` and configure the content in APISIX:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"'\"$(base64 -w0 /path/to/proto.pb)\"'\"\n}'\n```\n\nYou should see an `HTTP/1.1 201 Created` response with the following:\n\n```\n{\"node\":{\"value\":{\"create_time\":1643879753,\"update_time\":1643883085,\"content\":\"CmgKEnByb3RvL2ltcG9ydC5wcm90bxIDcGtnIhoKBFVzZXISEgoEbmFtZRgBIAEoCVIEbmFtZSIeCghSZXNwb25zZRISCgRib2R5GAEgASgJUgRib2R5QglaBy4vcHJvdG9iBnByb3RvMwq9AQoPcHJvdG8vc3JjLnByb3RvEgpoZWxsb3dvcmxkGhJwcm90by9pbXBvcnQucHJvdG8iPAoHUmVxdWVzdBIdCgR1c2VyGAEgASgLMgkucGtnLlVzZXJSBHVzZXISEgoEYm9keRgCIAEoCVIEYm9keTI5CgpUZXN0SW1wb3J0EisKA1J1bhITLmhlbGxvd29ybGQuUmVxdWVzdBoNLnBrZy5SZXNwb25zZSIAQglaBy4vcHJvdG9iBnByb3RvMw==\"},\"key\":\"\\/apisix\\/proto\\/1\"}}\n```\n\nNow, we can enable the `grpc-transcode` Plugin to a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/111 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/grpctest\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n            \"proto_id\": \"1\",\n            \"service\": \"helloworld.Greeter\",\n            \"method\": \"SayHello\"\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\n:::note\n\nThe Upstream service used here should be a gRPC service. Note that the `scheme` is set to `grpc`.\n\nYou can use the [grpc_server_example](https://github.com/api7/grpc_server_example) for testing.\n\n:::\n\n## Example usage\n\nOnce you configured the Plugin as mentioned above, you can make a request to APISIX to get a response back from the gRPC service (through APISIX):\n\n```shell\ncurl -i http://127.0.0.1:9080/grpctest?name=world\n```\n\nResponse:\n\n```shell\nHTTP/1.1 200 OK\nDate: Fri, 16 Aug 2019 11:55:36 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\nProxy-Connection: keep-alive\n\n{\"message\":\"Hello world\"}\n```\n\nYou can also configure the `pb_option` as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/23 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/zeebe/WorkflowInstanceCreate\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n            \"proto_id\": \"1\",\n            \"service\": \"gateway_protocol.Gateway\",\n            \"method\": \"CreateWorkflowInstance\",\n            \"pb_option\":[\"int64_as_string\"]\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:26500\": 1\n        }\n    }\n}'\n```\n\nNow if you check the configured Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/zeebe/WorkflowInstanceCreate?bpmnProcessId=order-process&version=1&variables=\\{\\\"orderId\\\":\\\"7\\\",\\\"ordervalue\\\":99\\}\"\n```\n\n```\nHTTP/1.1 200 OK\nDate: Wed, 13 Nov 2019 03:38:27 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\ngrpc-encoding: identity\ngrpc-accept-encoding: gzip\nServer: APISIX web server\nTrailer: grpc-status\nTrailer: grpc-message\n\n{\"workflowKey\":\"#2251799813685260\",\"workflowInstanceKey\":\"#2251799813688013\",\"bpmnProcessId\":\"order-process\",\"version\":1}\n```\n\n## Show `grpc-status-details-bin` in response body\n\nIf the gRPC service returns an error, there may be a `grpc-status-details-bin` field in the response header describing the error, which you can decode and display in the response body.\n\nUpload the proto file：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"syntax = \\\"proto3\\\";\n    package helloworld;\n    service Greeter {\n        rpc GetErrResp (HelloRequest) returns (HelloReply) {}\n    }\n    message HelloRequest {\n        string name = 1;\n        repeated string items = 2;\n    }\n    message HelloReply {\n        string message = 1;\n        repeated string items = 2;\n    }\"\n}'\n```\n\nEnable the `grpc-transcode` plugin，and set the option `show_status_in_body` to `true`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/grpctest\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n         \"proto_id\": \"1\",\n         \"service\": \"helloworld.Greeter\",\n         \"method\": \"GetErrResp\",\n         \"show_status_in_body\": true\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\nAccess the route configured above：\n\n```shell\ncurl -i http://127.0.0.1:9080/grpctest?name=world\n```\n\nResponse:\n\n```Shell\nHTTP/1.1 503 Service Temporarily Unavailable\nDate: Wed, 10 Aug 2022 08:59:46 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\nServer: APISIX web server\n\n{\"error\":{\"details\":[{\"type_url\":\"type.googleapis.com\\/helloworld.ErrorDetail\",\"value\":\"\\b\\u0001\\u0012\\u001cThe server is out of service\\u001a\\u0007service\"}],\"message\":\"Out of service\",\"code\":14}}\n```\n\nNote that there is an undecoded field in the return body. If you need to decode the field, you need to add the `message type` of the field in the uploaded proto file.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"syntax = \\\"proto3\\\";\n    package helloworld;\n    service Greeter {\n        rpc GetErrResp (HelloRequest) returns (HelloReply) {}\n    }\n    message HelloRequest {\n        string name = 1;\n        repeated string items = 2;\n    }\n    message HelloReply {\n        string message = 1;\n        repeated string items = 2;\n    }\n    message ErrorDetail {\n        int64 code = 1;\n        string message = 2;\n        string type = 3;\n    }\"\n}'\n```\n\nAlso configure the option `status_detail_type` to `helloworld.ErrorDetail`.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/grpctest\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n         \"proto_id\": \"1\",\n         \"service\": \"helloworld.Greeter\",\n         \"method\": \"GetErrResp\",\n         \"show_status_in_body\": true,\n         \"status_detail_type\": \"helloworld.ErrorDetail\"\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\nThe fully decoded result is returned.\n\n```Shell\nHTTP/1.1 503 Service Temporarily Unavailable\nDate: Wed, 10 Aug 2022 09:02:46 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\nServer: APISIX web server\n\n{\"error\":{\"details\":[{\"type\":\"service\",\"message\":\"The server is out of service\",\"code\":1}],\"message\":\"Out of service\",\"code\":14}}\n```\n\n## Delete Plugin\n\nTo remove the `grpc-transcode` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/111 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/grpctest\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/grpc-web.md",
    "content": "---\ntitle: grpc-web\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - gRPC Web\n  - grpc-web\ndescription: This document contains information about the Apache APISIX grpc-web Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `grpc-web` Plugin is a proxy Plugin that can process [gRPC Web](https://github.com/grpc/grpc-web) requests from JavaScript clients to a gRPC service.\n\n## Attributes\n\n| Name                    | Type    | Required | Default                                 | Description                                                                                              |\n|-------------------------|---------|----------|-----------------------------------------|----------------------------------------------------------------------------------------------------------|\n| cors_allow_headers      | string  | False    | \"content-type,x-grpc-web,x-user-agent\"  | Headers in the request allowed when accessing a cross-origin resource. Use `,` to add multiple headers.  |\n\n## Enable Plugin\n\nYou can enable the `grpc-web` Plugin on a specific Route as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\":\"/grpc/web/*\",\n    \"plugins\":{\n        \"grpc-web\":{}\n    },\n    \"upstream\":{\n        \"scheme\":\"grpc\",\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    }\n}'\n```\n\n## Example usage\n\nRefer to [gRPC-Web Client Runtime Library](https://www.npmjs.com/package/grpc-web) or [Apache APISIX gRPC Web Test Framework](https://github.com/apache/apisix/tree/master/t/plugin/grpc-web) to learn how to setup your web client.\n\nOnce you have your gRPC Web client running, you can make a request to APISIX from the browser or through Node.js.\n\n:::note\n\nThe supported request methods are `POST` and `OPTIONS`. See [CORS support](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support).\n\nThe supported `Content-Type` includes `application/grpc-web`, `application/grpc-web-text`, `application/grpc-web+proto`, and `application/grpc-web-text+proto`. See [Protocol differences vs gRPC over HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2).\n\n:::\n\n## Delete Plugin\n\nTo remove the `grpc-web` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\":\"/grpc/web/*\",\n    \"plugins\":{},\n    \"upstream\":{\n        \"scheme\":\"grpc\",\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/gzip.md",
    "content": "---\ntitle: gzip\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - gzip\ndescription: This document contains information about the Apache APISIX gzip Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `gzip` Plugin dynamically sets the behavior of [gzip in Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/compression/).\nWhen the `gzip` plugin is enabled, the client needs to include `Accept-Encoding: gzip` in the request header to indicate support for gzip compression. Upon receiving the request, APISIX dynamically determines whether to compress the response content based on the client's support and server configuration. If the conditions are met, `APISIX` adds the `Content-Encoding: gzip` header to the response, indicating that the response content has been compressed using gzip. Upon receiving the response, the client uses the corresponding decompression algorithm based on the `Content-Encoding` header to decompress the response content and obtain the original response content.\n\n:::info IMPORTANT\n\nThis Plugin requires APISIX to run on [APISIX-Runtime](../FAQ.md#how-do-i-build-the-apisix-runtime-environment).\n\n:::\n\n## Attributes\n\n| Name           | Type                 | Required | Default       | Valid values | Description                                                                             |\n|----------------|----------------------|----------|---------------|--------------|-----------------------------------------------------------------------------------------|\n| types          | array[string] or \"*\" | False    | [\"text/html\"] |              | Dynamically sets the `gzip_types` directive. Special value `\"*\"` matches any MIME type. |\n| min_length     | integer              | False    | 20            | >= 1         | Dynamically sets the `gzip_min_length` directive.                                       |\n| comp_level     | integer              | False    | 1             | [1, 9]       | Dynamically sets the `gzip_comp_level` directive.                                       |\n| http_version   | number               | False    | 1.1           | 1.1, 1.0     | Dynamically sets the `gzip_http_version` directive.                                     |\n| buffers.number | integer              | False    | 32            | >= 1         | Dynamically sets the `gzip_buffers` directive parameter `number`.                                          |\n| buffers.size   | integer              | False    | 4096          | >= 1         | Dynamically sets the `gzip_buffers` directive parameter `size`. The unit is in bytes.                                          |\n| vary           | boolean              | False    | false         |              | Dynamically sets the `gzip_vary` directive.                                             |\n\n## Enable Plugin\n\nThe example below enables the `gzip` Plugin on the specified Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"gzip\": {\n            \"buffers\": {\n                \"number\": 8\n            }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can make a request as shown below:\n\n```shell\ncurl http://127.0.0.1:9080/index.html -i -H \"Accept-Encoding: gzip\"\n```\n\n```\nHTTP/1.1 404 Not Found\nContent-Type: text/html; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Wed, 21 Jul 2021 03:52:55 GMT\nServer: APISIX/2.7\nContent-Encoding: gzip\n\nWarning: Binary output can mess up your terminal. Use \"--output -\" to tell\nWarning: curl to output it to your terminal anyway, or consider \"--output\nWarning: <FILE>\" to save to a file.\n```\n\n## Delete Plugin\n\nTo remove the `gzip` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/hmac-auth.md",
    "content": "---\ntitle: hmac-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - HMAC Authentication\n  - hmac-auth\ndescription: The hmac-auth Plugin supports HMAC authentication to ensure request integrity, preventing modifications during transmission and enhancing API security.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `hmac-auth` Plugin supports HMAC (Hash-based Message Authentication Code) authentication as a mechanism to ensure the integrity of requests, preventing them from being modified during transmissions. To use the Plugin, you would configure HMAC secret keys on [Consumers](../terminology/consumer.md) and enable the Plugin on Routes or Services.\n\nWhen a Consumer is successfully authenticated, APISIX adds additional headers, such as `X-Consumer-Username`, `X-Credential-Indentifier`, and other Consumer custom headers if configured, to the request, before proxying it to the Upstream service. The Upstream service will be able to differentiate between consumers and implement additional logics as needed. If any of these values is not available, the corresponding header will not be added.\n\nOnce enabled, the Plugin verifies the HMAC signature in the request's `Authorization` header and check that incoming requests are from trusted sources. Specifically, when APISIX receives an HMAC-signed request, the key ID is extracted from the `Authorization` header. APISIX then retrieves the corresponding Consumer configuration, including the secret key. If the key ID is valid and exists, APISIX generates an HMAC signature using the request's `Date` header and the secret key. If this generated signature matches the signature provided in the `Authorization` header, the request is authenticated and forwarded to Upstream services.\n\nThe Plugin implementation is based on [draft-cavage-http-signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt).\n\n## Attributes\n\nThe following attributes are available for configurations on Consumers or Credentials.\n\n| Name                  | Type          | Required | Default       | Valid values                                | Description                                                                                                                                                                                               |\n|-----------------------|---------------|----------|---------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| key_id            | string        | True     |               |                                             | Unique identifier for the Consumer, which identifies the associated configurations such as the secret key.                                                                                              |\n| secret_key            | string        | True     |               |                                             | Secret key used to generate an HMAC. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource.                                             |\n\nThe following attributes are available for configurations on Routes or Services.\n\n| Name                  | Type          | Required | Default       | Valid values                                | Description                                                                                                                                                                                               |\n|-----------------------|---------------|----------|---------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| allowed_algorithms             | array[string]        | False    | [\"hmac-sha1\",\"hmac-sha256\",\"hmac-sha512\"] | combination of \"hmac-sha1\",\"hmac-sha256\",and \"hmac-sha512\" | The list of HMAC algorithms allowed.                                                                                                                                                                                |\n| clock_skew            | integer       | False    | 300             |                 >=1                          | Maximum allowable time difference in seconds between the client request's timestamp and APISIX server's current time. This helps account for discrepancies in time synchronization between the client’s and server’s clocks and protect against replay attacks. The timestamp in the Date header (must be in GMT format) will be used for the calculation.        |\n| signed_headers        | array[string] | False    |               |                                             | The list of HMAC-signed headers that should be included in the client request's HMAC signature.  |\n| validate_request_body | boolean       | False    | false         |                              | If true, validate the integrity of the request body to ensure it has not been tampered with during transmission. Specifically, the Plugin creates a SHA-256 base64-encoded digest and compare it to the `Digest` header. If the `Digest` header is missing or if the digests do not match, the validation fails.                          |\n| hide_credentials | boolean       | False    | false         |                              | If true, do not pass the authorization request header to Upstream services.                        |\n| anonymous_consumer | string    | False    |          |                              | Anonymous Consumer name. If configured, allow anonymous users to bypass the authentication.                        |\n| realm              | string    | False    | hmac     |                              | The realm to include in the `WWW-Authenticate` header when authentication fails.                                                                                             |\n\nNOTE: `encrypt_fields = {\"secret_key\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\n## Examples\n\nThe examples below demonstrate how you can work with the `hmac-auth` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Implement HMAC Authentication on a Route\n\nThe following example demonstrates how to implement HMAC authentications on a route. You will also attach a Consumer custom ID to authenticated request in the `Consumer-Custom-Id` header, which can be used to implement additional logics as needed.\n\nCreate a Consumer `john` with a custom ID label:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\nCreate `hmac-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with the `hmac-auth` Plugin using its default configurations:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/get\",\n    \"methods\": [\"GET\"],\n    \"plugins\": {\n      \"hmac-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nGenerate a signature. You can use the below Python snippet or other stack of your choice:\n\n```python title=\"hmac-sig-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                # key id\nsecret_key = b\"john-secret-key\"    # secret key\nrequest_method = \"GET\"             # HTTP method\nrequest_path = \"/get\"              # Route URI\nalgorithm= \"hmac-sha256\"           # can use other algorithms in allowed_algorithms\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s)\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n  f\"{key_id}\\n\"\n  f\"{request_method} {request_path}\\n\"\n  f\"date: {gmt_time}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# construct the request headers\nheaders = {\n  \"Date\": gmt_time,\n  \"Authorization\": (\n    f'Signature keyId=\"{key_id}\",algorithm=\"{algorithm}\",'\n    f'headers=\"@request-target date\",'\n    f'signature=\"{signature_base64}\"'\n  )\n}\n\n# print headers\nprint(headers)\n```\n\nRun the script:\n\n```shell\npython3 hmac-sig-header-gen.py\n```\n\nYou should see the request headers printed:\n\n```text\n{'Date': 'Fri, 06 Sep 2024 06:41:29 GMT', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"'}\n```\n\nUsing the headers generated, send a request to the route:\n\n```shell\ncurl -X GET \"http://127.0.0.1:9080/get\" \\\n  -H \"Date: Fri, 06 Sep 2024 06:41:29 GMT\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"'\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Signature keyId=\\\"john-key\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date\\\",signature=\\\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\\\"\",\n    \"Date\": \"Fri, 06 Sep 2024 06:41:29 GMT\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d96513-2e52d4f35c9b6a2772d667ea\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 34.0.34.160\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n### Hide Authorization Information From Upstream\n\nAs seen the in the [last example](#implement-hmac-authentication-on-a-route), the `Authorization` header passed to the Upstream includes the signature and all other details. This could potentially introduce security risks.\n\nThe following example demonstrates how to prevent these information from being sent to the Upstream service.\n\nUpdate the Plugin configuration to set `hide_credentials` to `true`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/hmac-auth-route\" -X PATCH \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"plugins\": {\n    \"hmac-auth\": {\n      \"hide_credentials\": true\n    }\n  }\n}'\n```\n\nSend a request to the route:\n\n```shell\ncurl -X GET \"http://127.0.0.1:9080/get\" \\\n  -H \"Date: Fri, 06 Sep 2024 06:41:29 GMT\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"'\n```\n\nYou should see an `HTTP/1.1 200 OK` response and notice the `Authorization` header is entirely removed:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d96513-2e52d4f35c9b6a2772d667ea\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 34.0.34.160\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n### Enable Body Validation\n\nThe following example demonstrates how to enable body validation to ensure the integrity of the request body.\n\nCreate a Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `hmac-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with the `hmac-auth` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/post\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"validate_request_body\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nGenerate a signature. You can use the below Python snippet or other stack of your choice:\n\n```python title=\"hmac-sig-digest-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                 # key id\nsecret_key = b\"john-secret-key\"     # secret key\nrequest_method = \"POST\"             # HTTP method\nrequest_path = \"/post\"              # Route URI\nalgorithm= \"hmac-sha256\"            # can use other algorithms in allowed_algorithms\nbody = '{\"name\": \"world\"}'          # example request body\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s).\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n    f\"{key_id}\\n\"\n    f\"{request_method} {request_path}\\n\"\n    f\"date: {gmt_time}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# create the SHA-256 digest of the request body and base64 encode it\nbody_digest = hashlib.sha256(body.encode('utf-8')).digest()\nbody_digest_base64 = base64.b64encode(body_digest).decode('utf-8')\n\n# construct the request headers\nheaders = {\n    \"Date\": gmt_time,\n    \"Digest\": f\"SHA-256={body_digest_base64}\",\n    \"Authorization\": (\n        f'Signature keyId=\"{key_id}\",algorithm=\"hmac-sha256\",'\n        f'headers=\"@request-target date\",'\n        f'signature=\"{signature_base64}\"'\n    )\n}\n\n# print headers\nprint(headers)\n```\n\nRun the script:\n\n```shell\npython3 hmac-sig-digest-header-gen.py\n```\n\nYou should see the request headers printed:\n\n```text\n{'Date': 'Fri, 06 Sep 2024 09:16:16 GMT', 'Digest': 'SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"'}\n```\n\nUsing the headers generated, send a request to the route:\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Date: Fri, 06 Sep 2024 09:16:16 GMT\" \\\n  -H \"Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"' \\\n  -d '{\"name\": \"world\"}'\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"{\\\"name\\\": \\\"world\\\"}\": \"\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Signature keyId=\\\"john-key\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date\\\",signature=\\\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\\\"\",\n    \"Content-Length\": \"17\",\n    \"Content-Type\": \"application/x-www-form-urlencoded\",\n    \"Date\": \"Fri, 06 Sep 2024 09:16:16 GMT\",\n    \"Digest\": \"SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d978c3-49f929ad5237da5340bbbeb4\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"origin\": \"192.168.65.1, 34.0.34.160\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\nIf you send a request without the digest or with an invalid digest:\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Date: Fri, 06 Sep 2024 09:16:16 GMT\" \\\n  -H \"Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"' \\\n  -d '{\"name\": \"world\"}'\n```\n\nYou should see an `HTTP/1.1 401 Unauthorized` response with the following message:\n\n```text\n{\"message\":\"client request can't be validated\"}\n```\n\n### Mandate Signed Headers\n\nThe following example demonstrates how you can mandate certain headers to be signed in the request's HMAC signature.\n\nCreate a Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `hmac-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with the `hmac-auth` Plugin which requires three headers to be present in the HMAC signature:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/get\",\n    \"methods\": [\"GET\"],\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"signed_headers\": [\"date\",\"x-custom-header-a\",\"x-custom-header-b\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nGenerate a signature. You can use the below Python snippet or other stack of your choice:\n\n```python title=\"hmac-sig-req-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                # key id\nsecret_key = b\"john-secret-key\"    # secret key\nrequest_method = \"GET\"             # HTTP method\nrequest_path = \"/get\"              # Route URI\nalgorithm= \"hmac-sha256\"           # can use other algorithms in allowed_algorithms\ncustom_header_a = \"hello123\"       # required custom header\ncustom_header_b = \"world456\"       # required custom header\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s)\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n    f\"{key_id}\\n\"\n    f\"{request_method} {request_path}\\n\"\n    f\"date: {gmt_time}\\n\"\n    f\"x-custom-header-a: {custom_header_a}\\n\"\n    f\"x-custom-header-b: {custom_header_b}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# construct the request headers\nheaders = {\n    \"Date\": gmt_time,\n    \"Authorization\": (\n        f'Signature keyId=\"{key_id}\",algorithm=\"hmac-sha256\",'\n        f'headers=\"@request-target date x-custom-header-a x-custom-header-b\",'\n        f'signature=\"{signature_base64}\"'\n    ),\n    \"x-custom-header-a\": custom_header_a,\n    \"x-custom-header-b\": custom_header_b\n}\n\n# print headers\nprint(headers)\n```\n\nRun the script:\n\n```shell\npython3 hmac-sig-req-header-gen.py\n```\n\nYou should see the request headers printed:\n\n```text\n{'Date': 'Fri, 06 Sep 2024 09:58:49 GMT', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"', 'x-custom-header-a': 'hello123', 'x-custom-header-b': 'world456'}\n```\n\nUsing the headers generated, send a request to the route:\n\n```shell\ncurl -X GET \"http://127.0.0.1:9080/get\" \\\n     -H \"Date: Fri, 06 Sep 2024 09:58:49 GMT\" \\\n     -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"' \\\n     -H \"x-custom-header-a: hello123\" \\\n     -H \"x-custom-header-b: world456\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Signature keyId=\\\"john-key\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\\\"\",\n    \"Date\": \"Fri, 06 Sep 2024 09:58:49 GMT\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d98196-64a58db25ece71c077999ecd\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Custom-Header-A\": \"hello123\",\n    \"X-Custom-Header-B\": \"world456\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 103.97.2.206\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n### Rate Limit with Anonymous Consumer\n\nThe following example demonstrates how you can configure different rate limiting policies by regular and anonymous consumers, where the anonymous Consumer does not need to authenticate and has less quotas.\n\nCreate a regular Consumer `john` and configure the `limit-count` Plugin to allow for a quota of 3 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate the `hmac-auth` Credential for the Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\nCreate an anonymous user `anonymous` and configure the `limit-count` Plugin to allow for a quota of 1 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate a Route and configure the `hmac-auth` Plugin to accept anonymous Consumer `anonymous` from bypassing the authentication:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/get\",\n    \"methods\": [\"GET\"],\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nGenerate a signature. You can use the below Python snippet or other stack of your choice:\n\n```python title=\"hmac-sig-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                # key id\nsecret_key = b\"john-secret-key\"    # secret key\nrequest_method = \"GET\"             # HTTP method\nrequest_path = \"/get\"              # Route URI\nalgorithm= \"hmac-sha256\"           # can use other algorithms in allowed_algorithms\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s)\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n  f\"{key_id}\\n\"\n  f\"{request_method} {request_path}\\n\"\n  f\"date: {gmt_time}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# construct the request headers\nheaders = {\n  \"Date\": gmt_time,\n  \"Authorization\": (\n    f'Signature keyId=\"{key_id}\",algorithm=\"{algorithm}\",'\n    f'headers=\"@request-target date\",'\n    f'signature=\"{signature_base64}\"'\n  )\n}\n\n# print headers\nprint(headers)\n```\n\nRun the script:\n\n```shell\npython3 hmac-sig-header-gen.py\n```\n\nYou should see the request headers printed:\n\n```text\n{'Date': 'Mon, 21 Oct 2024 17:31:18 GMT', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"ztFfl9w7LmCrIuPjRC/DWSF4gN6Bt8dBBz4y+u1pzt8=\"'}\n```\n\nTo verify, send five consecutive requests with the generated headers:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H \"Date: Mon, 21 Oct 2024 17:31:18 GMT\" -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"ztFfl9w7LmCrIuPjRC/DWSF4gN6Bt8dBBz4y+u1pzt8=\"' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 5 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    3, 429:    2\n```\n\nSend five anonymous requests:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that only one request was successful:\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/http-dubbo.md",
    "content": "---\ntitle: http-dubbo\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - http-dubbo\n  - http to dubbo\n  - transcode\ndescription: This document contains information about the Apache APISIX http-dubbo Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `http-dubbo` plugin can transcode between http and Dubbo (Note: in\nDubbo 2.x, the serialization type of the upstream service must be fastjson).\n\n## Attributes\n\n| Name                     | Type    | Required | Default | Valid values | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n|--------------------------|---------|----------|---------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| service_name             | string  | True     |         |              | Dubbo service name                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |\n| service_version          | string  | False    | 0.0.0   |              | Dubbo service version                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |\n| method                   | string  | True     |         |              | Dubbo service method name                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |\n| params_type_desc         | string  | True     |         |              | Description of the Dubbo service method signature                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |\n| serialization_header_key | string  | False    |         |              | If `serialization_header_key` is set, the plugin will read this request header to determine if the body has already been serialized according to the Dubbo protocol. If the value of this request header is true, the plugin will not modify the body content and will directly consider it as Dubbo request parameters. If it is false, the developer is required to pass parameters in the format of Dubbo's generic invocation, and the plugin will handle serialization. Note: Due to differences in precision between Lua and Java, serialization by the plugin may lead to parameter precision discrepancies. |\n| serialized               | boolean | False    | false   | [true, false]  | Same as `serialization_header_key`. Priority is lower than `serialization_header_key`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n| connect_timeout          | number  | False    | 6000    |              | Upstream tcp connect timeout                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| read_timeout             | number  | False    | 6000    |              | Upstream tcp read_timeout                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |\n| send_timeout             | number  | False    | 6000    |              | Upstream tcp send_timeout                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |\n\n## Enable Plugin\n\nThe example below enables the `http-dubbo` Plugin on the specified Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/TestService/testMethod\",\n    \"plugins\": {\n      \"http-dubbo\": {\n      \"method\": \"testMethod\",\n      \"params_type_desc\": \"Ljava/lang/Long;Ljava/lang/Integer;\",\n      \"serialized\": true,\n      \"service_name\": \"com.xxx.xxx.TestService\",\n      \"service_version\": \"0.0.0\"\n    }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:20880\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can make a request as shown below:\n\n```shell\ncurl --location 'http://127.0.0.1:9080/TestService/testMethod' \\\n--data '1\n2'\n```\n\n## How to Get `params_type_desc`\n\n```java\nMethod[] declaredMethods = YourService.class.getDeclaredMethods();\nString params_type_desc = ReflectUtils.getDesc(Arrays.stream(declaredMethods).filter(it -> it.getName().equals(\"yourmethod\")).findAny().get().getParameterTypes());\n\n// If there are method overloads, you need to find the method you want to expose.\n// ReflectUtils is a Dubbo implementation.\n```\n\n## How to Serialize JSON According to Dubbo Protocol\n\nTo prevent loss of precision, we recommend using pre-serialized bodies for requests. The serialization rules for Dubbo's\nfastjson are as follows:\n\n- Convert each parameter to a JSON string using toJSONString.\n- Separate each parameter with a newline character `\\n`.\n\nSome languages and libraries may produce unchanged results when calling toJSONString on strings or numbers. In such\ncases, you may need to manually handle some special cases. For example:\n\n- The string `abc\"` needs to be encoded as `\"abc\\\"\"`.\n- The string `123` needs to be encoded as `\"123\"`.\n\nAbstract class, parent class, or generic type as input parameter signature, when the input parameter requires a specific\ntype. Serialization requires writing specific type information.\nRefer to [WriteClassName](https://github.com/alibaba/fastjson/wiki/SerializerFeature_cn) for more details.\n\n## Delete Plugin\n\nTo remove the `http-dubbo` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration.\nAPISIX will automatically reload and you do not have to restart for this to take effect.\n"
  },
  {
    "path": "docs/en/latest/plugins/http-logger.md",
    "content": "---\ntitle: http-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - HTTP Logger\ndescription: This document contains information about the Apache APISIX http-logger Plugin. Using this Plugin, you can push APISIX log data to HTTP or HTTPS servers.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `http-logger` Plugin is used to push log data requests to HTTP/HTTPS servers.\n\nThis will allow the ability to send log data requests as JSON objects to monitoring tools and other HTTP servers.\n\n## Attributes\n\n| Name                   | Type    | Required | Default       | Valid values         | Description                                                                                                                                                                                                              |\n| ---------------------- | ------- | -------- | ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| uri                    | string  | True     |               |                      | URI of the HTTP/HTTPS server.                                                                                                                                                                                            |\n| auth_header            | string  | False    |               |                      | Authorization headers if required.                                                                                                                                                                                       |\n| timeout                | integer | False    | 3             | [1,...]              | Time to keep the connection alive for after sending a request.                                                                                                                                                           |\n| log_format | object | False    |     |               | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| include_req_body       | boolean | False    | false         | [false, true]        | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitations.                                                         |\n| include_req_body_expr  | array   | False    |               |                      | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more. |\n| max_req_body_bytes     | integer | False    | 524288        |  >=1                 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean | False    | false         | [false, true]        | When set to `true` includes the response body in the log.                                                                                                                                                                |\n| include_resp_body_expr | array   | False    |               |                      | When the `include_resp_body` attribute is set to `true`, use this to filter based on [lua-resty-expr](https://github.com/api7/lua-resty-expr). If present, only logs the response if the expression evaluates to `true`. |\n| max_resp_body_bytes    | integer | False    | 524288        |  >=1                 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| concat_method          | string  | False    | \"json\"        | [\"json\", \"new_line\"] | Sets how to concatenate logs. When set to `json`, uses `json.encode` for all pending logs and when set to `new_line`, also uses `json.encode` but uses the newline (`\\n`) to concatenate lines.                          |\n| ssl_verify             | boolean | False    | false         | [false, true]        | When set to `true` verifies the SSL certificate.                                                                                                                                                                         |\n\n:::note\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n:::\n\n### Example of default log format\n\n  ```json\n  {\n    \"service_id\": \"\",\n    \"apisix_latency\": 100.99999809265,\n    \"start_time\": 1703907485819,\n    \"latency\": 101.99999809265,\n    \"upstream_latency\": 1,\n    \"client_ip\": \"127.0.0.1\",\n    \"route_id\": \"1\",\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"request\": {\n        \"headers\": {\n            \"host\": \"127.0.0.1:1984\",\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"user-agent\": \"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\",\n            \"content-length\": \"12\"\n        },\n        \"method\": \"POST\",\n        \"size\": 194,\n        \"url\": \"http://127.0.0.1:1984/hello?log_body=no\",\n        \"uri\": \"/hello?log_body=no\",\n        \"querystring\": {\n            \"log_body\": \"no\"\n        }\n    },\n    \"response\": {\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"content-length\": \"12\",\n            \"server\": \"APISIX/3.7.0\"\n        },\n        \"status\": 200,\n        \"size\": 123\n    },\n    \"upstream\": \"127.0.0.1:1982\"\n }\n  ```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `http-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/http-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"http-logger\": {\n                \"uri\": \"http://mockbin.org/bin/:ID\"\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\nAs an example the [mockbin](http://mockbin.org/bin/create) server is used for mocking an HTTP server to see the logs produced by APISIX.\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your mockbin server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo disable this Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/inspect.md",
    "content": "---\ntitle: inspect\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Inspect\n  - Dynamic Lua Debugging\ndescription: This document contains information about the Apache APISIX inspect Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nIt's useful to set arbitrary breakpoint in any Lua file to inspect the context information,\ne.g. print local variables if some condition satisfied.\n\nIn this way, you don't need to modify the source code of your project, and just get diagnose information\non demand, i.e. dynamic logging.\n\nThis plugin supports setting breakpoints within both interpretd function and jit compiled function.\nThe breakpoint could be at any position within the function. The function could be global/local/module/ananymous.\n\n## Features\n\n* Set breakpoint at any position\n* Dynamic breakpoint\n* customized breakpoint handler\n* You could define one-shot breakpoint\n* Work for jit compiled function\n* If function reference specified, then performance impact is only bound to that function (JIT compiled code will not trigger debug hook, so they would run fast even if hook is enabled)\n* If all breakpoints deleted, jit could recover\n\n## Operation Graph\n\n![Operation Graph](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/plugin/inspect.png)\n\n## API to define hook in hooks file\n\n### require(\"apisix.inspect.dbg\").set_hook(file, line, func, filter_func)\n\nThe breakpoint is specified by `file` (full qualified or short file name) and the `line` number.\n\nThe `func` specified the scope (which function or global) of jit cache to flush:\n\n* If the breakpoint is related to a module function or\nglobal function, you should set it that function reference, then only the jit cache of that function would\nbe flushed, and it would not affect other caches to avoid slowing down other parts of the program.\n\n* If the breakpointis related to local function or anonymous function,\nthen you have to set it to `nil` (because no way to get function reference), which would flush the whole jit cache of Lua vm.\n\nYou attach a `filter_func` function to the breakpoint. The function takes the `info` as an argument and returns\ntrue or false to determine whether the breakpoint would be removed. This allows you to set up a one-shot breakpoint\nat ease.\n\nThe `info` is a hash table which contains below keys:\n\n* `finfo`: `debug.getinfo(level, \"nSlf\")`\n* `uv`: upvalues hash table\n* `vals`: local variables hash table\n\n## Attributes\n\n| Name               | Type    | Required | Default | Description                                                                                    |\n|--------------------|---------|----------|---------|------------------------------------------------------------------------------------------------|\n| delay           | integer | False     | 3 | Time in seconds specifying how often to check the hooks file.                                       |\n| hooks_file           | string | False     | \"/usr/local/apisix/plugin_inspect_hooks.lua\"  | Lua file to define hooks, which could be a link file. Ensure only administrator could write this file, otherwise it may be a security risk. |\n\n## Enable Plugin\n\nPlugin is enabled by default:\n\n```yaml title=\"apisix/cli/config.lua\"\nlocal _M = {\n  plugins = {\n    \"inspect\",\n    ...\n  },\n  plugin_attr = {\n    inspect = {\n      delay = 3,\n      hooks_file = \"/usr/local/apisix/plugin_inspect_hooks.lua\"\n    },\n    ...\n  },\n  ...\n}\n```\n\n## Example usage\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```bash\n# create test route\ncurl http://127.0.0.1:9180/apisix/admin/routes/test_limit_req -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\": {\n        \"limit-req\": {\n            \"rate\": 100,\n            \"burst\": 0,\n            \"rejected_code\": 503,\n            \"key_type\": \"var\",\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n\n# create a hooks file to set a test breakpoint\n# Note that the breakpoint is associated with the line number,\n# so if the Lua code changes, you need to adjust the line number in the hooks file\ncat <<EOF >/usr/local/apisix/example_hooks.lua\nlocal dbg = require \"apisix.inspect.dbg\"\n\ndbg.set_hook(\"limit-req.lua\", 88, require(\"apisix.plugins.limit-req\").access, function(info)\n    ngx.log(ngx.INFO, debug.traceback(\"foo traceback\", 3))\n    ngx.log(ngx.INFO, dbg.getname(info.finfo))\n    ngx.log(ngx.INFO, \"conf_key=\", info.vals.conf_key)\n    return true\nend)\n\n--- more breakpoints could be defined via dbg.set_hook()\n--- ...\nEOF\n\n# enable the hooks file\nln -sf /usr/local/apisix/example_hooks.lua /usr/local/apisix/plugin_inspect_hooks.lua\n\n# check errors.log to confirm the test breakpoint is enabled\n2022/09/01 00:55:38 [info] 2754534#2754534: *3700 [lua] init.lua:29: setup_hooks(): set hooks: err=nil, hooks=[\"limit-req.lua#88\"], context: ngx.timer\n\n# access the test route\ncurl -i http://127.0.0.1:9080/get\n\n# check errors.log to confirm the test breakpoint is triggered\n2022/09/01 00:55:52 [info] 2754534#2754534: *4070 [lua] resty_inspect_hooks.lua:4: foo traceback\nstack traceback:\n        /opt/lua-resty-inspect/lib/resty/inspect/dbg.lua:50: in function </opt/lua-resty-inspect/lib/resty/inspect/dbg.lua:17>\n        /opt/apisix.fork/apisix/plugins/limit-req.lua:88: in function 'phase_func'\n        /opt/apisix.fork/apisix/plugin.lua:900: in function 'run_plugin'\n        /opt/apisix.fork/apisix/init.lua:456: in function 'http_access_phase'\n        access_by_lua(nginx.conf:303):2: in main chunk, client: 127.0.0.1, server: _, request: \"GET /get HTTP/1.1\", host: \"127.0.0.1:9080\"\n2022/09/01 00:55:52 [info] 2754534#2754534: *4070 [lua] resty_inspect_hooks.lua:5: /opt/apisix.fork/apisix/plugins/limit-req.lua:88 (phase_func), client: 127.0.0.1, server: _, request: \"GET /get HTTP/1.1\", host: \"127.0.0.1:9080\"\n2022/09/01 00:55:52 [info] 2754534#2754534: *4070 [lua] resty_inspect_hooks.lua:6: conf_key=remote_addr, client: 127.0.0.1, server: _, request: \"GET /get HTTP/1.1\", host: \"127.0.0.1:9080\"\n```\n\n## Delete Plugin\n\nTo remove the `inspect` Plugin, you can remove it from your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n    # - inspect\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ip-restriction.md",
    "content": "---\ntitle: ip-restriction\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - IP restriction\n  - ip-restriction\ndescription: The ip-restriction Plugin supports restricting access to upstream resources by IP addresses, through either configuring a whitelist or blacklist of IP addresses.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ip-restriction\" />\n</head>\n\n## Description\n\nThe `ip-restriction` Plugin supports restricting access to upstream resources by IP addresses, through either configuring a whitelist or blacklist of IP addresses. Restricting IP to resources helps prevent unauthorized access and harden API security.\n\n## Attributes\n\n| Name          | Type          | Required | Default                          | Valid values | Description                                                            |\n|---------------|---------------|----------|----------------------------------|--------------|------------------------------------------------------------------------|\n| whitelist     | array[string] | False    |                                  |              | List of IPs or CIDR ranges to whitelist.                               |\n| blacklist     | array[string] | False    |                                  |              | List of IPs or CIDR ranges to blacklist.                               |\n| message       | string        | False    | \"Your IP address is not allowed\" | [1, 1024]    | Message returned when the IP address is not allowed access.            |\n| response_code | integer       | False    | 403                              | [403, 404]   | HTTP response code returned when the IP address is not allowed access. |\n\n:::note\n\nAt least one of the `whitelist` or `blacklist` should be configured, but they cannot be configured at the same time.\n\n:::\n\n## Examples\n\nThe examples below demonstrate how you can configure the `ip-restriction` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Restrict Access by Whitelisting\n\nThe following example demonstrates how you can whitelist a list of IP addresses that should have access to the upstream resource and customize the error message for access denial.\n\nCreate a Route with the `ip-restriction` Plugin to whitelist a range of IPs and customize the error message when the access is denied:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ip-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ip-restriction\": {\n        \"whitelist\": [\n          \"192.168.0.1/24\"\n        ],\n        \"message\": \"Access denied\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nIf your IP is allowed, you should receive an `HTTP/1.1 200 OK` response. If not, you should receive an `HTTP/1.1 403 Forbidden` response with the following error message:\n\n```text\n{\"message\":\"Access denied\"}\n```\n\n### Restrict Access Using Modified IP\n\nThe following example demonstrates how you can modify the IP used for IP restriction, using the `real-ip` Plugin. This is particularly useful if APISIX is behind a reverse proxy and the real client IP is not available to APISIX.\n\nCreate a Route with the `ip-restriction` Plugin to whitelist a specific IP address and obtain client IP address from the URL parameter `realip`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ip-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ip-restriction\": {\n        \"whitelist\": [\n          \"192.168.1.241\"\n        ]\n      },\n      \"real-ip\": {\n        \"source\": \"arg_realip\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n      \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?realip=192.168.1.241\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nSend another request with a different IP address:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?realip=192.168.10.24\"\n```\n\nYou should receive an `HTTP/1.1 403 Forbidden` response.\n"
  },
  {
    "path": "docs/en/latest/plugins/jwe-decrypt.md",
    "content": "---\ntitle: jwe-decrypt\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - JWE Decrypt\n  - jwe-decrypt\ndescription: This document contains information about the Apache APISIX jwe-decrypt Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `jwe-decrypt` Plugin is used to decrypt [JWE](https://datatracker.ietf.org/doc/html/rfc7516) authorization headers in requests to an APISIX [Service](../terminology/service.md) or [Route](../terminology/route.md).\n\nThis Plugin adds an endpoint `/apisix/plugin/jwe/encrypt` for JWE encryption. For decryption, the key should be configured in [Consumer](../terminology/consumer.md).\n\n## Attributes\n\nFor Consumer:\n\n| Name          | Type    | Required                                              | Default | Valid values                | Description                                                                                                                                  |\n|---------------|---------|-------------------------------------------------------|---------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|\n| key           | string  | True                                                  |         |                             | Unique key for a Consumer.                                                                                                                   |\n| secret        | string  | True                                                 |         |                             | The decryption key. Must be 32 characters. The key could be saved in a secret manager using the [Secret](../terminology/secret.md) resource. |\n| is_base64_encoded | boolean | False                                                 | false   |                             | Set to true if the secret is base64 encoded.                                                                                                 |\n\n:::note\n\nAfter enabling `is_base64_encoded`, your `secret` length may exceed 32 chars. You only need to make sure that the length after decoding is still 32 chars.\n\n:::\n\nFor Route:\n\n| Name   | Type   | Required | Default       | Description                                                         |\n|--------|--------|----------|---------------|---------------------------------------------------------------------|\n| header | string | True    | Authorization | The header to get the token from.                                   |\n| forward_header | string | True     | Authorization  | Set the header name that passes the plaintext to the Upstream.   |\n| strict | boolean | False     | true  | If true, throw a 403 error if JWE token is missing from the request. If false, do not throw an error if JWE token cannot be found.  |\n\n## Example usage\n\nFirst, create a Consumer with `jwe-decrypt` and configure the decryption key:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"jwe-decrypt\": {\n            \"key\": \"user-key\",\n            \"secret\": \"-secret-length-must-be-32-chars-\"\n        }\n    }\n}'\n```\n\nNext, create a Route with `jwe-decrypt` enabled to decrypt the authorization header:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/anything*\",\n    \"plugins\": {\n        \"jwe-decrypt\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n\n### Encrypt Data with JWE\n\nThe Plugin creates an internal endpoint `/apisix/plugin/jwe/encrypt` to encrypt data with JWE. To expose it publicly, create a Route with the [public-api](public-api.md) Plugin:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/jwenew -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/plugin/jwe/encrypt\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\nSend a request to the endpoint passing the key configured in Consumer to the URI parameter to encrypt some sample data in the payload:\n\n```shell\ncurl -G --data-urlencode 'payload={\"uid\":10000,\"uname\":\"test\"}' 'http://127.0.0.1:9080/apisix/plugin/jwe/encrypt?key=user-key' -i\n```\n\nYou should see a response similar to the following, with the JWE encrypted data in the response body:\n\n```\nHTTP/1.1 200 OK\nDate: Mon, 25 Sep 2023 02:38:16 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.5.0\nApisix-Plugins: public-api\n\neyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.hfzMJ0YfmbMcJ0ojgv4PYAHxPjlgMivmv35MiA.7nilnBt2dxLR_O6kf-HQUA\n```\n\n### Decrypt Data with JWE\n\nSend a request to the route with the JWE encrypted data in the `Authorization` header:\n\n```shell\ncurl http://127.0.0.1:9080/anything/hello -H 'Authorization: eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.hfzMJ0YfmbMcJ0ojgv4PYAHxPjlgMivmv35MiA.7nilnBt2dxLR_O6kf-HQUA' -i\n```\n\nYou should see a response similar to the following, where the `Authorization` header shows the plaintext of the payload:\n\n```\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 452\nConnection: keep-alive\nDate: Mon, 25 Sep 2023 02:38:59 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/3.5.0\nApisix-Plugins: jwe-decrypt\n\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"{\\\"uid\\\":10000,\\\"uname\\\":\\\"test\\\"}\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.1.2\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6510f2c3-1586ec011a22b5094dbe1896\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 119.143.79.94\",\n  \"url\": \"http://127.0.0.1/anything/hello\"\n}\n```\n\n## Delete Plugin\n\nTo remove the `jwe-decrypt` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/anything*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/jwt-auth.md",
    "content": "---\ntitle: jwt-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - JWT Auth\n  - jwt-auth\ndescription: The jwt-auth Plugin supports the use of JSON Web Token (JWT) as a mechanism for clients to authenticate themselves before accessing Upstream resources.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/jwt-auth\" />\n</head>\n\n## Description\n\nThe `jwt-auth` Plugin supports the use of [JSON Web Token (JWT)](https://jwt.io/) as a mechanism for clients to authenticate themselves before accessing Upstream resources.\n\nOnce enabled, the Plugin exposes an endpoint to create JWT credentials by [Consumers](../terminology/consumer.md). The process generates a token that client requests should carry to identify themselves to APISIX. The token can be included in the request URL query string, request header, or cookie. APISIX will then verify the token to determine if a request should be allowed or denied to access Upstream resources.\n\nWhen a Consumer is successfully authenticated, APISIX adds additional headers, such as `X-Consumer-Username`, `X-Credential-Indentifier`, and other Consumer custom headers if configured, to the request, before proxying it to the Upstream service. The Upstream service will be able to differentiate between consumers and implement additional logics as needed. If any of these values is not available, the corresponding header will not be added.\n\n## Attributes\n\nFor Consumer/Credential:\n\n| Name          | Type    | Required                                              | Default | Valid values                | Description                                                                                                                                                                                 |\n|---------------|---------|-------------------------------------------------------|---------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| key           | string  | True                                                  |         |     non-empty       | Unique key for a Consumer.                                                                                                                                                                  |\n| secret        | string  | False                                                 |         |        non-empty        | Shared key used to sign and verify the JWT when the algorithm is symmetric. Required when using `HS256` or `HS512` as the algorithm. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource.       |\n| public_key    | string  | True if `RS256` or `ES256` is set for the `algorithm` attribute. |         |                             | RSA or ECDSA public key. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource.                      |\n| algorithm     | string  | False                                                 | HS256 | [\"HS256\", \"HS384\", \"HS512\", \"RS256\", \"RS384\", \"RS512\", \"ES256\", \"ES384\", \"ES512\", \"PS256\", \"PS384\", \"PS512\", \"EdDSA\"] | Encryption algorithm.                                                                                                                                                                       |\n| exp           | integer | False                                                 | 86400   | [1,...]                     | Expiry time of the token in seconds.                                                                                                                                                        |\n| base64_secret | boolean | False                                                 | false   |                             | Set to true if the secret is base64 encoded.                                                                                                                                                |\n| lifetime_grace_period | integer | False                                         | 0       | [0,...]                     | Grace period in seconds. Used to account for clock skew between the server generating the JWT and the server validating the JWT.  |\n| key_claim_name | string | False                                                 | key     |                             | The claim in the JWT payload that identifies the associated secret, such as `iss`. |\n\nNOTE: `encrypt_fields = {\"secret\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nFor Routes or Services:\n\n| Name   | Type   | Required | Default       | Description                                                         |\n|--------|--------|----------|---------------|---------------------------------------------------------------------|\n| header | string | False    | authorization | The header to get the token from.                                   |\n| query  | string | False    | jwt           | The query string to get the token from. Lower priority than header. |\n| cookie | string | False    | jwt           | The cookie to get the token from. Lower priority than query.        |\n| hide_credentials| boolean | False    | false  | If true, do not pass the header, query, or cookie with JWT to Upstream services.  |\n| key_claim_name  | string  | False    | key     | The name of the JWT claim that contains the user key (corresponds to Consumer's key attribute). |\n| anonymous_consumer | string | False  | false  | Anonymous Consumer name. If configured, allow anonymous users to bypass the authentication.   |\n| store_in_ctx     | boolean | False    | false   | Set to true will store the JWT payload in the request context (`ctx.jwt_auth_payload`). This allows lower-priority plugins that run afterwards on the same request to retrieve and use the JWT token. |\n| realm            | string  | False    | jwt     | The realm to include in the `WWW-Authenticate` header when authentication fails. |\n| claims_to_verify | array[string] | False | [\"exp\", \"nbf\"] | [\"exp\", \"nbf\"] | The claims that need to be verified in the JWT payload. |\n\nYou can implement `jwt-auth` with [HashiCorp Vault](https://www.vaultproject.io/) to store and fetch secrets and RSA keys pairs from its [encrypted KV engine](https://developer.hashicorp.com/vault/docs/secrets/kv) using the [APISIX Secret](../terminology/secret.md) resource.\n\n## Examples\n\nThe examples below demonstrate how you can work with the `jwt-auth` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Use JWT for Consumer Authentication\n\nThe following example demonstrates how to implement JWT for Consumer key authentication.\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `jwt-auth` Credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `jwt-auth` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo issue a JWT for `jack`, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), do the following:\n\n* Fill in `HS256` as the algorithm.\n* Update the secret in the __Valid secret__ section to be `jack-hs256-secret-that-is-very-long`.\n* Update payload with Consumer key `jack-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\nSend a request to the Route with the JWT in the `Authorization` header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: ${jwt_token}\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjY2NDk2NDAsImtleSI6ImphY2sta2V5In0.kdhumNWrZFxjUvYzWLt4lFr546PNsr9TXuf0Az5opoM\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66ea951a-4d740d724bd2a44f174d4daf\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-jwt-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\nSend a request with an invalid token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjY2NDk2NDAsImtleSI6ImphY2sta2V5In0.kdhumNWrZFxjU_random_random\"\n```\n\nYou should receive an `HTTP/1.1 401 Unauthorized` response similar to the following:\n\n```text\n{\"message\":\"failed to verify jwt\"}\n```\n\n### Carry JWT in Request Header, Query String, or Cookie\n\nThe following example demonstrates how to accept JWT in specified header, query string, and cookie.\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `jwt-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `jwt-auth` plugin, and specify the request parameters carrying the token:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"header\": \"jwt-auth-header\",\n        \"query\": \"jwt-query\",\n        \"cookie\": \"jwt-cookie\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo issue a JWT for `jack`, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), do the following:\n\n* Fill in `HS256` as the algorithm.\n* Update the secret in the __Valid secret__ section to be `jack-hs256-secret-that-is-very-long`.\n* Update payload with Consumer key `jack-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\n#### Verify With JWT in Header\n\nSending request with JWT in the header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"jwt-auth-header: ${jwt_token}\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"Jwt-Auth-Header\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\",\n    ...\n  },\n  ...\n}\n```\n\n#### Verify With JWT in Query String\n\nSending request with JWT in the query string:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get?jwt-query=${jwt_token}\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {\n    \"jwt-query\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    ...\n  },\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://127.0.0.1/get?jwt-query=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTY5NTEyOTA0NH0.EiktFX7di_tBbspbjmqDKoWAD9JG39Wo_CAQ1LZ9voQ\"\n}\n```\n\n#### Verify With JWT in Cookie\n\nSending request with JWT in the cookie:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" --cookie jwt-cookie=${jwt_token}\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Cookie\": \"jwt-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\",\n    ...\n  },\n  ...\n}\n```\n\n### Manage Secrets in Environment Variables\n\nThe following example demonstrates how to save `jwt-auth` Consumer key to an environment variable and reference it in configuration.\n\nAPISIX supports referencing system and user environment variables configured through the [NGINX `env` directive](https://nginx.org/en/docs/ngx_core_module.html#env).\n\nSave the key to an environment variable:\n\n```shell\nexport JACK_JWT_SECRET=jack-hs256-secret-that-is-very-long\n```\n\n:::tip\n\nIf you are running APISIX in Docker, you should set the environment variable using the `-e` flag when starting the container.\n\n:::\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `jwt-auth` Credential for the Consumer and reference the environment variable:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"$env://JACK_JWT_SECRET\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `jwt-auth` enabled:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo issue a JWT for `jack`, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), do the following:\n\n* Fill in `HS256` as the algorithm.\n* Update the secret in the __Valid secret__ section to be `jack-hs256-secret-that-is-very-long`.\n* Update payload with Consumer key `jack-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\nSending request with JWT in the header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Authorization: ${jwt_token}\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\n### Manage Secrets in Secret Manager\n\nThe following example demonstrates how to manage `jwt-auth` consumer key in [HashiCorp Vault](https://www.vaultproject.io) and reference it in plugin configuration.\n\nStart a Vault development server in Docker:\n\n```shell\ndocker run -d \\\n  --name vault \\\n  -p 8200:8200 \\\n  --cap-add IPC_LOCK \\\n  -e VAULT_DEV_ROOT_TOKEN_ID=root \\\n  -e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \\\n  vault:1.9.0 \\\n  vault server -dev\n```\n\nAPISIX currently supports [Vault KV engine version 1](https://developer.hashicorp.com/vault/docs/secrets/kv#kv-version-1). Enable it in Vault:\n\n```shell\ndocker exec -i vault sh -c \"VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv\"\n```\n\nYou should see a response similar to the following:\n\n```text\nSuccess! Enabled the kv secrets engine at: kv/\n```\n\nCreate a Secret and configure the Vault address and other connection information. Update the Vault address accordingly:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/secrets/vault/jwt\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"https://127.0.0.1:8200\",\n    \"prefix\": \"kv/apisix\",\n    \"token\": \"root\"\n  }'\n```\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `jwt-auth` Credential for the Consumer and reference the Secret:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jwt-vault-key\",\n        \"secret\": \"$secret://vault/jwt/jack/jwt-secret\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `jwt-auth` enabled:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSet `jwt-auth` key value to be `vault-hs256-secret-that-is-very-long` in Vault:\n\n```shell\ndocker exec -i vault sh -c \"VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jack jwt-secret=vault-hs256-secret-that-is-very-long\"\n```\n\nYou should see a response similar to the following:\n\n```text\nSuccess! Data written to: kv/apisix/jack\n```\n\nTo issue a JWT, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), do the following:\n\n* Fill in `HS256` as the algorithm.\n* Update the secret in the __Valid secret__ section to be `vault-hs256-secret-that-is-very-long`.\n* Update payload with consumer key `jwt-vault-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jwt-vault-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqd3QtdmF1bHQta2V5IiwibmJmIjoxNzI5MTMyMjcxfQ.i2pLj7QcQvnlSjB7iV5V522tIV43boQRtee7L0rwlkQ\n```\n\nSend a request with the token in the header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Authorization: ${jwt_token}\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\n### Sign JWT with RS256 Algorithm\n\nThe following example demonstrates how you can use asymmetric algorithms, such as RS256, to sign and validate JWT when implementing JWT for Consumer authentication. You will be generating RSA key pairs using [openssl](https://openssl-library.org/source/) and generating JWT using [JWT.io](https://jwt.io) to better understand the composition of JWT.\n\nGenerate a 2048-bit RSA private key and extract the corresponding public key in PEM format:\n\n```shell\nopenssl genrsa -out jwt-rsa256-private.pem 2048\nopenssl rsa -in jwt-rsa256-private.pem -pubout -out jwt-rsa256-public.pem\n```\n\nYou should see `jwt-rsa256-private.pem` and `jwt-rsa256-public.pem` generated in your current working directory.\n\nVisit [JWT.io's JWT encoder](https://jwt.io) and do the following:\n\n* Fill in `RS256` as the algorithm.\n* Copy and paste the private key content into the __SIGN JWT: PRIVATE KEY__ section.\n* Update payload with Consumer key `jack-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.K-I13em84kAcyH1jfIJl7ls_4jlwg1GzEzo5_xrDu-3wt3Xa3irS6naUsWpxX-a-hmcZZxRa9zqunqQjUP4kvn5e3xg2f_KyCR-_ZbwqYEPk3bXeFV1l4iypv6z5L7W1Niharun-dpMU03b1Tz64vhFx6UwxNL5UIZ7bunDAo_BXZ7Xe8rFhNHvIHyBFsDEXIBgx8lNYMq8QJk3iKxZhZZ5Om7lgYjOOKRgew4WkhBAY0v1AkO77nTlvSK0OEeeiwhkROyntggyx-S-U222ykMQ6mBLxkP4Cq5qHwXD8AUcLk5mhEij-3QhboYnt7yhKeZ3wDSpcjDvvL2aasC25ng\n```\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `jwt-auth` Credential for the Consumer and configure the RSA keys:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"algorithm\": \"RS256\",\n        \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoTxe7ZPycrEP0SK4OBA2\\n0OUQsDN9gSFSHVvx/t++nZNrFxzZnV6q6/TRsihNXUIgwaOu5icFlIcxPL9Mf9UJ\\na5/XCQExp1TxpuSmjkhIFAJ/x5zXrC8SGTztP3SjkhYnQO9PKVXI6ljwgakVCfpl\\numuTYqI+ev7e45NdK8gJoJxPp8bPMdf8/nHfLXZuqhO/btrDg1x+j7frDNrEw+6B\\nCK2SsuypmYN+LwHfaH4Of7MQFk3LNIxyBz0mdbsKJBzp360rbWnQeauWtDymZxLT\\nATRNBVyl3nCNsURRTkc7eyknLaDt2N5xTIoUGHTUFYSdE68QWmukYMVGcEHEEPkp\\naQIDAQAB\\n-----END PUBLIC KEY-----\"\n      }\n    }\n  }'\n```\n\n:::tip\n\nYou should add a newline character after the opening line and before the closing line, for example `-----BEGIN PUBLIC KEY-----\\n......\\n-----END PUBLIC KEY-----`.\n\nThe key content can be directly concatenated.\n\n:::\n\nCreate a Route with the `jwt-auth` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send a request to the Route with the JWT in the `Authorization` header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: ${jwt_token}\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\n### Add Consumer Custom ID to Header\n\nThe following example demonstrates how you can attach a Consumer custom ID to authenticated request in the `Consumer-Custom-Id` header, which can be used to implement additional logics as needed.\n\nCreate a Consumer `jack` with a custom ID label:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\nCreate `jwt-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `jwt-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo issue a JWT for `jack`, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), do the following:\n\n* Fill in `HS256` as the algorithm.\n* Update the secret in the __Valid secret__ section to be `jack-hs256-secret-that-is-very-long`.\n* Update payload with Consumer key `jack-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\nTo verify, send a request to the Route with the JWT in the `Authorization` header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: ${jwt_token}\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6873b19d-329331db76e5e7194c942b47\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-jwt-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### Rate Limit with Anonymous Consumer\n\nThe following example demonstrates how you can configure different rate limiting policies by regular and anonymous consumers, where the anonymous Consumer does not need to authenticate and has less quotas.\n\nCreate a regular Consumer `jack` and configure the `limit-count` Plugin to allow for a quota of 3 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate the `jwt-auth` Credential for the Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\nCreate an anonymous user `anonymous` and configure the `limit-count` Plugin to allow for a quota of 1 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate a Route and configure the `jwt-auth` Plugin to accept anonymous Consumer `anonymous` from bypassing the authentication:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo issue a JWT for `jack`, you could use [JWT.io's JWT encoder](https://jwt.io) or other utilities. If you are using [JWT.io's JWT encoder](https://jwt.io), do the following:\n\n* Fill in `HS256` as the algorithm.\n* Update the secret in the __Valid secret__ section to be `jack-hs256-secret-that-is-very-long`.\n* Update payload with Consumer key `jack-key`; and add `exp` or `nbf` in UNIX timestamp.\n\n  Your payload should look similar to the following:\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\nCopy the generated JWT and save to a variable:\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\nTo verify the rate limiting, send five consecutive requests with `jack`'s JWT:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H \"Authorization: ${jwt_token}\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 5 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    3, 429:    2\n```\n\nSend five anonymous requests:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that only one request was successful:\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/kafka-logger.md",
    "content": "---\ntitle: kafka-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Kafka Logger\ndescription: This document contains information about the Apache APISIX kafka-logger Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `kafka-logger` Plugin is used to push logs as JSON objects to Apache Kafka clusters. It works as a Kafka client driver for the ngx_lua Nginx module.\n\nIt might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name                   | Type    | Required | Default        | Valid values          | Description                                                                                                                                                                                                                                                                                                                                      |\n| ---------------------- | ------- | -------- | -------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| broker_list            | object  | True     |                |                       | Deprecated, use `brokers` instead. List of Kafka brokers.  (nodes).                                                                                                                                                                                                                                                                                                                   |\n| brokers                | array   | True     |                |                       | List of Kafka brokers (nodes).                                                                                                                                                                                                                                                                                                                   |\n| brokers.host           | string  | True     |                |                       | The host of Kafka broker, e.g, `192.168.1.1`.                                                                                                                                                                                                                                                                                                                   |\n| brokers.port           | integer | True     |                |   [0, 65535]                  |  The port of Kafka broker                                                                                                                                                                                                                                                                                                                  |\n| brokers.sasl_config    | object  | False    |                |                               |  The sasl config of Kafka broker                                                                                                                                                                                                                                                                                                                 |\n| brokers.sasl_config.mechanism  | string  | False    | \"PLAIN\"          | [\"PLAIN\", \"SCRAM-SHA-256\", \"SCRAM-SHA-512\"]           |     The mechaism of sasl config                                                                                                                                                                                                                                                                                                             |\n| brokers.sasl_config.user       | string  | True     |                  |                     |  The user of sasl_config. If sasl_config exists, it's required.                                                                                                                                                                                                                                                                                             |\n| brokers.sasl_config.password   | string  | True     |                  |                     | The password of sasl_config. If sasl_config exists, it's required.                                                                                                                                                                                                                                                                                                 |\n| kafka_topic            | string  | True     |                |                       | Target topic to push the logs for organisation.                                                                                                                                                                                                                                                                                                  |\n| producer_type          | string  | False    | async          | [\"async\", \"sync\"]     | Message sending mode of the producer.                                                                                                                                                                                                                                                                                                            |\n| required_acks          | integer | False    | 1              | [1, -1]            | Number of acknowledgements the leader needs to receive for the producer to consider the request complete. This controls the durability of the sent records. The attribute follows the same configuration as the Kafka `acks` attribute. `required_acks` cannot be 0. See [Apache Kafka documentation](https://kafka.apache.org/documentation/#producerconfigs_acks) for more. |\n| key                    | string  | False    |                |                       | Key used for allocating partitions for messages.                                                                                                                                                                                                                                                                                                 |\n| timeout                | integer | False    | 3              | [1,...]               | Timeout for the upstream to send data.                                                                                                                                                                                                                                                                                                           |\n| name                   | string  | False    | \"kafka logger\" |                       | Unique identifier for the batch processor. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`.                                                                                                                                                                                                                                                                                                  |\n| meta_format            | enum    | False    | \"default\"      | [\"default\"，\"origin\"] | Format to collect the request information. Setting to `default` collects the information in JSON format and `origin` collects the information with the original HTTP request. See [examples](#meta_format-example) below.                                                                                                                        |\n| log_format | object | False    |   |               | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| include_req_body       | boolean | False    | false          | [false, true]         | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitations.                                                                                                                                                                                 |\n| include_req_body_expr  | array   | False    |                |                       | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                          |\n| max_req_body_bytes     | integer | False    | 524288         | >=1                   | Maximum request body allowed in bytes. Request bodies falling within this limit will be pushed to Kafka. If the size exceeds the configured value, the body will be truncated before being pushed to Kafka.                                                                                                                                                                                                  |\n| include_resp_body      | boolean | False    | false          | [false, true]         | When set to `true` includes the response body in the log.                                                                                                                                                                                                                                                                                        |\n| include_resp_body_expr | array   | False    |                |                       | Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                        |\n| max_resp_body_bytes    | integer | False    | 524288         | >=1                   | Maximum response body allowed in bytes. Response bodies falling within this limit will be pushed to Kafka. If the size exceeds the configured value, the body will be truncated before being pushed to Kafka.                                                                                                                                                                                                  |\n| cluster_name           | integer | False    | 1              | [0,...]               | Name of the cluster. Used when there are two or more Kafka clusters. Only works if the `producer_type` attribute is set to `async`.                                                                                                                                                                                                              |\n| producer_batch_num     | integer | optional    | 200            | [1,...]               | `batch_num` parameter in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka). The merge message and batch is send to the server. Unit is message count.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n| producer_batch_size    | integer | optional    | 1048576        | [0,...]               | `batch_size` parameter in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) in bytes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n| producer_max_buffering | integer | optional    | 50000          | [1,...]               | `max_buffering` parameter in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) representing maximum buffer size. Unit is message count.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| producer_time_linger   | integer | optional    | 1              | [1,...]               | `flush_time` parameter in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) in seconds.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| meta_refresh_interval | integer | optional    | 30              | [1,...]               | `refresh_interval` parameter in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) specifies the time to auto refresh the metadata, in seconds.                                                                                                                                                                                                                                                                                                           |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n:::info IMPORTANT\n\nThe data is first written to a buffer. When the buffer exceeds the `batch_max_size` or `buffer_duration` attribute, the data is sent to the Kafka server and the buffer is flushed.\n\nIf the process is successful, it will return `true` and if it fails, returns `nil` with a string with the \"buffer overflow\" error.\n\n:::\n\n### meta_format example\n\n- `default`:\n\n  ```json\n  {\n    \"upstream\": \"127.0.0.1:1980\",\n    \"start_time\": 1619414294760,\n    \"client_ip\": \"127.0.0.1\",\n    \"service_id\": \"\",\n    \"route_id\": \"1\",\n    \"request\": {\n      \"querystring\": {\n        \"ab\": \"cd\"\n      },\n      \"size\": 90,\n      \"uri\": \"/hello?ab=cd\",\n      \"url\": \"http://localhost:1984/hello?ab=cd\",\n      \"headers\": {\n        \"host\": \"localhost\",\n        \"content-length\": \"6\",\n        \"connection\": \"close\"\n      },\n      \"body\": \"abcdef\",\n      \"method\": \"GET\"\n    },\n    \"response\": {\n      \"headers\": {\n        \"connection\": \"close\",\n        \"content-type\": \"text/plain; charset=utf-8\",\n        \"date\": \"Mon, 26 Apr 2021 05:18:14 GMT\",\n        \"server\": \"APISIX/2.5\",\n        \"transfer-encoding\": \"chunked\"\n      },\n      \"size\": 190,\n      \"status\": 200\n    },\n    \"server\": {\n      \"hostname\": \"localhost\",\n      \"version\": \"2.5\"\n    },\n    \"latency\": 0\n  }\n  ```\n\n- `origin`:\n\n  ```http\n  GET /hello?ab=cd HTTP/1.1\n  host: localhost\n  content-length: 6\n  connection: close\n\n  abcdef\n  ```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `kafka-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/kafka-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the `kafka-logger` Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"kafka-logger\": {\n           \"brokers\" : [\n             {\n               \"host\" :\"127.0.0.1\",\n               \"port\" : 9092\n             }\n            ],\n           \"kafka_topic\" : \"test2\",\n           \"key\" : \"key1\",\n           \"batch_max_size\": 1,\n           \"name\": \"kafka logger\"\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nThis Plugin also supports pushing to more than one broker at a time. You can specify multiple brokers in the Plugin configuration as shown below:\n\n```json\n \"brokers\" : [\n    {\n      \"host\" :\"127.0.0.1\",\n      \"port\" : 9092\n    },\n    {\n      \"host\" :\"127.0.0.1\",\n      \"port\" : 9093\n    }\n],\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your Kafka server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo remove the `kafka-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/kafka-proxy.md",
    "content": "---\ntitle: kafka-proxy\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Kafka proxy\ndescription: This document contains information about the Apache APISIX kafka-proxy Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `kafka-proxy` plugin can be used to configure advanced parameters for the kafka upstream of Apache APISIX, such as SASL authentication.\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values  | Description                        |\n|-------------------|---------|----------|---------|---------------|------------------------------------|\n| sasl              | object  | optional |         | {\"username\": \"user\", \"password\" :\"pwd\"} | SASL/PLAIN authentication configuration, when this configuration exists, turn on SASL authentication; this object will contain two parameters username and password, they must be configured. |\n| sasl.username     | string  | required |         |               | SASL/PLAIN authentication username |\n| sasl.password     | string  | required |         |               | SASL/PLAIN authentication password |\n\nNOTE: `encrypt_fields = {\"sasl.password\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\n:::note\nIf SASL authentication is enabled, the `sasl.username` and `sasl.password` must be set.\nThe current SASL authentication only supports PLAIN mode, which is the username password login method.\n:::\n\n## Example usage\n\nWhen we use scheme as the upstream of kafka, we can add kafka authentication configuration to it through this plugin.\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/kafka\",\n    \"plugins\": {\n        \"kafka-proxy\": {\n            \"sasl\": {\n                \"username\": \"user\",\n                \"password\": \"pwd\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"kafka-server1:9092\": 1,\n            \"kafka-server2:9092\": 1,\n            \"kafka-server3:9092\": 1\n        },\n        \"type\": \"none\",\n        \"scheme\": \"kafka\"\n    }\n}'\n```\n\nNow, we can test it by connecting to the `/kafka` endpoint via websocket.\n\n## Delete Plugin\n\nTo remove the `kafka-proxy` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n"
  },
  {
    "path": "docs/en/latest/plugins/key-auth.md",
    "content": "---\ntitle: key-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Key Auth\n  - key-auth\ndescription: The key-auth Plugin supports the use of an authentication key as a mechanism for clients to authenticate themselves before accessing Upstream resources.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n    <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/key-auth\" />\n</head>\n\n## Description\n\nThe `key-auth` Plugin supports the use of an authentication key as a mechanism for clients to authenticate themselves before accessing Upstream resources.\n\nTo use the plugin, you would configure authentication keys on [Consumers](../terminology/consumer.md) and enable the Plugin on routes or services. The key can be included in the request URL query string or request header. APISIX will then verify the key to determine if a request should be allowed or denied to access Upstream resources.\n\nWhen a Consumer is successfully authenticated, APISIX adds additional headers, such as `X-Consumer-Username`, `X-Credential-Indentifier`, and other Consumer custom headers if configured, to the request, before proxying it to the Upstream service. The Upstream service will be able to differentiate between consumers and implement additional logics as needed. If any of these values is not available, the corresponding header will not be added.\n\n## Attributes\n\nFor Consumer/Credential:\n\n| Name | Type   | Required | Description                |\n|------|--------|-------------|----------------------------|\n| key  | string | True    | Unique key for a Consumer. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. |\n\nNOTE: `encrypt_fields = {\"key\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nFor Route:\n\n| Name   | Type   | Required | Default | Description                                                                                                                                                                                                                                                                   |\n|--------|--------|-------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| header | string | False    | apikey | The header to get the key from.                                                                                                                                                                                                                                               |\n| query  | string | False    | apikey  | The query string to get the key from. Lower priority than header.                                                                                                                                                                                                             |\n| hide_credentials   | boolean | False    | false  | If true, do not pass the header or query string with key to Upstream services.  |\n| anonymous_consumer | string  | False    | false  | Anonymous Consumer name. If configured, allow anonymous users to bypass the authentication.  |\n| realm              | string  | False    | key    | The realm to include in the `WWW-Authenticate` header when authentication fails. |\n\n## Examples\n\nThe examples below demonstrate how you can work with the `key-auth` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Implement Key Authentication on Route\n\nThe following example demonstrates how to implement key authentications on a Route and include the key in the request header.\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n#### Verify with a Valid Key\n\nSend a request to with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'apikey: jack-key'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\n#### Verify with an Invalid Key\n\nSend a request with an invalid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'apikey: wrong-key'\n```\n\nYou should see an `HTTP/1.1 401 Unauthorized` response with the following:\n\n```text\n{\"message\":\"Invalid API key in request\"}\n```\n\n#### Verify without a Key\n\nSend a request to without a key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should see an `HTTP/1.1 401 Unauthorized` response with the following:\n\n```text\n{\"message\":\"Missing API key found in request\"}\n```\n\n### Hide Authentication Information From Upstream\n\nThe following example demonstrates how to prevent the key from being sent to the Upstream services by configuring `hide_credentials`. By default, the authentication key is forwarded to the Upstream services, which might lead to security risks in some circumstances.\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\n#### Without Hiding Credentials\n\nCreate a Route with `key-auth` and configure `hide_credentials` to `false`, which is the default configuration:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"id\": \"key-auth-route\",\n  \"uri\": \"/anything\",\n  \"plugins\": {\n    \"key-auth\": {\n      \"hide_credentials\": false\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\nSend a request with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?apikey=jack-key\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following:\n\n```json\n{\n  \"args\": {\n    \"auth\": \"jack-key\"\n  },\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-key-auth\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6502d8a5-2194962a67aa21dd33f94bb2\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 103.248.35.179\",\n  \"url\": \"http://127.0.0.1/anything?apikey=jack-key\"\n}\n```\n\nNote that the Credential `jack-key` is visible to the Upstream service.\n\n#### Hide Credentials\n\nUpdate the plugin's `hide_credentials` to `true`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/key-auth-route\" -X PATCH \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"plugins\": {\n    \"key-auth\": {\n      \"hide_credentials\": true\n    }\n  }\n}'\n```\n\nSend a request with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?apikey=jack-key\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-key-auth\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6502d85c-16f34dbb5629a5960183e803\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 103.248.35.179\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\nNote that the Credential `jack-key` is no longer visible to the Upstream service.\n\n### Demonstrate Priority of Keys in Header and Query\n\nThe following example demonstrates how to implement key authentication by consumers on a Route and customize the URL parameter that should include the key. The example also shows that when the API key is configured in both the header and the query string, the request header has a higher priority.\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"id\": \"key-auth-route\",\n  \"uri\": \"/anything\",\n  \"plugins\": {\n    \"key-auth\": {\n      \"query\": \"auth\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n#### Verify with a Valid Key\n\nSend a request to with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=jack-key\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\n#### Verify with an Invalid Key\n\nSend a request with an invalid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=wrong-key\"\n```\n\nYou should see an `HTTP/1.1 401 Unauthorized` response with the following:\n\n```text\n{\"message\":\"Invalid API key in request\"}\n```\n\n#### Verify with a Valid Key in Query String\n\nHowever, if you include the valid key in header with the invalid key still in the URL query string:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=wrong-key\" -H 'apikey: jack-key'\n```\n\nYou should see an `HTTP/1.1 200 OK` response. This shows that the key included in the header always has a higher priority.\n\n### Add Consumer Custom ID to Header\n\nThe following example demonstrates how you can attach a Consumer custom ID to authenticated request in the `Consumer-Custom-Id` header, which can be used to implement additional logics as needed.\n\nCreate a Consumer `jack` with a custom ID label:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send a request to the Route with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=jack-key\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {\n    \"auth\": \"jack-key\"\n  },\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66ea8d64-33df89052ae198a706e18c2a\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-key-auth\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/anything?apikey=jack-key\"\n}\n```\n\n### Rate Limit with Anonymous Consumer\n\nThe following example demonstrates how you can configure different rate limiting policies by regular and anonymous consumers, where the anonymous Consumer does not need to authenticate and has less quotas.\n\nCreate a regular Consumer `jack` and configure the `limit-count` Plugin to allow for a quota of 3 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate the `key-auth` Credential for the Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\nCreate an anonymous user `anonymous` and configure the `limit-count` Plugin to allow for a quota of 1 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate a Route and configure the `key-auth` Plugin to accept anonymous Consumer `anonymous` from bypassing the authentication:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send five consecutive requests with `jack`'s key:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: jack-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 5 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    3, 429:    2\n```\n\nSend five anonymous requests:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that only one request was successful:\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/lago.md",
    "content": "---\ntitle: lago\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - lago\n  - monetization\n  - github.com/getlago/lago\ndescription: The lago plugin reports usage to a Lago instance, which allows users to integrate Lago with APISIX for API monetization.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `lago` plugin pushes requests and responses to [Lago Self-hosted](https://github.com/getlago/lago) and [Lago Cloud](https://getlago.com) via the Lago REST API. the plugin allows you to use it with a variety of APISIX built-in features, such as the APISIX consumer and the request-id plugin.\n\nThis allows for API monetization or let APISIX to be an AI gateway for AI tokens billing scenarios.\n\n:::note disclaimer\n\nLago owns its trademarks and controls its commercial products and open source projects.\n\nThe [https://github.com/getlago/lago](https://github.com/getlago/lago) project uses the `AGPL-3.0` license instead of the `Apache-2.0` license that is the same as Apache APISIX. As a user, you will need to evaluate for yourself whether it is applicable to your business to use the project in a compliant way or to obtain another type of license from Lago. Apache APISIX community does not endorse it.\n\nThe plugin does not contain any proprietary code or SDKs from Lago, it is contributed by contributors to Apache APISIX and licensed under the `Apache-2.0` license, which is in line with any other part of APISIX and you don't need to worry about its compliance.\n\n:::\n\nWhen enabled, the plugin will collect information from the request context (e.g. event code, transaction ID, associated subscription ID) as configured and serialize them into [Event JSON objects](https://getlago.com/docs/api-reference/events/event-object) as required by Lago. They will be added to the buffer and sent to Lago in batches of up to 100. This batch size is a [requirement](https://getlago.com/docs/api-reference/events/batch) from Lago. If you want to modify it, see [batch processor](../batch-processor.md) for more details.\n\n## Attributes\n\n| Name | Type | Required | Default | Valid values | Description |\n|---|---|---|---|---|---|\n| endpoint_addrs | array[string] | True |  | | Lago API address, such as `http://127.0.0.1:3000`. It supports both self-hosted Lago and Lago Cloud. If multiple endpoints are configured, the log will be pushed to a randomly selected endpoint from the list. |\n| endpoint_uri | string | False | /api/v1/events/batch | | Lago API endpoint for [batch usage events](https://docs.getlago.com/api-reference/events/batch). |\n| token | string | True |  | | Lago API key created in the Lago dashboard. |\n| event_transaction_id | string | True |  | | Event's transaction ID, used to identify and de-duplicate the event. It supports string templates containing APISIX and NGINX variables, such as `req_${request_id}`, which allows you to use values returned by upstream services or the `request-id` plugin. |\n| event_subscription_id | string | True |  | | Event's subscription ID, which is automatically generated or configured when you assign the plan to the customer on Lago. This is used to associate API consumption to a customer subscription and supports string templates containing APISIX and NGINX variables, such as `cus_${consumer_name}`, which allows you to use values returned by upstream services or APISIX consumer. |\n| event_code | string | True |  | | Lago billable metric's code for associating an event to a specified billable item. |\n| event_properties | object | False |  | | Event's properties, used to attach information to an event. This allows you to send certain information on an event to Lago, such as the HTTP status to exclude failed requests from billing, or the AI token consumption in the response body for accurate billing. The keys are fixed strings, while the values can be string templates containing APISIX and NGINX variables, such as `${status}`. |\n| ssl_verify        | boolean       | False    | true | | If true, verify Lago's SSL certificates. |\n| timeout           | integer       | False    | 3000 | [1, 60000] | Timeout for the Lago service HTTP call in milliseconds.  |\n| keepalive         | boolean       | False    | true |  | If true, keep the connection alive for multiple requests. |\n| keepalive_timeout | integer       | False    | 60000 | >=1000 | Keepalive timeout in milliseconds.  |\n| keepalive_pool    | integer       | False    | 5       | >=1 | Maximum number of connections in the connection pool.  |\n\nThis Plugin supports using batch processors to aggregate and process events in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n## Examples\n\nThe examples below demonstrate how you can configure `lago` Plugin for typical scenario.\n\nTo follow along the examples, start a Lago instance. Refer to [https://github.com/getlago/lago](https://github.com/getlago/lago) or use Lago Cloud.\n\nFollow these brief steps to configure Lago:\n\n1. Get the Lago API Key (also known as `token`), from the __Developer__ page of the Lago dashboard.\n2. Next, create a billable metric used by APISIX, assuming its code is `test`. Set the `Aggregation type` to `Count`; and add a filter with a key of `tier` whose value contains `expensive` to allow us to distinguish between API values, which will be demonstrated later.\n3. Create a plan and add the created metric to it. Its code can be configured however you like. In the __Usage-based charges__ section, add the billable metric created previously as a `Metered charge` item. Specify the default price as `$1`. Add a filter, use `tier: expensive` to perform the filtering, and specify its price as `$10`.\n4. Select an existing consumer or create a new one to assign the plan you just created. You need to specify a `Subscription external ID` (or you can have Lago generate it), which will be used as the APISIX consumer username.\n\nNext we need to configure APISIX for demonstrations.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Report API call usage\n\nThe following example demonstrates how you can configure the `lago` Plugin on a Route to measuring API call usage.\n\nCreate a Route with the `lago`, `request-id`, `key-auth` Plugins as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"lago-route-1\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"request-id\": {\n        \"include_in_response\": true\n      },\n      \"key-auth\": {},\n      \"lago\": {\n        \"endpoint_addrs\": [\"http://12.0.0.1:3000\"],\n        \"token\": \"<Get token from Lago dashboard>\",\n        \"event_transaction_id\": \"${http_x_request_id}\",\n        \"event_subscription_id\": \"${http_x_consumer_username}\",\n        \"event_code\": \"test\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nCreate a second route with the `lago`, `request-id`, `key-auth` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"lago-route-2\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"include_in_response\": true\n      },\n      \"key-auth\": {},\n      \"lago\": {\n        \"endpoint_addrs\": [\"http://12.0.0.1:3000\"],\n        \"token\": \"<Get token from Lago dashboard>\",\n        \"event_transaction_id\": \"${http_x_request_id}\",\n        \"event_subscription_id\": \"${http_x_consumer_username}\",\n        \"event_code\": \"test\",\n        \"event_properties\": {\n          \"tier\": \"expensive\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nCreate a Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"<Lago subscription external ID>\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"demo\"\n      }\n    }\n  }'\n```\n\nSend three requests to the two routes respectively:\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\ncurl \"http://127.0.0.1:9080/get\"\ncurl \"http://127.0.0.1:9080/get\"\ncurl \"http://127.0.0.1:9080/anything\"\ncurl \"http://127.0.0.1:9080/anything\"\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses for all requests.\n\nWait a few seconds, then navigate to the __Developer__ page in the Lago dashboard. Under __Events__, you should see 6 event entries sent by APISIX.\n\nIf the self-hosted instance's event worker is configured correctly (or if you're using Lago Cloud), you can also see the total amount consumed in real time in the consumer's subscription usage, which should be `3 * $1 + 3 * $10 = $33` according to our demo use case.\n\n## FAQ\n\n### Purpose of the Plugin\n\nWhen you make an effort to monetize your API, it's hard to find a ready-made, low-cost solution, so you may have to build your own billing stack, which is complicated.\n\nThis plugin allows you to use APISIX to handle API proxies and use Lago as a billing stack through direct integration with Lago, and both the APISIX open source project and Lago will be part of your portfolio, which is a huge time saver.\n\nEvery API call results in a Lago event, which allows you to bill users for real usage, i.e. pay-as-you-go, and thanks to our built-in transaction ID (request ID) support, you can simply implement API call logging and troubleshooting for your customers.\n\nIn addition to typical API monetization scenarios, APISIX can also do AI tokens-based billing when it is acting as an AI gateway, where each Lago event generated by an API request includes exactly how many tokens were consumed, to allow you to charge the user for a fine-grained per-tokens usage.\n\n### Is it flexible?\n\nOf course, the fact that we make transaction ID, subscription ID as a configuration item and allow you to use APISIX and NGINX variables in it means that it's simple to integrate the plugin with any existing or your own authentication and internal services.\n\n- Use custom authentication: as long as the Lago subscription ID represented by the user ID is registered as an APISIX variable, it will be available from there, so custom authentication is completely possible!\n- Integration with internal services: You might not need the APISIX built-in request-id plugin. That's OK. You can have your internal service (APISIX upstream) generate it and include it in the HTTP response header. Then you can access it via an NGINX variable in the transaction ID.\n\nEvent properties are supported, allowing you to set special values for specific APIs. For example, if your service has 100 APIs, you can enable general billing for all of them while customizing a few with different pricing—just as demonstrated above.\n\n### Which Lago versions does it work with?\n\nWhen we first developed the Lago plugin, it was released to `1.17.0`, which we used for integration, so it works at least with `1.17.0`.\n\nTechnically, we use the Lago batch event API to submit events in batches, and APISIX will only use this API, so as long as Lago doesn't make any disruptive changes to this API, APISIX will be able to integrate with it.\n\nHere's an [archive page](https://web.archive.org/web/20250516073803/https://getlago.com/docs/api-reference/events/batch) of the API documentation, which allows you to check the differences between the API at the time of our integration and the latest API.\n\nIf the latest API changes, you can submit an issue to inform the APISIX maintainers that this may require some changes.\n\n### Why Lago can't receive events?\n\nLook at `error.log` for such a log.\n\n```text\n2023/04/30 13:45:46 [error] 19381#19381: *1075673 [lua] batch-processor.lua:95: Batch Processor[lago logger] failed to process entries: lago api returned status: 400, body: <error message>, context: ngx.timer, client: 127.0.0.1, server: 0.0.0.0:9080\n```\n\nThe error can be diagnosed based on the error code in the `failed to process entries: lago api returned status: 400, body: <error message>` and the response body of the lago server.\n\n### Reliability of reporting\n\nThe plugin may encounter a network problem that prevents the node where the gateway is located from communicating with the Lago API, in which case APISIX will discard the batch according to the [batch processor](../batch-processor.md) configuration, the batch will be discarded if the specified number of retries are made and the dosage still cannot be sent.\n\nDiscarded events are permanently lost, so it is recommended that you use this plugin in conjunction with other logging mechanisms and perform event replay after Lago is unavailable causing data to be discarded to ensure that all logs are correctly sent to Lago.\n\n### Will the event duplicate?\n\nWhile APISIX performs retries based on the [batch processor](../batch-processor.md) configuration, you don't need to worry about duplicate events being reported to Lago.\n\nThe `event_transcation_id` and `timestamp` are generated and logged after the request is processed on the APISIX side, and Lago de-duplicates the event based on them.\nSo even if a retry is triggered because the network causes Lago to send a `success` response that is not received by APISIX, the event is still not duplicated on Lago.\n\n### Performance Impacts\n\nThe plugin is logically simple and reliable; it simply builds a Lago event object for each request, buffers and sends them in bulk. The logic is not coupled to the request proxy path, so this does not cause latency to rise for requests going through the gateway.\n\nTechnically, the logic is executed in the NGINX log phase and [batch processor](../batch-processor.md) timer, so this does not affect the request itself.\n\n### Resource overhead\n\nAs explained earlier in the performance impact section, the plugin doesn't cause a significant increase in system resources. It only uses a small amount of memory to store events for batching.\n"
  },
  {
    "path": "docs/en/latest/plugins/ldap-auth.md",
    "content": "---\ntitle: ldap-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - LDAP Authentication\n  - ldap-auth\ndescription: This document contains information about the Apache APISIX ldap-auth Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ldap-auth` Plugin can be used to add LDAP authentication to a Route or a Service.\n\nThis Plugin works with the Consumer object and the consumers of the API can authenticate with an LDAP server using [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).\n\nThis Plugin uses [lua-resty-ldap](https://github.com/api7/lua-resty-ldap) for connecting with an LDAP server.\n\n## Attributes\n\nFor Consumer:\n\n| Name    | Type   | Required | Description                                                                      |\n| ------- | ------ | -------- | -------------------------------------------------------------------------------- |\n| user_dn | string | True     | User dn of the LDAP client. For example, `cn=user01,ou=users,dc=example,dc=org`. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. |\n\nFor Route:\n\n| Name     | Type    | Required | Default | Description                                                            |\n|----------|---------|----------|---------|------------------------------------------------------------------------|\n| base_dn  | string  | True     |         | Base dn of the LDAP server. For example, `ou=users,dc=example,dc=org`. |\n| ldap_uri | string  | True     |         | URI of the LDAP server.                                                |\n| use_tls  | boolean | False    | `false` | If set to `true` uses TLS.                                             |\n| tls_verify| boolean  | False     | `false`        | Whether to verify the server certificate when `use_tls` is enabled; If set to `true`, you must set `ssl_trusted_certificate` in `config.yaml`, and make sure the host of `ldap_uri` matches the host in server certificate. |\n| uid      | string  | False    | `cn`    | uid attribute.                                                         |\n| realm    | string  | False    | ldap    | The realm to include in the `WWW-Authenticate` header when authentication fails.                 |\n\n## Enable plugin\n\nFirst, you have to create a Consumer and enable the `ldap-auth` Plugin on it:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"foo\",\n    \"plugins\": {\n        \"ldap-auth\": {\n            \"user_dn\": \"cn=user01,ou=users,dc=example,dc=org\"\n        }\n    }\n}'\n```\n\nNow you can enable the Plugin on a specific Route or a Service as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"ldap-auth\": {\n            \"base_dn\": \"ou=users,dc=example,dc=org\",\n            \"ldap_uri\": \"localhost:1389\",\n            \"uid\": \"cn\"\n        },\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nAfter configuring the Plugin as mentioned above, clients can make requests with authorization to access the API:\n\n```shell\ncurl -i -uuser01:password1 http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\nIf an authorization header is missing or invalid, the request is denied:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Missing authorization in request\"}\n```\n\n```shell\ncurl -i -uuser:password1 http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Invalid user authorization\"}\n```\n\n```shell\ncurl -i -uuser01:passwordfalse http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Invalid user authorization\"}\n```\n\n## Delete Plugin\n\nTo remove the `ldap-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/limit-conn.md",
    "content": "---\ntitle: limit-conn\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Limit Connection\ndescription: The limit-conn plugin restricts the rate of requests by managing concurrent connections. Requests exceeding the threshold may be delayed or rejected, ensuring controlled API usage and preventing overload.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/limit-conn\" />\n</head>\n\n## Description\n\nThe `limit-conn` Plugin limits the rate of requests by the number of concurrent connections. Requests exceeding the threshold will be delayed or rejected based on the configuration, ensuring controlled resource usage and preventing overload.\n\n## Attributes\n\n| Name       | Type    | Required | Default     | Valid values      | Description     |\n|------------|---------|----------|-------------|-------------------|-----------------|\n| conn       | integer | False     |     | > 0   | The maximum number of concurrent requests allowed. Requests exceeding the configured limit and below `conn + burst` will be delayed. Required if `rules` is not configured.      |\n| burst      | integer | False     |     | >= 0        | The number of excessive concurrent requests allowed to be delayed per second. Requests exceeding the limit will be rejected immediately. Required if `rules` is not configured.       |\n| default_conn_delay       | number  | True     |     | > 0    | Processing latency allowed in seconds for concurrent requests exceeding `conn + burst`, which can be dynamically adjusted based on `only_use_default_delay` setting.           |\n| only_use_default_delay   | boolean | False    | false       |      | If false, delay requests proportionally based on how much they exceed the `conn` limit. The delay grows larger as congestion increases. For instance, with `conn` being `5`, `burst` being `3`, and `default_conn_delay` being `1`, 6 concurrent requests would result in a 1-second delay, 7 requests a 2-second delay, 8 requests a 3-second delay, and so on, until the total limit of `conn + burst` is reached, beyond which requests are rejected. If true, use `default_conn_delay` to delay all excessive requests within the `burst` range. Requests beyond `conn + burst` are rejected immediately. For instance, with `conn` being `5`, `burst` being `3`, and `default_conn_delay` being `1`, 6, 7, or 8 concurrent requests are all delayed by exactly 1 second each. |\n| rules                    | array[object] | False    |       |                   | A list of connection limiting rules. Each rule is an object containing `conn`, `burst`, and `key`. If configured, this takes precedence over `conn`, `burst`, and `key`. |\n| rules.conn               | integer or string | True |       | > 0 or variable expression | The maximum number of concurrent requests allowed. Can be a static integer or a variable expression like `$http_custom_conn`. |\n| rules.burst              | integer or string | True |       | >= 0 or variable expression | The number of excessive concurrent requests allowed to be delayed. Can be a static integer or a variable expression. |\n| rules.key                | string  | True     |       |                   | The key to count requests by. If the configured key does not exist, the rule will not be executed. The `key` is interpreted as a combination of variables, for example: `$http_custom_a $http_custom_b`. |\n| key_type        | string  | False      | var   | [\"var\",\"var_combination\"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables.    |\n| key       | string  | False      | remote_addr |   | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. Required if `rules` is not configured. |\n| key_ttl   | integer | False      | 3600          |   | The TTL of the Redis key in seconds. Used when `policy` is `redis` or `redis-cluster`. |\n| rejected_code   | integer | False      | 503   | [200,...,599]   | The HTTP status code returned when a request is rejected for exceeding the threshold.     |\n| rejected_msg    | string  | False        |       | non-empty   | The response body returned when a request is rejected for exceeding the threshold.     |\n| allow_degradation       | boolean | False      | false   |   | If true, allow APISIX to continue handling requests without the Plugin when the Plugin or its dependencies become unavailable.        |\n| policy    | string  | False      | local       | [\"local\",\"redis\",\"redis-cluster\"]    | The policy for rate limiting counter. If it is `local`, the counter is stored in memory locally. If it is `redis`, the counter is stored on a Redis instance. If it is `redis-cluster`, the counter is stored in a Redis cluster.    |\n| redis_host      | string  | False   |       |   | The address of the Redis node. Required when `policy` is `redis`.    |\n| redis_port      | integer | False      | 6379    | [1,...]   | The port of the Redis node when `policy` is `redis`.       |\n| redis_username    | string  | False      |       |   | The username for Redis if Redis ACL is used. If you use the legacy authentication method `requirepass`, configure only the `redis_password`. Used when `policy` is `redis`.        |\n| redis_password    | string  | False      |       |   | The password of the Redis node when `policy` is `redis` or `redis-cluster`.        |\n| redis_ssl       | boolean | False      | false   |   | If true, use SSL to connect to Redis cluster when `policy` is `redis`.       |\n| redis_ssl_verify        | boolean | False      | false   |   | If true, verify the server SSL certificate when `policy` is `redis`.    |\n| redis_database    | integer | False      | 0     | >= 0      | The database number in Redis when `policy` is `redis`.    |\n| redis_timeout   | integer | False      | 1000    | [1,...]   | The Redis timeout value in milliseconds when `policy` is `redis` or `redis-cluster`.      |\n| redis_keepalive_timeout | integer | False | 10000 | ≥ 1000 | Keepalive timeout in milliseconds for redis when `policy` is `redis` or `redis-cluster`. |\n| redis_keepalive_pool | integer | False | 100 | ≥ 1 | Keepalive pool size for redis when `policy` is `redis` or `redis-cluster`.|\n| redis_cluster_nodes     | array[string]   | False |       |   | The list of the Redis cluster nodes with at least two addresses. Required when policy is redis-cluster.     |\n| redis_cluster_name      | string  | False |       |   | The name of the Redis cluster. Required when `policy` is `redis-cluster`.      |\n| redis_cluster_ssl      | boolean  |  False |     false   |   | If true, use SSL to connect to Redis cluster when `policy` is      |\n| redis_cluster_ssl_verify      | boolean  | False |    false      |   | If true, verify the server SSL certificate when `policy` is `redis-cluster`.     |\n\n## Examples\n\nThe examples below demonstrate how you can configure `limit-conn` in different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Apply Rate Limiting by Remote Address\n\nThe following example demonstrates how to use `limit-conn` to rate limit requests by `remote_addr`, with example connection and burst thresholds.\n\nCreate a Route with `limit-conn` Plugin to allow 2 concurrent requests and 1 excessive concurrent request. Additionally:\n\n* Configure the Plugin to allow 0.1 second of processing latency for concurrent requests exceeding `conn + burst`.\n* Set the key type to `vars` to interpret `key` as a variable.\n* Calculate rate limiting count by request's `remote_address`.\n* Set `policy` to `local` to use the local counter in memory.\n* Customize the `rejected_code` to `429`.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-conn\": {\n        \"conn\": 2,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\",\n        \"policy\": \"local\",\n        \"rejected_code\": 429\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend five concurrent requests to the route:\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\"'\n```\n\nYou should see responses similar to the following, where excessive requests are rejected:\n\n```text\nResponse: 200\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\n```\n\n### Apply Rate Limiting by Remote Address and Consumer Name\n\nThe following example demonstrates how to use `limit-conn` to rate limit requests by a combination of variables, `remote_addr` and `consumer_name`.\n\nCreate a Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a second Consumer `jane`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth` and `limit-conn` Plugins, and specify in the `limit-conn` Plugin to use a combination of variables as the rate limiting key:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"limit-conn\": {\n        \"conn\": 2,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"rejected_code\": 429,\n        \"key_type\": \"var_combination\",\n        \"key\": \"$remote_addr $consumer_name\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend five concurrent requests as the Consumer `john`:\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\" -H \"apikey: john-key\"'\n```\n\nYou should see responses similar to the following, where excessive requests are rejected:\n\n```text\nResponse: 200\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\n```\n\nImmediately send five concurrent requests as the Consumer `jane`:\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\" -H \"apikey: jane-key\"'\n```\n\nYou should also see responses similar to the following, where excessive requests are rejected:\n\n```text\nResponse: 200\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\n```\n\n### Rate Limit WebSocket Connections\n\nThe following example demonstrates how you can use the `limit-conn` Plugin to limit the number of concurrent WebSocket connections.\n\nStart a [sample upstream WebSocket server](https://hub.docker.com/r/jmalloc/echo-server):\n\n```shell\ndocker run -d \\\n  -p 8080:8080 \\\n  --name websocket-server \\\n  --network=apisix-quickstart-net \\\n  jmalloc/echo-server\n```\n\nCreate a Route to the server WebSocket endpoint and enable WebSocket for the route. Adjust the WebSocket server address accordingly.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"ws-route\",\n  \"uri\": \"/.ws\",\n  \"plugins\": {\n    \"limit-conn\": {\n      \"conn\": 2,\n      \"burst\": 1,\n      \"default_conn_delay\": 0.1,\n      \"key_type\": \"var\",\n      \"key\": \"remote_addr\",\n      \"rejected_code\": 429\n    }\n  },\n  \"enable_websocket\": true,\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"websocket-server:8080\": 1\n    }\n  }\n}'\n```\n\nInstall a WebSocket client, such as [websocat](https://github.com/vi/websocat), if you have not already. Establish connection with the WebSocket server through the route:\n\n```shell\nwebsocat \"ws://127.0.0.1:9080/.ws\"\n```\n\nSend a \"hello\" message in the terminal, you should see the WebSocket server echoes back the same message:\n\n```text\nRequest served by 1cd244052136\nhello\nhello\n```\n\nOpen three more terminal sessions and run:\n\n```shell\nwebsocat \"ws://127.0.0.1:9080/.ws\"\n```\n\nYou should see the last terminal session prints `429 Too Many Requests` when you try to establish a WebSocket connection with the server, due to the rate limiting effect.\n\n### Share Quota Among APISIX Nodes with a Redis Server\n\nThe following example demonstrates the rate limiting of requests across multiple APISIX nodes with a Redis server, such that different APISIX nodes share the same rate limiting quota.\n\nOn each APISIX instance, create a Route  with the following configurations. Adjust the address of the Admin API, Redis host, port, password, and database accordingly.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-conn\": {\n        \"conn\": 1,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"rejected_code\": 429,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis\",\n        \"redis_host\": \"192.168.xxx.xxx\",\n        \"redis_port\": 6379,\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_database\": 1\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend five concurrent requests to the route:\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\"'\n```\n\nYou should see responses similar to the following, where excessive requests are rejected:\n\n```text\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\nResponse: 429\n```\n\nThis shows the two routes configured in different APISIX instances share the same quota.\n\n### Share Quota Among APISIX Nodes with a Redis Cluster\n\nYou can also use a Redis cluster to apply the same quota across multiple APISIX nodes, such that different APISIX nodes share the same rate limiting quota.\n\nEnsure that your Redis instances are running in [cluster mode](https://redis.io/docs/management/scaling/#create-and-use-a-redis-cluster). A minimum of two nodes are required for the `limit-conn` Plugin configurations.\n\nOn each APISIX instance, create a Route with the following configurations. Adjust the address of the Admin API, Redis cluster nodes, password, cluster name, and SSL varification accordingly.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-conn\": {\n        \"conn\": 1,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"rejected_code\": 429,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis-cluster\",\n        \"redis_cluster_nodes\": [\n          \"192.168.xxx.xxx:6379\",\n          \"192.168.xxx.xxx:16379\"\n        ],\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_cluster_name\": \"redis-cluster-1\",\n        \"redis_cluster_ssl\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend five concurrent requests to the route:\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\"'\n```\n\nYou should see responses similar to the following, where excessive requests are rejected:\n\n```text\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\nResponse: 429\n```\n\nThis shows the two routes configured in different APISIX instances share the same quota.\n"
  },
  {
    "path": "docs/en/latest/plugins/limit-count.md",
    "content": "---\ntitle: limit-count\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Limit Count\ndescription: The limit-count plugin uses a fixed window algorithm to limit the rate of requests by the number of requests within a given time interval. Requests exceeding the configured quota will be rejected.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/limit-count\" />\n</head>\n\n## Description\n\nThe `limit-count` plugin uses a fixed window algorithm to limit the rate of requests by the number of requests within a given time interval. Requests exceeding the configured quota will be rejected.\n\nYou may see the following rate limiting headers in the response:\n\n* `X-RateLimit-Limit`: the total quota\n* `X-RateLimit-Remaining`: the remaining quota\n* `X-RateLimit-Reset`: number of seconds left for the counter to reset\n\n## Attributes\n\n| Name                    | Type    | Required                                  | Default       | Valid values                           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| ----------------------- | ------- | ----------------------------------------- | ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| count                   | integer | False                                     |               | > 0                              | The maximum number of requests allowed within a given time interval. Required if `rules` is not configured.                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| time_window             | integer | False                                     |               | > 0                        | The time interval corresponding to the rate limiting `count` in seconds. Required if `rules` is not configured.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |\n| rules                   | array[object] | False                               |               |                            | A list of rate limiting rules. Each rule is an object containing `count`, `time_window`, and `key`.                                                                                                                                                                                                                |\n| rules.count             | integer | True                                      |               | > 0                        | The maximum number of requests allowed within a given time interval.                                                                                                                                                                                                                                             |\n| rules.time_window       | integer | True                                      |               | > 0                        | The time interval corresponding to the rate limiting `count` in seconds.                                                                                                                                                                                                                                         |\n| rules.key               | string  | True                                      |               |                            | The key to count requests by. If the configured key does not exist, the rule will not be executed. The `key` is interpreted as a combination of variables, for example: `$http_custom_a $http_custom_b`.                                                                                                                                                                                                                                                                   |\n| rules.header_prefix     | string  | False                                     |               |                            | Prefix for rate limit headers. If configured, the response will include `X-{header_prefix}-RateLimit-Limit`, `X-{header_prefix}-RateLimit-Remaining`, and `X-{header_prefix}-RateLimit-Reset` headers. If not configured, the index of the rule in the rules array is used as the prefix. For example, headers for the first rule will be `X-1-RateLimit-Limit`, `X-1-RateLimit-Remaining`, and `X-1-RateLimit-Reset`.                                                                                                                                                                                                                                                                  |\n| key_type                | string  | False                                     | var         | [\"var\",\"var_combination\",\"constant\"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. If the `key_type` is `constant`, the `key` is interpreted as a constant.                  |\n| key                     | string  | False                                     | remote_addr |                                        | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. If the `key_type` is `constant`, the `key` is interpreted as a constant value. |\n| rejected_code           | integer | False                                     | 503           | [200,...,599]                          | The HTTP status code returned when a request is rejected for exceeding the threshold.                                                                                                                                                                                                                                                                                                                                                                                                                    |\n| rejected_msg            | string  | False                                     |               | non-empty                              | The response body returned when a request is rejected for exceeding the threshold.                                                                                                                                                                                                                                                                                                                                                                                                                |\n| policy                  | string  | False                                     | local       | [\"local\",\"redis\",\"redis-cluster\"]    | The policy for rate limiting counter. If it is `local`, the counter is stored in memory locally. If it is `redis`, the counter is stored on a Redis instance. If it is `redis-cluster`, the counter is stored in a Redis cluster.                                                                                                            |\n| allow_degradation       | boolean | False                                     | false         |                                        | If true, allow APISIX to continue handling requests without the plugin when the plugin or its dependencies become unavailable.                                                                                                                                                                                                                                                                                             |\n| show_limit_quota_header | boolean | False                                     | true          |                                        | If true, include `X-RateLimit-Limit` to show the total quota and `X-RateLimit-Remaining` to show the remaining quota in the response header.                                                                                                                                                                                                                                                                                                                                   |\n| group                   | string  | False                                     |               | non-empty                              | The `group` ID for the plugin, such that routes of the same `group` can share the same rate limiting counter.                                                                                                                                                                                                                                                                                                                                                                   |\n| redis_host              | string  | False         |               |                                        | The address of the Redis node. Required when `policy` is `redis`.                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| redis_port              | integer | False                                     | 6379          | [1,...]                                | The port of the Redis node when `policy` is `redis`.                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| redis_username          | string  | False                                     |               |                                        | The username for Redis if Redis ACL is used. If you use the legacy authentication method `requirepass`, configure only the `redis_password`. Used when `policy` is `redis`.                                                                                                                                                                                                                                                                                                                                                                                                          |\n| redis_password          | string  | False                                     |               |                                        | The password of the Redis node when `policy` is `redis` or `redis-cluster`.                                                                                                                                                                                                                                                                                                                                                                                                           |\n| redis_ssl               | boolean | False                                     | false         |                                        | If true, use SSL to connect to Redis cluster when `policy` is `redis`.                                                                                                                                                                                                                                                                                                                                                                                                         |\n| redis_ssl_verify        | boolean | False                                     | false         |                                        | If true, verify the server SSL certificate when `policy` is `redis`.                                                                                                                                                                                                                                                                                                                                                                                         |\n| redis_database          | integer | False                                     | 0             | >= 0                    | The database number in Redis when `policy` is `redis`.                                                                                                                                                                                                                                                                                                                       |\n| redis_timeout           | integer | False                                     | 1000          | [1,...]                                | The Redis timeout value in milliseconds when `policy` is `redis` or `redis-cluster`.                                                                                                                                                                                                                                                                                                                                                        |\n| redis_keepalive_timeout       | integer | False                                     | 10000         | ≥ 1000                                 | Keepalive timeout in milliseconds for redis when `policy` is `redis` or `redis-cluster`.                                      |\n| redis_keepalive_pool          | integer | False                                     | 100           | ≥ 1                                    | Keepalive pool size for redis when `policy` is `redis` or `redis-cluster`.                                                    |\n| redis_cluster_nodes     | array[string]   | False |               |                                        | The list of the Redis cluster nodes with at least two addresses. Required when policy is redis-cluster.                                                                                                                                                                                                                                                                                                                                                                                                       |\n| redis_cluster_name      | string  | False |               |                                        | The name of the Redis cluster. Required when `policy` is `redis-cluster`.                                                                                                                                                                                                                                                                                                                                                                                                |\n| redis_cluster_ssl      | boolean  |  False |     false         |                                        | If true, use SSL to connect to Redis cluster when `policy` is `redis-cluster`.                                                                                                                                                                                                                                                                                                                                                                               |\n| redis_cluster_ssl_verify      | boolean  | False |    false      |                                        | If true, verify the server SSL certificate when `policy` is `redis-cluster`.                                                                                                                                                                                                                                                                                                                                                                                               |\n\n## Examples\n\nThe examples below demonstrate how you can configure `limit-count` in different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Apply Rate Limiting by Remote Address\n\nThe following example demonstrates the rate limiting of requests by a single variable, `remote_addr`.\n\nCreate a Route with `limit-count` plugin that allows for a quota of 1 within a 30-second window per remote address:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response.\n\nThe request has consumed all the quota allowed for the time window. If you send the request again within the same 30-second time interval, you should receive an `HTTP/1.1 429 Too Many Requests` response, indicating the request surpasses the quota threshold.\n\n### Apply Rate Limiting by Remote Address and Consumer Name\n\nThe following example demonstrates the rate limiting of requests by a combination of variables, `remote_addr` and `consumer_name`. It allows for a quota of 1 within a 30-second window per remote address and for each consumer.\n\nCreate a Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `key-auth` Credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a second Consumer `jane`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth` and `limit-count` plugins, and specify in the `limit-count` plugin to use a combination of variables as the rate limiting key:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key_type\": \"var_combination\",\n        \"key\": \"$remote_addr $consumer_name\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request as the Consumer `jane`:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: jane-key'\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the corresponding response body.\n\nThis request has consumed all the quota set for the time window. If you send the same request as the Consumer `jane` within the same 30-second time interval, you should receive an `HTTP/1.1 429 Too Many Requests` response, indicating the request surpasses the quota threshold.\n\nSend the same request as the Consumer `john` within the same 30-second time interval:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key'\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the corresponding response body, indicating the request is not rate limited.\n\nSend the same request as the Consumer `john` again within the same 30-second time interval, you should receive an `HTTP/1.1 429 Too Many Requests` response.\n\nThis verifies the plugin rate limits by the combination of variables, `remote_addr` and `consumer_name`.\n\n### Share Quota among Routes\n\nThe following example demonstrates the sharing of rate limiting quota among multiple routes by configuring the `group` of the `limit-count` plugin.\n\nNote that the configurations of the `limit-count` plugin of the same `group` should be identical. To avoid update anomalies and repetitive configurations, you can create a Service with `limit-count` plugin and Upstream for routes to connect to.\n\nCreate a service:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/services\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-service\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"group\": \"srv1\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nCreate two Routes and configure their `service_id` to be `limit-count-service`, so that they share the same configurations for the Plugin and Upstream:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route-1\",\n    \"service_id\": \"limit-count-service\",\n    \"uri\": \"/get1\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/get\"\n      }\n    }\n  }'\n```\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route-2\",\n    \"service_id\": \"limit-count-service\",\n    \"uri\": \"/get2\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/get\"\n      }\n    }\n  }'\n```\n\n:::note\n\nThe [`proxy-rewrite`](./proxy-rewrite.md) plugin is used to rewrite the URI to `/get` so that requests are forwarded to the correct endpoint.\n\n:::\n\nSend a request to Route `/get1`:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get1\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the corresponding response body.\n\nSend the same request to Route `/get2` within the same 30-second time interval:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get2\"\n```\n\nYou should receive an `HTTP/1.1 429 Too Many Requests` response, which verifies the two routes share the same rate limiting quota.\n\n### Share Quota Among APISIX Nodes with a Redis Server\n\nThe following example demonstrates the rate limiting of requests across multiple APISIX nodes with a Redis server, such that different APISIX nodes share the same rate limiting quota.\n\nOn each APISIX instance, create a Route with the following configurations. Adjust the address of the Admin API, Redis host, port, password, and database accordingly.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis\",\n        \"redis_host\": \"192.168.xxx.xxx\",\n        \"redis_port\": 6379,\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_database\": 1\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to an APISIX instance:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the corresponding response body.\n\nSend the same request to a different APISIX instance within the same 30-second time interval, you should receive an `HTTP/1.1 429 Too Many Requests` response, verifying routes configured in different APISIX nodes share the same quota.\n\n### Share Quota Among APISIX Nodes with a Redis Cluster\n\nYou can also use a Redis cluster to apply the same quota across multiple APISIX nodes, such that different APISIX nodes share the same rate limiting quota.\n\nEnsure that your Redis instances are running in [cluster mode](https://redis.io/docs/management/scaling/#create-and-use-a-redis-cluster). A minimum of two nodes are required for the `limit-count` plugin configurations.\n\nOn each APISIX instance, create a Route with the following configurations. Adjust the address of the Admin API, Redis cluster nodes, password, cluster name, and SSL varification accordingly.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis-cluster\",\n        \"redis_cluster_nodes\": [\n          \"192.168.xxx.xxx:6379\",\n          \"192.168.xxx.xxx:16379\"\n        ],\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_cluster_name\": \"redis-cluster-1\",\n        \"redis_cluster_ssl\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to an APISIX instance:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the corresponding response body.\n\nSend the same request to a different APISIX instance within the same 30-second time interval, you should receive an `HTTP/1.1 429 Too Many Requests` response, verifying routes configured in different APISIX nodes share the same quota.\n\n### Rate Limit with Anonymous Consumer\n\ndoes not need to authenticate and has less quotas. While this example uses [`key-auth`](./key-auth.md) for authentication, the anonymous Consumer can also be configured with [`basic-auth`](./basic-auth.md), [`jwt-auth`](./jwt-auth.md), and [`hmac-auth`](./hmac-auth.md).\n\nCreate a regular Consumer `john` and configure the `limit-count` plugin to allow for a quota of 3 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate the `key-auth` Credential for the Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate an anonymous user `anonymous` and configure the `limit-count` Plugin to allow for a quota of 1 within a 30-second window:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\nCreate a Route and configure the `key-auth` Plugin to accept anonymous Consumer `anonymous` from bypassing the authentication:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send five consecutive requests with `john`'s key:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: john-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 5 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    3, 429:    2\n```\n\nSend five anonymous requests:\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that only one request was successful:\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/limit-req.md",
    "content": "---\ntitle: limit-req\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Limit Request\n  - limit-req\ndescription: The limit-req Plugin uses the leaky bucket algorithm to rate limit the number of the requests and allow for throttling.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/limit-req\" />\n</head>\n\n## Description\n\nThe `limit-req` Plugin uses the [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm to rate limit the number of the requests and allow for throttling.\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values               | Description                                                                                                           |\n|-------------------|---------|----------|---------|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| rate              | integer | True     |         | > 0                   | The maximum number of requests allowed per second. Requests exceeding the rate and below burst will be delayed.                                                                                                                                                                                                                                                                    |\n| burst             | integer | True     |         | >= 0                 | The number of requests allowed to be delayed per second for throttling. Requests exceeding the rate and burst will get rejected.                                                                                                                                                                                                                                               |\n| key_type          | string  | False    | var   | [\"var\", \"var_combination\"] | The type of key. If the `key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables.                                                                                                                                                                                                                                                  |\n| key               | string  | True     |  remote_addr  |   | The key to count requests by. If the `key_type` is `var`, the `key` is interpreted a variable. The variable does not need to be prefixed by a dollar sign (`$`). If the `key_type` is `var_combination`, the `key` is interpreted as a combination of variables. All variables should be prefixed by dollar signs (`$`). For example, to configure the `key` to use a combination of two request headers `custom-a` and `custom-b`, the `key` should be configured as `$http_custom_a $http_custom_b`. |\n| rejected_code     | integer | False    | 503     | [200,...,599]              | The HTTP status code returned when a request is rejected for exceeding the threshold.                                                                   |\n| rejected_msg      | string  | False    |         | non-empty                  | The response body returned when a request is rejected for exceeding the threshold.                                                              |\n| nodelay           | boolean | False    | false   |                            | If true, do not delay requests within the burst threshold.                                                                        |\n| allow_degradation | boolean | False    | false   |                            | If true, allow APISIX to continue handling requests without the Plugin when the Plugin or its dependencies become unavailable.                                                                                                                                                                                                                                                                             |\n| policy            | string  | False                                     | local   | [\"local\", \"redis\", \"redis-cluster\"] | The policy for rate limiting counter. If it is `local`, the counter is stored in memory locally. If it is `redis`, the counter is stored on a Redis instance. If it is `redis-cluster`, the counter is stored in a Redis cluster.                                                                                                            |\n| redis_host        | string  | False         |         |                            | The address of the Redis node. Required when `policy` is `redis`.                                                                  |\n| redis_port        | integer | False                                     | 6379    | [1,...]                    | The port of the Redis node when `policy` is `redis`.                                                                     |\n| redis_username    | string  | False                                     |         |                            | The username for Redis if Redis ACL is used. If you use the legacy authentication method `requirepass`, configure only the `redis_password`. Used when `policy` is `redis`.                                                                                                                                                  |\n| redis_password    | string  | False                                     |         |                            | The password of the Redis node when `policy` is `redis` or `redis-cluster`.                                                   |\n| redis_ssl               | boolean | False                                     | false         |                                        | If true, use SSL to connect to Redis cluster when `policy` is `redis`.                                                                                                                         |\n| redis_ssl_verify        | boolean | False                                     | false         |                                        | If true, verify the server SSL certificate when `policy` is `redis`.                                                                                                         |\n| redis_database          | integer | False                                     | 0             | >= 0                    | The database number in Redis when `policy` is `redis`.                                                                     |\n| redis_timeout           | integer | False                                     | 1000          | [1,...]                                | The Redis timeout value in milliseconds when `policy` is `redis` or `redis-cluster`.                                                                        |\n| redis_keepalive_timeout       | integer | False                                     | 10000         | ≥ 1000                                 | Keepalive timeout in milliseconds for redis when `policy` is `redis` or `redis-cluster`.                                      |\n| redis_keepalive_pool          | integer | False                                     | 100           | ≥ 1                                    | Keepalive pool size for redis when `policy` is `redis` or `redis-cluster`.                                                    |\n| redis_cluster_nodes     | array[string]   | False |               |                                        | The list of the Redis cluster nodes with at least two addresses. Required when policy is redis-cluster.                                                                                                                       |\n| redis_cluster_name      | string  | False |               |                                        | The name of the Redis cluster. Required when `policy` is `redis-cluster`.                                                                                                                |\n| redis_cluster_ssl      | boolean  |  False |     false         |                                        | If true, use SSL to connect to Redis cluster when `policy` is                                                                                                                |\n| redis_cluster_ssl_verify      | boolean  | False |    false      |                                        | If true, verify the server SSL certificate when `policy` is `redis-cluster`.                                                                                                               |\n\n## Examples\n\nThe examples below demonstrate how you can configure `limit-req` in different scenarios.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n### Apply Rate Limiting by Remote Address\n\nThe following example demonstrates the rate limiting of HTTP requests by a single variable, `remote_addr`.\n\nCreate a Route with `limit-req` Plugin that allows for 1 QPS per remote address:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '\n  {\n    \"id\": \"limit-req-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-req\": {\n        \"rate\": 1,\n        \"burst\": 0,\n        \"key\": \"remote_addr\",\n        \"key_type\": \"var\",\n        \"rejected_code\": 429,\n        \"nodelay\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response.\n\nThe request has consumed all the quota allowed for the time window. If you send the request again within the same second, you should receive an `HTTP/1.1 429 Too Many Requests` response, indicating the request surpasses the quota threshold.\n\n### Implement API Throttling\n\nThe following example demonstrates how to configure `burst` to allow overrun of the rate limiting threshold by the configured value and achieve request throttling. You will also see a comparison against when throttling is not implemented.\n\nCreate a Route with `limit-req` Plugin that allows for 1 QPS per remote address, with a `burst` of 1 to allow for 1 request exceeding the `rate` to be delayed for processing:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-req-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-req\": {\n        \"rate\": 1,\n        \"burst\": 1,\n        \"key\": \"remote_addr\",\n        \"rejected_code\": 429\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nGenerate three requests to the Route:\n\n```shell\nresp=$(seq 3 | xargs -I{} curl -i \"http://127.0.0.1:9080/get\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200 responses: $count_200 ; 429 responses: $count_429\"\n```\n\nYou are likely to see that all three requests are successful:\n\n```text\n200 responses: 3 ; 429 responses: 0\n```\n\nTo see the effect without `burst`, update `burst` to 0 or set `nodelay` to `true` as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/limit-req-route\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"limit-req\": {\n        \"nodelay\": true\n      }\n    }\n  }'\n```\n\nGenerate three requests to the Route again:\n\n```shell\nresp=$(seq 3 | xargs -I{} curl -i \"http://127.0.0.1:9080/get\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200 responses: $count_200 ; 429 responses: $count_429\"\n```\n\nYou should see a response similar to the following, showing requests surpassing the rate have been rejected:\n\n```text\n200 responses: 1 ; 429 responses: 2\n```\n\n### Apply Rate Limiting by Remote Address and Consumer Name\n\nThe following example demonstrates the rate limiting of requests by a combination of variables, `remote_addr` and `consumer_name`.\n\nCreate a Route with `limit-req` Plugin that allows for 1 QPS per remote address and for each Consumer.\n\nCreate a Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a second Consumer `jane`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\nCreate `key-auth` Credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth` and `limit-req` Plugins, and specify in the `limit-req` Plugin to use a combination of variables as the rate-limiting key:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-req-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"limit-req\": {\n        \"rate\": 1,\n        \"burst\": 0,\n        \"key\": \"$remote_addr $consumer_name\",\n        \"key_type\": \"var_combination\",\n        \"rejected_code\": 429\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend two requests simultaneously, each for one Consumer:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: jane-key' & \\\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key' &\n```\n\nYou should receive `HTTP/1.1 200 OK` for both requests, indicating the request has not exceeded the threshold for each Consumer.\n\nIf you send more requests as either Consumer within the same second, you should receive an `HTTP/1.1 429 Too Many Requests` response.\n\nThis verifies the Plugin rate limits by the combination of variables, `remote_addr` and `consumer_name`.\n"
  },
  {
    "path": "docs/en/latest/plugins/log-rotate.md",
    "content": "---\ntitle: log-rotate\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Log rotate\ndescription: This document contains information about the Apache APISIX log-rotate Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `log-rotate` Plugin is used to keep rotating access and error log files in the log directory at regular intervals.\n\nYou can configure how often the logs are rotated and how many logs to keep. When the number of logs exceeds, older logs are automatically deleted.\n\n## Attributes\n\n| Name               | Type    | Required | Default | Description                                                                                    |\n|--------------------|---------|----------|---------|------------------------------------------------------------------------------------------------|\n| interval           | integer | True     | 60 * 60 | Time in seconds specifying how often to rotate the logs.                                       |\n| max_kept           | integer | True     | 24 * 7  | Maximum number of historical logs to keep. If this number is exceeded, older logs are deleted. |\n| max_size           | integer | False    | -1      | Max size(Bytes) of log files to be rotated, size check would be skipped with a value less than 0 or time is up specified by interval. |\n| enable_compression | boolean | False    | false   | When set to `true`, compresses the log file (gzip). Requires `tar` to be installed.            |\n\n## Enable Plugin\n\nTo enable the Plugin, add it in your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n    - log-rotate\n\nplugin_attr:\n    log-rotate:\n        interval: 3600    # rotate interval (unit: second)\n        max_kept: 168     # max number of log files will be kept\n        max_size: -1      # max size of log files will be kept\n        enable_compression: false    # enable log file compression(gzip) or not, default false\n```\n\n## Example usage\n\nOnce you enable the Plugin as shown above, the logs will be stored and rotated based on your configuration.\n\nIn the example below the `interval` is set to `10` and `max_kept` is set to `10`. This will create logs as shown:\n\n```shell\nll logs\n```\n\n```shell\ntotal 44K\n-rw-r--r--. 1 resty resty    0 Mar 20 20:32 2020-03-20_20-32-40_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:32 2020-03-20_20-32-40_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:32 2020-03-20_20-32-50_access.log\n-rw-r--r--. 1 resty resty 2.8K Mar 20 20:32 2020-03-20_20-32-50_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:32 2020-03-20_20-33-00_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:33 2020-03-20_20-33-00_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-10_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:33 2020-03-20_20-33-10_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-20_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:33 2020-03-20_20-33-20_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-30_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:33 2020-03-20_20-33-30_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-40_access.log\n-rw-r--r--. 1 resty resty 2.8K Mar 20 20:33 2020-03-20_20-33-40_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-50_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:33 2020-03-20_20-33-50_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-34-00_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:34 2020-03-20_20-34-00_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:34 2020-03-20_20-34-10_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:34 2020-03-20_20-34-10_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:34 access.log\n-rw-r--r--. 1 resty resty 1.5K Mar 20 21:31 error.log\n```\n\nIf you have enabled compression, the logs will be as shown below:\n\n```shell\ntotal 10.5K\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:33 2020-03-20_20-33-50_access.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:33 2020-03-20_20-33-50_error.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:33 2020-03-20_20-34-00_access.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:34 2020-03-20_20-34-00_error.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:34 2020-03-20_20-34-10_access.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:34 2020-03-20_20-34-10_error.log.tar.gz\n-rw-r--r--. 1 resty resty    0 Mar 20 20:34 access.log\n-rw-r--r--. 1 resty resty 1.5K Mar 20 21:31 error.log\n```\n\n## Delete Plugin\n\nTo remove the `log-rotate` Plugin, you can remove it from your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n    # - log-rotate\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/loggly.md",
    "content": "---\ntitle: loggly\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - SolarWinds Loggly\ndescription: This document contains information about the Apache APISIX loggly Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `loggly` Plugin is used to forward logs to [SolarWinds Loggly](https://www.solarwinds.com/loggly) for analysis and storage.\n\nWhen the Plugin is enabled, APISIX will serialize the request context information to [Loggly Syslog](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/streaming-syslog-without-using-files.htm?cshid=loggly_streaming-syslog-without-using-files) data format which is Syslog events with [RFC5424](https://datatracker.ietf.org/doc/html/rfc5424) compliant headers.\n\nWhen the maximum batch size is exceeded, the data in the queue is pushed to Loggly enterprise syslog endpoint. See [batch processor](../batch-processor.md) for more details.\n\n## Attributes\n\n| Name                   | Type          | Required | Default | Description                                                                                                                                                                                                                    |\n|------------------------|---------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| customer_token         | string        | True     |         | Unique identifier used when sending logs to Loggly to ensure that they are sent to the right organisation account.                                                                                                             |\n| severity               | string (enum) | False    | INFO    | Syslog log event severity level. Choose between: `DEBUG`, `INFO`, `NOTICE`, `WARNING`, `ERR`, `CRIT`, `ALERT`, and `EMEGR`.                                                                                                    |\n| severity_map           | object        | False    | nil     | A way to map upstream HTTP response codes to Syslog severity. Key-value pairs where keys are the HTTP response codes and the values are the Syslog severity levels. For example `{\"410\": \"CRIT\"}`.                             |\n| tags                   | array         | False    |         | Metadata to be included with any event log to aid in segmentation and filtering.                                                                                                                                               |\n| log_format             | object        | False    | {\"host\": \"$host\", \"@timestamp\": \"$time_iso8601\", \"client_ip\": \"$remote_addr\"} | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| include_req_body       | boolean       | False    | false   | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitations.                                                               |\n| include_req_body_expr  | array         | False    |         | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.        |\n| max_req_body_bytes | integer | False | 524288 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean       | False    | false   | When set to `true` includes the response body in the log.                                                                                                                                                                      |\n| include_resp_body_expr | array         | False    |         | When the `include_resp_body` attribute is set to `true`, use this to filter based on [lua-resty-expr](https://github.com/api7/lua-resty-expr). If present, only logs the response if the expression evaluates to `true`.       |\n| max_resp_body_bytes | integer | False | 524288 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\nTo generate a Customer token, go to `<your assigned subdomain>/loggly.com/tokens` or navigate to Logs > Source setup > Customer tokens.\n\n### Example of default log format\n\n```text\n<10>1 2024-01-06T06:50:51.739Z 127.0.0.1 apisix 58525 - [token-1@41058 tag=\"apisix\"] {\"service_id\":\"\",\"server\":{\"version\":\"3.7.0\",\"hostname\":\"localhost\"},\"apisix_latency\":100.99985313416,\"request\":{\"url\":\"http://127.0.0.1:1984/opentracing\",\"headers\":{\"content-type\":\"application/x-www-form-urlencoded\",\"user-agent\":\"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\",\"host\":\"127.0.0.1:1984\"},\"querystring\":{},\"uri\":\"/opentracing\",\"size\":155,\"method\":\"GET\"},\"response\":{\"headers\":{\"content-type\":\"text/plain\",\"server\":\"APISIX/3.7.0\",\"transfer-encoding\":\"chunked\",\"connection\":\"close\"},\"size\":141,\"status\":200},\"route_id\":\"1\",\"latency\":103.99985313416,\"upstream_latency\":3,\"client_ip\":\"127.0.0.1\",\"upstream\":\"127.0.0.1:1982\",\"start_time\":1704523851634}\n```\n\n## Metadata\n\nYou can also configure the Plugin through Plugin metadata. The following configurations are available:\n\n| Name       | Type    | Required | Default              | Valid values                   | Description                                                                                                                                                                                                                    |\n|------------|---------|----------|----------------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| host       | string  | False    | \"logs-01.loggly.com\" |                                | Endpoint of the host where the logs are being sent.                                                                                                                                                                            |\n| port       | integer | False    | 514                  |                                | Loggly port to connect to. Only used for `syslog` protocol.                                                                                                                                                                    |\n| timeout    | integer | False    | 5000                 |                                | Loggly send data request timeout in milliseconds.                                                                                                                                                                              |\n| protocol   | string  | False    | \"syslog\"             | [ \"syslog\" , \"http\", \"https\" ] | Protocol in which the logs are sent to Loggly.                                                                                                                                                                                 |\n| log_format | object  | False    | nil                  |                                | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\nWe support [Syslog](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/streaming-syslog-without-using-files.htm), [HTTP/S](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/http-bulk-endpoint.htm) (bulk endpoint) protocols to send log events to Loggly. By default, in APISIX side, the protocol is set to \"syslog\". It lets you send RFC5424 compliant syslog events with some fine-grained control (log severity mapping based on upstream HTTP response code). But HTTP/S bulk endpoint is great to send larger batches of log events with faster transmission speed. If you wish to update it, just update the metadata.\n\n:::note\n\nAPISIX supports [Syslog](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/streaming-syslog-without-using-files.htm) and [HTTP/S](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/http-bulk-endpoint.htm) protocols to send data to Loggly. Syslog lets you send RFC5424 compliant syslog events with fine-grained control. But, HTTP/S bulk endpoint is better while sending large batches of logs at a fast transmission speed. You can configure the metadata to update the protocol as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/loggly -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n   \"protocol\": \"http\"\n}'\n```\n\n:::\n\n## Enable Plugin\n\n### Full configuration\n\nThe example below shows a complete configuration of the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"loggly\":{\n            \"customer_token\":\"0e6fe4bf-376e-40f4-b25f-1d55cb29f5a2\",\n            \"tags\":[\"apisix\", \"testroute\"],\n            \"severity\":\"info\",\n            \"severity_map\":{\n                \"503\": \"err\",\n                \"410\": \"alert\"\n            },\n            \"buffer_duration\":60,\n            \"max_retry_count\":0,\n            \"retry_delay\":1,\n            \"inactive_timeout\":2,\n            \"batch_max_size\":10\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:80\":1\n        }\n    },\n    \"uri\":\"/index.html\"\n}'\n```\n\n### Minimal configuration\n\nThe example below shows a bare minimum configuration of the Plugin on a Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"loggly\":{\n            \"customer_token\":\"0e6fe4bf-376e-40f4-b25f-1d55cb29f5a2\",\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:80\":1\n        }\n    },\n    \"uri\":\"/index.html\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in Loggly:\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\nYou can then view the logs on your Loggly Dashboard:\n\n![Loggly Dashboard](../../../assets/images/plugin/loggly-dashboard.png)\n\n## Delete Plugin\n\nTo remove the `file-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/loki-logger.md",
    "content": "---\ntitle: loki-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Loki-logger\n  - Grafana Loki\ndescription: The loki-logger Plugin pushes request and response logs in batches to Grafana Loki, via the Loki HTTP API /loki/api/v1/push. The Plugin also supports the customization of log formats.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/loki-logger\" />\n</head>\n\n## Description\n\nThe `loki-logger` Plugin pushes request and response logs in batches to [Grafana Loki](https://grafana.com/oss/loki/), via the [Loki HTTP API](https://grafana.com/docs/loki/latest/reference/loki-http-api/#loki-http-api) `/loki/api/v1/push`. The Plugin also supports the customization of log formats.\n\nWhen enabled, the Plugin will serialize the request context information to [JSON objects](https://grafana.com/docs/loki/latest/api/#push-log-entries-to-loki) and add them to the queue, before they are pushed to Loki. See [batch processor](../batch-processor.md) for more details.\n\n## Attributes\n\n| Name | Type | Required | Default | Valid values | Description |\n|---|---|---|---|---|---|\n| endpoint_addrs | array[string] | True |  | | Loki API base URLs, such as `http://127.0.0.1:3100`. If multiple endpoints are configured, the log will be pushed to a randomly determined endpoint from the list. |\n| endpoint_uri | string | False | /loki/api/v1/push | | URI path to the Loki ingest endpoint. |\n| tenant_id | string | False | fake | | Loki tenant ID. According to Loki's [multi-tenancy documentation](https://grafana.com/docs/loki/latest/operations/multi-tenancy/#multi-tenancy), the default value is set to `fake` under single-tenancy. |\n| headers | object | False |  |  | Key-value pairs of request headers (settings for `X-Scope-OrgID` and `Content-Type` will be ignored). |\n| log_labels | object | False | {job = \"apisix\"} | | Loki log label. Support [NGINX variables](https://nginx.org/en/docs/varindex.html) and constant strings in values. Variables should be prefixed with a `$` sign. For example, the label can be `{\"origin\" = \"apisix\"}` or `{\"origin\" = \"$remote_addr\"}`. |\n| ssl_verify        | boolean       | False    | true | | If true, verify Loki's SSL certificates. |\n| timeout           | integer       | False    | 3000 | [1, 60000] | Timeout for the Loki service HTTP call in milliseconds.  |\n| keepalive         | boolean       | False    | true |  | If true, keep the connection alive for multiple requests. |\n| keepalive_timeout | integer       | False    | 60000 | >=1000 | Keepalive timeout in milliseconds.  |\n| keepalive_pool    | integer       | False    | 5       | >=1 | Maximum number of connections in the connection pool.  |\n| log_format | object | False    |          | | Custom log format as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX variables](../apisix-variable.md) and [NGINX variables](http://nginx.org/en/docs/varindex.html) can be referenced by prefixing with `$`. |\n| name | string | False    | loki-logger | | Unique identifier of the Plugin for the batch processor. If you use [Prometheus](./prometheus.md) to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`. |\n| include_req_body       | boolean | False    | false | | If true, include the request body in the log. Note that if the request body is too big to be kept in the memory, it can not be logged due to NGINX's limitations. |\n| include_req_body_expr  | array[array]   | False    |  | | An array of one or more conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the `include_req_body` is true. Request body would only be logged when the expressions configured here evaluate to true. |\n| max_req_body_bytes | integer | False | 524288 | >=1 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean | False    | false | | If true, include the response body in the log.  |\n| include_resp_body_expr | array[array]   | False    |  | | An array of one or more conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the `include_resp_body` is true. Response body would only be logged when the expressions configured here evaluate to true. |\n| max_resp_body_bytes | integer | False | 524288 | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n## Plugin Metadata\n\nYou can also configure log format on a global scale using the [Plugin Metadata](../terminology/plugin-metadata.md), which configures the log format for all `loki-logger` Plugin instances. If the log format configured on the individual Plugin instance differs from the log format configured on Plugin metadata, the log format configured on the individual Plugin instance takes precedence.\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| log_format | object | False |  | Custom log format as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX variables](../apisix-variable.md) and [NGINX variables](http://nginx.org/en/docs/varindex.html) can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `loki-logger` Plugin for different scenarios.\n\nTo follow along the examples, start a sample Loki instance in Docker:\n\n```shell\nwget https://raw.githubusercontent.com/grafana/loki/v3.0.0/cmd/loki/loki-local-config.yaml -O loki-config.yaml\ndocker run --name loki -d -v $(pwd):/mnt/config -p 3100:3100 grafana/loki:3.2.1 -config.file=/mnt/config/loki-config.yaml\n```\n\nAdditionally, start a Grafana instance to view and visualize the logs:\n\n```shell\ndocker run -d --name=apisix-quickstart-grafana \\\n  -p 3000:3000 \\\n  grafana/grafana-oss\n```\n\nTo connect Loki and Grafana, visit Grafana at [`http://localhost:3000`](http://localhost:3000). Under __Connections > Data sources__, add a new data source and select Loki. Your connection URL should follow the format of `http://{your_ip_address}:3100`. When saving the new data source, Grafana should also test the connection, and you are expected to see Grafana notifying the data source is successfully connected.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Log Requests and Responses in Default Log Format\n\nThe following example demonstrates how you can configure the `loki-logger` Plugin on a Route to log requests and responses going through the route.\n\nCreate a Route with the `loki-logger` Plugin and configure the address of Loki:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"loki-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"loki-logger\": {\n        \"endpoint_addrs\": [\"http://192.168.1.5:3100\"]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend a few requests to the Route to generate log entries:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses for all requests.\n\nNavigate to the [Grafana explore view](http://localhost:3000/explore) and run a query `job = apisix`. You should see a number of logs corresponding to your requests, such as the following:\n\n```json\n{\n  \"route_id\": \"loki-logger-route\",\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"date\": \"Fri, 03 Jan 2025 03:54:26 GMT\",\n      \"server\": \"APISIX/3.11.0\",\n      \"access-control-allow-credentials\": \"true\",\n      \"content-length\": \"391\",\n      \"access-control-allow-origin\": \"*\",\n      \"content-type\": \"application/json\",\n      \"connection\": \"close\"\n    },\n    \"size\": 619\n  },\n  \"start_time\": 1735876466,\n  \"client_ip\": \"192.168.65.1\",\n  \"service_id\": \"\",\n  \"apisix_latency\": 5.0000038146973,\n  \"upstream\": \"34.197.122.172:80\",\n  \"upstream_latency\": 666,\n  \"server\": {\n    \"hostname\": \"0b9a772e68f8\",\n    \"version\": \"3.11.0\"\n  },\n  \"request\": {\n    \"headers\": {\n      \"user-agent\": \"curl/8.6.0\",\n      \"accept\": \"*/*\",\n      \"host\": \"127.0.0.1:9080\"\n    },\n    \"size\": 85,\n    \"method\": \"GET\",\n    \"url\": \"http://127.0.0.1:9080/anything\",\n    \"querystring\": {},\n    \"uri\": \"/anything\"\n  },\n  \"latency\": 671.0000038147\n}\n```\n\nThis verifies that Loki has been receiving logs from APISIX. You may also create dashboards in Grafana to further visualize and analyze the logs.\n\n### Customize Log Format with Plugin Metadata\n\nThe following example demonstrates how you can customize log format using [Plugin Metadata](../terminology/plugin-metadata.md).\n\nCreate a Route with the `loki-logger` plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"loki-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"loki-logger\": {\n        \"endpoint_addrs\": [\"http://192.168.1.5:3100\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nConfigure Plugin metadata for `loki-logger`, which will update the log format for all routes of which requests would be logged:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/loki-logger\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"log_format\": {\n      \"host\": \"$host\",\n      \"client_ip\": \"$remote_addr\",\n      \"route_id\": \"$route_id\",\n      \"@timestamp\": \"$time_iso8601\"\n    }\n  }'\n```\n\nSend a request to the Route to generate a new log entry:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nNavigate to the [Grafana explore view](http://localhost:3000/explore) and run a query `job = apisix`. You should see a log entry corresponding to your request, similar to the following:\n\n```json\n{\n  \"@timestamp\":\"2025-01-03T21:11:34+00:00\",\n  \"client_ip\":\"192.168.65.1\",\n  \"route_id\":\"loki-logger-route\",\n  \"host\":\"127.0.0.1\"\n}\n```\n\nIf the Plugin on a Route specifies a specific log format, it will take precedence over the log format specified in the Plugin metadata. For instance, update the Plugin on the previous Route as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/loki-logger-route\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"loki-logger\": {\n        \"log_format\": {\n          \"route_id\": \"$route_id\",\n          \"client_ip\": \"$remote_addr\",\n          \"@timestamp\": \"$time_iso8601\"\n        }\n      }\n    }\n  }'\n```\n\nSend a request to the Route to generate a new log entry:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nNavigate to the [Grafana explore view](http://localhost:3000/explore) and re-run the query `job = apisix`. You should see a log entry corresponding to your request, consistent with the format configured on the route, similar to the following:\n\n```json\n{\n  \"client_ip\":\"192.168.65.1\",\n  \"route_id\":\"loki-logger-route\",\n  \"@timestamp\":\"2025-01-03T21:19:45+00:00\"\n}\n```\n\n### Log Request Bodies Conditionally\n\nThe following example demonstrates how you can conditionally log request body.\n\nCreate a Route with `loki-logger` to only log request body if the URL query string `log_body` is `yes`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"loki-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"loki-logger\": {\n        \"endpoint_addrs\": [\"http://192.168.1.5:3100\"],\n        \"include_req_body\": true,\n        \"include_req_body_expr\": [[\"arg_log_body\", \"==\", \"yes\"]]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend a request to the Route with a URL query string satisfying the condition:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?log_body=yes\" -X POST -d '{\"env\": \"dev\"}'\n```\n\nNavigate to the [Grafana explore view](http://localhost:3000/explore) and run the query `job = apisix`. You should see a log entry corresponding to your request, where the request body is logged:\n\n```json\n{\n  \"route_id\": \"loki-logger-route\",\n  ...,\n  \"request\": {\n    \"headers\": {\n      ...\n    },\n    \"body\": \"{\\\"env\\\": \\\"dev\\\"}\",\n    \"size\": 182,\n    \"method\": \"POST\",\n    \"url\": \"http://127.0.0.1:9080/anything?log_body=yes\",\n    \"querystring\": {\n      \"log_body\": \"yes\"\n    },\n    \"uri\": \"/anything?log_body=yes\"\n  },\n  \"latency\": 809.99994277954\n}\n```\n\nSend a request to the Route without any URL query string:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST -d '{\"env\": \"dev\"}'\n```\n\nNavigate to the [Grafana explore view](http://localhost:3000/explore) and run the query `job = apisix`. You should see a log entry corresponding to your request, where the request body is not logged:\n\n```json\n{\n  \"route_id\": \"loki-logger-route\",\n  ...,\n  \"request\": {\n    \"headers\": {\n      ...\n    },\n    \"size\": 169,\n    \"method\": \"POST\",\n    \"url\": \"http://127.0.0.1:9080/anything\",\n    \"querystring\": {},\n    \"uri\": \"/anything\"\n  },\n  \"latency\": 557.00016021729\n}\n```\n\n:::info\n\nIf you have customized the `log_format` in addition to setting `include_req_body` or `include_resp_body` to `true`, the Plugin would not include the bodies in the logs.\n\nAs a workaround, you may be able to use the NGINX variable `$request_body` in the log format, such as:\n\n```json\n{\n  \"kafka-logger\": {\n    ...,\n    \"log_format\": {\"body\": \"$request_body\"}\n  }\n}\n```\n\n:::\n\n## FAQ\n\n### Logs are not pushed properly\n\nLook at `error.log` for such a log.\n\n```text\n2023/04/30 13:45:46 [error] 19381#19381: *1075673 [lua] batch-processor.lua:95: Batch Processor[loki logger] failed to process entries: loki server returned status: 401, body: no org id, context: ngx.timer, client: 127.0.0.1, server: 0.0.0.0:9081\n```\n\nThe error can be diagnosed based on the error code in the `failed to process entries: loki server returned status: 401, body: no org id` and the response body of the loki server.\n\n### Getting errors when RPS is high?\n\n- Make sure to `keepalive` related configuration is set properly. See [Attributes](#attributes) for more information.\n- Check the logs in `error.log`, look for such a log.\n\n    ```text\n    2023/04/30 13:49:34 [error] 19381#19381: *1082680 [lua] batch-processor.lua:95: Batch Processor[loki logger] failed to process entries: loki server returned status: 429, body: Ingestion rate limit exceeded for user tenant_1 (limit: 4194304 bytes/sec) while attempting to ingest '1000' lines totaling '616307' bytes, reduce log volume or contact your Loki administrator to see if the limit can be increased, context: ngx.timer, client: 127.0.0.1, server: 0.0.0.0:9081\n    ```\n\n  - The logs usually associated with high QPS look like the above. The error is: `Ingestion rate limit exceeded for user tenant_1 (limit: 4194304 bytes/sec) while attempting to ingest '1000' lines totaling '616307' bytes, reduce log volume or contact your Loki administrator to see if the limit can be increased`.\n  - Refer to [Loki documentation](https://grafana.com/docs/loki/latest/configuration/#limits_config) to add limits on the amount of default and burst logs, such as `ingestion_rate_mb` and `ingestion_burst_size_mb`.\n\n    As the test during development, setting the `ingestion_burst_size_mb` to 100 allows APISIX to push the logs correctly at least at 10000 RPS.\n"
  },
  {
    "path": "docs/en/latest/plugins/mocking.md",
    "content": "---\ntitle: mocking\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Mocking\ndescription: This document contains information about the Apache APISIX mocking Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `mocking` Plugin is used for mocking an API. When executed, it returns random mock data in the format specified and the request is not forwarded to the Upstream.\n\n## Attributes\n\n| Name             | Type    | Required | Default          | Description                                                                            |\n|------------------|---------|----------|------------------|----------------------------------------------------------------------------------------|\n| delay            | integer | False    |                  | Response delay in seconds.                                                             |\n| response_status  | integer | False    | 200              | HTTP status code of the response.                                                      |\n| content_type     | string  | False    | application/json | Header `Content-Type` of the response.                                                 |\n| response_example | string  | False    |                  | Body of the response, support use variables, like `$remote_addr $consumer_name`.       |\n| response_schema  | object  | False    |                  | The JSON schema object for the response. Works when `response_example` is unspecified. |\n| with_mock_header | boolean | False    | true             | When set to `true`, adds a response header `x-mock-by: APISIX/{version}`.              |\n| response_headers | object  | false    |                  | Headers to be added in the mocked response. Example: `{\"X-Foo\": \"bar\", \"X-Few\": \"baz\"}`|\n\nThe JSON schema supports the following types in their fields:\n\n- `string`\n- `number`\n- `integer`\n- `boolean`\n- `object`\n- `array`\n\nHere is a JSON schema example:\n\n```json\n{\n    \"properties\":{\n        \"field0\":{\n            \"example\":\"abcd\",\n            \"type\":\"string\"\n        },\n        \"field1\":{\n            \"example\":123.12,\n            \"type\":\"number\"\n        },\n        \"field3\":{\n            \"properties\":{\n                \"field3_1\":{\n                    \"type\":\"string\"\n                },\n                \"field3_2\":{\n                    \"properties\":{\n                        \"field3_2_1\":{\n                            \"example\":true,\n                            \"type\":\"boolean\"\n                        },\n                        \"field3_2_2\":{\n                            \"items\":{\n                                \"example\":155.55,\n                                \"type\":\"integer\"\n                            },\n                            \"type\":\"array\"\n                        }\n                    },\n                    \"type\":\"object\"\n                }\n            },\n            \"type\":\"object\"\n        },\n        \"field2\":{\n            \"items\":{\n                \"type\":\"string\"\n            },\n            \"type\":\"array\"\n        }\n    },\n    \"type\":\"object\"\n}\n```\n\nThis is the response generated by the Plugin from this JSON schema:\n\n```json\n{\n    \"field1\": 123.12,\n    \"field3\": {\n        \"field3_1\": \"LCFE0\",\n        \"field3_2\": {\n            \"field3_2_1\": true,\n            \"field3_2_2\": [\n                155,\n                155\n            ]\n        }\n    },\n    \"field0\": \"abcd\",\n    \"field2\": [\n        \"sC\"\n    ]\n}\n```\n\n## Enable Plugin\n\nThe example below configures the `mocking` Plugin for a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"mocking\": {\n            \"delay\": 1,\n            \"content_type\": \"application/json\",\n            \"response_status\": 200,\n            \"response_schema\": {\n               \"properties\":{\n                   \"field0\":{\n                       \"example\":\"abcd\",\n                       \"type\":\"string\"\n                   },\n                   \"field1\":{\n                       \"example\":123.12,\n                       \"type\":\"number\"\n                   },\n                   \"field3\":{\n                       \"properties\":{\n                           \"field3_1\":{\n                               \"type\":\"string\"\n                           },\n                           \"field3_2\":{\n                               \"properties\":{\n                                   \"field3_2_1\":{\n                                       \"example\":true,\n                                       \"type\":\"boolean\"\n                                   },\n                                   \"field3_2_2\":{\n                                       \"items\":{\n                                           \"example\":155.55,\n                                           \"type\":\"integer\"\n                                       },\n                                       \"type\":\"array\"\n                                   }\n                               },\n                               \"type\":\"object\"\n                           }\n                       },\n                       \"type\":\"object\"\n                   },\n                   \"field2\":{\n                       \"items\":{\n                           \"type\":\"string\"\n                       },\n                       \"type\":\"array\"\n                   }\n               },\n               \"type\":\"object\"\n           }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as mentioned above, you can test the Route.\n\nThe example used here uses this mocked response:\n\n```json\n{\n  \"delay\":0,\n  \"content_type\":\"\",\n  \"with_mock_header\":true,\n  \"response_status\":201,\n  \"response_example\":\"{\\\"a\\\":1,\\\"b\\\":2}\"\n}\n```\n\nNow to test the Route:\n\n```shell\ncurl http://127.0.0.1:9080/test-mock -i\n```\n\n```\nHTTP/1.1 201 Created\n...\nContent-Type: application/json;charset=utf8\nx-mock-by: APISIX/2.10.0\n...\n\n{\"a\":1,\"b\":2}\n```\n\n## Delete Plugin\n\nTo remove the `mocking` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/mqtt-proxy.md",
    "content": "---\ntitle: mqtt-proxy\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - MQTT Proxy\ndescription: This document contains information about the Apache APISIX mqtt-proxy Plugin. The `mqtt-proxy` Plugin is used for dynamic load balancing with `client_id` of MQTT.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `mqtt-proxy` Plugin is used for dynamic load balancing with `client_id` of MQTT. It only works in stream model.\n\nThis Plugin supports both the protocols [3.1.*](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) and [5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html).\n\n## Attributes\n\n| Name           | Type    | Required   | Description                                                                       |\n|----------------|---------|------------|-----------------------------------------------------------------------------------|\n| protocol_name  | string  | False      | Name of the protocol. Defaults to `MQTT`.                                         |\n| protocol_level | integer | True       | Level of the protocol. It should be `4` for MQTT `3.1.*` and `5` for MQTT `5.0`.  |\n\n## Enable Plugin\n\nTo enable the Plugin, you need to first enable the `stream_proxy` configuration in your configuration file (`conf/config.yaml`). The below configuration represents listening on the `9100` TCP port:\n\n```yaml title=\"conf/config.yaml\"\n    ...\n    router:\n        http: 'radixtree_uri'\n        ssl: 'radixtree_sni'\n    proxy_mode: http&stream\n    stream_proxy:                 # TCP/UDP proxy\n      tcp:                        # TCP proxy port list\n        - 9100\n    dns_resolver:\n    ...\n```\n\nYou can now send the MQTT request to port `9100`.\n\nYou can now create a stream Route and enable the `mqtt-proxy` Plugin:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"mqtt-proxy\": {\n            \"protocol_name\": \"MQTT\",\n            \"protocol_level\": 4\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": [{\n            \"host\": \"127.0.0.1\",\n            \"port\": 1980,\n            \"weight\": 1\n        }]\n    }\n}'\n```\n\n:::note\n\nIf you are using Docker in macOS, then `host.docker.internal` is the right parameter for the `host` attribute.\n\n:::\n\nThis Plugin exposes a variable `mqtt_client_id` which can be used for load balancing as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"mqtt-proxy\": {\n            \"protocol_name\": \"MQTT\",\n            \"protocol_level\": 4\n        }\n    },\n    \"upstream\": {\n        \"type\": \"chash\",\n        \"key\": \"mqtt_client_id\",\n        \"nodes\": [\n        {\n            \"host\": \"127.0.0.1\",\n            \"port\": 1995,\n            \"weight\": 1\n        },\n        {\n            \"host\": \"127.0.0.2\",\n            \"port\": 1995,\n            \"weight\": 1\n        }\n        ]\n    }\n}'\n```\n\nMQTT connections with different client ID will be forwarded to different nodes based on the consistent hash algorithm. If client ID is missing, client IP is used instead for load balancing.\n\n## Enabling mTLS with mqtt-proxy plugin\n\nStream proxies use TCP connections and can accept TLS. Follow the guide about [how to accept tls over tcp connections](../stream-proxy.md/#accept-tls-over-tcp-connection) to open a stream proxy with enabled TLS.\n\nThe `mqtt-proxy` plugin is enabled through TCP communications on the specified port for the stream proxy, and will also require clients to authenticate via TLS if `tls` is set to `true`.\n\nConfigure `ssl` providing the CA certificate and the server certificate, together with a list of SNIs. Steps to protect `stream_routes` with `ssl` are equivalent to the ones to [protect Routes](../mtls.md/#protect-route).\n\n### Create a stream_route using mqtt-proxy plugin and mTLS\n\nHere is an example of how create a stream_route which is using the `mqtt-proxy` plugin, providing the CA certificate, the client certificate and the client key (for self-signed certificates which are not trusted by your host, use the `-k` flag):\n\n```shell\ncurl 127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"mqtt-proxy\": {\n            \"protocol_name\": \"MQTT\",\n            \"protocol_level\": 4\n        }\n    },\n    \"sni\": \"${your_sni_name}\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nThe `sni` name must match one or more of the SNIs provided to the SSL object that you created with the CA and server certificates.\n\n## Delete Plugin\n\nTo remove the `mqtt-proxy` Plugin you can remove the corresponding configuration as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X DELETE\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/multi-auth.md",
    "content": "---\ntitle: multi-auth\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Multi Auth\n  - multi-auth\ndescription: This document contains information about the Apache APISIX multi-auth Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `multi-auth` Plugin is used to add multiple authentication methods to a Route or a Service. It supports plugins of type 'auth'. You can combine different authentication methods using `multi-auth` plugin.\n\nThis plugin provides a flexible authentication mechanism by iterating through the list of authentication plugins specified in the `auth_plugins` attribute. It allows multiple consumers to share the same route while using different authentication methods. For example, one consumer can authenticate using basic authentication, while another consumer can authenticate using JWT.\n\n## Attributes\n\nFor Route:\n\n| Name         | Type  | Required | Default | Description                                                           |\n|--------------|-------|----------|---------|-----------------------------------------------------------------------|\n| auth_plugins | array | True     | -       | Add supporting auth plugins configuration. expects at least 2 plugins |\n\n## Enable Plugin\n\nTo enable the Plugin, you have to create two or more Consumer objects with different authentication configurations:\n\nFirst create a Consumer using basic authentication:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"foo1\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\": \"foo1\",\n            \"password\": \"bar1\"\n        }\n    }\n}'\n```\n\nThen create a Consumer using key authentication:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"foo2\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    }\n}'\n```\n\nOnce you have created Consumer objects, you can then configure a Route or a Service to authenticate requests:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"multi-auth\":{\n         \"auth_plugins\":[\n            {\n               \"basic-auth\":{ }\n            },\n            {\n               \"key-auth\":{\n                  \"query\":\"apikey\",\n                  \"hide_credentials\":true,\n                  \"header\":\"apikey\"\n               }\n            }\n         ]\n      }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nAfter you have configured the Plugin as mentioned above, you can make a request to the Route as shown below:\n\nSend a request with `basic-auth` credentials:\n\n```shell\ncurl -i -ufoo1:bar1 http://127.0.0.1:9080/hello\n```\n\nSend a request with `key-auth` credentials:\n\n```shell\ncurl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -i\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\nIf the request is not authorized, an `401 Unauthorized` error will be thrown:\n\n```json\n{\"message\":\"Authorization Failed\"}\n```\n\n## Delete Plugin\n\nTo remove the `multi-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/node-status.md",
    "content": "---\ntitle: node-status\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Node status\ndescription: This document contains information about the Apache APISIX node-status Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `node-status` Plugin can be used get the status of requests to APISIX by exposing an API endpoint.\n\n## Attributes\n\nNone.\n\n## API\n\nThis Plugin will add the endpoint `/apisix/status` to expose the status of APISIX.\n\nYou may need to use the [public-api](public-api.md) Plugin to expose the endpoint.\n\n## Enable Plugin\n\nTo configure the `node-status` Plugin, you have to first enable it in your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - example-plugin\n  - limit-req\n  - jwt-auth\n  - zipkin\n  - node-status\n  ......\n```\n\nYou have to the setup the Route for the status API and expose it using the [public-api](public-api.md) Plugin.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/ns -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/status\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin, you can make a request to the `apisix/status` endpoint to get the status:\n\n```shell\ncurl http://127.0.0.1:9080/apisix/status -i\n```\n\n```shell\nHTTP/1.1 200 OK\nDate: Tue, 03 Nov 2020 11:12:55 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\n{\"status\":{\"total\":\"23\",\"waiting\":\"0\",\"accepted\":\"22\",\"writing\":\"1\",\"handled\":\"22\",\"active\":\"1\",\"reading\":\"0\"},\"id\":\"6790a064-8f61-44ba-a6d3-5df42f2b1bb3\"}\n```\n\nThe parameters in the response are described below:\n\n| Parameter | Description                                                                                                            |\n|-----------|------------------------------------------------------------------------------------------------------------------------|\n| status    | Status of APISIX.                                                                                                      |\n| total     | Total number of client requests.                                                                                       |\n| waiting   | Number of idle client connections waiting for a request.                                                               |\n| accepted  | Number of accepted client connections.                                                                                 |\n| writing   | Number of connections to which APISIX is writing back a response.                                                      |\n| handled   | Number of handled connections. Generally, this value is the same as `accepted` unless any a resource limit is reached. |\n| active    | Number of active client connections including `waiting` connections.                                                   |\n| reading   | Number of connections where APISIX is reading the request header.                                                      |\n| id        | UID of APISIX instance saved in `apisix/conf/apisix.uid`.                                                              |\n\n## Delete Plugin\n\nTo remove the Plugin, you can remove it from your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - example-plugin\n  - limit-req\n  - jwt-auth\n  - zipkin\n  ......\n```\n\nYou can also remove the Route on `/apisix/status`:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/ns -H \"X-API-KEY: $admin_key\" -X DELETE\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ocsp-stapling.md",
    "content": "---\ntitle: ocsp-stapling\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - ocsp-stapling\ndescription: This document contains information about the Apache APISIX ocsp-stapling Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `ocsp-stapling` Plugin dynamically sets the behavior of [OCSP stapling](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_stapling) in Nginx.\n\n## Enable Plugin\n\nThis Plugin is disabled by default. Modify the config file to enable the plugin:\n\n```yaml title=\"./conf/config.yaml\"\nplugins:\n  - ...\n  - ocsp-stapling\n```\n\nAfter modifying the config file, reload APISIX or send an hot-loaded HTTP request through the Admin API to take effect:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n\n## Attributes\n\nThe attributes of this plugin are stored in specific field `ocsp_stapling` within SSL Resource.\n\n| Name           | Type                 | Required | Default       | Valid values | Description                                                                                   |\n|----------------|----------------------|----------|---------------|--------------|-----------------------------------------------------------------------------------------------|\n| enabled        | boolean              | False    | false         |              | Like the `ssl_stapling` directive, enables or disables OCSP stapling feature.                 |\n| skip_verify    | boolean              | False    | false         |              | Like the `ssl_stapling_verify` directive, enables or disables verification of OCSP responses. |\n| cache_ttl      | integer              | False    | 3600          | >= 60        | Specifies the expired time of OCSP response cache.                                            |\n\n## Example usage\n\nYou should create an SSL Resource first, and the certificate of the server certificate issuer should be known. Normally the fullchain certificate works fine.\n\nCreate an SSL Resource as such:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\" : \"'\"$(cat server.crt)\"'\",\n    \"key\": \"'\"$(cat server.key)\"'\",\n    \"snis\": [\"test.com\"],\n    \"ocsp_stapling\": {\n        \"enabled\": true\n    }\n}'\n```\n\nNext, establish a secure connection to the server, request the SSL/TLS session status, and display the output from the server:\n\n```shell\necho -n \"Q\" | openssl s_client -status -connect localhost:9443 -servername test.com 2>&1 | cat\n```\n\n```\n...\nCONNECTED(00000003)\nOCSP response:\n======================================\nOCSP Response Data:\n    OCSP Response Status: successful (0x0)\n...\n```\n\nTo disable OCSP stapling feature, you can make a request as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\" : \"'\"$(cat server.crt)\"'\",\n    \"key\": \"'\"$(cat server.key)\"'\",\n    \"snis\": [\"test.com\"],\n    \"ocsp_stapling\": {\n        \"enabled\": false\n    }\n}'\n```\n\n## Delete Plugin\n\nMake sure all your SSL Resource doesn't contains `ocsp_stapling` field anymore. To remove this field, you can make a request as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PATCH -d '\n{\n    \"ocsp_stapling\": null\n}'\n```\n\nModify the config file `./conf/config.yaml` to disable the plugin:\n\n```yaml title=\"./conf/config.yaml\"\nplugins:\n  - ...\n  # - ocsp-stapling\n```\n\nAfter modifying the config file, reload APISIX or send an hot-loaded HTTP request through the Admin API to take effect:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/opa.md",
    "content": "---\ntitle: opa\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Open Policy Agent\n  - opa\ndescription: This document contains information about the Apache APISIX opa Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `opa` Plugin can be used to integrate with [Open Policy Agent (OPA)](https://www.openpolicyagent.org). OPA is a policy engine that helps defininig and enforcing authorization policies, which determines whether a user or application has the necessary permissions to perform a particular action or access a particular resource. Using OPA with APISIX decouples authorization logics from APISIX.\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values  | Description                                                                                                                                                                                |\n|-------------------|---------|----------|---------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| host              | string  | True     |         |               | Host address of the OPA service. For example, `https://localhost:8181`.                                                                                                                    |\n| ssl_verify        | boolean | False    | true    |               | When set to `true` verifies the SSL certificates.                                                                                                                                          |\n| policy            | string  | True     |         |               | OPA policy path. A combination of `package` and `decision`. While using advanced features like custom response, you can omit `decision`. When specifying a namespace, use the slash format (`examples/echo`) instead of dot notation (`examples.echo`).  |\n| timeout           | integer | False    | 3000ms  | [1, 60000]ms  | Timeout for the HTTP call.                                                                                                                                                                 |\n| keepalive         | boolean | False    | true    |               | When set to `true`, keeps the connection alive for multiple requests.                                                                                                                      |\n| keepalive_timeout | integer | False    | 60000ms | [1000, ...]ms | Idle time after which the connection is closed.                                                                                                                                            |\n| keepalive_pool    | integer | False    | 5       | [1, ...]ms    | Connection pool limit.                                                                                                                                                                     |\n| with_route        | boolean | False    | false   |               | When set to true, sends information about the current Route.                                                                                                                               |\n| with_service      | boolean | False    | false   |               | When set to true, sends information about the current Service.                                                                                                                             |\n| with_consumer     | boolean | False    | false   |               | When set to true, sends information about the current Consumer. Note that this may send sensitive information like the API key. Make sure to turn it on only when you are sure it is safe. |\n\n## Data definition\n\n### APISIX to OPA service\n\nThe JSON below shows the data sent to the OPA service by APISIX:\n\n```json\n{\n    \"type\": \"http\",\n    \"request\": {\n        \"scheme\": \"http\",\n        \"path\": \"\\/get\",\n        \"headers\": {\n            \"user-agent\": \"curl\\/7.68.0\",\n            \"accept\": \"*\\/*\",\n            \"host\": \"127.0.0.1:9080\"\n        },\n        \"query\": {},\n        \"port\": 9080,\n        \"method\": \"GET\",\n        \"host\": \"127.0.0.1\"\n    },\n    \"var\": {\n        \"timestamp\": 1701234567,\n        \"server_addr\": \"127.0.0.1\",\n        \"server_port\": \"9080\",\n        \"remote_port\": \"port\",\n        \"remote_addr\": \"ip address\"\n    },\n    \"route\": {},\n    \"service\": {},\n    \"consumer\": {}\n}\n```\n\nEach of these keys are explained below:\n\n- `type` indicates the request type (`http` or `stream`).\n- `request` is used when the `type` is `http` and contains the basic request information (URL, headers etc).\n- `var` contains the basic information about the requested connection (IP, port, request timestamp etc).\n- `route`, `service` and `consumer` contains the same data as stored in APISIX and are only sent if the `opa` Plugin is configured on these objects.\n\n### OPA service to APISIX\n\nThe JSON below shows the response from the OPA service to APISIX:\n\n```json\n{\n    \"result\": {\n        \"allow\": true,\n        \"reason\": \"test\",\n        \"headers\": {\n            \"an\": \"header\"\n        },\n        \"status_code\": 401\n    }\n}\n```\n\nThe keys in the response are explained below:\n\n- `allow` is indispensable and indicates whether the request is allowed to be forwarded through APISIX.\n- `reason`, `headers`, and `status_code` are optional and are only returned when you configure a custom response. See the next section use cases for this.\n\n## Example usage\n\nFirst, you need to launch the Open Policy Agent environment:\n\n```shell\ndocker run -d --name opa -p 8181:8181 openpolicyagent/opa:0.35.0 run -s\n```\n\n### Basic usage\n\nOnce you have the OPA service running, you can create a basic policy:\n\n```shell\ncurl -X PUT '127.0.0.1:8181/v1/policies/example1' \\\n    -H 'Content-Type: text/plain' \\\n    -d 'package example1\n\nimport input.request\n\ndefault allow = false\n\nallow {\n    # HTTP method must GET\n    request.method == \"GET\"\n}'\n```\n\nThen, you can configure the `opa` Plugin on a specific Route:\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"opa\": {\n            \"host\": \"http://127.0.0.1:8181\",\n            \"policy\": \"example1\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nNow, to test it out:\n\n```shell\ncurl -i -X GET 127.0.0.1:9080/get\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\nNow if we try to make a request to a different endpoint the request will fail:\n\n```\ncurl -i -X POST 127.0.0.1:9080/post\n```\n\n```shell\nHTTP/1.1 403 FORBIDDEN\n```\n\n### Using custom response\n\nYou can also configure custom responses for more complex scenarios:\n\n```shell\ncurl -X PUT '127.0.0.1:8181/v1/policies/example2' \\\n    -H 'Content-Type: text/plain' \\\n    -d 'package example2\n\nimport input.request\n\ndefault allow = false\n\nallow {\n    request.method == \"GET\"\n}\n\n# custom response body (Accepts a string or an object, the object will respond as JSON format)\nreason = \"test\" {\n    not allow\n}\n\n# custom response header (The data of the object can be written in this way)\nheaders = {\n    \"Location\": \"http://example.com/auth\"\n} {\n    not allow\n}\n\n# custom response status code\nstatus_code = 302 {\n    not allow\n}'\n```\n\nNow you can test it out by changing the `opa` Plugin's policy parameter to `example2` and then making a request:\n\n```shell\ncurl -i -X GET 127.0.0.1:9080/get\n```\n\n```\nHTTP/1.1 200 OK\n```\n\nNow if you make a failing request, you will see the custom response from the OPA service:\n\n```\ncurl -i -X POST 127.0.0.1:9080/post\n```\n\n```\nHTTP/1.1 302 FOUND\nLocation: http://example.com/auth\n\ntest\n```\n\n### Sending APISIX data\n\nLet's think about another scenario, when your decision needs to use some APISIX data, such as `route`, `consumer`, etc., how should we do it?\n\nIf your OPA service needs to make decisions based on APISIX data like Route and Consumer details, you can configure the Plugin to do so.\n\nThe example below shows a simple `echo` policy which will return the data sent by APISIX as it is:\n\n```shell\ncurl -X PUT '127.0.0.1:8181/v1/policies/echo' \\\n    -H 'Content-Type: text/plain' \\\n    -d 'package echo\n\nallow = false\nreason = input'\n```\n\nNow we can configure the Plugin on the Route to send APISIX data:\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"opa\": {\n            \"host\": \"http://127.0.0.1:8181\",\n            \"policy\": \"echo\",\n            \"with_route\": true\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nNow if you make a request, you can see the data from the Route through the custom response:\n\n```shell\ncurl -X GET 127.0.0.1:9080/get\n{\n    \"type\": \"http\",\n    \"request\": {\n        xxx\n    },\n    \"var\": {\n        xxx\n    },\n    \"route\": {\n        xxx\n    }\n}\n```\n\n## Delete Plugin\n\nTo remove the `opa` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/openfunction.md",
    "content": "---\ntitle: openfunction\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - OpenFunction\ndescription: This document contains information about the Apache APISIX openfunction Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `openfunction` Plugin is used to integrate APISIX with [CNCF OpenFunction](https://openfunction.dev/) serverless platform.\n\nThis Plugin can be configured on a Route and requests will be sent to the configured OpenFunction API endpoint as the upstream.\n\n## Attributes\n\n| Name                        | Type    | Required | Default | Valid values | Description                                                                                                |\n| --------------------------- | ------- | -------- | ------- | ------------ | ---------------------------------------------------------------------------------------------------------- |\n| function_uri                | string  | True     |         |              | function uri. For example, `https://localhost:30858/default/function-sample`.                              |\n| ssl_verify                  | boolean | False    | true    |              | When set to `true` verifies the SSL certificate.                                                           |\n| authorization               | object  | False    |         |              | Authorization credentials to access functions of OpenFunction.                                      |\n| authorization.service_token | string  | False    |         |              | The token format is 'xx:xx' which supports basic auth for function entry points.                                      |\n| timeout                     | integer | False    | 3000 ms | [100, ...] ms| OpenFunction action and HTTP call timeout in ms.                                                              |\n| keepalive                   | boolean | False    | true    |              | When set to `true` keeps the connection alive for reuse.                                                   |\n| keepalive_timeout           | integer | False    | 60000 ms| [1000,...] ms| Time is ms for connection to remain idle without closing.                                                  |\n| keepalive_pool              | integer | False    | 5       | [1,...]      | Maximum number of requests that can be sent on this connection before closing it.                          |\n\n:::note\n\nThe `timeout` attribute sets the time taken by the OpenFunction to execute, and the timeout for the HTTP client in APISIX. OpenFunction calls may take time to pull the runtime image and start the container. So, if the value is set too small, it may cause a large number of requests to fail.\n\n:::\n\n## Prerequisites\n\nBefore configuring the plugin, you need to have OpenFunction running.\nInstallation of OpenFunction requires a certain version Kubernetes cluster.\nFor details, please refer to [Installation](https://openfunction.dev/docs/getting-started/installation/).\n\n### Create and Push a Function\n\nYou can then create a function following the [sample](https://github.com/OpenFunction/samples)\n\nYou'll need to push your function container image to a container registry like Docker Hub or Quay.io when building a function. To do that, you'll need to generate a secret for your container registry first.\n\n```shell\nREGISTRY_SERVER=https://index.docker.io/v1/ REGISTRY_USER= ${your_registry_user} REGISTRY_PASSWORD= ${your_registry_password}\nkubectl create secret docker-registry push-secret \\\n    --docker-server=$REGISTRY_SERVER \\\n    --docker-username=$REGISTRY_USER \\\n    --docker-password=$REGISTRY_PASSWORD\n```\n\n## Enable the Plugin\n\nYou can now configure the Plugin on a specific Route and point to this running OpenFunction service:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"openfunction\": {\n            \"function_uri\": \"http://localhost:3233/default/function-sample/test\",\n            \"authorization\": {\n                \"service_token\": \"test:test\"\n            }\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the plugin, you can send a request to the Route and it will invoke the configured function:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\nThis will give back the response from the function:\n\n```\nhello, test!\n```\n\n### Configure Path Transforming\n\nThe `OpenFunction` Plugin also supports transforming the URL path while proxying requests to the OpenFunction API endpoints. Extensions to the base request path get appended to the `function_uri` specified in the Plugin configuration.\n\n:::info IMPORTANT\n\nThe `uri` configured on a Route must end with `*` for this feature to work properly. APISIX Routes are matched strictly and the `*` implies that any subpath to this URI would be matched to the same Route.\n\n:::\n\nThe example below configures this feature:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello/*\",\n    \"plugins\": {\n        \"openfunction\": {\n            \"function_uri\": \"http://localhost:3233/default/function-sample\",\n            \"authorization\": {\n                \"service_token\": \"test:test\"\n            }\n        }\n    }\n}'\n```\n\nNow, any requests to the path `hello/123` will invoke the OpenFunction, and the added path is forwarded:\n\n```shell\ncurl  http://127.0.0.1:9080/hello/123\n```\n\n```shell\nHello, 123!\n```\n\n## Delete Plugin\n\nTo remove the `openfunction` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/openid-connect.md",
    "content": "---\ntitle: openid-connect\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - OpenID Connect\n  - OIDC\ndescription: The openid-connect Plugin supports the integration with OpenID Connect (OIDC) identity providers, such as Keycloak, Auth0, Microsoft Entra ID, Google, Okta, and more. It allows APISIX to authenticate clients and obtain their information from the identity provider before allowing or denying their access to upstream protected resources.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/openid-connect\" />\n</head>\n\n## Description\n\nThe `openid-connect` Plugin supports the integration with [OpenID Connect (OIDC)](https://openid.net/connect/) identity providers, such as Keycloak, Auth0, Microsoft Entra ID, Google, Okta, and more. It allows APISIX to authenticate clients and obtain their information from the identity provider before allowing or denying their access to upstream protected resources.\n\n## Attributes\n\n| Name               | Type     | Required | Default               | Valid values | Description      |\n|--------------------------------------|----------|----------|-----------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| client_id          | string   | True     |     |              | OAuth client ID.                  |\n| client_secret      | string   | True     |     |              | OAuth client secret.              |\n| discovery          | string   | True     |     |              | URL to the well-known discovery document of the OpenID provider, which contains a list of OP API endpoints. The Plugin can directly utilize the endpoints from the discovery document. You can also configure these endpoints individually, which takes precedence over the endpoints supplied in the discovery document.    |\n| scope              | string   | False    | openid              |              | OIDC scope that corresponds to information that should be returned about the authenticated user, also known as [claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). This is used to authorize users with proper permission. The default value is `openid`, the required scope for OIDC to return a `sub` claim that uniquely identifies the authenticated user. Additional scopes can be appended and delimited by spaces, such as `openid email profile`.    |\n| required_scopes    | array[string] | False    |     |              | Scopes required to be present in the access token. Used in conjunction with the introspection endpoint when `bearer_only` is `true`. If any required scope is missing, the Plugin rejects the request with a 403 forbidden error.  |\n| realm              | string   | False    | apisix              |              |  Realm in [`WWW-Authenticate`](https://www.rfc-editor.org/rfc/rfc6750#section-3) response header accompanying a 401 unauthorized request due to invalid bearer token.                 |\n| bearer_only        | boolean  | False    | false                 |              | If true, strictly require bearer access token in requests for authentication.                |\n| logout_path        | string   | False    | /logout             |              | Path to activate the logout.  |\n| post_logout_redirect_uri             | string   | False    |     |              | URL to redirect users to after the `logout_path` receive a request to log out. |\n| redirect_uri       | string  | False    |     |              | URI to redirect to after authentication with the OpenID provider. Note that the redirect URI should not be the same as the request URI, but a sub-path of the request URI. For example, if the `uri` of the Route is `/api/v1/*`, `redirect_uri` can be configured as `/api/v1/redirect`. If `redirect_uri` is not configured, APISIX will append `/.apisix/redirect` to the request URI to determine the value for `redirect_uri`. |\n| timeout            | integer  | False    | 3   | [1,...]      | Request timeout time in seconds.   |\n| ssl_verify         | boolean  | True    | false                 |              | If true, verify the OpenID provider 's SSL certificates.                 |\n| introspection_endpoint               | string   | False    |     |              | URL of the [token introspection](https://datatracker.ietf.org/doc/html/rfc7662) endpoint for the OpenID provider used to introspect access tokens. If this is unset, the introspection endpoint presented in the well-known discovery document is used [as a fallback](https://github.com/zmartzone/lua-resty-openidc/commit/cdaf824996d2b499de4c72852c91733872137c9c).                      |\n| introspection_endpoint_auth_method   | string   | False    | client_secret_basic |              | Authentication method for the token introspection endpoint. The value should be one of the authentication methods specified in the `introspection_endpoint_auth_methods_supported` [authorization server metadata](https://www.rfc-editor.org/rfc/rfc8414.html) as seen in the well-known discovery document, such as `client_secret_basic`, `client_secret_post`, `private_key_jwt`, and `client_secret_jwt`.              |\n| token_endpoint_auth_method           | string   | False    |   client_secret_basic      |              | Authentication method for the token endpoint. The value should be one of the authentication methods specified in the `token_endpoint_auth_methods_supported` [authorization server metadata](https://www.rfc-editor.org/rfc/rfc8414.html) as seen in the well-known discovery document, such as `client_secret_basic`, `client_secret_post`, `private_key_jwt`, and `client_secret_jwt`. If the configured method is not supported, fall back to the first method in the `token_endpoint_auth_methods_supported` array.       |\n| public_key         | string   | False    |     |              | Public key used to verify JWT signature id asymmetric algorithm is used. Providing this value to perform token verification will skip token introspection in client credentials flow. You can pass the public key in `-----BEGIN PUBLIC KEY-----\\\\n……\\\\n-----END PUBLIC KEY-----` format.          |\n| use_jwks           | boolean  | False    | false                 |              | If true and if `public_key` is not set, use the JWKS to verify JWT signature and skip token introspection in client credentials flow. The JWKS endpoint is parsed from the discovery document.     |\n| use_pkce           | boolean  | False    | false                 |              | If true, use the Proof Key for Code Exchange (PKCE) for Authorization Code Flow as defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636).  |\n| token_signing_alg_values_expected    | string   | False    |     |              | Algorithm used for signing JWT, such as `RS256`.       |\n| set_access_token_header              | boolean  | False    | true                  |              |  If true, set the access token in a request header. By default, the `X-Access-Token` header is used.        |\n| access_token_in_authorization_header | boolean  | False    | false                 |              | If true and if `set_access_token_header` is also true, set the access token in the `Authorization` header.      |\n| set_id_token_header                  | boolean  | False    | true                  |              | If true and if the ID token is available, set the value in the `X-ID-Token` request header.    |\n| set_userinfo_header                  | boolean  | False    | true                  |              | If true and if user info data is available, set the value in the `X-Userinfo` request header.    |\n| set_refresh_token_header             | boolean  | False    | false                 |              | If true and if the refresh token is available, set the value in the `X-Refresh-Token` request header.        |\n| session            | object   | False    |     |              | Session configuration used when `bearer_only` is `false` and the Plugin uses Authorization Code flow.              |\n| session.secret     | string   | True     |  | 16 or more characters | Key used for session encryption and HMAC operation when `bearer_only` is `false`.         |\n| session.cookie     | object   | False    |     |             |   Cookie configurations.    |\n| session.cookie.lifetime              | integer   | False    | 3600                  |             | Cookie lifetime in seconds. |\n| session.storage    | string   | False    | cookie | [\"cookie\", \"redis\"] | Session storage method. |\n| session.redis        | object   | False    |     |             |   Redis configuration when `storage` is `redis`.    |\n| session.redis.host   | string   | False    | 127.0.0.1 |             |   Redis host.    |\n| session.redis.port   | integer   | False    | 6379 |             |   Redis port.    |\n| session.redis.password | string   | False    |     |             |   Redis password.    |\n| session.redis.username | string   | False    |     |             |   Redis username.    |\n| session.redis.database | integer   | False    | 0 |             |   Redis database index.    |\n| session.redis.prefix | string   | False    | sessions |             |   Redis key prefix.    |\n| session.redis.ssl    | boolean   | False    | false |             |   Enable SSL for Redis connection.    |\n| session.redis.ssl_verify | boolean   | True    | false |             |   Verify SSL certificate.    |\n| session.redis.server_name | string   | False    |     |             |   Redis server name for SNI.    |\n| session.redis.connect_timeout | integer   | False    | 1000 |             |   Connect timeout in milliseconds.    |\n| session.redis.send_timeout   | integer   | False    | 1000 |             |   Send timeout in milliseconds.    |\n| session.redis.read_timeout   | integer   | False    | 1000 |             |   Read timeout in milliseconds.    |\n| session.redis.keepalive_timeout | integer   | False    | 10000 |             |   Keepalive timeout in milliseconds.    |\n| session_contents   | object   | False    |                   |             | Session content configurations. If unconfigured, all data will be stored in the session. |\n| session_contents.access_token   | boolean   | False    |          |             | If true, store the access token in the session.  |\n| session_contents.id_token   | boolean   | False    |          |             | If true, store the ID token in the session.  |\n| session_contents.enc_id_token   | boolean   | False    |          |             | If true, store the encrypted ID token in the session.  |\n| session_contents.user   | boolean   | False    |          |             | If true, store the user info in the session.  |\n| unauth_action      | string   | False    | auth                |  [\"auth\",\"deny\",\"pass\"]            | Action for unauthenticated requests. When set to `auth`, redirect to the authentication endpoint of the OpenID provider. When set to `pass`, allow the request without authentication. When set to `deny`, return 401 unauthenticated responses rather than start the authorization code grant flow.    |\n| proxy_opts         | object   | False    |     |                | Configurations for the proxy server that the OpenID provider is behind.               |\n| proxy_opts.http_proxy     | string   | False    |     |          | Proxy server address for HTTP requests, such as `http://<proxy_host>:<proxy_port>`.   |\n| proxy_opts.https_proxy    | string   | False    |     |          | Proxy server address for HTTPS requests, such as `http://<proxy_host>:<proxy_port>`.   |\n| proxy_opts.http_proxy_authorization  | string   | False    |     | Basic [base64 username:password] | Default `Proxy-Authorization` header value to be used with `http_proxy`. Can be overridden with custom `Proxy-Authorization` request header.      |\n| proxy_opts.https_proxy_authorization | string   | False    |     | Basic [base64 username:password] | Default `Proxy-Authorization` header value to be used with `https_proxy`. Cannot be overridden with custom `Proxy-Authorization` request header since with HTTPS, the authorization is completed when connecting.               |\n| proxy_opts.no_proxy                  | string   | False    |     |                | Comma separated list of hosts that should not be proxied.       |\n| authorization_params                 | object   | False    |     |                | Additional parameters to send in the request to the authorization endpoint.         |\n| client_rsa_private_key | string | False |  |  | Client RSA private key used to sign JWT for authentication to the OP. Required when `token_endpoint_auth_method` is `private_key_jwt`. |\n| client_rsa_private_key_id | string | False |  |  | Client RSA private key ID used to compute a signed JWT. Optional when `token_endpoint_auth_method` is `private_key_jwt`.   |\n| client_jwt_assertion_expires_in | integer | False | 60 |  | Life duration of the signed JWT for authentication to the OP, in seconds. Used when `token_endpoint_auth_method` is `private_key_jwt` or `client_secret_jwt`.  |\n| renew_access_token_on_expiry | boolean | False | true |  | If true, attempt to silently renew the access token when it expires or if a refresh token is available. If the token fails to renew, redirect user for re-authentication. |\n| access_token_expires_in | integer | False |  |  | Lifetime of the access token in seconds if no `expires_in` attribute is present in the token endpoint response. |\n| refresh_session_interval | integer | False |  |  | Time interval to refresh user ID token without requiring re-authentication. When not set, it will not check the expiration time of the session issued to the client by the gateway. If set to 900, it means refreshing the user's id_token (or session in the browser) after 900 seconds without requiring re-authentication. |\n| iat_slack | integer | False | 120 |  | Tolerance of clock skew in seconds with the `iat` claim in an ID token. |\n| accept_none_alg | boolean | False | false |  | Set to true if the OpenID provider does not sign its ID token, such as when the signature algorithm is set to `none`. |\n| accept_unsupported_alg | boolean | False | true |  | If true, ignore ID token signature to accept unsupported signature algorithm. |\n| access_token_expires_leeway | integer | False | 0 |  | Expiration leeway in seconds for access token renewal. When set to a value greater than 0, token renewal will take place the set amount of time before token expiration. This avoids errors in case the access token just expires when arriving to the resource server. |\n| force_reauthorize | boolean | False | false |  | If true, execute the authorization flow even when a token has been cached. |\n| use_nonce | boolean | False | false |  | If true, enable nonce parameter in authorization request. |\n| revoke_tokens_on_logout | boolean | False | false |  | If true, notify the authorization server a previously obtained refresh or access token is no longer needed at the revocation endpoint. |\n| jwk_expires_in | integer | False | 86400 |  | Expiration time for JWK cache in seconds. |\n| jwt_verification_cache_ignore | boolean | False | false |  | If true, force re-verification for a bearer token and ignore any existing cached verification results. |\n| cache_segment | string | False |  |  | Optional name of a cache segment, used to separate and differentiate caches used by token introspection or JWT verification. |\n| introspection_interval | integer | False | 0 |  | TTL of the cached and introspected access token in seconds. The default value is 0, which means this option is not used and the Plugin defaults to use the TTL passed by expiry claim defined in `introspection_expiry_claim`. If `introspection_interval` is larger than 0 and less than the TTL passed by expiry claim defined in `introspection_expiry_claim`, use `introspection_interval`. |\n| introspection_expiry_claim | string | False | exp |  | Name of the expiry claim, which controls the TTL of the cached and introspected access token. |\n| introspection_addon_headers | array[string] | False |  |  | Used to append additional header values to the introspection HTTP request. If the specified header does not exist in origin request, value will not be appended. |\n| claim_validator                      | object   | False    |                       |              | JWT claim validation configurations. |\n| claim_validator.issuer.valid_issuers | array[string] | False |  |  | An array of trusted JWT issuers. If unconfigured, the issuer returned by discovery endpoint will be used. If both are unavailable, the issuer will not be validated. |\n| claim_validator.audience             | object   | False    |                       |              | [Audience claim](https://openid.net/specs/openid-connect-core-1_0.html) validation configurations.  |\n| claim_validator.audience.claim       | string   | False    | aud                  |              | Name of the claim that contains the audience. |\n| claim_validator.audience.required    | boolean  | False    | false                 |              | If true, audience claim is required and the name of the claim will be the name defined in `claim`. |\n| claim_validator.audience.match_with_client_id | boolean | False | false            |              | If true, require the audience to match the client ID. If the audience is a string, it must exactly match the client ID. If the audience is an array of strings, at least one of the values must match the client ID. If no match is found, you will receive a `mismatched audience` error. This requirement is stated in the OpenID Connect specification to ensure that the token is intended for the specific client. |\n| claim_schema | object | False |  |  | JSON schema of OIDC response claim. Example: `{\"type\":\"object\",\"properties\":{\"access_token\":{\"type\":\"string\"}},\"required\":[\"access_token\"]}` - validates that the response contains a required string field `access_token`. |\n\nNOTE: `encrypt_fields = {\"client_secret\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\nIn addition, you can use Environment Variables or APISIX secret to store and reference plugin attributes. APISIX currently supports storing secrets in two ways - [Environment Variables and HashiCorp Vault](../terminology/secret.md).\n\nFor example, use below command to set environment variable\n`export keycloak_secret=abc`\n\nand use it in plugin conf like below\n\n`\"client_secret\": \"$ENV://keycloak_secret\"`\n\n## Examples\n\nThe examples below demonstrate how you can configure the `openid-connect` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Authorization Code Flow\n\nThe authorization code flow is defined in [RFC 6749, Section 4.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1). It involves exchanging an temporary authorization code for an access token, and is typically used by confidential and public clients.\n\nThe following diagram illustrates the interaction between different entities when you implement the authorization code flow:\n\n![Authorization code flow diagram](https://static.api7.ai/uploads/2023/11/27/Ga2402sb_oidc-code-auth-flow-revised.png)\n\nWhen an incoming request does not contain an access token in its header nor in an appropriate session cookie, the Plugin acts as a relying party and redirects to the authorization server to continue the authorization code flow.\n\nAfter successful authentication, the Plugin keeps the token in the session cookie, and subsequent requests will use the token stored in the cookie.\n\nSee [Implement Authorization Code Grant](../tutorials/keycloak-oidc.md#implement-authorization-code-grant) for an example to use the `openid-connect` Plugin to integrate with Keycloak using the authorization code flow.\n\n### Proof Key for Code Exchange (PKCE)\n\nThe Proof Key for Code Exchange (PKCE) is defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636). PKCE enhances the authorization code flow by adding a code challenge and verifier to prevent authorization code interception attacks.\n\nThe following diagram illustrates the interaction between different entities when you implement the authorization code flow with PKCE:\n\n![Authorization code flow with PKCE diagram](https://static.api7.ai/uploads/2024/11/04/aJ2ZVuTC_auth-code-with-pkce.png)\n\nSee [Implement Authorization Code Grant](../tutorials/keycloak-oidc.md#implement-authorization-code-grant) for an example to use the `openid-connect` Plugin to integrate with Keycloak using the authorization code flow with PKCE.\n\n### Client Credential Flow\n\nThe client credential flow is defined in [RFC 6749, Section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). It involves clients requesting an access token with its own credentials to access protected resources, typically used in machine to machine authentication and is not on behalf of a specific user.\n\nThe following diagram illustrates the interaction between different entities when you implement the client credential flow:\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/28/sbHxqnOz_client-credential-no-introspect.png\" alt=\"Client credential flow diagram\" style={{width: '70%'}} />\n</div>\n<br />\n\nSee [Implement Client Credentials Grant](../tutorials/keycloak-oidc.md#implement-client-credentials-grant) for an example to use the `openid-connect` Plugin to integrate with Keycloak using the client credentials flow.\n\n### Introspection Flow\n\nThe introspection flow is defined in [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662). It involves verifying the validity and details of an access token by querying an authorization server’s introspection endpoint.\n\nIn this flow, when a client presents an access token to the resource server, the resource server sends a request to the authorization server’s introspection endpoint, which responds with token details if the token is active, including information like token expiration, associated scopes, and the user or client it belongs to.\n\nThe following diagram illustrates the interaction between different entities when you implement the authorization code flow with token introspection:\n\n<br />\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/29/Y2RWIUV9_client-cred-flow-introspection.png\" alt=\"Client credential with introspection diagram\" style={{width: '55%'}} />\n</div>\n<br />\n\nSee [Implement Client Credentials Grant](../tutorials/keycloak-oidc.md#implement-client-credentials-grant) for an example to use the `openid-connect` Plugin to integrate with Keycloak using the client credentials flow with token introspection.\n\n### Password Flow\n\nThe password flow is defined in [RFC 6749, Section 4.3](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). It is designed for trusted applications, allowing them to obtain an access token directly using a user’s username and password. In this grant type, the client app sends the user’s credentials along with its own client ID and secret to the authorization server, which then authenticates the user and, if valid, issues an access token.\n\nThough efficient, this flow is intended for highly trusted, first-party applications only, as it requires the app to handle sensitive user credentials directly, posing significant security risks if used in third-party contexts.\n\nThe following diagram illustrates the interaction between different entities when you implement the password flow:\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/30/njkWZVgX_pass-grant.png\" alt=\"Password flow diagram\" style={{width: '70%'}} />\n</div>\n<br />\n\nSee [Implement Password Grant](../tutorials/keycloak-oidc.md#implement-password-grant) for an example to use the `openid-connect` Plugin to integrate with Keycloak using the password flow.\n\n### Refresh Token Grant\n\nThe refresh token grant is defined in [RFC 6749, Section 6](https://datatracker.ietf.org/doc/html/rfc6749#section-6). It enables clients to request a new access token without requiring the user to re-authenticate, using a previously issued refresh token. This flow is typically used when an access token expires, allowing the client to maintain continuous access to resources without user intervention. Refresh tokens are issued along with access tokens in certain OAuth flows and their lifespan and security requirements depend on the authorization server’s configuration.\n\nThe following diagram illustrates the interaction between different entities when implementing password flow with refresh token flow:\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/30/YBF7rI6M_password-with-refresh-token.png\" alt=\"Password grant with refresh token flow diagram\" style={{width: '100%'}} />\n</div>\n<br />\n\nSee [Refresh Token](../tutorials/keycloak-oidc.md#refresh-token) for an example to use the `openid-connect` Plugin to integrate with Keycloak using the password flow with token refreshes.\n\n## Troubleshooting\n\nThis section covers a few commonly seen issues when working with this Plugin to help you troubleshoot.\n\n### APISIX Cannot Connect to OpenID provider\n\nIf APISIX fails to resolve or cannot connect to the OpenID provider, double check the DNS settings in your configuration file `config.yaml` and modify as needed.\n\n### No Session State Found\n\nIf you encounter a `500 internal server error` with the following message in the log when working with [authorization code flow](#authorization-code-flow), there could be a number of reasons.\n\n```text\nthe error request to the redirect_uri path, but there's no session state found\n```\n\n#### 1. Misconfigured Redirection URI\n\nA common misconfiguration is to configure the `redirect_uri` the same as the URI of the route. When a user initiates a request to visit the protected resource, the request directly hits the redirection URI with no session cookie in the request, which leads to the no session state found error.\n\nTo properly configure the redirection URI, make sure that the `redirect_uri` matches the Route where the Plugin is configured, without being fully identical. For instance, a correct configuration would be to configure `uri` of the Route to `/api/v1/*` and the path portion of the `redirect_uri` to `/api/v1/redirect`.\n\nYou should also ensure that the `redirect_uri` include the scheme, such as `http` or `https`.\n\n#### 2. Cookie Not Sent or Absent\n\nCheck if the [`SameSite`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) cookie attribute is properly set (i.e. if your application needs to send the cookie cross sites) to see if this could be a factor that prevents the cookie being saved to the browser's cookie jar or being sent from the browser.\n\n#### 3. Upstream Sent Too Big Header\n\nIf you have NGINX sitting in front of APISIX to proxy client traffic, see if you observe the following error in NGINX's `error.log`:\n\n```text\nupstream sent too big header while reading response header from upstream\n```\n\nIf so, try adjusting `proxy_buffers`, `proxy_buffer_size`, and `proxy_busy_buffers_size` to larger values.\n\nAnother option is to configure the `session_content` attribute to adjust which data to store in session. For instance, you can set `session_content.access_token` to `true`.\n\n#### 4. Invalid Client Secret\n\nVerify if `client_secret` is valid and correct. An invalid `client_secret` would lead to an authentication failure and no token shall be returned and stored in session.\n\n#### 5. PKCE IdP Configuration\n\nIf you are enabling PKCE with the authorization code flow, make sure you have configured the IdP client to use PKCE. For example, in Keycloak, you should configure the PKCE challenge method in the client's advanced settings:\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/11/04/xvnCNb20_pkce-keycloak-revised.jpeg\" alt=\"PKCE keycloak configuration\" style={{width: '70%'}} />\n</div>\n"
  },
  {
    "path": "docs/en/latest/plugins/opentelemetry.md",
    "content": "---\ntitle: opentelemetry\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - OpenTelemetry\ndescription: The opentelemetry Plugin instruments APISIX and sends traces to OpenTelemetry collector based on the OpenTelemetry specification, in binary-encoded OLTP over HTTP.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/opentelemetry\" />\n</head>\n\n## Description\n\nThe `opentelemetry` Plugin can be used to report tracing data according to the [OpenTelemetry Specification](https://opentelemetry.io/docs/reference/specification/).\n\nThe Plugin only supports binary-encoded [OLTP over HTTP](https://opentelemetry.io/docs/reference/specification/protocol/otlp/#otlphttp).\n\n## Configurations\n\nBy default, configurations of the Service name, tenant ID, collector, and batch span processor are pre-configured in [default configuration](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua).\n\nYou can change this configuration of the Plugin through the endpoint `apisix/admin/plugin_metadata/opentelemetry` For example:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/opentelemetry -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"trace_id_source\": \"x-request-id\",\n    \"resource\": {\n      \"service.name\": \"APISIX\"\n    },\n    \"collector\": {\n      \"address\": \"127.0.0.1:4318\",\n      \"request_timeout\": 3,\n      \"request_headers\": {\n        \"Authorization\": \"token\"\n      }\n    },\n    \"batch_span_processor\": {\n      \"drop_on_queue_full\": false,\n      \"max_queue_size\": 1024,\n      \"batch_timeout\": 2,\n      \"inactive_timeout\": 1,\n      \"max_export_batch_size\": 16\n    },\n    \"set_ngx_var\": false\n}'\n```\n\n## Attributes\n\n| Name                                  | Type          | Required | Default      | Valid Values | Description |\n|---------------------------------------|---------------|----------|--------------|--------------|-------------|\n| sampler                               | object        | False    | -            | -            | Sampling configuration. |\n| sampler.name                          | string        | False    | `always_off` | [\"always_on\", \"always_off\", \"trace_id_ratio\", \"parent_base\"]  | Sampling strategy.<br />To always sample, use `always_on`.<br />To never sample, use `always_off`.<br />To randomly sample based on a given ratio, use `trace_id_ratio`.<br />To use the sampling decision of the span's parent, use `parent_base`. If there is no parent, use the root sampler. |\n| sampler.options                       | object        | False    | -            | -            | Parameters for sampling strategy. |\n| sampler.options.fraction              | number        | False    | 0            | [0, 1]       | Sampling ratio when the sampling strategy is `trace_id_ratio`. |\n| sampler.options.root                  | object        | False    | -            | -            | Root sampler when the sampling strategy is `parent_base` strategy. |\n| sampler.options.root.name             | string        | False    | -            | [\"always_on\", \"always_off\", \"trace_id_ratio\"] | Root sampling strategy. |\n| sampler.options.root.options          | object        | False    | -            | -            | Root sampling strategy parameters. |\n| sampler.options.root.options.fraction | number        | False    | 0            | [0, 1]       | Root sampling ratio when the sampling strategy is `trace_id_ratio`. |\n| additional_attributes                 | array[string] | False    | -            | -            | Additional attributes appended to the trace span. Support [built-in variables](https://apisix.apache.org/docs/apisix/apisix-variable/) in values. |\n| additional_header_prefix_attributes   | array[string] | False    | -            | -            | Headers or header prefixes appended to the trace span's attributes. For example, use `x-my-header\"` or `x-my-headers-*` to include all headers with the prefix `x-my-headers-`. |\n\n## Examples\n\nThe examples below demonstrate how you can work with the `opentelemetry` Plugin for different scenarios.\n\n### Enable Comprehensive Request Lifecycle Tracing\n\n:::note\n\nEnabling comprehensive tracing adds span creation and export overhead across the request lifecycle, which may impact throughput and latency.\n\n:::\n\nTo enable comprehensive tracing across the request lifecycle (SSL/SNI, rewrite/access, header_filter/body_filter, and log), set the `tracing` field to `true` in the configuration file:\n\n```yaml title=\"config.yaml\"\napisix:\n  tracing: true\n```\n\n### Enable `opentelemetry` Plugin\n\nBy default, the `opentelemetry` Plugin is disabled in APISIX. To enable, add the Plugin to your configuration file as such:\n\n```yaml title=\"config.yaml\"\nplugins:\n  - ...\n  - opentelemetry\n```\n\nReload APISIX for changes to take effect.\n\n### Send Traces to OpenTelemetry\n\nThe following example demonstrates how to trace requests to a Route and send traces to OpenTelemetry.\n\nStart an OpenTelemetry collector instance in Docker:\n\n```shell\ndocker run -d --name otel-collector -p 4318:4318 otel/opentelemetry-collector-contrib\n```\n\nCreate a Route with `opentelemetry` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"otel-tracing-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"opentelemetry\": {\n        \"sampler\": {\n          \"name\": \"always_on\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nIn OpenTelemetry collector's log, you should see information similar to the following:\n\n```text\ninfo\tResourceSpans #0\nResource SchemaURL:\nResource attributes:\n     -> telemetry.sdk.language: Str(lua)\n     -> telemetry.sdk.name: Str(opentelemetry-lua)\n     -> telemetry.sdk.version: Str(0.1.1)\n     -> hostname: Str(RC)\n     -> service.name: Str(APISIX)\nScopeSpans #0\nScopeSpans SchemaURL:\nInstrumentationScope opentelemetry-lua\nSpan #0\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0adf392b5c84111\n    ID             : d9816bbaef5ee63d\n    Name           : http_router_match\n    Kind           : Internal\n    Start time     : 2026-02-04 05:57:04.846881024 +0000 UTC\n    End time       : 2026-02-04 05:57:04.846951936 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #1\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : d0adf392b5c84111\n    Name           : apisix.phase.access\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.846562048 +0000 UTC\n    End time       : 2026-02-04 05:57:04.84724608 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #2\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : 4eb72d55359331fa\n    Name           : resolve_dns\n    Kind           : Internal\n    Start time     : 2026-02-04 05:57:04.847251968 +0000 UTC\n    End time       : 2026-02-04 05:57:04.84726912 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #3\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : de572aad9bad3b47\n    Name           : apisix.phase.header_filter\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.84793088 +0000 UTC\n    End time       : 2026-02-04 05:57:04.848005888 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #4\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : 0baddeee6e5d500d\n    Name           : apisix.phase.body_filter\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.848007936 +0000 UTC\n    End time       : 2026-02-04 05:57:04.848103936 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #5\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : d57d53882c40612a\n    Name           : apisix.phase.log.plugins.opentelemetry\n    Kind           : Internal\n    Start time     : 2026-02-04 05:57:04.84823296 +0000 UTC\n    End time       : 2026-02-04 05:57:04.848385024 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #6\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      :\n    ID             : d0c33adf97b099f3\n    Name           : GET /anything\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.84655488 +0000 UTC\n    End time       : 2026-02-04 05:57:04.84839296 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nAttributes:\n     -> net.host.name: Str(localhost)\n     -> http.method: Str(GET)\n     -> http.scheme: Str(http)\n     -> http.target: Str(/anything)\n     -> http.user_agent: Str(curl/7.81.0)\n     -> http.request.method: Str(GET)\n     -> url.scheme: Str(http)\n     -> uri.path: Str(/anything)\n     -> user_agent.original: Str(curl/7.81.0)\n     -> apisix.route_id: Str(otel-tracing-route)\n     -> apisix.route_name: Empty()\n     -> http.route: Str(/anything)\n     -> http.status_code: Int(200)\n     -> http.response.status_code: Int(200)\n{\"resource\": {\"service.instance.id\": \"ed436c1a-6ee7-46b0-ad58-527d0aaf4ade\", \"service.name\": \"otelcol-contrib\", \"service.version\": \"0.144.0\"}, \"otelcol.component.id\": \"debug\", \"otelcol.component.kind\": \"exporter\", \"otelcol.signal\": \"traces\"}\n```\n\nTo visualize these traces, you can export your telemetry to backend Services, such as Zipkin and Prometheus. See [exporters](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter) for more details.\n\n### Using Trace Variables in Logging\n\nThe following example demonstrates how to configure the `opentelemetry` Plugin to set the following built-in variables, which can be used in logger Plugins or access logs:\n\n- `opentelemetry_context_traceparent`: [trace parent](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format) ID\n- `opentelemetry_trace_id`: trace ID of the current span\n- `opentelemetry_span_id`: span ID of the current span\n\nConfigure the plugin metadata to set `set_ngx_var` as true:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/opentelemetry -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"set_ngx_var\": true\n}'\n```\n\nUpdate the configuration file as below. You should customize the access log format to use the `opentelemetry` Plugin variables.\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    enable_access_log: true\n    access_log_format: '{\"time\": \"$time_iso8601\",\"opentelemetry_context_traceparent\": \"$opentelemetry_context_traceparent\",\"opentelemetry_trace_id\": \"$opentelemetry_trace_id\",\"opentelemetry_span_id\": \"$opentelemetry_span_id\",\"remote_addr\": \"$remote_addr\"}'\n    access_log_format_escape: json\n```\n\nReload APISIX for configuration changes to take effect.\n\nYou should see access log entries similar to the following when you generate requests:\n\n```text\n{\"time\": \"18/Feb/2024:15:09:00 +0000\",\"opentelemetry_context_traceparent\": \"00-fbd0a38d4ea4a128ff1a688197bc58b0-8f4b9d9970a02629-01\",\"opentelemetry_trace_id\": \"fbd0a38d4ea4a128ff1a688197bc58b0\",\"opentelemetry_span_id\": \"af3dc7642104748a\",\"remote_addr\": \"172.10.0.1\"}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/openwhisk.md",
    "content": "---\ntitle: openwhisk\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - OpenWhisk\ndescription: This document contains information about the Apache openwhisk Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `openwhisk` Plugin is used to integrate APISIX with [Apache OpenWhisk](https://openwhisk.apache.org) serverless platform.\n\nThis Plugin can be configured on a Route and requests will be send to the configured OpenWhisk API endpoint as the upstream.\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values | Description                                                                                                |\n| ----------------- | ------- | -------- | ------- | ------------ | ---------------------------------------------------------------------------------------------------------- |\n| api_host          | string  | True     |         |              | OpenWhisk API host address. For example, `https://localhost:3233`.                                         |\n| ssl_verify        | boolean | False    | true    |              | When set to `true` verifies the SSL certificate.                                                           |\n| service_token     | string  | True     |         |              | OpenWhisk service token. The format is `xxx:xxx` and it is passed through basic auth when calling the API. |\n| namespace         | string  | True     |         |              | OpenWhisk namespace. For example `guest`.                                                                  |\n| action            | string  | True     |         |              | OpenWhisk action. For example `hello`.                                                                     |\n| result            | boolean | False    | true    |              | When set to `true` gets the action metadata (executes the function and gets response).                     |\n| timeout           | integer | False    | 60000ms | [1, 60000]ms | OpenWhisk action and HTTP call timeout in ms.                                                              |\n| keepalive         | boolean | False    | true    |              | When set to `true` keeps the connection alive for reuse.                                                   |\n| keepalive_timeout | integer | False    | 60000ms | [1000,...]ms | Time is ms for connection to remain idle without closing.                                                  |\n| keepalive_pool    | integer | False    | 5       | [1,...]      | Maximum number of requests that can be sent on this connection before closing it.                          |\n\n:::note\n\nThe `timeout` attribute sets the time taken by the OpenWhisk action to execute, and the timeout for the HTTP client in APISIX. OpenWhisk action calls may take time to pull the runtime image and start the container. So, if the value is set too small, it may cause a large number of requests to fail.\n\nOpenWhisk supports timeouts in the range 1ms to 60000ms and it is recommended to set it to at least 1000ms.\n\n:::\n\n## Enable Plugin\n\nBefore configuring the Plugin, you need to have OpenWhisk running. The example below shows OpenWhisk in standalone mode:\n\n```shell\ndocker run --rm -d \\\n  -h openwhisk --name openwhisk \\\n  -p 3233:3233 -p 3232:3232 \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  openwhisk/standalone:nightly\ndocker exec openwhisk waitready\n```\n\nInstall the [openwhisk-cli](https://github.com/apache/openwhisk-cli) utility.\n\nYou can download the released executable binaries wsk for Linux systems from the [openwhisk-cli](https://github.com/apache/openwhisk-cli) repository.\n\nYou can then create an action to test:\n\n```shell\nwsk property set --apihost \"http://localhost:3233\" --auth \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\"\nwsk action update test <(echo 'function main(){return {\"ready\":true}}') --kind nodejs:14\n```\n\nYou can now configure the Plugin on a specific Route and point to this running OpenWhisk service:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"openwhisk\": {\n            \"api_host\": \"http://localhost:3233\",\n            \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n            \"namespace\": \"guest\",\n            \"action\": \"test\"\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin, you can send a request to the Route and it will invoke the configured action:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\nThis will give back the response from the action:\n\n```json\n{ \"ready\": true }\n```\n\n## Delete Plugin\n\nTo remove the `openwhisk` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/prometheus.md",
    "content": "---\ntitle: prometheus\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Prometheus\ndescription: The prometheus Plugin provides the capability to integrate APISIX with Prometheus for metric collection and continuous monitoring.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/prometheus\" />\n</head>\n\n## Description\n\nThe `prometheus` Plugin provides the capability to integrate APISIX with [Prometheus](https://prometheus.io).\n\nAfter enabling the Plugin, APISIX will start collecting relevant metrics, such as API requests and latencies, and exporting them in a [text-based exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/#exposition-formats) to Prometheus. You can then create event monitoring and alerting in Prometheus to monitor the health of your API gateway and APIs.\n\n## Static Configurations\n\nBy default, `prometheus` configurations are pre-configured in the [default configuration](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua).\n\nTo customize these values, add the corresponding configurations to `config.yaml`. For example:\n\n```yaml\nplugin_attr:\n  prometheus:                               # Plugin: prometheus attributes\n    export_uri: /apisix/prometheus/metrics  # Set the URI for the Prometheus metrics endpoint.\n    metric_prefix: apisix_                  # Set the prefix for Prometheus metrics generated by APISIX.\n    enable_export_server: true              # Enable the Prometheus export server.\n    export_addr:                            # Set the address for the Prometheus export server.\n      ip: 127.0.0.1                         # Set the IP.\n      port: 9091                            # Set the port.\n    # metrics:                              # Create extra labels for metrics.\n    #  http_status:                         # These metrics will be prefixed with `apisix_`.\n    #    extra_labels:                      # Set the extra labels for http_status metrics.\n    #      - upstream_addr: $upstream_addr\n    #      - status: $upstream_status\n    #    expire: 0                          # The expiration time of metrics in seconds.\n                                            # 0 means the metrics will not expire.\n    #  http_latency:\n    #    extra_labels:                      # Set the extra labels for http_latency metrics.\n    #      - upstream_addr: $upstream_addr\n    #    expire: 0                          # The expiration time of metrics in seconds.\n                                            # 0 means the metrics will not expire.\n    #  bandwidth:\n    #    extra_labels:                      # Set the extra labels for bandwidth metrics.\n    #      - upstream_addr: $upstream_addr\n    #    expire: 0                          # The expiration time of metrics in seconds.\n                                            # 0 means the metrics will not expire.\n    # default_buckets:                      # Set the default buckets for the `http_latency` metrics histogram.\n    #   - 10\n    #   - 50\n    #   - 100\n    #   - 200\n    #   - 500\n    #   - 1000\n    #   - 2000\n    #   - 5000\n    #   - 10000\n    #   - 30000\n    #   - 60000\n    #   - 500\n```\n\nYou can use the [Nginx variable](https://nginx.org/en/docs/http/ngx_http_core_module.html) to create `extra_labels`. See [add extra labels](#add-extra-labels-for-metrics).\n\nReload APISIX for changes to take effect.\n\n## Attribute\n\n| Name          | Type    | Required | Default | Valid values | Description                                |\n| ------------- | ------- | -------- | ------- | ------------ | ------------------------------------------ |\n| prefer_name | boolean |          | False   |              | If true, export Route/Service name instead of their ID in Prometheus metrics. |\n\n## Metrics\n\nThere are different types of metrics in Prometheus. To understand their differences, see [metrics types](https://prometheus.io/docs/concepts/metric_types/).\n\nThe following metrics are exported by the `prometheus` Plugin by default. See [get APISIX metrics](#get-apisix-metrics) for an example. Note that some metrics, such as `apisix_batch_process_entries`, are not readily visible if there are no data.\n\n| Name                    | Type      | Description                                                                                                                                                                   |\n| ------------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| apisix_bandwidth                      | counter   | Total amount of traffic flowing through APISIX in bytes.                                                       |\n| apisix_etcd_modify_indexes            | gauge     | Number of changes to etcd by APISIX keys.                                                                                                                                         |\n| apisix_batch_process_entries          | gauge     | Number of remaining entries in a batch when sending data in batches, such as with `http logger`, and other logging Plugins.  |\n| apisix_etcd_reachable                 | gauge     | Whether APISIX can reach etcd. A value of `1` represents reachable and `0` represents unreachable.                                          |\n| apisix_http_status                    | counter   | HTTP status codes returned from upstream Services.                                                            |\n| apisix_http_requests_total            | gauge     | Number of HTTP requests from clients.                                                                                                                                     |\n| apisix_nginx_http_current_connections | gauge     | Number of current connections with clients.                                                                                   |\n| apisix_nginx_metric_errors_total      | counter   | Total number of `nginx-lua-prometheus` errors.                                                                                                                                |\n| apisix_http_latency                   | histogram | HTTP request latency in milliseconds.                                                                                                              |\n| apisix_node_info                      | gauge     | Information of the APISIX node, such as host name and the current APISIX version.                                                                                                                                                         |\n| apisix_shared_dict_capacity_bytes     | gauge     | The total capacity of an [NGINX shared dictionary](https://github.com/openresty/lua-nginx-module#ngxshareddict).                                                                                                                     |\n| apisix_shared_dict_free_space_bytes   | gauge     | The remaining space in an [NGINX shared dictionary](https://github.com/openresty/lua-nginx-module#ngxshareddict).                                                                                                                   |\n| apisix_upstream_status                | gauge     | Health check status of upstream nodes, available if health checks are configured on the upstream. A value of `1` represents healthy and `0` represents unhealthy.                                                                 |\n| apisix_stream_connection_total        | counter   | Total number of connections handled per Stream Route.                                                                                                               |\n\n## Labels\n\n[Labels](https://prometheus.io/docs/practices/naming/#labels) are attributes of metrics that are used to differentiate metrics.\n\nFor example, the `apisix_http_status` metric can be labeled with `route` information to identify which Route the HTTP status originates from.\n\nThe following are labels for a non-exhaustive list of APISIX metrics and their descriptions.\n\n### Labels for `apisix_http_status`\n\nThe following labels are used to differentiate `apisix_http_status` metrics.\n\n| Name   | Description                                                                                                                   |\n| ------------ | ----------------------------------------------------------------------------------------------------------------------------- |\n| code         | HTTP response code returned by the upstream node.                                                                            |\n| route        | ID of the Route that the HTTP status originates from when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| matched_uri  | URI of the Route that matches the request. Default to an empty string if a request does not match any Route.                              |\n| matched_host | Host of the Route that matches the request. Default to an empty string if a request does not match any Route, or host is not configured on the Route.                             |\n| service      | ID of the Service that the HTTP status originates from when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer     | Name of the Consumer associated with a request. Default to an empty string if no Consumer is associated with the request.                       |\n| node         | IP address of the upstream node.                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n### Labels for `apisix_bandwidth`\n\nThe following labels are used to differentiate `apisix_bandwidth` metrics.\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| type       | Type of traffic, `egress` or `ingress`.                                                                                             |\n| route      | ID of the Route that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| service    | ID of the Service that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer   | Name of the Consumer associated with a request. Default to an empty string if no Consumer is associated with the request.                       |\n| node       | IP address of the upstream node.                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n### Labels for `apisix_llm_latency`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |                                                                                             |\n| route_id      | ID of the Route that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| service_id    | ID of the Service that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer   | Name of the Consumer associated with a request. Default to an empty string if no Consumer is associated with the request.                       |\n| node       | IP address of the upstream node.                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n### Labels for `apisix_llm_active_connections`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| route      | Name of the Route that bandwidth corresponds to. Default to an empty string if a request does not match any Route.                                                                                 |\n| route_id      | ID of the Route that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| matched_uri  | URI of the Route that matches the request. Default to an empty string if a request does not match any Route.                              |\n| matched_host | Host of the Route that matches the request. Default to an empty string if a request does not match any Route, or host is not configured on the Route.                             |\n| service    | Name of the Service that bandwidth corresponds to. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| service_id    | ID of the Service that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer   | Name of the Consumer associated with a request. Default to an empty string if no Consumer is associated with the request.                       |\n| node       | IP address of the upstream node.                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n### Labels for `apisix_llm_completion_tokens`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |                                                                                             |\n| route_id      | ID of the Route that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| service_id    | ID of the Service that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer   | Name of the Consumer associated with a request. Default to an empty string if no Consumer is associated with the request.                       |\n| node       | IP address of the upstream node.                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n### Labels for `apisix_llm_prompt_tokens`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |                                                                                             |\n| route_id      | ID of the Route that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| service_id    | ID of the Service that bandwidth corresponds to when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer   | Name of the Consumer associated with a request. Default to an empty string if no Consumer is associated with the request.                       |\n| node       | IP address of the upstream node.                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n### Labels for `apisix_http_latency`\n\nThe following labels are used to differentiate `apisix_http_latency` metrics.\n\n| Name | Description                                                                                                                         |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |\n| type       | Type of latencies. See [latency types](#latency-types) for details. |\n| route      | ID of the Route that latencies correspond to when `prefer_name` is `false` (default), and name of the Route when `prefer_name` to `true`. Default to an empty string if a request does not match any Route.                         |\n| service    | ID of the Service that latencies correspond to when `prefer_name` is `false` (default), and name of the Service when `prefer_name` to `true`. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| consumer   | Name of the Consumer associated with latencies. Default to an empty string if no Consumer is associated with the request.                             |\n| node       | IP address of the upstream node associated with latencies.                                                                                                |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | For non-traditional_http requests, name of the llm_model                                                                                          |\n\n#### Latency Types\n\n`apisix_http_latency` can be labeled with one of the three types:\n\n* `request` represents the time elapsed between the first byte was read from the client and the log write after the last byte was sent to the client.\n\n* `upstream` represents the time elapsed waiting on responses from the upstream Service.\n\n* `apisix` represents the difference between the `request` latency and `upstream` latency.\n\nIn other words, the APISIX latency is not only attributed to the Lua processing. It should be understood as follows:\n\n```text\nAPISIX latency\n  = downstream request time - upstream response time\n  = downstream traffic latency + NGINX latency\n```\n\n### Labels for `apisix_upstream_status`\n\nThe following labels are used to differentiate `apisix_upstream_status` metrics.\n\n| Name | Description                                                                                         |\n| ---------- | --------------------------------------------------------------------------------------------------- |\n| name       | Resource ID corresponding to the upstream configured with health checks, such as `/apisix/routes/1` and `/apisix/upstreams/1`. |\n| ip         | IP address of the upstream node.                                                                         |\n| port       | Port number of the node.                                                                            |\n\n## Examples\n\nThe examples below demonstrate how you can work with the `prometheus` Plugin for different scenarios.\n\n### Get APISIX Metrics\n\nThe following example demonstrates how you can get metrics from APISIX.\n\nThe default Prometheus metrics endpoint and other Prometheus related configurations can be found in the [static configuration](#static-configurations). If you would like to customize these configuration, update `config.yaml` and reload APISIX.\n\nIf you deploy APISIX in a containerized environment and would like to access the Prometheus metrics endpoint externally, update the configuration file as follows and reload APISIX:\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:\n    export_addr:\n      ip: 0.0.0.0\n```\n\nSend a request to the APISIX Prometheus metrics endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\nYou should see an output similar to the following:\n\n```text\n# HELP apisix_bandwidth Total bandwidth in bytes consumed per Service in Apisix\n# TYPE apisix_bandwidth counter\napisix_bandwidth{type=\"egress\",route=\"\",service=\"\",consumer=\"\",node=\"\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 8417\napisix_bandwidth{type=\"egress\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 1420\napisix_bandwidth{type=\"egress\",route=\"2\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 1420\napisix_bandwidth{type=\"ingress\",route=\"\",service=\"\",consumer=\"\",node=\"\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 189\napisix_bandwidth{type=\"ingress\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 332\napisix_bandwidth{type=\"ingress\",route=\"2\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 332\n# HELP apisix_etcd_modify_indexes Etcd modify index for APISIX keys\n# TYPE apisix_etcd_modify_indexes gauge\napisix_etcd_modify_indexes{key=\"consumers\"} 0\napisix_etcd_modify_indexes{key=\"global_rules\"} 0\n...\n```\n\n### Expose APISIX Metrics on Public API Endpoint\n\nThe following example demonstrates how you can disable the Prometheus export server that, by default, exposes an endpoint on port `9091`, and expose APISIX Prometheus metrics on a new public API endpoint on port `9080`, which APISIX uses to listen to other client requests.\n\nDisable the Prometheus export server in the configuration file and reload APISIX for changes to take effect:\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:\n    enable_export_server: false\n```\n\nNext, create a Route with [`public-api`](../../../en/latest/plugins/public-api.md) Plugin and expose a public API endpoint for APISIX metrics:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/prometheus-metrics\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/apisix/prometheus/metrics\",\n    \"plugins\": {\n      \"public-api\": {}\n    }\n  }'\n```\n\nSend a request to the new metrics endpoint to verify:\n\n```shell\ncurl \"http://127.0.0.1:9080/apisix/prometheus/metrics\"\n```\n\nYou should see an output similar to the following:\n\n```text\n# HELP apisix_http_requests_total The total number of client requests since APISIX started\n# TYPE apisix_http_requests_total gauge\napisix_http_requests_total 1\n# HELP apisix_nginx_http_current_connections Number of HTTP connections\n# TYPE apisix_nginx_http_current_connections gauge\napisix_nginx_http_current_connections{state=\"accepted\"} 1\napisix_nginx_http_current_connections{state=\"active\"} 1\napisix_nginx_http_current_connections{state=\"handled\"} 1\napisix_nginx_http_current_connections{state=\"reading\"} 0\napisix_nginx_http_current_connections{state=\"waiting\"} 0\napisix_nginx_http_current_connections{state=\"writing\"} 1\n...\n```\n\n### Monitor Upstream Health Statuses\n\nThe following example demonstrates how to monitor the health status of upstream nodes.\n\nCreate a Route with the `prometheus` Plugin and configure upstream active health checks:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"prometheus-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"prometheus\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1,\n        \"127.0.0.1:20001\": 1\n      },\n      \"checks\": {\n        \"active\": {\n          \"timeout\": 5,\n          \"http_path\": \"/status\",\n          \"healthy\": {\n            \"interval\": 2,\n            \"successes\": 1\n          },\n          \"unhealthy\": {\n            \"interval\": 1,\n            \"http_failures\": 2\n          }\n        },\n        \"passive\": {\n          \"healthy\": {\n            \"http_statuses\": [200, 201],\n            \"successes\": 3\n          },\n          \"unhealthy\": {\n            \"http_statuses\": [500],\n            \"http_failures\": 3,\n            \"tcp_failures\": 3\n          }\n        }\n      }\n    }\n  }'\n```\n\nSend a request to the APISIX Prometheus metrics endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\nYou should see an output similar to the following:\n\n```text\n# HELP apisix_upstream_status upstream status from health check\n# TYPE apisix_upstream_status gauge\napisix_upstream_status{name=\"/apisix/routes/1\",ip=\"54.237.103.220\",port=\"80\"} 1\napisix_upstream_status{name=\"/apisix/routes/1\",ip=\"127.0.0.1\",port=\"20001\"} 0\n```\n\nThis shows that the upstream node `httpbin.org:80` is healthy and the upstream node `127.0.0.1:20001` is unhealthy.\n\n### Add Extra Labels for Metrics\n\nThe following example demonstrates how to add additional labels to metrics and use the [Nginx variable](https://nginx.org/en/docs/http/ngx_http_core_module.html) in label values.\n\nCurrently, only the following metrics support extra labels:\n\n* apisix_http_status\n* apisix_http_latency\n* apisix_bandwidth\n\nInclude the following configurations in the configuration file to add labels for metrics and reload APISIX for changes to take effect:\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:                                # Plugin: prometheus\n    metrics:                                 # Create extra labels from the NGINX variables.\n      http_status:\n        extra_labels:                        # Set the extra labels for http_status metrics.\n          - upstream_addr: $upstream_addr    # Add an extra upstream_addr label with value being the NGINX variable $upstream_addr.\n          - route_name: $route_name          # Add an extra route_name label with value being the APISIX variable $route_name.\n```\n\nNote that if you define a variable in the label value but it does not correspond to any existing [APISIX variables](https://apisix.apache.org/docs/apisix/apisix-variable/) and [Nginx variable](https://nginx.org/en/docs/http/ngx_http_core_module.html), the label value will default to an empty string.\n\nCreate a Route with the `prometheus` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"prometheus-route\",\nInclude the following configurations in the configuration file to add labels for metrics and reload APISIX for changes to take effect:\n    \"name\": \"extra-label\",\n    \"plugins\": {\n      \"prometheus\": {}\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response.\n\nSend a request to the APISIX Prometheus metrics endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\nYou should see an output similar to the following:\n\n```text\n# HELP apisix_http_status HTTP status codes per Service in APISIX\n# TYPE apisix_http_status counter\napisix_http_status{code=\"200\",route=\"1\",matched_uri=\"/get\",matched_host=\"\",service=\"\",consumer=\"\",node=\"54.237.103.220\",upstream_addr=\"54.237.103.220:80\",route_name=\"extra-label\"} 1\n```\n\n### Monitor TCP/UDP Traffic with Prometheus\n\nThe following example demonstrates how to collect TCP/UDP traffic metrics in APISIX.\n\nInclude the following configurations in `config.yaml` to enable stream proxy and `prometheus` Plugin for stream proxy. Reload APISIX for changes to take effect:\n\n```yaml title=\"conf/config.yaml\"\napisix:\n  proxy_mode: http&stream   # Enable both L4 & L7 proxies\n  stream_proxy:             # Configure L4 proxy\n    tcp:\n      - 9100                # Set TCP proxy listening port\n    udp:\n      - 9200                # Set UDP proxy listening port\n\nstream_plugins:\n  - prometheus              # Enable prometheus for stream proxy\n```\n\nCreate a Stream Route with the `prometheus` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/stream_routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\nInclude the following configurations in `config.yaml` to enable stream proxy and enable `prometheus` Plugin for stream proxy. Reload APISIX for changes to take effect:\n    \"plugins\": {\n      \"prometheus\":{}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Stream Route to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9100\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response.\n\nSend a request to the APISIX Prometheus metrics endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\nYou should see an output similar to the following:\n\n```text\n# HELP apisix_stream_connection_total Total number of connections handled per Stream Route in APISIX\n# TYPE apisix_stream_connection_total counter\napisix_stream_connection_total{route=\"1\"} 1\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/proxy-cache.md",
    "content": "---\ntitle: proxy-cache\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Proxy Cache\ndescription: The proxy-cache Plugin caches responses based on keys, supporting disk and memory caching for GET, POST, and HEAD requests, enhancing API performance.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/proxy-cache\" />\n</head>\n\n## Description\n\nThe `proxy-cache` Plugin provides the capability to cache responses based on a cache key. The Plugin supports both disk-based and memory-based caching options to cache for [GET](https://anything.org/learn/serving-over-http/#get-request), [POST](https://anything.org/learn/serving-over-http/#post-request), and [HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) requests.\n\nResponses can be conditionally cached based on request HTTP methods, response status codes, request header values, and more.\n\n## Attributes\n\n| Name               | Type           | Required | Default                   | Valid values            | Description                                                                                                                                                                                                                                                                                           |\n|--------------------|----------------|----------|---------------------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| cache_strategy     | string         | False    | disk                      | [\"disk\",\"memory\"]       | Caching strategy. Cache on disk or in memory.          |\n| cache_zone         | string         | False    | disk_cache_one            |                         | Cache zone used with the caching strategy. The value should match one of the cache zones defined in the [configuration files](#static-configurations) and should correspond to the caching strategy. For example, when using the in-memory caching strategy, you should use an in-memory cache zone. |\n| cache_key          | array[string]  | False    | [\"$host\", \"$request_uri\"] |                         | Key to use for caching. Support [NGINX variables](https://nginx.org/en/docs/varindex.html) and constant strings in values. Variables should be prefixed with a `$` sign.    |\n| cache_bypass       | array[string]  | False    |                           |                         | One or more parameters to parse value from, such that if any of the values is not empty and is not equal to `0`, response will not be retrieved from cache. Support [NGINX variables](https://nginx.org/en/docs/varindex.html) and constant strings in values. Variables should be prefixed with a `$` sign.     |\n| cache_method       | array[string]  | False    | [\"GET\", \"HEAD\"]           | [\"GET\", \"POST\", \"HEAD\"] | Request methods of which the response should be cached.       |\n| cache_http_status  | array[integer] | False    | [200, 301, 404]           | [200, 599]              | Response HTTP status codes of which the response should be cached.   |\n| hide_cache_headers | boolean        | False    | false                     |                         | If true, hide `Expires` and `Cache-Control` response headers.   |\n| cache_control      | boolean        | False    | false                     |                         | If true, comply with `Cache-Control` behavior in the HTTP specification. Only valid for in-memory strategy.     |\n| no_cache           | array[string]  | False    |                           |                         | One or more parameters to parse value from, such that if any of the values is not empty and is not equal to `0`, response will not be cached. Support [NGINX variables](https://nginx.org/en/docs/varindex.html) and constant strings in values. Variables should be prefixed with a `$` sign.       |\n| cache_ttl          | integer        | False    | 300          |        >=1          | Cache time to live (TTL) in seconds when caching in memory. To adjust the TTL when caching on disk, update `cache_ttl` in the [configuration files](#static-configurations). The TTL value is evaluated in conjunction with the values in the response headers  [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) and [`Expires`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires) received from the Upstream service.     |\n\n## Static Configurations\n\nBy default, values such as `cache_ttl` when caching on disk and cache `zones` are pre-configured in the [default configuration](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua).\n\nTo customize these values, add the corresponding configurations to `config.yaml`. For example:\n\n```yaml\napisix:\n  proxy_cache:\n    cache_ttl: 10s  # default cache TTL used when caching on disk, only if none of the `Expires`\n                    # and `Cache-Control` response headers is present, or if APISIX returns\n                    # `502 Bad Gateway` or `504 Gateway Timeout` due to unavailable upstreams\n    zones:\n      - name: disk_cache_one\n        memory_size: 50m\n        disk_size: 1G\n        disk_path: /tmp/disk_cache_one\n        cache_levels: 1:2\n      # - name: disk_cache_two\n      #   memory_size: 50m\n      #   disk_size: 1G\n      #   disk_path: \"/tmp/disk_cache_two\"\n      #   cache_levels: \"1:2\"\n      - name: memory_cache\n        memory_size: 50m\n```\n\nReload APISIX for changes to take effect.\n\n## Examples\n\nThe examples below demonstrate how you can configure `proxy-cache` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Cache Data on Disk\n\nOn-disk caching strategy offers the advantages of data persistency when system restarts and having larger storage capacity compared to in-memory cache. It is suitable for applications that prioritize durability and can tolerate slightly larger cache access latency.\n\nThe following example demonstrates how you can use `proxy-cache` Plugin on a Route to cache data on disk.\n\nWhen using the on-disk caching strategy, the cache TTL is determined by value from the response header `Expires` or `Cache-Control`. If none of these headers is present or if APISIX returns `502 Bad Gateway` or `504 Gateway Timeout` due to unavailable Upstreams, the cache TTL defaults to the value configured in the [configuration files](#static-configuration).\n\nCreate a Route with the `proxy-cache` Plugin to cache data on disk:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"cache_strategy\": \"disk\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following header, showing the Plugin is successfully enabled:\n\n```text\nApisix-Cache-Status: MISS\n```\n\nAs there is no cache available before the first response, `Apisix-Cache-Status: MISS` is shown.\n\nSend the same request again within the cache TTL window. You should see an `HTTP/1.1 200 OK` response with the following headers, showing the cache is hit:\n\n```text\nApisix-Cache-Status: HIT\n```\n\nWait for the cache to expire after the TTL and send the same request again. You should see an `HTTP/1.1 200 OK` response with the following headers, showing the cache has expired:\n\n```text\nApisix-Cache-Status: EXPIRED\n```\n\n### Cache Data in Memory\n\nIn-memory caching strategy offers the advantage of low-latency access to the cached data, as retrieving data from RAM is faster than retrieving data from disk storage. It also works well for storing temporary data that does not need to be persisted long-term, allowing for efficient caching of frequently changing data.\n\nThe following example demonstrates how you can use `proxy-cache` Plugin on a Route to cache data in memory.\n\nCreate a Route with `proxy-cache` and configure it to use memory-based caching:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"cache_strategy\": \"memory\",\n        \"cache_zone\": \"memory_cache\",\n        \"cache_ttl\": 10\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following header, showing the Plugin is successfully enabled:\n\n```text\nApisix-Cache-Status: MISS\n```\n\nAs there is no cache available before the first response, `Apisix-Cache-Status: MISS` is shown.\n\nSend the same request again within the cache TTL window. You should see an `HTTP/1.1 200 OK` response with the following headers, showing the cache is hit:\n\n```text\nApisix-Cache-Status: HIT\n```\n\n### Cache Responses Conditionally\n\nThe following example demonstrates how you can configure the `proxy-cache` Plugin to conditionally cache responses.\n\nCreate a Route with the `proxy-cache` Plugin and configure the `no_cache` attribute, such that if at least one of the values of the URL parameter `no_cache` and header `no_cache` is not empty and is not equal to `0`, the response will not be cached:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"no_cache\": [\"$arg_no_cache\", \"$http_no_cache\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a few requests to the Route with the URL parameter `no_cache` value indicating cache bypass:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?no_cache=1\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses for all requests and observe the following header every time:\n\n```text\nApisix-Cache-Status: EXPIRED\n```\n\nSend a few other requests to the Route with the URL parameter `no_cache` value being zero:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?no_cache=0\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses for all requests and start seeing the cache being hit:\n\n```text\nApisix-Cache-Status: HIT\n```\n\nYou can also specify the value in the `no_cache` header as such:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"no_cache: 1\"\n```\n\nThe response should not be cached:\n\n```text\nApisix-Cache-Status: EXPIRED\n```\n\n### Retrieve Responses from Cache Conditionally\n\nThe following example demonstrates how you can configure the `proxy-cache` Plugin to conditionally retrieve responses from cache.\n\nCreate a Route with the `proxy-cache` Plugin and configure the `cache_bypass` attribute, such that if at least one of the values of the URL parameter `bypass` and header `bypass` is not empty and is not equal to `0`, the response will not be retrieved from the cache:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"cache_bypass\": [\"$arg_bypass\", \"$http_bypass\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route with the URL parameter `bypass` value indicating cache bypass:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?bypass=1\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following header:\n\n```text\nApisix-Cache-Status: BYPASS\n```\n\nSend another request to the Route with the URL parameter `bypass` value being zero:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?bypass=0\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response with the following header:\n\n```text\nApisix-Cache-Status: MISS\n```\n\nYou can also specify the value in the `bypass` header as such:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"bypass: 1\"\n```\n\nThe cache should be bypassed:\n\n```text\nApisix-Cache-Status: BYPASS\n```\n\n### Cache for 502 and 504 Error Response Code\n\nWhen the Upstream services return server errors in the 500 range, `proxy-cache` Plugin will cache the responses if and only if the returned status is `502 Bad Gateway` or `504 Gateway Timeout`.\n\nThe following example demonstrates the behavior of `proxy-cache` Plugin when the Upstream service returns `504 Gateway Timeout`.\n\nCreate a Route with the `proxy-cache` Plugin and configure a dummy Upstream service:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/timeout\",\n    \"plugins\": {\n      \"proxy-cache\": { }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"12.34.56.78\": 1\n      }\n    }\n  }'\n```\n\nGenerate a few requests to the Route:\n\n```shell\nseq 4 | xargs -I{} curl -I \"http://127.0.0.1:9080/timeout\"\n```\n\nYou should see a response similar to the following:\n\n```text\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: MISS\n\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: HIT\n\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: HIT\n\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: HIT\n```\n\nHowever, if the Upstream services returns `503 Service Temporarily Unavailable`, the response will not be cached.\n"
  },
  {
    "path": "docs/en/latest/plugins/proxy-control.md",
    "content": "---\ntitle: proxy-control\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Proxy Control\ndescription: This document contains information about the Apache APISIX proxy-control Plugin, you can use it to control the behavior of the NGINX proxy dynamically.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe proxy-control Plugin dynamically controls the behavior of the NGINX proxy.\n\n:::info IMPORTANT\n\nThis Plugin requires APISIX to run on [APISIX-Runtime](../FAQ.md#how-do-i-build-the-apisix-runtime-environment). See [apisix-build-tools](https://github.com/api7/apisix-build-tools) for more info.\n\n:::\n\n## Attributes\n\n| Name              | Type    | Required | Default | Description                                                                                                                                                                 |\n| ----------------- | ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| request_buffering | boolean | False    | true    | When set to `true`, the Plugin dynamically sets the [`proxy_request_buffering`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering) directive. |\n\n## Enable Plugin\n\nThe example below enables the Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/upload\",\n    \"plugins\": {\n        \"proxy-control\": {\n            \"request_buffering\": false\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nThe example below shows the use case of uploading a big file:\n\n```shell\ncurl -i http://127.0.0.1:9080/upload -d @very_big_file\n```\n\nIt's expected to not find a message \"a client request body is buffered to a temporary file\" in the error log.\n\n## Delete Plugin\n\nTo remove the `proxy-control` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/upload\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/proxy-mirror.md",
    "content": "---\ntitle: proxy-mirror\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Proxy Mirror\ndescription: The proxy-mirror Plugin duplicates ingress traffic to APISIX and forwards them to a designated Upstream without interrupting the regular services.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/proxy-mirror\" />\n</head>\n\n## Description\n\nThe `proxy-mirror` Plugin duplicates ingress traffic to APISIX and forwards them to a designated upstream, without interrupting the regular services. You can configure the Plugin to mirror all traffic or only a portion. The mechanism benefits a few use cases, including troubleshooting, security inspection, analytics, and more.\n\nNote that APISIX ignores any response from the Upstream host receiving mirrored traffic.\n\n## Attributes\n\n| Name         | Type   | Required | Default | Valid values | Description                                                                                                               |\n|--------------|--------|----------|---------|--------------|---------------------------------------------------------------------------------------------------------------------------|\n| host         | string | True     |         |              | Address of the host to forward the mirrored traffic to. The address should contain the scheme but without the path, such as `http://127.0.0.1:8081`.  |\n| path         | string | False    |         |              | Path of the host to forward the mirrored traffic to. If unspecified, default to the current URI path of the Route. Not applicable if the Plugin is mirroring gRPC traffic.    |\n| path_concat_mode | string | False   |   replace     | [\"replace\", \"prefix\"]       | Concatenation mode when `path` is specified. When set to `replace`, the configured `path` would be directly used as the path of the host to forward the mirrored traffic to. When set to `prefix`, the path to forward to would be the configured `path`, appended by the requested URI path of the Route. Not applicable if the Plugin is mirroring gRPC traffic.  |\n| sample_ratio | number | False    | 1       | [0.00001, 1] |  Ratio of the requests that will be mirrored. By default, all traffic are mirrored.                         |\n\n## Static Configurations\n\nBy default, timeout values for the Plugin are pre-configured in the [default configuration](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua).\n\nTo customize these values, add the corresponding configurations to `config.yaml`. For example:\n\n```yaml\nplugin_attr:\n  proxy-mirror:\n    timeout:\n      connect: 60s\n      read: 60s\n      send: 60s\n```\n\nReload APISIX for changes to take effect.\n\n## Examples\n\nThe examples below demonstrate how to configure `proxy-mirror` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Mirror Partial Traffic\n\nThe following example demonstrates how you can configure `proxy-mirror` to mirror 50% of the traffic to a Route and forward them to another Upstream service.\n\nStart a sample NGINX server for receiving mirrored traffic:\n\n```shell\ndocker run -p 8081:80 --name nginx nginx\n```\n\nYou should see NGINX access log and error log on the terminal session.\n\nOpen a new terminal session and create a Route with `proxy-mirror` to mirror 50% of the traffic:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"traffic-mirror-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"proxy-mirror\": {\n        \"host\": \"http://127.0.0.1:8081\",\n        \"sample_ratio\": 0.5\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend Generate a few requests to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses for all requests.\n\nNavigating back to the NGINX terminal session, you should see a number of access log entries, roughly half the number of requests generated:\n\n```text\n172.17.0.1 - - [29/Jan/2024:23:11:01 +0000] \"GET /get HTTP/1.1\" 404 153 \"-\" \"curl/7.64.1\" \"-\"\n```\n\nThis suggests APISIX has mirrored the request to the NGINX server. Here, the HTTP response status is `404` since the sample NGINX server does not implement the Route.\n\n### Configure Mirroring Timeouts\n\nThe following example demonstrates how you can update the default connect, read, and send timeouts for the Plugin. This could be useful when mirroring traffic to a very slow backend service.\n\nAs the request mirroring was implemented as sub-requests, excessive delays in the sub-requests could lead to the blocking of the original requests. By default, the connect, read, and send timeouts are set to 60 seconds. To update these values, you can configure them in the `plugin_attr` section of the configuration file as such:\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  proxy-mirror:\n    timeout:\n      connect: 2000ms\n      read: 2000ms\n      send: 2000ms\n```\n\nReload APISIX for changes to take effect.\n"
  },
  {
    "path": "docs/en/latest/plugins/proxy-rewrite.md",
    "content": "---\ntitle: proxy-rewrite\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Proxy Rewrite\n  - proxy-rewrite\ndescription: The proxy-rewrite Plugin offers options to rewrite requests that APISIX forwards to Upstream services. With this plugin, you can modify the HTTP methods, request destination Upstream addresses, request headers, and more.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/proxy-rewrite\" />\n</head>\n\n## Description\n\nThe `proxy-rewrite` Plugin offers options to rewrite requests that APISIX forwards to Upstream services. With this plugin, you can modify the HTTP methods, request destination Upstream addresses, request headers, and more.\n\n## Attributes\n\n| Name                        | Type          | Required | Default | Valid values                                                                                                                           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n|-----------------------------|---------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| uri                         | string        | False    |         |                                                                                                                                        |  New Upstream URI path. Value supports [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). For example, `$arg_name`.                                                                                                                                                                                                                                                                                                                       |\n| method                      | string        | False    |         | [\"GET\", \"POST\", \"PUT\", \"HEAD\", \"DELETE\", \"OPTIONS\",\"MKCOL\", \"COPY\", \"MOVE\", \"PROPFIND\", \"PROPFIND\",\"LOCK\", \"UNLOCK\", \"PATCH\", \"TRACE\"] | HTTP method to rewrite requests to use.                                                                                                                                                                                                                                                                                                                                                                                                                                                    |\n| regex_uri                   | array[string] | False    |         |                                                                                                                                        | Regular expressions used to match the URI path from client requests and compose a new Upstream URI path. When both `uri` and `regex_uri` are configured, `uri` has a higher priority. The array should contain one or more **key-value pairs**, with the key being the regular expression to match URI against and value being the new Upstream URI path. For example, with `[\"^/iresty/(. *)/(. *)\", \"/$1-$2\", ^/theothers/*\", \"/theothers\"]`, if a request is originally sent to `/iresty/hello/world`, the Plugin will rewrite the Upstream URI path to `/iresty/hello-world`; if a request is originally sent to `/theothers/hello/world`, the Plugin will rewrite the Upstream URI path to `/theothers`. |\n| host                        | string        | False    |         |                                                                                                                                        | Set [`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) request header.                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| headers                     | object        | False    |         |                                                                                                                                   |   Header actions to be executed. Can be set to objects of action verbs `add`, `remove`, and/or `set`; or an object consisting of headers to be `set`. When multiple action verbs are configured, actions are executed in the order of `add`, `remove`, and `set`.                |\n| headers.add     | object   | False     |        |                 | Headers to append to requests. If a header already present in the request, the header value will be appended. Header value could be set to a constant, one or more [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or the matched result of `regex_uri` using variables such as `$1-$2-$3`.                                                                                              |\n| headers.set     | object  | False     |        |                 | Headers to set to requests. If a header already present in the request, the header value will be overwritten. Header value could be set to a constant, one or more [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html), or the matched result of `regex_uri` using variables such as `$1-$2-$3`. Should not be used to set `Host`.                                                                                       |\n| headers.remove  | array[string]   | False     |        |                 | Headers to remove from requests.\n| use_real_request_uri_unsafe | boolean       | False    | false   |                                                                                                                                        | If true, bypass URI normalization and allow for the full original request URI. Enabling this option is considered unsafe.         |\n\n## Examples\n\nThe examples below demonstrate how you can configure `proxy-rewrite` on a Route in different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Rewrite Host Header\n\nThe following example demonstrates how you can modify the `Host` header in a request. Note that you should not use `headers.set` to set the `Host` header.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"host\": \"myapisix.demo\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to `/headers` to check all the request headers sent to upstream:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"myapisix.demo\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef198-29da0970383150175bd2d76d\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### Rewrite URI And Set Headers\n\nThe following example demonstrates how you can rewrite the request Upstream URI and set additional header values. If the same headers present in the client request, the corresponding header values set in the Plugin will overwrite the values present in the client request.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/anything\",\n        \"headers\": {\n          \"set\": {\n            \"X-Api-Version\": \"v1\",\n            \"X-Api-Engine\": \"apisix\"\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl \"http://127.0.0.1:9080/\" -H '\"X-Api-Version\": \"v2\"'\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fed73a-59cd3bd640d76ab16c97f1f1\",\n    \"X-Api-Engine\": \"apisix\",\n    \"X-Api-Version\": \"v1\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"::1, 103.248.35.179\",\n  \"url\": \"http://localhost/anything\"\n}\n```\n\nNote that both headers present and the header value of `X-Api-Version` configured in the Plugin overwrites the header value passed in the request.\n\n### Rewrite URI And Append Headers\n\nThe following example demonstrates how you can rewrite the request Upstream URI and append additional header values. If the same headers present in the client request, their headers values will append to the configured header values in the plugin.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/headers\",\n        \"headers\": {\n          \"add\": {\n            \"X-Api-Version\": \"v1\",\n            \"X-Api-Engine\": \"apisix\"\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl \"http://127.0.0.1:9080/\" -H '\"X-Api-Version\": \"v2\"'\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fed73a-59cd3bd640d76ab16c97f1f1\",\n    \"X-Api-Engine\": \"apisix\",\n    \"X-Api-Version\": \"v1,v2\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\nNote that both headers present and the header value of `X-Api-Version` configured in the Plugin is appended by the header value passed in the request.\n\n### Remove Existing Header\n\nThe following example demonstrates how you can remove an existing header `User-Agent`.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"headers\": {\n          \"remove\":[\n            \"User-Agent\"\n          ]\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify if the specified header is removed:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\nYou should see a response similar to the following, where the `User-Agent` header is not present:\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef302-07f2b13e0eb006ba776ad91d\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### Rewrite URI Using RegEx\n\nThe following example demonstrates how you can parse text from the original Upstream URI path and use them to compose a new Upstream URI path. In this example, APISIX is configured to forward all requests from `/test/user/agent` to `/user-agent`.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"uri\": \"/test/*\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"regex_uri\": [\"^/test/(.*)/(.*)\", \"/$1-$2\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to `/test/user/agent` to check if it is redirected to `/user-agent`:\n\n```shell\ncurl \"http://127.0.0.1:9080/test/user/agent\"\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"user-agent\": \"curl/8.2.1\"\n}\n```\n\n### Add URL Parameters\n\nThe following example demonstrates how you can add URL parameters to the request.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/get?arg1=apisix&arg2=plugin\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify if the URL parameters are also forwarded to upstream:\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"args\": {\n    \"arg1\": \"apisix\",\n    \"arg2\": \"plugin\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef6dc-2b0e09591db7353a275cdae4\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"127.0.0.1, 103.248.35.148\",\n  \"url\": \"http://127.0.0.1/get?arg1=apisix&arg2=plugin\"\n}\n```\n\n### Rewrite HTTP Method\n\nThe following example demonstrates how you can rewrite a GET request into a POST request.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/anything\",\n        \"method\":\"POST\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a GET request to `/get` to verify if it is transformed into a POST request to `/anything`:\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef7de-0c63387645353998196317f2\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"POST\",\n  \"origin\": \"::1, 103.248.35.179\",\n  \"url\": \"http://localhost/anything\"\n}\n```\n\n### Forward Consumer Names to Upstream\n\nThe following example demonstrates how you can forward the name of consumers who authenticates successfully to Upstream services. As an example, you will be using `key-auth` as the authentication method.\n\nCreate a Consumer `JohnDoe`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"JohnDoe\"\n  }'\n```\n\nCreate `key-auth` credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/JohnDoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nNext, create a Route with key authentication enabled, configure `proxy-rewrite` to add Consumer name to the header, and remove the authentication key so that it is not visible to the Upstream service:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"consumer-restricted-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"proxy-rewrite\": {\n        \"headers\": {\n          \"set\": {\n            \"X-Apisix-Consumer\": \"$consumer_name\"\n          },\n          \"remove\": [ \"Apikey\" ]\n        }\n      }\n    },\n    \"upstream\" : {\n      \"nodes\": {\n        \"httpbin.org\":1\n      }\n    }\n  }'\n```\n\nSend a request to the Route as Consumer `JohnDoe`:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response with the following body:\n\n```text\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.4.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-664b01a6-2163c0156ed4bff51d87d877\",\n    \"X-Apisix-Consumer\": \"JohnDoe\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"172.19.0.1, 203.12.12.12\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\nSend another request to the Route without the valid credential:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should receive an `HTTP/1.1 403 Forbidden` response.\n"
  },
  {
    "path": "docs/en/latest/plugins/public-api.md",
    "content": "---\ntitle: public-api\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Public API\ndescription: The public-api plugin exposes an internal API endpoint, making it publicly accessible. One of the primary use cases of this plugin is to expose internal endpoints created by other plugins.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/public-api\" />\n</head>\n\n## Description\n\nThe `public-api` Plugin exposes an internal API endpoint, making it publicly accessible. One of the primary use cases of this Plugin is to expose internal endpoints created by other Plugins.\n\n## Attributes\n\n| Name    | Type      | Required | Default | Valid Values | Description |\n|---------|-----------|----------|---------|--------------|-------------|\n| uri     | string    | False    |         |              | Internal endpoint to expose. If not configured, expose the Route URI. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `public-api` in different scenarios.\n\n### Expose Prometheus Metrics at Custom Endpoint\n\nThe following example demonstrates how you can disable the Prometheus export server that, by default, exposes an endpoint on port `9091`, and expose APISIX Prometheus metrics on a new public API endpoint on port `9080`, which APISIX uses to listen to other client requests.\n\nYou will also configure the Route such that the internal endpoint `/apisix/prometheus/metrics` is exposed at a custom endpoint.\n\n:::caution\n\nIf a large quantity of metrics is being collected, the Plugin could take up a significant amount of CPU resources for metric computations and negatively impact the processing of regular requests.\n\nTo address this issue, APISIX uses [privileged agent](https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/process.md#enable_privileged_agent) and offloads the metric computations to a separate process. This optimization applies automatically if you use the metric endpoint configured under `plugin_attr.prometheus.export_addr` in the configuration file. If you expose the metric endpoint with the `public-api` Plugin, you will not benefit from this optimization.\n\n:::\n\nDisable the Prometheus export server in the configuration file and reload APISIX for changes to take effect:\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:\n    enable_export_server: false\n```\n\nNext, create a Route with the `public-api` Plugin and expose a public API endpoint for APISIX metrics. You should set the Route `uri` to the custom endpoint path and set the Plugin `uri` to the internal endpoint to be exposed.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"prometheus-metrics\",\n    \"uri\": \"/prometheus_metrics\",\n    \"plugins\": {\n      \"public-api\": {\n        \"uri\": \"/apisix/prometheus/metrics\"\n      }\n    }\n  }'\n```\n\nSend a request to the custom metrics endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9080/prometheus_metrics\"\n```\n\nYou should see an output similar to the following:\n\n```text\n# HELP apisix_http_requests_total The total number of client requests since APISIX started\n# TYPE apisix_http_requests_total gauge\napisix_http_requests_total 1\n# HELP apisix_nginx_http_current_connections Number of HTTP connections\n# TYPE apisix_nginx_http_current_connections gauge\napisix_nginx_http_current_connections{state=\"accepted\"} 1\napisix_nginx_http_current_connections{state=\"active\"} 1\napisix_nginx_http_current_connections{state=\"handled\"} 1\napisix_nginx_http_current_connections{state=\"reading\"} 0\napisix_nginx_http_current_connections{state=\"waiting\"} 0\napisix_nginx_http_current_connections{state=\"writing\"} 1\n...\n```\n\n### Expose Batch Requests Endpoint\n\nThe following example demonstrates how you can use the `public-api` Plugin to expose an endpoint for the `batch-requests` Plugin, which is used for assembling multiple requests into one single request before sending them to the gateway.\n\n[//]:<TODO: update link to batch-requests plugin doc when it is available>\n\nCreate a sample Route to httpbin's `/anything` endpoint for verification purpose:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"httpbin-anything\",\n    \"uri\": \"/anything\",\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nCreate a Route with `public-api` Plugin and set the Route `uri` to the internal endpoint to be exposed:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"batch-requests\",\n    \"uri\": \"/apisix/batch-requests\",\n    \"plugins\": {\n      \"public-api\": {}\n    }\n  }'\n```\n\nSend a pipelined request consisting of a GET and a POST request to the exposed batch requests endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9080/apisix/batch-requests\" -X POST -d '\n{\n  \"pipeline\": [\n    {\n      \"method\": \"GET\",\n      \"path\": \"/anything\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/anything\",\n      \"body\": \"a post request\"\n    }\n  ]\n}'\n```\n\nYou should receive responses from both requests, similar to the following:\n\n```json\n[\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-5a30174f5534287928c54ca9\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"GET\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  },\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"a post request\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Content-Length\\\": \\\"14\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-0eddcec07f154dac0d77876f\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"POST\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  }\n]\n```\n\nIf you would like to expose the batch requests endpoint at a custom endpoint, create a Route with `public-api` Plugin as such. You should set the Route `uri` to the custom endpoint path and set the plugin `uri` to the internal endpoint to be exposed.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"batch-requests\",\n    \"uri\": \"/batch-requests\",\n    \"plugins\": {\n      \"public-api\": {\n        \"uri\": \"/apisix/batch-requests\"\n      }\n    }\n  }'\n```\n\nThe batch requests endpoint should now be exposed as `/batch-requests`, instead of `/apisix/batch-requests`.\n\nSend a pipelined request consisting of a GET and a POST request to the exposed batch requests endpoint:\n\n```shell\ncurl \"http://127.0.0.1:9080/batch-requests\" -X POST -d '\n{\n  \"pipeline\": [\n    {\n      \"method\": \"GET\",\n      \"path\": \"/anything\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/anything\",\n      \"body\": \"a post request\"\n    }\n  ]\n}'\n```\n\nYou should receive responses from both requests, similar to the following:\n\n```json\n[\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-5a30174f5534287928c54ca9\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"GET\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  },\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"a post request\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Content-Length\\\": \\\"14\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-0eddcec07f154dac0d77876f\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"POST\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  }\n]\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/real-ip.md",
    "content": "---\ntitle: real-ip\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Real IP\ndescription: The real-ip plugin allows Apache APISIX to set the client's real IP by the IP address passed in the HTTP header or HTTP query string.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/real-ip\" />\n</head>\n\n## Description\n\nThe `real-ip` Plugin allows APISIX to set the client's real IP by the IP address passed in the HTTP header or HTTP query string. This is particularly useful when APISIX is behind a reverse proxy since the proxy could act as the request-originating client otherwise.\n\nThe Plugin is functionally similar to NGINX's [ngx_http_realip_module](https://nginx.org/en/docs/http/ngx_http_realip_module.html) but offers more flexibility.\n\n## Attributes\n\n| Name      | Type    | Required | Default | Valid values   | Description   |\n|-----------|---------|----------|---------|----------------|---------------|\n| source    | string  | True      |     |    |A built-in [APISIX variable](https://apisix.apache.org/docs/apisix/apisix-variable/) or [NGINX variable](https://nginx.org/en/docs/varindex.html), such as `http_x_forwarded_for` or `arg_realip`. The variable value should be a valid IP address that represents the client's real IP address, with an optional port.|\n| trusted_addresses | array[string] | False |     | array of IPv4 or IPv6 addresses (CIDR notation acceptable)  | Trusted addresses that are known to send correct replacement addresses. This configuration sets the [`set_real_ip_from`](https://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from) directive. |\n| recursive  | boolean | False |  False   |    | If false, replace the original client address that matches one of the trusted addresses by the last address sent in the configured `source`.<br />If true, replace the original client address that matches one of the trusted addresses by the last non-trusted address sent in the configured `source`. |\n\n:::note\nOnly `X-Forwarded-*` headers sent from addresses in the `apisix.trusted_addresses` configuration (supports IP and CIDR) will be trusted and passed to plugins or upstream. If `apisix.trusted_addresses` is not configured or the IP is not within the configured address range, all `X-Forwarded-*` headers will be overridden with trusted values.\n:::\n\n:::note\nIf the address specified in `source` is missing or invalid, the Plugin would not change the client address.\n:::\n\n## Examples\n\nThe examples below demonstrate how you can configure `real-ip` in different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Obtain Real Client Address From URI Parameter\n\nThe following example demonstrates how to update the client IP address with a URI parameter.\n\nCreate a Route as follows. You should configure `source` to obtain value from the URL parameter `realip` using [APISIX variable](https://apisix.apache.org/docs/apisix/apisix-variable/) or [NGINX variable](https://nginx.org/en/docs/varindex.html). Use the `response-rewrite` Plugin to set response headers to verify if the client IP and port were actually updated.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"real-ip-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"real-ip\": {\n        \"source\": \"arg_realip\",\n        \"trusted_addresses\": [\"127.0.0.0/24\"]\n      },\n      \"response-rewrite\": {\n        \"headers\": {\n          \"remote_addr\": \"$remote_addr\",\n          \"remote_port\": \"$remote_port\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route with real IP and port in the URL parameter:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get?realip=1.2.3.4:9080\"\n```\n\nYou should see the response includes the following header:\n\n```text\nremote-addr: 1.2.3.4\nremote-port: 9080\n```\n\n### Obtain Real Client Address From Header\n\nThe following example shows how to set the real client IP when APISIX is behind a reverse proxy, such as a load balancer when the proxy exposes the real client IP in the [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) header.\n\nCreate a Route as follows. You should configure `source` to obtain value from the request header `X-Forwarded-For` using [APISIX variable](https://apisix.apache.org/docs/apisix/apisix-variable/) or [NGINX variable](https://nginx.org/en/docs/varindex.html). Use the `response-rewrite` Plugin to set a response header to verify if the client IP was actually updated.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"real-ip-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"real-ip\": {\n        \"source\": \"http_x_forwarded_for\",\n        \"trusted_addresses\": [\"127.0.0.0/24\"]\n      },\n      \"response-rewrite\": {\n        \"headers\": {\n          \"remote_addr\": \"$remote_addr\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should see a response including the following header:\n\n```text\nremote-addr: 10.26.3.19\n```\n\nThe IP address should correspond to the IP address of the request-originating client.\n\n### Obtain Real Client Address Behind Multiple Proxies\n\nThe following example shows how to get the real client IP when APISIX is behind multiple proxies, which causes [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) header to include a list of proxy IP addresses.\n\nCreate a Route as follows. You should configure `source` to obtain value from the request header `X-Forwarded-For` using [APISIX variable](https://apisix.apache.org/docs/apisix/apisix-variable/) or [NGINX variable](https://nginx.org/en/docs/varindex.html). Set `recursive` to `true` so that the original client address that matches one of the trusted addresses is replaced by the last non-trusted address sent in the configured `source`. Then, use the `response-rewrite` Plugin to set a response header to verify if the client IP was actually updated.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n  \"id\": \"real-ip-route\",\n  \"uri\": \"/get\",\n  \"plugins\": {\n    \"real-ip\": {\n      \"source\": \"http_x_forwarded_for\",\n      \"recursive\": true,\n      \"trusted_addresses\": [\"192.128.0.0/16\", \"127.0.0.0/24\"]\n    },\n    \"response-rewrite\": {\n      \"headers\": {\n        \"remote_addr\": \"$remote_addr\"\n      }\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" \\\n  -H \"X-Forwarded-For: 127.0.0.2, 192.128.1.1, 127.0.0.1\"\n```\n\nYou should see a response including the following header:\n\n```text\nremote-addr: 127.0.0.2\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/redirect.md",
    "content": "---\ntitle: redirect\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Redirect\ndescription: This document contains information about the Apache APISIX redirect Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `redirect` Plugin can be used to configure redirects.\n\n## Attributes\n\n| Name                | Type          | Required | Default | Valid values | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                         |\n|---------------------|---------------|----------|---------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| http_to_https       | boolean       | False    | false   |              | When set to `true` and the request is HTTP, it will be redirected to HTTPS with the same URI with a 301 status code.  Note the querystring from the raw URI will also be contained in the Location header.                                                                                                                                                                                                                                                          |\n| uri                 | string        | False    |         |              | URI to redirect to. Can contain Nginx variables. For example, `/test/index.html`, `$uri/index.html`, `${uri}/index.html`, `https://example.com/foo/bar`. If you refer to a variable name that doesn't exist, instead of throwing an error, it will treat it as an empty variable.                                                                                                                                                                                   |\n| regex_uri           | array[string] | False    |         |              | Match the URL from client with a regular expression and redirect. If it doesn't match, the request will be forwarded to the Upstream. Only either of `uri` or `regex_uri` can be used at a time. For example, [\" ^/iresty/(.*)/(.*)/(.*)\", \"/$1-$2-$3\"], where the first element is the regular expression to match and the second element is the URI to redirect to. APISIX only support one `regex_uri` currently, so the length of the `regex_uri` array is `2`. |\n| ret_code            | integer       | False    | 302     | [200, ...]   | HTTP response code.                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| encode_uri          | boolean       | False    | false   |              | When set to `true` the URI in the `Location` header will be encoded as per [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986).                                                                                                                                                                                                                                                                                                                                |\n| append_query_string | boolean       | False    | false   |              | When set to `true`, adds the query string from the original request to the `Location` header. If the configured `uri` or `regex_uri` already contains a query string, the query string from the request will be appended to it with an `&`. Do not use this if you have already handled the query string (for example, with an Nginx variable `$request_uri`) to avoid duplicates.                                                                                  |\n\n:::note\n\n* Only one of `http_to_https`, `uri` and `regex_uri` can be configured.\n* Only one of `http_to_https` and `append_query_string` can be configured.\n* When enabling `http_to_https`, the ports in the redirect URL will pick a value in the following order (in descending order of priority)\n  * Read `plugin_attr.redirect.https_port` from the configuration file (`conf/config.yaml`).\n  * If `apisix.ssl` is enabled, read `apisix.ssl.listen` and select a port randomly from it.\n  * Use 443 as the default https port.\n\n:::\n\n## Enable Plugin\n\nThe example below shows how you can enable the `redirect` Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/test/index.html\",\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"/test/default.html\",\n            \"ret_code\": 301\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n\nYou can also use any built-in Nginx variables in the new URI:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/test\",\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"$uri/index.html\",\n            \"ret_code\": 301\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nFirst, we configure the Plugin as mentioned above. We can then make a request and it will be redirected as shown below:\n\n```shell\ncurl http://127.0.0.1:9080/test/index.html -i\n```\n\n```shell\nHTTP/1.1 301 Moved Permanently\nDate: Wed, 23 Oct 2019 13:48:23 GMT\nContent-Type: text/html\nContent-Length: 166\nConnection: keep-alive\nLocation: /test/default.html\n...\n```\n\nThe response shows the response code and the `Location` header implying that the Plugin is in effect.\n\nThe example below shows how you can redirect HTTP to HTTPS:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"redirect\": {\n            \"http_to_https\": true\n        }\n    }\n}'\n```\n\nTo test this:\n\n```shell\ncurl http://127.0.0.1:9080/hello -i\n```\n\n```\nHTTP/1.1 301 Moved Permanently\n...\nLocation: https://127.0.0.1:9443/hello\n...\n```\n\n## Delete Plugin\n\nTo remove the `redirect` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/test/index.html\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/referer-restriction.md",
    "content": "---\ntitle: referer-restriction\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Referer restriction\ndescription: This document contains information about the Apache APISIX referer-restriction Plugin, which can be used to restrict access to a Service or a Route by whitelisting/blacklisting the Referer request header.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `referer-restriction` Plugin can be used to restrict access to a Service or a Route by whitelisting/blacklisting the `Referer` request header.\n\n## Attributes\n\n| Name           | Type          | Required | Default                          | Valid values | Description                                                                                       |\n|----------------|---------------|----------|----------------------------------|--------------|---------------------------------------------------------------------------------------------------|\n| whitelist      | array[string] | False    |                                  |              | List of hostnames to whitelist. A hostname can start with `*` for wildcard.                       |\n| blacklist      | array[string] | False    |                                  |              | List of hostnames to blacklist. A hostname can start with `*` for wildcard.                       |\n| message        | string        | False    | \"Your referer host is not allowed\" | [1, 1024]    | Message returned when access is not allowed.                                                      |\n| bypass_missing | boolean       | False    | false                            |              | When set to `true`, bypasses the check when the `Referer` request header is missing or malformed. |\n\n:::info IMPORTANT\n\nOnly one of `whitelist` or `blacklist` attribute must be specified. They cannot work together.\n\n:::\n\n## Enable Plugin\n\nYou can enable the Plugin on a specific Route or a Service as shown below:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"referer-restriction\": {\n            \"bypass_missing\": true,\n            \"whitelist\": [\n                \"xx.com\",\n                \"*.xx.com\"\n            ]\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can test it by setting `Referer: http://xx.com/x`:\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'Referer: http://xx.com/x'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n```\n\nNow, if you make a request with `Referer: http://yy.com/x`, the request will be blocked:\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'Referer: http://yy.com/x'\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"Your referer host is not allowed\"}\n```\n\nSince we have set `bypass_missing` to `true`, a request without the `Referer` header will be successful as the check is skipped:\n\n```shell\ncurl http://127.0.0.1:9080/index.html\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n```\n\n## Delete Plugin\n\nTo remove the `referer-restriction` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/request-id.md",
    "content": "---\ntitle: request-id\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Request ID\ndescription: The request-id Plugin adds a unique ID to each request proxied through APISIX, which can be used to track API requests.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/request-id\" />\n</head>\n\n## Description\n\nThe `request-id` Plugin adds a unique ID to each request proxied through APISIX, which can be used to track API requests. If a request carries an ID in the header and is not empty (\"\") corresponding to `header_name`, the Plugin will use the header value as the unique ID and will not overwrite with the automatically generated ID.\n\n## Attributes\n\n| Name                | Type    | Required | Default        | Valid values                    | Description                                                            |\n| ------------------- | ------- | -------- | -------------- | ------------------------------- | ---------------------------------------------------------------------- |\n| header_name         | string  | False    | \"X-Request-Id\" |                                 | Name of the header that carries the request unique ID. Note that if a request carries an ID in the `header_name` header, the Plugin will use the header value as the unique ID and will not overwrite it with the generated ID.                                 |\n| include_in_response | boolean | False    | true           |                                 | If true, include the generated request ID in the response header, where the name of the header is the `header_name` value. |\n| algorithm           | string  | False    | \"uuid\"         | [\"uuid\",\"nanoid\",\"range_id\",\"ksuid\"] | Algorithm used for generating the unique ID. When set to `uuid` , the Plugin generates a universally unique identifier. When set to `nanoid`, the Plugin generates a compact, URL-safe ID. When set to `range_id`, the Plugin generates a sequential ID with specific parameters. When set to `ksuid`, the Plugin generates a sequential ID with timestamp and random number.                  |\n| range_id      | object | False | |   | Configuration for generating a request ID using the `range_id` algorithm.  |\n| range_id.char_set      | string | False | \"abcdefghijklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789\" | minimum length 6 | Character set used for the `range_id` algorithm. |\n| range_id.length    | integer | False | 16             | >=6 | Length of the generated ID for the `range_id` algorithm. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `request-id` in different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Attach Request ID to Default Response Header\n\nThe following example demonstrates how to configure `request-id` on a Route which attaches a generated request ID to the default `X-Request-Id` response header, if the header value is not passed in the request. When the `X-Request-Id` header is set in the request, the Plugin will take the value in the request header as the request ID.\n\nCreate a Route with the `request-id` Plugin using its default configurations (explicitly defined):\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"X-Request-Id\",\n        \"include_in_response\": true,\n        \"algorithm\": \"uuid\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the `X-Request-Id` header with a generated ID:\n\n```text\nX-Request-Id: b9b2c0d4-d058-46fa-bafc-dd91a0ccf441\n```\n\nSend a request to the Route with a empty request ID in the header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'X-Request-Id;'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the `X-Request-Id` header with a generated ID:\n\n```text\nX-Request-Id: b9b2c0d4-d058-46fa-bafc-dd91a0ccf441\n```\n\nSend a request to the Route with a custom request ID in the header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'X-Request-Id: some-custom-request-id'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the `X-Request-Id` header with the custom request ID:\n\n```text\nX-Request-Id: some-custom-request-id\n```\n\n### Attach Request ID to Custom Response Header\n\nThe following example demonstrates how to configure `request-id` on a Route which attaches a generated request ID to a specified header.\n\nCreate a Route with the `request-id` Plugin to define a custom header that carries the request ID and include the request ID in the response header:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"X-Req-Identifier\",\n        \"include_in_response\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the `X-Req-Identifier` header with a generated ID:\n\n```text\nX-Req-Identifier: 1c42ff59-ee4c-4103-a980-8359f4135b21\n```\n\n### Hide Request ID in Response Header\n\nThe following example demonstrates how to configure `request-id` on a Route which attaches a generated request ID to a specified header. The header containing the request ID should be forwarded to the Upstream service but not returned in the response header.\n\nCreate a Route with the `request-id` Plugin to define a custom header that carries the request ID and not include the request ID in the response header:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"X-Req-Identifier\",\n        \"include_in_response\": false\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response not and see `X-Req-Identifier` header among the response headers. In the response body, you should see:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6752748c-7d364f48564508db1e8c9ea8\",\n    \"X-Forwarded-Host\": \"127.0.0.1\",\n    \"X-Req-Identifier\": \"268092bc-15e1-4461-b277-bf7775f2856f\"\n  },\n  ...\n}\n```\n\nThis shows the request ID is forwarded to the Upstream service but not returned in the response header.\n\n### Use `nanoid` Algorithm\n\nThe following example demonstrates how to configure `request-id` on a Route and use the `nanoid` algorithm to generate the request ID.\n\nCreate a Route with the `request-id` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"algorithm\": \"nanoid\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the `X-Req-Identifier` header with an ID generated using the `nanoid` algorithm:\n\n```text\nX-Request-Id: kepgHWCH2ycQ6JknQKrX2\n```\n\n### Use `ksuid` Algorithm\n\nThe following example demonstrates how to configure `request-id` on a Route and use the `ksuid` algorithm to generate the request ID.\n\nCreate a Route with the `request-id` Plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"algorithm\": \"ksuid\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the `X-Request-Id` header with an ID generated using the `ksuid` algorithm:\n\n```text\nX-Request-Id: 325ghCANEKjw6Jsfejg5p6QrLYB\n```\n\nIf the [ksuid](https://github.com/segmentio/ksuid?tab=readme-ov-file#command-line-tool) is installed, this ID can be viewed through `ksuid -f inspect 325ghCANEKjw6Jsfejg5p6QrLYB`:\n\n``` text\nREPRESENTATION:\n\n    String: 325ghCANEKjw6Jsfejg5p6QrLYB\n    Raw: 15430DBBD7F68AD7CA0AE277772AB36DDB1A3C13\n\nCOMPONENTS:\n\n    Time: 2025-09-01 16:39:23 +0800 CST\n    Timestamp: 356715963\n    Payload: D7F68AD7CA0AE277772AB36DDB1A3C13\n```\n\n### Attach Request ID Globally and on a Route\n\nThe following example demonstrates how to configure `request-id` as a global Plugin and on a Route to attach two IDs.\n\nCreate a global rule for the `request-id` Plugin which adds request ID to a custom header:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/global_rules\" -X PUT -d '{\n  \"id\": \"rule-for-request-id\",\n  \"plugins\": {\n    \"request-id\": {\n      \"header_name\": \"Global-Request-ID\"\n    }\n  }\n}'\n```\n\nCreate a Route with the `request-id` Plugin which adds request ID to a different custom header:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"Route-Request-ID\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and see the response includes the following headers:\n\n```text\nGlobal-Request-ID: 2e9b99c1-08ed-4a74-b347-49c0891b07ad\nRoute-Request-ID: d755666b-732c-4f0e-a30e-a7a71ace4e26\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/request-validation.md",
    "content": "---\ntitle: request-validation\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Request Validation\ndescription: The request-validation Plugin validates requests before forwarding them to Upstream services. This Plugin uses JSON Schema for validation and can validate headers and body of a request.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/request-validation\" />\n</head>\n\n## Description\n\nThe `request-validation` Plugin validates requests before forwarding them to Upstream services. This Plugin uses [JSON Schema](https://github.com/api7/jsonschema) for validation and can validate headers and body of a request.\n\nSee [JSON schema specification](https://json-schema.org/specification) to learn more about the syntax.\n\n## Attributes\n\n| Name          | Type    | Required | Default | Valid values  | Description                                       |\n|---------------|---------|----------|---------|---------------|---------------------------------------------------|\n| header_schema | object  | False    |         |               | Schema for the request header data.               |\n| body_schema   | object  | False    |         |               | Schema for the request body data.                 |\n| rejected_code | integer | False    | 400     | [200,...,599] | Status code to return when rejecting requests. |\n| rejected_msg  | string  | False    |         |               | Message to return when rejecting requests.     |\n\n:::note\n\nAt least one of `header_schema` or `body_schema` should be filled in.\n\n:::\n\n## Examples\n\nThe examples below demonstrate how you can configure `request-validation` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Validate Request Header\n\nThe following example demonstrates how to validate request headers against a defined JSON schema, which requires two specific headers and the header value to conform to specified requirements.\n\nCreate a Route with `request-validation` Plugin as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"User-Agent\", \"Host\"],\n          \"properties\": {\n            \"User-Agent\": {\n              \"type\": \"string\",\n              \"pattern\": \"^curl\\/\"\n            },\n            \"Host\": {\n              \"type\": \"string\",\n              \"enum\": [\"httpbin.org\", \"httpbin\"]\n            }\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n#### Verify with Request Conforming to the Schema\n\nSend a request with header `Host: httpbin`, which complies with the schema:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Host: httpbin\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin\",\n    \"User-Agent\": \"curl/7.74.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6509ae35-63d1e0fd3934e3f221a95dd8\",\n    \"X-Forwarded-Host\": \"httpbin\"\n  },\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://httpbin/get\"\n}\n```\n\n#### Verify with Request Not Conforming to the Schema\n\nSend a request without any header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` response, showing that the request fails to pass validation:\n\n```text\nproperty \"Host\" validation failed: matches none of the enum value\n```\n\nSend a request with the required headers but with non-conformant header value:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Host: httpbin\" -H \"User-Agent: cli-mock\"\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` response showing the `User-Agent` header value does not match the expected pattern:\n\n```text\nproperty \"User-Agent\" validation failed: failed to match pattern \"^curl/\" with \"cli-mock\"\n```\n\n### Customize Rejection Message and Status Code\n\nThe following example demonstrates how to customize response status and message when the validation fails.\n\nConfigure the Route with `request-validation` as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"Host\"],\n          \"properties\": {\n            \"Host\": {\n              \"type\": \"string\",\n              \"enum\": [\"httpbin.org\", \"httpbin\"]\n            }\n          }\n        },\n        \"rejected_code\": 403,\n        \"rejected_msg\": \"Request header validation failed.\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request with a misconfigured `Host` in the header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Host: httpbin2\"\n```\n\nYou should receive an `HTTP/1.1 403 Forbidden` response with the custom message:\n\n```text\nRequest header validation failed.\n```\n\n### Validate Request Body\n\nThe following example demonstrates how to validate request body against a defined JSON schema.\n\nThe `request-validation` Plugin supports validation of two types of media types:\n\n* `application/json`\n* `application/x-www-form-urlencoded`\n\n#### Validate JSON Request Body\n\nCreate a Route with `request-validation` Plugin as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"Content-Type\"],\n          \"properties\": {\n            \"Content-Type\": {\n            \"type\": \"string\",\n            \"pattern\": \"^application\\/json$\"\n            }\n          }\n        },\n        \"body_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"required_payload\"],\n          \"properties\": {\n            \"required_payload\": {\"type\": \"string\"},\n            \"boolean_payload\": {\"type\": \"boolean\"},\n            \"array_payload\": {\n              \"type\": \"array\",\n              \"minItems\": 1,\n              \"items\": {\n                \"type\": \"integer\",\n                \"minimum\": 200,\n                \"maximum\": 599\n              },\n              \"uniqueItems\": true,\n              \"default\": [200]\n            }\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request with JSON body that conforms to the schema to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"required_payload\":\"hello\", \"array_payload\":[301]}'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"array_payload\\\":[301],\\\"required_payload\\\":\\\"hello\\\"}\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    ...\n  },\n  \"json\": {\n    \"array_payload\": [\n      301\n    ],\n    \"required_payload\": \"hello\"\n  },\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\nIf you send a request without specifying `Content-Type: application/json`:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -d '{\"required_payload\":\"hello,world\"}'\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` response similar to the following:\n\n```text\nproperty \"Content-Type\" validation failed: failed to match pattern \"^application/json$\" with \"application/x-www-form-urlencoded\"\n```\n\nSimilarly, if you send a request without the required JSON field `required_payload`:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` response:\n\n```text\nproperty \"required_payload\" is required\n```\n\n#### Validate URL-Encoded Form Body\n\nCreate a Route with `request-validation` Plugin as follows:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"Content-Type\"],\n          \"properties\": {\n            \"Content-Type\": {\n              \"type\": \"string\",\n              \"pattern\": \"^application\\/x-www-form-urlencoded$\"\n            }\n          }\n        },\n        \"body_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"required_payload\",\"enum_payload\"],\n          \"properties\": {\n            \"required_payload\": {\"type\": \"string\"},\n            \"enum_payload\": {\n              \"type\": \"string\",\n              \"enum\": [\"enum_string_1\", \"enum_string_2\"],\n              \"default\": \"enum_string_1\"\n            }\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request with URL-encoded form data to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"required_payload=hello&enum_payload=enum_string_1\"\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"enum_payload\": \"enum_string_1\",\n    \"required_payload\": \"hello\"\n  },\n  \"headers\": {\n    ...\n  },\n  \"json\": null,\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\nSend a request without the URL-encoded field `enum_payload`:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"required_payload=hello\"\n```\n\nYou should receive an `HTTP/1.1 400 Bad Request` of the following:\n\n```text\nproperty \"enum_payload\" is required\n```\n\n## Appendix: JSON Schema\n\nThe following section provides boilerplate JSON schema for you to adjust, combine, and use with this Plugin. For a complete reference, see [JSON schema specification](https://json-schema.org/specification).\n\n### Enumerated Values\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"enum_payload\"],\n    \"properties\": {\n      \"enum_payload\": {\n        \"type\": \"string\",\n        \"enum\": [\"enum_string_1\", \"enum_string_2\"],\n        \"default\": \"enum_string_1\"\n      }\n    }\n  }\n}\n```\n\n### Boolean Values\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"bool_payload\"],\n    \"properties\": {\n      \"bool_payload\": {\n        \"type\": \"boolean\",\n        \"default\": true\n      }\n    }\n  }\n}\n```\n\n### Numeric Values\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"integer_payload\"],\n    \"properties\": {\n      \"integer_payload\": {\n        \"type\": \"integer\",\n        \"minimum\": 1,\n        \"maximum\": 65535\n      }\n    }\n  }\n}\n```\n\n### Strings\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"string_payload\"],\n    \"properties\": {\n      \"string_payload\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 32\n      }\n    }\n  }\n}\n```\n\n### RegEx for Strings\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"regex_payload\"],\n    \"properties\": {\n      \"regex_payload\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 32,\n        \"pattern\": \"[[^[a-zA-Z0-9_]+$]]\"\n      }\n    }\n  }\n}\n```\n\n### Arrays\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"array_payload\"],\n    \"properties\": {\n      \"array_payload\": {\n        \"type\": \"array\",\n        \"minItems\": 1,\n        \"items\": {\n          \"type\": \"integer\",\n          \"minimum\": 200,\n          \"maximum\": 599\n        },\n        \"uniqueItems\": true,\n        \"default\": [200, 302]\n      }\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/response-rewrite.md",
    "content": "---\ntitle: response-rewrite\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Response Rewrite\n  - response-rewrite\ndescription: The response-rewrite Plugin offers options to rewrite responses that APISIX and its Upstream services return to clients. With the Plugin, you can modify HTTP status codes, request headers, response body, and more.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/response-rewrite\" />\n</head>\n\n## Description\n\nThe `response-rewrite` Plugin offers options to rewrite responses that APISIX and its Upstream services return to clients. With the Plugin, you can modify HTTP status codes, request headers, response body, and more.\n\nFor instance, you can use this Plugin to:\n\n- Support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) by setting `Access-Control-Allow-*` headers.\n- Indicate redirection by setting HTTP status codes and `Location` header.\n\n:::tip\n\nYou can also use the [redirect](./redirect.md) Plugin to set up redirects.\n\n:::\n\n## Attributes\n\n| Name            | Type    | Required | Default | Valid values                                                                                                  | Description                                                                                                                                                                                                                                                                         |\n|-----------------|---------|----------|---------|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| status_code     | integer | False    |         | [200, 598]                                                                                                    | New HTTP status code in the response. If unset, falls back to the original status code.                                                                                                                                                                                             |\n| body            | string  | False    |         |                                                                                                               | New response body. The `Content-Length` header would also be reset. Should not be configured with `filters`.                                                                                                                                                                                                                   |\n| body_base64     | boolean | False    | false   |                                                                                                               | If true, decode the response body configured in `body` before sending to client, which is useful for image and protobuf decoding. Note that this configuration cannot be used to decode Upstream response.                                                                                                   |\n| headers         | object  | False    |         |                                                                                                               |  Actions to be executed in the order of `add`, `remove`, and `set`.                                                                           |\n| headers.add     | array[string]   | False    |         |                                                                                                               | Headers to append to requests. If a header already present in the request, the header value will be appended. Header value could be set to a constant, or one or more [Nginx variables](https://nginx.org/en/docs/http/ngx_http_core_module.html).                                                                                                          |\n| headers.set     | object  | False    |         |                                                                                                               |Headers to set to requests. If a header already present in the request, the header value will be overwritten. Header value could be set to a constant, or one or more[Nginx variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). |\n| headers.remove  | array[string]   | False    |         |                                                                                                               | Headers to remove from requests.      |\n| vars            | array[array] | False    |         |  | An array of one or more matching conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list).                                           |\n| filters         | array[object] | False    |         |                                                                                                               | List of filters that modify the response body by replacing one specified string with another. Should not be configured with `body`.                                                         |\n| filters.regex   | string  | True     |         |                                                                                                               | RegEx pattern to match on the response body.    |\n| filters.scope   | string  | False    | \"once\"  | [\"once\",\"global\"]                                                                                               | Scope of substitution. `once` substitutes the first matched instance and `global` substitutes globally.                                                                                                                                                                   |\n| filters.replace | string  | True     |         |                                                                                                               |   Content to substitute with.             |\n| filters.options | string  | False    | \"jo\"    |                                                                                                               | RegEx options to control how the match operation should be performed. See [Lua NGINX module](https://github.com/openresty/lua-nginx-module#ngxrematch) for the available options.                                                                                                                                                                                     |\n\n## Examples\n\nThe examples below demonstrate how you can configure `response-rewrite` on a Route in different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Rewrite Header and Body\n\nThe following example demonstrates how to add response body and headers, only to responses with `200` HTTP status codes.\n\nCreate a Route with the `response-rewrite` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"response-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"response-rewrite\": {\n        \"body\": \"{\\\"code\\\":\\\"ok\\\",\\\"message\\\":\\\"new json body\\\"}\",\n        \"headers\": {\n          \"set\": {\n            \"X-Server-id\": 3,\n            \"X-Server-status\": \"on\",\n            \"X-Server-balancer-addr\": \"$balancer_ip:$balancer_port\"\n          }\n        },\n        \"vars\": [\n          [ \"status\",\"==\",200 ]\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\"\n```\n\nYou should receive a `HTTP/1.1 200 OK` response similar to the following:\n\n```text\n...\nX-Server-id: 3\nX-Server-status: on\nX-Server-balancer-addr: 50.237.103.220:80\n\n{\"code\":\"ok\",\"message\":\"new json body\"}\n```\n\n### Rewrite Header With RegEx Filter\n\nThe following example demonstrates how to use RegEx filter matching to replace `X-Amzn-Trace-Id` for responses.\n\nCreate a Route with the `response-rewrite` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"response-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\":{\n      \"response-rewrite\":{\n        \"filters\":[\n          {\n            \"regex\":\"X-Amzn-Trace-Id\",\n            \"scope\":\"global\",\n            \"replace\":\"X-Amzn-Trace-Id-Replace\"\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\"\n```\n\nYou should see a response similar to the following:\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id-Replace\": \"Root=1-6500095d-1041b05e2ba9c6b37232dbc7\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### Decode Body from Base64\n\nThe following example demonstrates how to Decode Body from Base64 format.\n\nCreate a Route with the `response-rewrite` Plugin:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"response-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\":{\n      \"response-rewrite\": {\n        \"body\": \"SGVsbG8gV29ybGQ=\",\n        \"body_base64\": true\n        }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to verify:\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\n```\n\nYou should see a response of the following:\n\n```text\nHello World\n```\n\n### Rewrite Response and Its Connection with Execution Phases\n\nThe following example demonstrates the connection between the `response-rewrite` Plugin and [execution phases](/apisix/key-concepts/plugins#plugins-execution-lifecycle) by configuring the Plugin with the `key-auth` Plugin, and see how the response is still rewritten to `200 OK` in the case of an unauthenticated request.\n\nCreate a Consumer `jack`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\nCreate `key-auth` credential for the Consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth` and configure `response-rewrite` to rewrite the response status code and body:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n   -H \"X-API-KEY: ${admin_key}\" \\\n   -d '{\n    \"id\": \"response-rewrite-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"response-rewrite\": {\n        \"status_code\": 200,\n        \"body\": \"{\\\"code\\\": 200, \\\"msg\\\": \\\"success\\\"}\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: jack-key'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response of the following:\n\n```text\n{\"code\": 200, \"msg\": \"success\"}\n```\n\nSend a request to the Route without any key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\nYou should still receive an `HTTP/1.1 200 OK` response of the same, instead of `HTTP/1.1 401 Unauthorized` from the `key-auth` Plugin. This shows that the `response-rewrite` Plugin still rewrites the response.\n\nThis is because **header_filter** and **body_filter** phase logics of the `response-rewrite` Plugin will continue to run after [`ngx.exit`](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/#ngxexit) in the **access** or **rewrite** phases from other plugins.\n\nThe following table summarizes the impact of `ngx.exit` on execution phases.\n\n| Phase         | rewrite  | access   | header_filter | body_filter |\n|---------------|----------|----------|---------------|-------------|\n| **rewrite**       | ngx.exit |          |               |           |\n| **access**        | ×        | ngx.exit |               |           |\n| **header_filter** | ✓        | ✓        | ngx.exit      |           |\n| **body_filter**   | ✓        | ✓        | ×             | ngx.exit  |\n\nFor example, if `ngx.exit` takes places in the **rewrite** phase, it will interrupt the execution of **access** phase but not interfere with **header_filter** and **body_filter** phases.\n"
  },
  {
    "path": "docs/en/latest/plugins/rocketmq-logger.md",
    "content": "---\ntitle: rocketmq-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - RocketMQ Logger\ndescription: This document contains information about the Apache APISIX rocketmq-logger Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `rocketmq-logger` Plugin provides the ability to push logs as JSON objects to your RocketMQ clusters.\n\nIt might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name                   | Type    | Required | Default                                                                       | Valid values         | Description                                                                                                                                                                                                                    |\n|------------------------|---------|----------|-------------------------------------------------------------------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| nameserver_list        | object  | True     |                                                                               |                      | List of RocketMQ nameservers.                                                                                                                                                                                                  |\n| topic                  | string  | True     |                                                                               |                      | Target topic to push the data to.                                                                                                                                                                                              |\n| key                    | string  | False    |                                                                               |                      | Key of the messages.                                                                                                                                                                                                           |\n| tag                    | string  | False    |                                                                               |                      | Tag of the messages.                                                                                                                                                                                                           |\n| log_format             | object  | False    |  |                      | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| timeout                | integer | False    | 3                                                                             | [1,...]              | Timeout for the upstream to send data.                                                                                                                                                                                         |\n| use_tls                | boolean | False    | false                                                                         |                      | When set to `true`, uses TLS.                                                                                                                                                                                                  |\n| access_key             | string  | False    | \"\"                                                                            |                      | Access key for ACL. Setting to an empty string will disable the ACL.                                                                                                                                                           |\n| secret_key             | string  | False    | \"\"                                                                            |                      | secret key for ACL.                                                                                                                                                                                                            |\n| name                   | string  | False    | \"rocketmq logger\"                                                             |                      | Unique identifier for the batch processor. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`. processor.                                                                                                                                                                                     |\n| meta_format            | enum    | False    | \"default\"                                                                     | [\"default\"，\"origin\"] | Format to collect the request information. Setting to `default` collects the information in JSON format and `origin` collects the information with the original HTTP request. See [examples](#meta_format-example) below.      |\n| include_req_body       | boolean | False    | false                                                                         | [false, true]        | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitations.                                                               |\n| include_req_body_expr  | array   | False    |                                                                               |                      | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.        |\n| max_req_body_bytes     | integer | False    | 524288                                                                        |   >=1                  | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean | False    | false                                                                         | [false, true]        | When set to `true` includes the response body in the log.                                                                                                                                                                      |\n| include_resp_body_expr | array   | False    |                                                                               |                      | Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.      |\n| max_resp_body_bytes    | integer | False | 524288 | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nNOTE: `encrypt_fields = {\"secret_key\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n:::info IMPORTANT\n\nThe data is first written to a buffer. When the buffer exceeds the `batch_max_size` or `buffer_duration` attribute, the data is sent to the RocketMQ server and the buffer is flushed.\n\nIf the process is successful, it will return `true` and if it fails, returns `nil` with a string with the \"buffer overflow\" error.\n\n:::\n\n### meta_format example\n\n- default:\n\n```json\n    {\n     \"upstream\": \"127.0.0.1:1980\",\n     \"start_time\": 1619414294760,\n     \"client_ip\": \"127.0.0.1\",\n     \"service_id\": \"\",\n     \"route_id\": \"1\",\n     \"request\": {\n       \"querystring\": {\n         \"ab\": \"cd\"\n       },\n       \"size\": 90,\n       \"uri\": \"/hello?ab=cd\",\n       \"url\": \"http://localhost:1984/hello?ab=cd\",\n       \"headers\": {\n         \"host\": \"localhost\",\n         \"content-length\": \"6\",\n         \"connection\": \"close\"\n       },\n       \"method\": \"GET\"\n     },\n     \"response\": {\n       \"headers\": {\n         \"connection\": \"close\",\n         \"content-type\": \"text/plain; charset=utf-8\",\n         \"date\": \"Mon, 26 Apr 2021 05:18:14 GMT\",\n         \"server\": \"APISIX/2.5\",\n         \"transfer-encoding\": \"chunked\"\n       },\n       \"size\": 190,\n       \"status\": 200\n     },\n     \"server\": {\n       \"hostname\": \"localhost\",\n       \"version\": \"2.5\"\n     },\n     \"latency\": 0\n    }\n```\n\n- origin:\n\n```http\n    GET /hello?ab=cd HTTP/1.1\n    host: localhost\n    content-length: 6\n    connection: close\n\n    abcdef\n```\n\n### meta_format example\n\n- `default`:\n\n  ```json\n      {\n       \"upstream\": \"127.0.0.1:1980\",\n       \"start_time\": 1619414294760,\n       \"client_ip\": \"127.0.0.1\",\n       \"service_id\": \"\",\n       \"route_id\": \"1\",\n       \"request\": {\n         \"querystring\": {\n           \"ab\": \"cd\"\n         },\n         \"size\": 90,\n         \"uri\": \"/hello?ab=cd\",\n         \"url\": \"http://localhost:1984/hello?ab=cd\",\n         \"headers\": {\n           \"host\": \"localhost\",\n           \"content-length\": \"6\",\n           \"connection\": \"close\"\n         },\n         \"body\": \"abcdef\",\n         \"method\": \"GET\"\n       },\n       \"response\": {\n         \"headers\": {\n           \"connection\": \"close\",\n           \"content-type\": \"text/plain; charset=utf-8\",\n           \"date\": \"Mon, 26 Apr 2021 05:18:14 GMT\",\n           \"server\": \"APISIX/2.5\",\n           \"transfer-encoding\": \"chunked\"\n         },\n         \"size\": 190,\n         \"status\": 200\n       },\n       \"server\": {\n         \"hostname\": \"localhost\",\n         \"version\": \"2.5\"\n       },\n       \"latency\": 0\n      }\n  ```\n\n- `origin`:\n\n  ```http\n      GET /hello?ab=cd HTTP/1.1\n      host: localhost\n      content-length: 6\n      connection: close\n\n      abcdef\n  ```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                    |\n|------------|--------|----------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `rocketmq-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/rocketmq-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the `rocketmq-logger` Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"rocketmq-logger\": {\n           \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n           \"topic\" : \"test2\",\n           \"batch_max_size\": 1,\n           \"name\": \"rocketmq logger\"\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\nThis Plugin also supports pushing to more than one nameserver at a time. You can specify multiple nameserver in the Plugin configuration as shown below:\n\n```json\n\"nameserver_list\" : [\n    \"127.0.0.1:9876\",\n    \"127.0.0.2:9876\"\n]\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your RocketMQ server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo remove the `rocketmq-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload, and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/server-info.md",
    "content": "---\ntitle: server-info\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Server info\n  - server-info\ndescription: This document contains information about the Apache APISIX server-info Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `server-info` Plugin periodically reports basic server information to etcd.\n\n:::warning\n\nThe `server-info` Plugin is deprecated and will be removed in a future release. For more details about the deprecation and removal plan, please refer to [this discussion](https://github.com/apache/apisix/discussions/12298).\n\n:::\n\nThe information reported by the Plugin is explained below:\n\n| Name         | Type    | Description                                                                                                            |\n|--------------|---------|------------------------------------------------------------------------------------------------------------------------|\n| boot_time    | integer | Bootstrap time (UNIX timestamp) of the APISIX instance. Resets when hot updating but not when APISIX is just reloaded. |\n| id           | string  | APISIX instance ID.                                                                                                    |\n| etcd_version | string  | Version of the etcd cluster used by APISIX. Will be `unknown` if the network to etcd is partitioned.                   |\n| version      | string  | Version of APISIX instance.                                                                                            |\n| hostname     | string  | Hostname of the machine/pod APISIX is deployed to.                                                                     |\n\n## Attributes\n\nNone.\n\n## API\n\nThis Plugin exposes the endpoint `/v1/server_info` to the [Control API](../control-api.md)\n\n## Enable Plugin\n\nAdd `server-info` to the Plugin list in your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - ...\n  - server-info\n```\n\n## Customizing server info report configuration\n\nWe can change the report configurations in the `plugin_attr` section of `conf/config.yaml`.\n\nThe following configurations of the server info report can be customized:\n\n| Name         | Type   | Default  | Description                                                          |\n| ------------ | ------ | -------- | -------------------------------------------------------------------- |\n| report_ttl | integer | 36 | Time in seconds after which the report is deleted from etcd (maximum: 86400, minimum: 3). |\n\nTo customize, you can modify the `plugin_attr` attribute in your configuration file (`conf/config.yaml`):\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  server-info:\n    report_ttl: 60\n```\n\n## Example usage\n\nAfter you enable the Plugin as mentioned above, you can access the server info report through the Control API:\n\n```shell\ncurl http://127.0.0.1:9090/v1/server_info -s | jq .\n```\n\n```json\n{\n  \"etcd_version\": \"3.5.0\",\n  \"id\": \"b7ce1c5c-b1aa-4df7-888a-cbe403f3e948\",\n  \"hostname\": \"fedora32\",\n  \"version\": \"2.1\",\n  \"boot_time\": 1608522102\n}\n```\n\n:::tip\n\nYou can also view the server info report through the [APISIX Dashboard](/docs/dashboard/USER_GUIDE).\n\n:::\n\n## Delete Plugin\n\nTo remove the Plugin, you can remove `server-info` from the list of Plugins in your configuration file:\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - ...\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/serverless.md",
    "content": "---\ntitle: serverless\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Serverless\ndescription: This document contains information about the Apache APISIX serverless Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThere are two `serverless` Plugins in APISIX: `serverless-pre-function` and `serverless-post-function`. The former runs at the beginning of the specified phase, while the latter runs at the end of the specified phase.\n\nBoth Plugins have the same attributes.\n\n## Attributes\n\n| Name      | Type          | Required | Default    | Valid values                                                                 | Description                                                      |\n|-----------|---------------|----------|------------|------------------------------------------------------------------------------|------------------------------------------------------------------|\n| phase     | string        | False    | [\"access\"] | [\"rewrite\", \"access\", \"header_filter\", \"body_filter\", \"log\", \"before_proxy\"] | Phase before or after which the serverless function is executed. |\n| functions | array[string] | True     |            |                                                                              | List of functions that are executed sequentially.                |\n\n:::info IMPORTANT\n\nOnly Lua functions are allowed here and not other Lua code.\n\nFor example, anonymous functions are legal:\n\n```lua\nreturn function()\n    ngx.log(ngx.ERR, 'one')\nend\n```\n\nClosures are also legal:\n\n```lua\nlocal count = 1\nreturn function()\n    count = count + 1\n    ngx.say(count)\nend\n```\n\nBut code other than functions are illegal:\n\n```lua\nlocal count = 1\nngx.say(count)\n```\n\n:::\n\n:::note\n\nFrom v2.6, `conf` and `ctx` are passed as the first two arguments to a serverless function like regular Plugins.\n\nPrior to v2.12.0, the phase `before_proxy` was called `balancer`. This was updated considering that this method would run after `access` and before the request goes Upstream and is unrelated to `balancer`.\n\n:::\n\n## Enable Plugin\n\nThe example below enables the Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\" : [\"return function() ngx.log(ngx.ERR, \\\"serverless pre function\\\"); end\"]\n        },\n        \"serverless-post-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.ERR, \\\"match uri \\\", ctx.curr_req_matched and ctx.curr_req_matched._path); end\"]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can make a request as shown below:\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\nYou will find a message \"serverless pre-function\" and \"match uri /index.html\" in the error.log.\n\n## Delete Plugin\n\nTo remove the `serverless` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/skywalking-logger.md",
    "content": "---\ntitle: skywalking-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - SkyWalking Logger\n  - skywalking-logger\ndescription: The skywalking-logger pushes request and response logs as JSON objects to SkyWalking OAP server in batches and supports the customization of log formats.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/skywalking-logger\" />\n</head>\n\n## Description\n\nThe `skywalking-logger` Plugin pushes request and response logs as JSON objects to SkyWalking OAP server in batches and supports the customization of log formats.\n\nIf there is an existing tracing context, it sets up the trace-log correlation automatically and relies on [SkyWalking Cross Process Propagation Headers Protocol](https://skywalking.apache.org/docs/main/next/en/api/x-process-propagation-headers-v3/).\n\n## Attributes\n\n| Name                  | Type    | Required | Default                | Valid values  | Description                                                                                                  |\n|-----------------------|---------|----------|------------------------|---------------|--------------------------------------------------------------------------------------------------------------|\n| endpoint_addr         | string  | True     |                        |               | URI of the SkyWalking OAP server.                                                                            |\n| service_name          | string  | False    | \"APISIX\"               |               | Service name for the SkyWalking reporter.                                                                    |\n| service_instance_name | string  | False    | \"APISIX Instance Name\" |               | Service instance name for the SkyWalking reporter. Set it to `$hostname` to directly get the local hostname. |\n| log_format | object | False    |                             | Custom log format as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX variables](http://nginx.org/en/docs/varindex.html) can be referenced by prefixing with `$`. |\n| timeout               | integer | False    | 3                      | [1,...]       | Time to keep the connection alive for after sending a request.                                               |\n| name                  | string  | False    | \"skywalking logger\"    |               | Unique identifier to identify the logger. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`.                                                                    |\n| include_req_body       | boolean       | False    | false   |  If true, include the request body in the log. Note that if the request body is too big to be kept in the memory, it can not be logged due to NGINX's limitations.       |\n| include_req_body_expr  | array[array]  | False    |         | An array of one or more conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the `include_req_body` is true. Request body would only be logged when the expressions configured here evaluate to true.      |\n| max_req_body_bytes     | integer        | False   | 524288  | >=1 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean       | False    | false   | If true, include the response body in the log.       |\n| include_resp_body_expr | array[array]  | False    |         | An array of one or more conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the `include_resp_body` is true. Response body would only be logged when the expressions configured here evaluate to true.     |\n| max_resp_body_bytes    | integer       | False    | 524288  | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Custom log format as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX variables](http://nginx.org/en/docs/varindex.html) can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n## Examples\n\nThe examples below demonstrate how you can configure `skywalking-logger` Plugin for different scenarios.\n\nTo follow along the example, start a storage, OAP and Booster UI with Docker Compose, following [Skywalking's documentation](https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-docker/). Once set up, the OAP server should be listening on `12800` and you should be able to access the UI at [http://localhost:8080](http://localhost:8080).\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Log Requests in Default Log Format\n\nThe following example demonstrates how you can configure the `skywalking-logger` Plugin on a Route to log information of requests hitting the Route.\n\nCreate a Route with the `skywalking-logger` Plugin and configure the Plugin with your OAP server URI:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nIn [Skywalking UI](http://localhost:8080), navigate to __General Service__ > __Services__. You should see a service called `APISIX` with a log entry corresponding to your request:\n\n```json\n{\n  \"upstream_latency\": 674,\n  \"request\": {\n    \"method\": \"GET\",\n    \"headers\": {\n      \"user-agent\": \"curl/8.6.0\",\n      \"host\": \"127.0.0.1:9080\",\n      \"accept\": \"*/*\"\n    },\n    \"url\": \"http://127.0.0.1:9080/anything\",\n    \"size\": 85,\n    \"querystring\": {},\n    \"uri\": \"/anything\"\n  },\n  \"client_ip\": \"192.168.65.1\",\n  \"route_id\": \"skywalking-logger-route\",\n  \"start_time\": 1736945107345,\n  \"upstream\": \"3.210.94.60:80\",\n  \"server\": {\n    \"version\": \"3.11.0\",\n    \"hostname\": \"7edbcebe8eb3\"\n  },\n  \"service_id\": \"\",\n  \"response\": {\n    \"size\": 619,\n    \"status\": 200,\n    \"headers\": {\n      \"content-type\": \"application/json\",\n      \"date\": \"Thu, 16 Jan 2025 12:45:08 GMT\",\n      \"server\": \"APISIX/3.11.0\",\n      \"access-control-allow-origin\": \"*\",\n      \"connection\": \"close\",\n      \"access-control-allow-credentials\": \"true\",\n      \"content-length\": \"391\"\n    }\n  },\n  \"latency\": 764.9998664856,\n  \"apisix_latency\": 90.999866485596\n}\n```\n\n### Log Request and Response Headers With Plugin Metadata\n\nThe following example demonstrates how you can customize log format using Plugin metadata and built-in variables to log specific headers from request and response.\n\nIn APISIX, Plugin metadata is used to configure the common metadata fields of all Plugin instances of the same Plugin. It is useful when a Plugin is enabled across multiple resources and requires a universal update to their metadata fields.\n\nFirst, create a Route with the `skywalking-logger` Plugin and configure the Plugin with your OAP server URI:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nNext, configure the Plugin metadata for `skywalking-logger` to log the custom request header `env` and the response header `Content-Type`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/skywalking-logger\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"log_format\": {\n      \"host\": \"$host\",\n      \"@timestamp\": \"$time_iso8601\",\n      \"client_ip\": \"$remote_addr\",\n      \"env\": \"$http_env\",\n      \"resp_content_type\": \"$sent_http_Content_Type\"\n    }\n  }'\n```\n\nSend a request to the Route with the `env` header:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"env: dev\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response. In [Skywalking UI](http://localhost:8080), navigate to __General Service__ > __Services__. You should see a service called `APISIX` with a log entry corresponding to your request:\n\n```json\n[\n  {\n    \"route_id\": \"skywalking-logger-route\",\n    \"client_ip\": \"192.168.65.1\",\n    \"@timestamp\": \"2025-01-16T12:51:53+00:00\",\n    \"host\": \"127.0.0.1\",\n    \"env\": \"dev\",\n    \"resp_content_type\": \"application/json\"\n  }\n]\n```\n\n### Log Request Bodies Conditionally\n\nThe following example demonstrates how you can conditionally log request body.\n\nCreate a Route with the `skywalking-logger` Plugin as such, to only include request body if the URL query string `log_body` is `yes`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\",\n        \"include_req_body\": true,\n        \"include_req_body_expr\": [[\"arg_log_body\", \"==\", \"yes\"]]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend a request to the Route with a URL query string satisfying the condition:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?log_body=yes\" -X POST -d '{\"env\": \"dev\"}'\n```\n\nYou should receive an `HTTP/1.1 200 OK` response. In [Skywalking UI](http://localhost:8080), navigate to __General Service__ > __Services__. You should see a service called `APISIX` with a log entry corresponding to your request, with the request body logged:\n\n```json\n[\n  {\n    \"request\": {\n      \"url\": \"http://127.0.0.1:9080/anything?log_body=yes\",\n      \"querystring\": {\n        \"log_body\": \"yes\"\n      },\n      \"uri\": \"/anything?log_body=yes\",\n      ...,\n      \"body\": \"{\\\"env\\\": \\\"dev\\\"}\",\n    },\n    ...\n  }\n]\n```\n\nSend a request to the Route without any URL query string:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST -d '{\"env\": \"dev\"}'\n```\n\nYou should not observe a log entry without the request body.\n\n:::info\n\nIf you have customized the `log_format` in addition to setting `include_req_body` or `include_resp_body` to `true`, the Plugin would not include the bodies in the logs.\n\nAs a workaround, you may be able to use the NGINX variable `$request_body` in the log format, such as:\n\n```json\n{\n  \"skywalking-logger\": {\n    ...,\n    \"log_format\": {\"body\": \"$request_body\"}\n  }\n}\n```\n\n:::\n\n### Associate Traces with Logs\n\nThe following example demonstrates how you can configure the `skywalking-logger` Plugin on a Route to log information of requests hitting the route.\n\nCreate a Route with the `skywalking-logger` Plugin and configure the Plugin with your OAP server URI:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking\": {\n        \"sample_ratio\": 1\n      },\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nGenerate a few requests to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses.\n\nIn [Skywalking UI](http://localhost:8080), navigate to __General Service__ > __Services__. You should see a service called `APISIX` with a trace corresponding to your request, where you can view the associated logs:\n\n![trace context](https://static.apiseven.com/uploads/2025/01/16/soUpXm6b_trace-view-logs.png)\n\n![associated log](https://static.apiseven.com/uploads/2025/01/16/XD934LvU_associated-logs.png)\n"
  },
  {
    "path": "docs/en/latest/plugins/skywalking.md",
    "content": "---\ntitle: skywalking\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - SkyWalking\ndescription: The skywalking Plugin supports the integrating with Apache SkyWalking for request tracing.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/skywalking\" />\n</head>\n\n## Description\n\nThe `skywalking` Plugin supports the integrating with [Apache SkyWalking](https://skywalking.apache.org) for request tracing.\n\nSkyWalking uses its native Nginx Lua tracer to provide tracing, topology analysis, and metrics from both service and URI perspectives. APISIX supports HTTP protocol to interact with the SkyWalking server.\n\nThe server currently supports two protocols: HTTP and gRPC. In APISIX, only HTTP is currently supported.\n\n## Static Configurations\n\nBy default, service names and endpoint address for the Plugin are pre-configured in the [default configuration](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua).\n\nTo customize these values, add the corresponding configurations to `config.yaml`. For example:\n\n```yaml\nplugin_attr:\n  skywalking:\n    report_interval: 3      # Reporting interval time in seconds.\n    service_name: APISIX    # Service name for SkyWalking reporter.\n    service_instance_name: \"APISIX Instance Name\"   # Service instance name for SkyWalking reporter.\n                                                    # Set to $hostname to get the local hostname.\n    endpoint_addr: http://127.0.0.1:12800           # SkyWalking HTTP endpoint.\n```\n\nReload APISIX for changes to take effect.\n\n## Attributes\n\n| Name         | Type   | Required | Default | Valid values | Description                                                                |\n|--------------|--------|----------|---------|--------------|----------------------------------------------------------------------------|\n| sample_ratio | number | True     | 1       | [0.00001, 1] | Frequency of request sampling. Setting the sample ratio to `1` means to sample all requests. |\n\n## Example\n\nTo follow along the example, start a storage, OAP and Booster UI with Docker Compose, following [Skywalking's documentation](https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-docker/). Once set up, the OAP server should be listening on `12800` and you should be able to access the UI at [http://localhost:8080](http://localhost:8080).\n\nUpdate APISIX configuration file to enable the `skywalking` plugin, which is disabled by default, and update the endpoint address:\n\n```yaml title=\"config.yaml\"\nplugins:\n  - skywalking\n  - ...\n\nplugin_attr:\n  skywalking:\n    report_interval: 3\n    service_name: APISIX\n    service_instance_name: APISIX Instance\n    endpoint_addr: http://192.168.2.103:12800\n```\n\nReload APISIX for configuration changes to take effect.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Trace All Requests\n\nThe following example demonstrates how you can trace all requests passing through a Route.\n\nCreate a Route with `skywalking` and configure the sampling ratio to be 1 to trace all requests:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking\": {\n        \"sample_ratio\": 1\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nSend a few requests to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses.\n\nIn [Skywalking UI](http://localhost:8080), navigate to __General Service__ > __Services__. You should see a service called `APISIX` with traces corresponding to your requests:\n\n![SkyWalking APISIX traces](https://static.apiseven.com/uploads/2025/01/15/UdwiO8NJ_skywalking-traces.png)\n\n### Associate Traces with Logs\n\nThe following example demonstrates how you can configure the `skywalking-logger` Plugin on a Route to log information of requests hitting the Route.\n\nCreate a Route with the `skywalking-logger` Plugin and configure the Plugin with your OAP server URI:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking\": {\n        \"sample_ratio\": 1\n      },\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\nGenerate a few requests to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses.\n\nIn [Skywalking UI](http://localhost:8080), navigate to __General Service__ > __Services__. You should see a service called `APISIX` with a trace corresponding to your request, where you can view the associated logs:\n\n![trace context](https://static.apiseven.com/uploads/2025/01/16/soUpXm6b_trace-view-logs.png)\n\n![associated log](https://static.apiseven.com/uploads/2025/01/16/XD934LvU_associated-logs.png)\n"
  },
  {
    "path": "docs/en/latest/plugins/sls-logger.md",
    "content": "---\ntitle: sls-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - SLS Logger\n  - Alibaba Cloud Log Service\ndescription: This document contains information about the Apache APISIX sls-logger Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `sls-logger` Plugin is used to push logs to [Alibaba Cloud log Service](https://www.alibabacloud.com/help/en/log-service/latest/use-the-syslog-protocol-to-upload-logs) using [RF5424](https://tools.ietf.org/html/rfc5424).\n\nIt might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name              | Required | Description                                                                                                                                                                                                                                     |\n|-------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| host              | True     | IP address or the hostname of the TCP server. See [Alibaba Cloud log service documentation](https://www.alibabacloud.com/help/en/log-service/latest/endpoints) for details. Use IP address instead of domain. |\n| port              | True     | Target upstream port. Defaults to `10009`.                                                                                                                                                                                                      |\n| timeout           | False    | Timeout for the upstream to send data.                                                                                                                                                                                                          |\n| log_format       | False    | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| project           | True     | Project name in Alibaba Cloud log service. Create SLS before using this Plugin.                                                                                                                                                                     |\n| logstore          | True     | logstore name in Ali Cloud log service. Create SLS before using this Plugin.                                                                                                                                                                    |\n| access_key_id     | True     | AccessKey ID in Alibaba Cloud. See [Authorization](https://www.alibabacloud.com/help/en/log-service/latest/create-a-ram-user-and-authorize-the-ram-user-to-access-log-service) for more details.                                                                     |\n| access_key_secret | True     | AccessKey Secret in Alibaba Cloud. See [Authorization](https://www.alibabacloud.com/help/en/log-service/latest/create-a-ram-user-and-authorize-the-ram-user-to-access-log-service) for more details.                                                                 |\n| include_req_body  | True     | When set to `true`, includes the request body in the log.                                                                                                                                                                                       |\n| include_req_body_expr | No      | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                          |\n| max_req_body_bytes | False | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body  | No  | When set to `true` includes the response body in the log.                                                                                                        |\n| include_resp_body_expr | No  |  Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                        |\n| max_resp_body_bytes | False | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| name              | False    | Unique identifier for the batch processor. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`.                                                                                                                                                                                                      |\n\nNOTE: `encrypt_fields = {\"access_key_secret\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n    \"route_conf\": {\n        \"host\": \"100.100.99.135\",\n        \"buffer_duration\": 60,\n        \"timeout\": 30000,\n        \"include_req_body\": false,\n        \"logstore\": \"your_logstore\",\n        \"log_format\": {\n            \"vip\": \"$remote_addr\"\n        },\n        \"project\": \"your_project\",\n        \"inactive_timeout\": 5,\n        \"access_key_id\": \"your_access_key_id\",\n        \"access_key_secret\": \"your_access_key_secret\",\n        \"batch_max_size\": 1000,\n        \"max_retry_count\": 0,\n        \"retry_delay\": 1,\n        \"port\": 10009,\n        \"name\": \"sls-logger\"\n    },\n    \"data\": \"<46>1 2024-01-06T03:29:56.457Z localhost apisix 28063 - [logservice project=\\\"your_project\\\" logstore=\\\"your_logstore\\\" access-key-id=\\\"your_access_key_id\\\" access-key-secret=\\\"your_access_key_secret\\\"] {\\\"vip\\\":\\\"127.0.0.1\\\",\\\"route_id\\\":\\\"1\\\"}\\n\"\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `sls-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/sls-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can configure the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"sls-logger\": {\n            \"host\": \"100.100.99.135\",\n            \"port\": 10009,\n            \"project\": \"your_project\",\n            \"logstore\": \"your_logstore\",\n            \"access_key_id\": \"your_access_key_id\",\n            \"access_key_secret\": \"your_access_key_secret\",\n            \"timeout\": 30000\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your Ali Cloud log server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\nNow if you check your Ali Cloud log server, you will be able to see the logs:\n\n![sls logger view](../../../assets/images/plugin/sls-logger-1.png \"sls logger view\")\n\n## Delete Plugin\n\nTo remove the `sls-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/splunk-hec-logging.md",
    "content": "---\ntitle: splunk-hec-logging\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Splunk HTTP Event Collector\n  - splunk-hec-logging\ndescription: This document contains information about the Apache APISIX splunk-hec-logging Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `splunk-hec-logging` Plugin is used to forward logs to [Splunk HTTP Event Collector (HEC)](https://docs.splunk.com/Documentation/Splunk/8.2.6/Data/UsetheHTTPEventCollector) for analysis and storage.\n\nWhen the Plugin is enabled, APISIX will serialize the request context information to [Splunk Event Data format](https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector#Event_metadata) and submit it to the batch queue. When the maximum batch size is exceeded, the data in the queue is pushed to Splunk HEC. See [batch processor](../batch-processor.md) for more details.\n\n## Attributes\n\n| Name             | Required | Default | Description                                                                                                                                                                      |\n|------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| endpoint         | True     |         | Splunk HEC endpoint configurations.                                                                                                                                              |\n| endpoint.uri     | True     |         | Splunk HEC event collector API endpoint.                                                                                                                                         |\n| endpoint.token   | True     |         | Splunk HEC authentication token.                                                                                                                                                 |\n| endpoint.channel | False    |         | Splunk HEC send data channel identifier. Read more: [About HTTP Event Collector Indexer Acknowledgment](https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/AboutHECIDXAck). |\n| endpoint.timeout | False    | 10      | Splunk HEC send data timeout in seconds.                                                                                                                                         |\n| endpoint.keepalive_timeout | False    | 60000      | Keepalive timeout in milliseconds.                                                                                                                                  |\n| ssl_verify       | False    | true    | When set to `true` enables SSL verification as per [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).                                          |\n| log_format       | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n    \"sourcetype\": \"_json\",\n    \"time\": 1704513555.392,\n    \"event\": {\n        \"upstream\": \"127.0.0.1:1980\",\n        \"request_url\": \"http://localhost:1984/hello\",\n        \"request_query\": {},\n        \"request_size\": 59,\n        \"response_headers\": {\n            \"content-length\": \"12\",\n            \"server\": \"APISIX/3.7.0\",\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\"\n        },\n        \"response_status\": 200,\n        \"response_size\": 118,\n        \"latency\": 108.00004005432,\n        \"request_method\": \"GET\",\n        \"request_headers\": {\n            \"connection\": \"close\",\n            \"host\": \"localhost\"\n        }\n    },\n    \"source\": \"apache-apisix-splunk-hec-logging\",\n    \"host\": \"localhost\"\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `splunk-hec-logging` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/splunk-hec-logging -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```json\n[{\"time\":1673976669.269,\"source\":\"apache-apisix-splunk-hec-logging\",\"event\":{\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"request\":{\"method\":\"GET\",\"uri\":\"/splunk.do\"},\"response\":{\"status\":200},\"route_id\":\"1\"},\"host\":\"DESKTOP-2022Q8F-wsl\",\"sourcetype\":\"_json\"}]\n```\n\n## Enable Plugin\n\n### Full configuration\n\nThe example below shows a complete configuration of the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"splunk-hec-logging\":{\n            \"endpoint\":{\n                \"uri\":\"http://127.0.0.1:8088/services/collector\",\n                \"token\":\"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                \"channel\":\"FE0ECFAD-13D5-401B-847D-77833BD77131\",\n                \"timeout\":60\n            },\n            \"buffer_duration\":60,\n            \"max_retry_count\":0,\n            \"retry_delay\":1,\n            \"inactive_timeout\":2,\n            \"batch_max_size\":10\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    },\n    \"uri\":\"/splunk.do\"\n}'\n```\n\n### Minimal configuration\n\nThe example below shows a bare minimum configuration of the Plugin on a Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"splunk-hec-logging\":{\n            \"endpoint\":{\n                \"uri\":\"http://127.0.0.1:8088/services/collector\",\n                \"token\":\"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n            }\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    },\n    \"uri\":\"/splunk.do\"\n}'\n```\n\n## Example usage\n\nOnce you have configured the Route to use the Plugin, when you make a request to APISIX, it will be logged in your Splunk server:\n\n```shell\ncurl -i http://127.0.0.1:9080/splunk.do?q=hello\n```\n\nYou should be able to login and search these logs from your Splunk dashboard:\n\n![splunk hec search view](../../../assets/images/plugin/splunk-hec-admin-en.png)\n\n## Delete Plugin\n\nTo remove the `splunk-hec-logging` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/syslog.md",
    "content": "---\ntitle: syslog\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Syslog\ndescription: This document contains information about the Apache APISIX syslog Plugin.\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `syslog` Plugin is used to push logs to a Syslog server.\n\nLogs can be set as JSON objects.\n\n## Attributes\n\n| Name             | Type    | Required | Default      | Valid values  | Description                                                                                                              |\n|------------------|---------|----------|--------------|---------------|--------------------------------------------------------------------------------------------------------------------------|\n| host             | string  | True     |              |               | IP address or the hostname of the Syslog server.                                                                         |\n| port             | integer | True     |              |               | Target port of the Syslog server.                                                                                        |\n| name             | string  | False    | \"sys logger\" |               | Identifier for the server. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`.                                                                                               |\n| timeout          | integer | False    | 3000         | [1, ...]      | Timeout in ms for the upstream to send data.                                                                             |\n| tls              | boolean | False    | false        |               | When set to `true` performs TLS verification.                                                                            |\n| flush_limit      | integer | False    | 4096         | [1, ...]      | Maximum size of the buffer (KB) and the current message before it is flushed and written to the server.                  |\n| drop_limit       | integer | False    | 1048576      |               | Maximum size of the buffer (KB) and the current message before the current message is dropped because of the size limit. |\n| sock_type        | string  | False    | \"tcp\"        | [\"tcp\", \"udp] | Transport layer protocol to use.                                                                                         |\n| pool_size        | integer | False    | 5            | [5, ...]      | Keep-alive pool size used by `sock:keepalive`.                                                                           |\n| log_format       | object  | False    |  |              | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| include_req_body | boolean | False    | false        | [false, true] | When set to `true` includes the request body in the log.                                                                 |\n| include_req_body_expr  | array         | False    |         |               | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.        |\n| max_req_body_bytes | integer | False | 524288 | >=1 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body      | boolean       | False    | false   | [false, true] | When set to `true` includes the response body in the log.                                                                                                                                                                      |\n| include_resp_body_expr | array         | False    |         |               | When the `include_resp_body` attribute is set to `true`, use this to filter based on [lua-resty-expr](https://github.com/api7/lua-resty-expr). If present, only logs the response if the expression evaluates to `true`.       |\n| max_resp_body_bytes | integer | False | 524288 | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### meta_format example\n\n```text\n\"<46>1 2024-01-06T02:30:59.145Z 127.0.0.1 apisix 82324 - - {\\\"response\\\":{\\\"status\\\":200,\\\"size\\\":141,\\\"headers\\\":{\\\"content-type\\\":\\\"text/plain\\\",\\\"server\\\":\\\"APISIX/3.7.0\\\",\\\"transfer-encoding\\\":\\\"chunked\\\",\\\"connection\\\":\\\"close\\\"}},\\\"route_id\\\":\\\"1\\\",\\\"server\\\":{\\\"hostname\\\":\\\"baiyundeMacBook-Pro.local\\\",\\\"version\\\":\\\"3.7.0\\\"},\\\"request\\\":{\\\"uri\\\":\\\"/opentracing\\\",\\\"url\\\":\\\"http://127.0.0.1:1984/opentracing\\\",\\\"querystring\\\":{},\\\"method\\\":\\\"GET\\\",\\\"size\\\":155,\\\"headers\\\":{\\\"content-type\\\":\\\"application/x-www-form-urlencoded\\\",\\\"host\\\":\\\"127.0.0.1:1984\\\",\\\"user-agent\\\":\\\"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\\\"}},\\\"upstream\\\":\\\"127.0.0.1:1982\\\",\\\"apisix_latency\\\":100.99999809265,\\\"service_id\\\":\\\"\\\",\\\"upstream_latency\\\":1,\\\"start_time\\\":1704508259044,\\\"client_ip\\\":\\\"127.0.0.1\\\",\\\"latency\\\":101.99999809265}\\n\"\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `syslog` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/syslog -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the Plugin for a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"syslog\": {\n                \"host\" : \"127.0.0.1\",\n                \"port\" : 5044,\n                \"flush_limit\" : 1\n            }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your Syslog server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo remove the `syslog` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/tcp-logger.md",
    "content": "---\ntitle: tcp-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - TCP Logger\n  - tcp-logger\ndescription: This document contains information about the Apache APISIX tcp-logger Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `tcp-logger` Plugin can be used to push log data requests to TCP servers.\n\nThis provides the ability to send log data requests as JSON objects to monitoring tools and other TCP servers.\n\nThis plugin also allows to push logs as a batch to your external TCP server. It might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name             | Type    | Required | Default | Valid values | Description                                              |\n|------------------|---------|----------|---------|--------------|----------------------------------------------------------|\n| host             | string  | True     |         |              | IP address or the hostname of the TCP server.            |\n| port             | integer | True     |         | [0,...]      | Target upstream port.                                    |\n| timeout          | integer | False    | 1000    | [1,...]      | Timeout for the upstream to send data.                   |\n| log_format       | object  | False    |  |              | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| tls              | boolean | False    | false   |              | When set to `true` performs SSL verification.            |\n| tls_options      | string  | False    |         |              | TLS options.                                             |\n| include_req_body | boolean | False    | false   | [false, true] | When set to `true` includes the request body in the log. |\n| include_req_body_expr  | array   | No       |         |               | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                          |\n| max_req_body_bytes | integer | False   | 524288   | >=1            | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body | boolean | No       | false   | [false, true] | When set to `true` includes the response body in the log.                                                                                                        |\n| include_resp_body_expr | array   | No  |         |               | Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                        |\n| max_resp_body_bytes | integer | False  | 524288   | >=1           | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n    \"response\": {\n        \"status\": 200,\n        \"headers\": {\n            \"server\": \"APISIX/3.7.0\",\n            \"content-type\": \"text/plain\",\n            \"content-length\": \"12\",\n            \"connection\": \"close\"\n        },\n        \"size\": 118\n    },\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"start_time\": 1704527628474,\n    \"client_ip\": \"127.0.0.1\",\n    \"service_id\": \"\",\n    \"latency\": 102.9999256134,\n    \"apisix_latency\": 100.9999256134,\n    \"upstream_latency\": 2,\n    \"request\": {\n        \"headers\": {\n            \"connection\": \"close\",\n            \"host\": \"localhost\"\n        },\n        \"size\": 59,\n        \"method\": \"GET\",\n        \"uri\": \"/hello\",\n        \"url\": \"http://localhost:1984/hello\",\n        \"querystring\": {}\n    },\n    \"upstream\": \"127.0.0.1:1980\",\n    \"route_id\": \"1\"\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `tcp-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/tcp-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```json\n{\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"route_id\":\"1\",\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200}}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the `tcp-logger` Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"tcp-logger\": {\n                 \"host\": \"127.0.0.1\",\n                 \"port\": 5044,\n                 \"tls\": false,\n                 \"batch_max_size\": 1,\n                 \"name\": \"tcp logger\"\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your TCP server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo remove the `tcp-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/tencent-cloud-cls.md",
    "content": "---\ntitle: tencent-cloud-cls\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - CLS\n  - Tencent Cloud\ndescription: This document contains information about the Apache APISIX tencent-cloud-cls Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `tencent-cloud-cls` Plugin uses [TencentCloud CLS](https://cloud.tencent.com/document/product/614) API to forward APISIX logs to your topic.\n\n## Attributes\n\n| Name              | Type    | Required | Default | Valid values  | Description                                                                                                                                                      |\n| ----------------- | ------- |----------|---------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| cls_host          | string  | Yes      |         |               | CLS API host，please refer [Uploading Structured Logs](https://www.tencentcloud.com/document/api/614/16873).                                                      |\n| cls_topic         | string  | Yes      |         |               | topic id of CLS.                                                                                                                                                 |\n| scheme            | string  | No       | https   | [\"http\", \"https\"] | The protocol scheme to use when connecting to CLS. Defaults to `https` for secure connections.                                                                  |\n| secret_id         | string  | Yes      |         |               | SecretId of your API key.                                                                                                                                        |\n| secret_key        | string  | Yes      |         |               | SecretKey of your API key.                                                                                                                                       |\n| sample_ratio      | number  | No       | 1       | [0.00001, 1]  | How often to sample the requests. Setting to `1` will sample all requests.                                                                                       |\n| include_req_body  | boolean | No       | false   | [false, true] | When set to `true` includes the request body in the log. If the request body is too big to be kept in the memory, it can't be logged due to NGINX's limitations. |\n| include_req_body_expr  | array   | No       |         |               | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                          |\n| max_req_body_bytes | integer | False | 524288 | >=1 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body | boolean | No       | false   | [false, true] | When set to `true` includes the response body in the log.                                                                                                        |\n| include_resp_body_expr | array   | No  |         |               | Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                        |\n| max_resp_body_bytes | integer | False | 524288 | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| global_tag        | object  | No       |         |               | kv pairs in JSON，send with each log.                                                                                                                             |\n| log_format       | object  | No       |         |               | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n\nNOTE: `encrypt_fields = {\"secret_key\"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields).\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n    \"response\": {\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"server\": \"APISIX/3.7.0\",\n            \"transfer-encoding\": \"chunked\"\n        },\n        \"size\": 136,\n        \"status\": 200\n    },\n    \"route_id\": \"1\",\n    \"upstream\": \"127.0.0.1:1982\",\n    \"client_ip\": \"127.0.0.1\",\n    \"apisix_latency\": 100.99985313416,\n    \"service_id\": \"\",\n    \"latency\": 103.99985313416,\n    \"start_time\": 1704525145772,\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"upstream_latency\": 3,\n    \"request\": {\n        \"headers\": {\n            \"connection\": \"close\",\n            \"host\": \"localhost\"\n        },\n        \"url\": \"http://localhost:1984/opentracing\",\n        \"querystring\": {},\n        \"method\": \"GET\",\n        \"size\": 65,\n        \"uri\": \"/opentracing\"\n    }\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |   | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `tencent-cloud-cls` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/tencent-cloud-cls \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"tencent-cloud-cls\": {\n            \"cls_host\": \"ap-guangzhou.cls.tencentyun.com\",\n            \"cls_topic\": \"${your CLS topic name}\",\n            \"global_tag\": {\n                \"module\": \"cls-logger\",\n                \"server_name\": \"YourApiGateWay\"\n            },\n            \"include_req_body\": true,\n            \"include_resp_body\": true,\n            \"secret_id\": \"${your secret id}\",\n            \"secret_key\": \"${your secret key}\"\n        }\n    },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your cls topic:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo disable this Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/traffic-split.md",
    "content": "---\ntitle: traffic-split\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Traffic Split\n  - Blue-green Deployment\n  - Canary Deployment\ndescription: The traffic-split Plugin directs traffic to various Upstream services based on conditions and/or weights. It provides a dynamic and flexible approach to implement release strategies and manage traffic.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/traffic-split\" />\n</head>\n\n## Description\n\nThe `traffic-split` Plugin directs traffic to various Upstream services based on conditions and/or weights. It provides a dynamic and flexible approach to implement release strategies and manage traffic.\n\n:::note\n\nThe traffic ratio between Upstream services may be less accurate since round robin algorithm is used to direct traffic (especially when the state is reset).\n\n:::\n\n## Attributes\n\n| Name                           | Type           | Required | Default    | Valid values                | Description                                                                                                                                                                                                                                                                                                                                               |\n|--------------------------------|----------------|----------|------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| rules.match                    | array[object]  | False    |            |                             | An array of one or more pairs of matching conditions and actions to be executed.     |\n| rules.match                    | array[object]  | False    |            |                             | Rules to match for conditional traffic split.           |\n| rules.match.vars               | array[array]   | False    |            |                             | An array of one or more matching conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) to conditionally execute the plugin. |\n| rules.weighted_upstreams       | array[object]  | False    |            |                             | List of Upstream configurations.    |\n| rules.weighted_upstreams.upstream_id | string/integer | False    |            |                             | ID of the configured Upstream object.    |\n| rules.weighted_upstreams.weight      | integer        | False    | weight = 1 |                             | Weight for each upstream.  |\n| rules.weighted_upstreams.upstream    | object         | False    |            |                             | Configuration of the upstream. Certain configuration options Upstream are not supported here. These fields are `service_name`, `discovery_type`, `checks`, `retries`, `retry_timeout`, `desc`, and `labels`. As a workaround, you can create an Upstream object and configure it in `upstream_id`.    |\n| rules.weighted_upstreams.upstream.type                  | array           | False    | roundrobin | [roundrobin, chash]         | Algorithm for traffic splitting. `roundrobin` for weighted round robin and `chash` for consistent hashing.        |\n| rules.weighted_upstreams.upstream.hash_on               | array           | False    | vars       |                             | Used when `type` is `chash`. Support hashing on [NGINX  variables](https://nginx.org/en/docs/varindex.html), headers, cookie, Consumer, or a combination of [NGINX  variables](https://nginx.org/en/docs/varindex.html).         |\n| rules.weighted_upstreams.upstream.key                   | string         | False    |            |                             | Used when `type` is `chash`. When `hash_on` is set to `header` or `cookie`, `key` is required. When `hash_on` is set to `consumer`, `key` is not required as the Consumer name will be used as the key automatically.          |\n| rules.weighted_upstreams.upstream.nodes                 | object         | False    |            |                             | Addresses of the Upstream nodes.   |\n| rules.weighted_upstreams.upstream.timeout               | object         | False    | 15         |                             |  Timeout in seconds for connecting, sending and receiving messages.                |\n| rules.weighted_upstreams.upstream.pass_host             | array           | False    | \"pass\"     | [\"pass\", \"node\", \"rewrite\"] | Mode deciding how the host name is passed. `pass` passes the client's host name to the upstream. `node` passes the host configured in the node of the upstream. `rewrite` passes the value configured in `upstream_host`.             |\n| rules.weighted_upstreams.upstream.name                  | string         | False    |            |                             |  Identifier for the Upstream for specifying service name, usage scenarios, and so on.        |\n| rules.weighted_upstreams.upstream.upstream_host         | string         | False    |            |                             | Used when `pass_host` is `rewrite`. Host name of the upstream.         |\n\n## Examples\n\nThe examples below show different use cases for using the `traffic-split` Plugin.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Implement Canary Release\n\nThe following example demonstrates how to implement canary release with this Plugin.\n\nA Canary release is a gradual deployment in which an increasing percentage of traffic is directed to a new release, allowing for a controlled and monitored rollout. This method ensures that any potential issues or bugs in the new release can be identified and addressed early on, before fully redirecting all traffic.\n\nCreate a Route and configure `traffic-split` Plugin with the following rules:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 3\n              },\n              {\n                \"weight\": 2\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\nThe proportion of traffic to each Upstream is determined by the weight of the Upstream relative to the total weight of all upstreams. Here, the total weight is calculated as: 3 + 2 = 5.\n\nTherefore, 60% of the traffic are to be forwarded to `httpbin.org` and the other 40% of the traffic are to be forwarded to `mock.api7.ai`.\n\nSend 10 consecutive requests to the Route to verify:\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers\" -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\nYou should see a response similar to the following:\n\n```text\nhttpbin.org: 6, mock.api7.ai: 4\n```\n\nAdjust the Upstream weights accordingly to complete the canary release.\n\n### Implement Blue-Green Deployment\n\nThe following example demonstrates how to implement blue-green deployment with this Plugin.\n\nBlue-green deployment is a deployment strategy that involves maintaining two identical environments: the _blue_ and the _green_. The blue environment refers to the current production deployment and the green environment refers to the new deployment. Once the green environment is tested to be ready for production, traffic will be routed to the green environment, making it the new production deployment.\n\nCreate a Route and configure `traffic-split` Plugin to execute the Plugin to redirect traffic only when the request contains a header `release: new_release`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"http_release\",\"==\",\"new_release\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                }\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\nSend a request to the Route with the `release` header:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\" -H 'release: new_release'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    ...\n  }\n}\n```\n\nSend a request to the Route without any additional header:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"mock.api7.ai\",\n    ...\n  }\n}\n```\n\n### Define Matching Condition for POST Request With APISIX Expressions\n\nThe following example demonstrates how to use [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) in rules to conditionally execute the Plugin when certain condition of a POST request is satisfied.\n\nCreate a Route and configure `traffic-split` Plugin with the following rules:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/post\",\n    \"methods\": [\"POST\"],\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"post_arg_id\", \"==\", \"1\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                }\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\nSend a POST request with body `id=1`:\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'id=1'\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"id\": \"1\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"4\",\n    \"Content-Type\": \"application/x-www-form-urlencoded\",\n    \"Host\": \"httpbin.org\",\n    ...\n  },\n  ...\n}\n```\n\nSend a POST request without `id=1` in the body:\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'random=string'\n```\n\nYou should see that the request was forwarded to `mock.api7.ai`.\n\n### Define AND Matching Conditions With APISIX Expressions\n\nThe following example demonstrates how to use [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) in rules to conditionally execute the Plugin when multiple conditions are satisfied.\n\nCreate a Route and configure `traffic-split` Plugin to redirect traffic only when all three conditions are satisfied:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"arg_name\",\"==\",\"jack\"],\n                  [\"http_user-id\",\">\",\"23\"],\n                  [\"http_apisix-key\",\"~~\",\"[a-z]+\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 3\n              },\n              {\n                \"weight\": 2\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\nIf conditions are satisfied, 60% of the traffic should be directed to `httpbin.org` and the other 40% should be directed to `mock.api7.ai`. If conditions are not satisfied, all traffic should be directed to `mock.api7.ai`.\n\nSend 10 consecutive requests that satisfy all conditions to verify:\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name=jack\" -H 'user-id: 30' -H 'apisix-key: helloapisix' -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\nYou should see a response similar to the following:\n\n```text\nhttpbin.org: 6, mock.api7.ai: 4\n```\n\nSend 10 consecutive requests that do not satisfy the conditions to verify:\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name=random\" -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\nYou should see a response similar to the following:\n\n```text\nhttpbin.org: 0, mock.api7.ai: 10\n```\n\n### Define OR Matching Conditions With APISIX Expressions\n\nThe following example demonstrates how to use [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) in rules to conditionally execute the Plugin when either set of the condition is satisfied.\n\nCreate a Route and configure `traffic-split` Plugin to redirect traffic when either set of the configured conditions are satisfied:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"arg_name\",\"==\",\"jack\"],\n                  [\"http_user-id\",\">\",\"23\"],\n                  [\"http_apisix-key\",\"~~\",\"[a-z]+\"]\n                ]\n              },\n              {\n                \"vars\": [\n                  [\"arg_name2\",\"==\",\"rose\"],\n                  [\"http_user-id2\",\"!\",\">\",\"33\"],\n                  [\"http_apisix-key2\",\"~~\",\"[a-z]+\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 3\n              },\n              {\n                \"weight\": 2\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\nAlternatively, you can also use the OR operator in the [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) for these conditions.\n\nIf conditions are satisfied, 60% of the traffic should be directed to `httpbin.org` and the other 40% should be directed to `mock.api7.ai`. If conditions are not satisfied, all traffic should be directed to `mock.api7.ai`.\n\nSend 10 consecutive requests that satisfy the second set of conditions to verify:\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name2=rose\" -H 'user-id:30' -H 'apisix-key2: helloapisix' -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\nYou should see a response similar to the following:\n\n```json\nhttpbin.org: 6, mock.api7.ai: 4\n```\n\nSend 10 consecutive requests that do not satisfy any set of conditions to verify:\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name=random\" -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\nYou should see a response similar to the following:\n\n```json\nhttpbin.org: 0, mock.api7.ai: 10\n```\n\n### Configure Different Rules for Different Upstreams\n\nThe following example demonstrates how to set one-to-one mapping between rule sets and upstreams.\n\nCreate a Route and configure `traffic-split` Plugin with the following matching rules to redirect traffic when the request contains a header `x-api-id: 1` or `x-api-id: 2`, to the corresponding Upstream service:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"http_x-api-id\",\"==\",\"1\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 1\n              }\n            ]\n          },\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"http_x-api-id\",\"==\",\"2\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"mock.api7.ai:443\":1\n                  }\n                },\n                \"weight\": 1\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"postman-echo.com:443\": 1\n      },\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\"\n    }\n  }'\n```\n\nSend a request with header `x-api-id: 1`:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\" -H 'x-api-id: 1'\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    ...\n  }\n}\n```\n\nSend a request with header `x-api-id: 2`:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\" -H 'x-api-id: 2'\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"mock.api7.ai\",\n    ...\n  }\n}\n```\n\nSend a request without any additional header:\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\nYou should see a response similar to the following:\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"postman-echo.com\",\n    ...\n  }\n}\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/ua-restriction.md",
    "content": "---\ntitle: ua-restriction\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - UA restriction\ndescription: The ua-restriction Plugin restricts access to upstream resources using an allowlist or denylist of user agents, preventing overload from web crawlers and enhancing API security.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ua-restriction\" />\n</head>\n\n## Description\n\nThe `ua-restriction` Plugin supports restricting access to upstream resources through either configuring an allowlist or denylist of user agents. A common use case is to prevent web crawlers from overloading the upstream resources and causing service degradation.\n\n## Attributes\n\n| Name           | Type          | Required | Default      | Valid values            | Description                                                                     |\n|----------------|---------------|----------|--------------|-------------------------|---------------------------------------------------------------------------------|\n| bypass_missing | boolean       | False    | false        |                         | If true, bypass the user agent restriction check when the `User-Agent` header is missing. |\n| allowlist      | array[string] | False    |              |                         | List of user agents to allow. Support regular expressions. At least one of the `allowlist` and `denylist` should be configured, but they cannot be configured at the same time.   |\n| denylist       | array[string] | False    |              |                         | List of user agents to deny. Support regular expressions. At least one of the `allowlist` and `denylist` should be configured, but they cannot be configured at the same time.   |\n| message        | string        | False    | \"Not allowed\" |  | Message returned when the user agent is denied access.    |\n\n## Examples\n\nThe examples below demonstrate how you can configure `ua-restriction` for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Reject Web Crawlers and Customize Error Message\n\nThe following example demonstrates how you can configure the Plugin to fend off unwanted web crawlers and customize the rejection message.\n\nCreate a Route and configure the Plugin to block specific crawlers from accessing resources with a customized message:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ua-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ua-restriction\": {\n        \"bypass_missing\": false,\n        \"denylist\": [\n          \"(Baiduspider)/(\\\\d+)\\\\.(\\\\d+)\",\n          \"bad-bot-1\"\n        ],\n        \"message\": \"Access denied\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nSend another request to the Route with a disallowed user agent:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'User-Agent: Baiduspider/5.0'\n```\n\nYou should receive an `HTTP/1.1 403 Forbidden` response with the following message:\n\n```text\n{\"message\":\"Access denied\"}\n```\n\n### Bypass UA Restriction Checks\n\nThe following example demonstrates how to configure the Plugin to allow requests of a specific user agent to bypass the UA restriction.\n\nCreate a Route as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ua-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ua-restriction\": {\n        \"bypass_missing\": true,\n        \"allowlist\": [\n          \"good-bot-1\"\n        ],\n        \"message\": \"Access denied\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route without modifying the user agent:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 403 Forbidden` response with the following message:\n\n```text\n{\"message\":\"Access denied\"}\n```\n\nSend another request to the Route with an empty user agent:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'User-Agent: '\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n"
  },
  {
    "path": "docs/en/latest/plugins/udp-logger.md",
    "content": "---\ntitle: udp-logger\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - UDP Logger\ndescription: This document contains information about the Apache APISIX udp-logger Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `udp-logger` Plugin can be used to push log data requests to UDP servers.\n\nThis provides the ability to send log data requests as JSON objects to monitoring tools and other UDP servers.\n\nThis plugin also allows to push logs as a batch to your external UDP server. It might take some time to receive the log data. It will be automatically sent after the timer function in the [batch processor](../batch-processor.md) expires.\n\n## Attributes\n\n| Name             | Type    | Required | Default      | Valid values | Description                                              |\n|------------------|---------|----------|--------------|--------------|----------------------------------------------------------|\n| host             | string  | True     |              |              | IP address or the hostname of the UDP server.            |\n| port             | integer | True     |              | [0,...]      | Target upstream port.                                    |\n| timeout          | integer | False    | 3            | [1,...]      | Timeout for the upstream to send data.                   |\n| log_format       | object  | False    |  |              | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| name             | string  | False    | \"udp logger\" |              | Unique identifier for the batch processor. If you use Prometheus to monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`. processor.               |\n| include_req_body | boolean | False    | false        | [false, true] | When set to `true` includes the request body in the log. |\n| include_req_body_expr  | array   | No       |         |               | Filter for when the `include_req_body` attribute is set to `true`. Request body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                          |\n| max_req_body_bytes | integer | False | 524288 | >=1 | Request bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n| include_resp_body | boolean | No       | false   | [false, true] | When set to `true` includes the response body in the log.                                                                                                        |\n| include_resp_body_expr | array   | No  |         |               | Filter for when the `include_resp_body` attribute is set to `true`. Response body is only logged when the expression set here evaluates to `true`. See [lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.                                                                                                                        |\n| max_resp_body_bytes | integer | False | 524288 | >=1 | Response bodies within this size will be logged, if the size exceeds the configured value it will be truncated before logging. |\n\nThis Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration.\n\n### Example of default log format\n\n```json\n{\n  \"apisix_latency\": 99.999988555908,\n  \"service_id\": \"\",\n  \"server\": {\n    \"version\": \"3.7.0\",\n    \"hostname\": \"localhost\"\n  },\n  \"request\": {\n    \"method\": \"GET\",\n    \"headers\": {\n      \"connection\": \"close\",\n      \"host\": \"localhost\"\n    },\n    \"url\": \"http://localhost:1984/opentracing\",\n    \"size\": 65,\n    \"querystring\": {},\n    \"uri\": \"/opentracing\"\n  },\n  \"start_time\": 1704527399740,\n  \"client_ip\": \"127.0.0.1\",\n  \"response\": {\n    \"status\": 200,\n    \"size\": 136,\n    \"headers\": {\n      \"server\": \"APISIX/3.7.0\",\n      \"content-type\": \"text/plain\",\n      \"transfer-encoding\": \"chunked\",\n      \"connection\": \"close\"\n    }\n  },\n  \"upstream\": \"127.0.0.1:1982\",\n  \"route_id\": \"1\",\n  \"upstream_latency\": 12,\n  \"latency\": 111.99998855591\n}\n```\n\n## Metadata\n\nYou can also set the format of the logs by configuring the Plugin metadata. The following configurations are available:\n\n| Name       | Type   | Required | Default                                                                       | Description                                                                                                                                                                                                                                             |\n| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| log_format | object | False    |  | Log format declared as key-value pairs in JSON. Values support strings and nested objects (up to five levels deep; deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by prefixing with `$`. |\n| max_pending_entries | integer | False | | Maximum number of pending entries that can be buffered in batch processor before it starts dropping them. |\n\n:::info IMPORTANT\n\nConfiguring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `udp-logger` Plugin.\n\n:::\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/udp-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```json\n{\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"route_id\":\"1\",\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200}}\n```\n\n## Enable Plugin\n\nThe example below shows how you can enable the Plugin on a specific Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"udp-logger\": {\n                 \"host\": \"127.0.0.1\",\n                 \"port\": 3000,\n                 \"batch_max_size\": 1,\n                 \"name\": \"udp logger\"\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n## Example usage\n\nNow, if you make a request to APISIX, it will be logged in your UDP server:\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## Delete Plugin\n\nTo remove the `udp-logger` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/uri-blocker.md",
    "content": "---\ntitle: uri-blocker\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - URI Blocker\ndescription: This document contains information about the Apache APISIX uri-blocker Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `uri-blocker` Plugin intercepts user requests with a set of `block_rules`.\n\n## Attributes\n\n| Name             | Type          | Required | Default | Valid values | Description                                                                                                                                                                                           |\n|------------------|---------------|----------|---------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| block_rules      | array[string] | True     |         |              | List of regex filter rules. If the request URI hits any one of the rules, the response code is set to the `rejected_code` and the user request is terminated. For example, `[\"root.exe\", \"root.m+\"]`. |\n| rejected_code    | integer       | False    | 403     | [200, ...]   | HTTP status code returned when the request URI hits any of the `block_rules`.                                                                                                                         |\n| rejected_msg     | string        | False    |         | non-empty    | HTTP response body returned when the request URI hits any of the `block_rules`.                                                                                                                       |\n| case_insensitive | boolean       | False    | false   |              | When set to `true`, ignores the case when matching request URI.                                                                                                                                       |\n\n## Enable Plugin\n\nThe example below enables the `uri-blocker` Plugin on a specific Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"uri-blocker\": {\n            \"block_rules\": [\"root.exe\", \"root.m+\"]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example usage\n\nOnce you have configured the Plugin as shown above, you can try accessing the file:\n\n```shell\ncurl -i http://127.0.0.1:9080/root.exe?a=a\n```\n\n```shell\nHTTP/1.1 403 Forbidden\nDate: Wed, 17 Jun 2020 13:55:41 GMT\nContent-Type: text/html; charset=utf-8\nContent-Length: 150\nConnection: keep-alive\nServer: APISIX web server\n\n... ...\n```\n\nYou can also set a `rejected_msg` and it will be added to the response body:\n\n```shell\nHTTP/1.1 403 Forbidden\nDate: Wed, 17 Jun 2020 13:55:41 GMT\nContent-Type: text/html; charset=utf-8\nContent-Length: 150\nConnection: keep-alive\nServer: APISIX web server\n\n{\"error_msg\":\"access is not allowed\"}\n```\n\n## Delete Plugin\n\nTo remove the `uri-blocker` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/wolf-rbac.md",
    "content": "---\ntitle: wolf-rbac\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - wolf RBAC\n  - wolf-rbac\ndescription: This document contains information about the Apache APISIX wolf-rbac Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe `wolf-rbac` Plugin provides a [role-based access control](https://en.wikipedia.org/wiki/Role-based_access_control) system with [wolf](https://github.com/iGeeky/wolf) to a Route or a Service. This Plugin can be used with a [Consumer](../terminology/consumer.md).\n\n## Attributes\n\n| Name          | Type   | Required | Default                  | Description                                                                                                                                                                                                                 |\n|---------------|--------|----------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| server        | string | False    | \"http://127.0.0.1:12180\" | Service address of wolf server.                                                                                                                                                                                             |\n| appid         | string | False    | \"unset\"                  | App id added in wolf console. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource.            |\n| header_prefix | string | False    | \"X-\"                     | Prefix for a custom HTTP header. After authentication is successful, three headers will be added to the request header (for backend) and response header (for frontend) namely: `X-UserId`, `X-Username`, and `X-Nickname`. |\n\n## API\n\nThis Plugin will add the following endpoints when enabled:\n\n- `/apisix/plugin/wolf-rbac/login`\n- `/apisix/plugin/wolf-rbac/change_pwd`\n- `/apisix/plugin/wolf-rbac/user_info`\n\n:::note\n\nYou may need to use the [public-api](public-api.md) Plugin to expose this endpoint.\n\n:::\n\n## Pre-requisites\n\nTo use this Plugin, you have to first [install wolf](https://github.com/iGeeky/wolf/blob/master/quick-start-with-docker/README.md) and start it.\n\nOnce you have done that you need to add `application`, `admin`, `normal user`, `permission`, `resource` and user authorize to the [wolf-console](https://github.com/iGeeky/wolf/blob/master/docs/usage.md).\n\n## Enable Plugin\n\nYou need to first configure the Plugin on a Consumer:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"username\":\"wolf_rbac\",\n  \"plugins\":{\n    \"wolf-rbac\":{\n      \"server\":\"http://127.0.0.1:12180\",\n      \"appid\":\"restful\"\n    }\n  },\n  \"desc\":\"wolf-rbac\"\n}'\n```\n\n:::note\n\nThe `appid` added in the configuration should already exist in wolf.\n\n:::\n\nYou can now add the Plugin to a Route or a Service:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"wolf-rbac\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"www.baidu.com:80\": 1\n        }\n    }\n}'\n```\n\nYou can also use the [APISIX Dashboard](/docs/dashboard/USER_GUIDE) to complete the operation through a web UI.\n\n<!--\n![add a consumer](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/plugin/wolf-rbac-1.png)\n\n![enable wolf-rbac plugin](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/plugin/wolf-rbac-2.png)\n-->\n\n## Example usage\n\nYou can use the `public-api` Plugin to expose the API:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/wal -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/plugin/wolf-rbac/login\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\nSimilarly, you can setup the Routes for `change_pwd` and `user_info`.\n\nYou can now login and get a wolf `rbac_token`:\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \\\n-H \"Content-Type: application/json\" \\\n-d '{\"appid\": \"restful\", \"username\":\"test\", \"password\":\"user-password\", \"authType\":1}'\n```\n\n```shell\nHTTP/1.1 200 OK\nDate: Wed, 24 Jul 2019 10:33:31 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n{\"rbac_token\":\"V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts\",\"user_info\":{\"nickname\":\"test\",\"username\":\"test\",\"id\":\"749\"}}\n```\n\n:::note\n\nThe `appid`, `username`, and `password` must be configured in the wolf system.\n\n`authType` is the authentication type—1 for password authentication (default) and 2 for LDAP authentication (v0.5.0+).\n\n:::\n\nYou can also make a post request with `x-www-form-urlencoded` instead of JSON:\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \\\n-H \"Content-Type: application/x-www-form-urlencoded\" \\\n-d 'appid=restful&username=test&password=user-password'\n```\n\nNow you can test the Route:\n\n- without token:\n\n```shell\ncurl http://127.0.0.1:9080/ -H\"Host: www.baidu.com\" -i\n```\n\n```\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Missing rbac token in request\"}\n```\n\n- with token in `Authorization` header:\n\n```shell\ncurl http://127.0.0.1:9080/ -H\"Host: www.baidu.com\" \\\n-H 'Authorization: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- with token in `x-rbac-token` header:\n\n```shell\ncurl http://127.0.0.1:9080/ -H\"Host: www.baidu.com\" \\\n-H 'x-rbac-token: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- with token in request parameters:\n\n```shell\ncurl 'http://127.0.0.1:9080?rbac_token=V1%23restful%23eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -H\"Host: www.baidu.com\" -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- with token in cookie:\n\n```shell\ncurl http://127.0.0.1:9080 -H\"Host: www.baidu.com\" \\\n--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i\n```\n\n```\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\nAnd to get a user information:\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/user_info \\\n--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i\n```\n\n```shell\nHTTP/1.1 200 OK\n{\n    \"user_info\":{\n        \"nickname\":\"test\",\n        \"lastLogin\":1582816780,\n        \"id\":749,\n        \"username\":\"test\",\n        \"appIDs\":[\"restful\"],\n        \"manager\":\"none\",\n        \"permissions\":{\"USER_LIST\":true},\n        \"profile\":null,\n        \"roles\":{},\n        \"createTime\":1578820506,\n        \"email\":\"\"\n    }\n}\n```\n\nAnd to change a user's password:\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/change_pwd \\\n-H \"Content-Type: application/json\" \\\n--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i \\\n-X PUT -d '{\"oldPassword\": \"old password\", \"newPassword\": \"new password\"}'\n```\n\n```shell\nHTTP/1.1 200 OK\n{\"message\":\"success to change password\"}\n```\n\n## Delete Plugin\n\nTo remove the `wolf-rbac` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/*\",\n    \"plugins\": {\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"www.baidu.com:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/workflow.md",
    "content": "---\ntitle: workflow\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - workflow\n  - traffic control\ndescription: The workflow Plugin supports the conditional execution of user-defined actions to client traffic based a given set of rules. This provides a granular approach to implement complex traffic management.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/workflow\" />\n</head>\n\n## Description\n\nThe `workflow` Plugin supports the conditional execution of user-defined actions to client traffic based a given set of rules, defined using [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). This provides a granular approach to traffic management.\n\n## Attributes\n\n| Name                         | Type          | Required | Default | Valid values | Description                                                  |\n| ---------------------------- | ------------- | -------- | ------- | ------------ | ------------------------------------------------------------ |\n| rules                   | array[object]  | True     |         |              |  An array of one or more pairs of matching conditions and actions to be executed. |\n| rules.case                   | array[array]  | False     |         |              | An array of one or more matching conditions in the form of [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). For example, `{\"arg_name\", \"==\", \"json\"}`.  |\n| rules.actions                | array[object] | True     |         |              | An array of actions to be executed when a condition is successfully matched. Currently, the array only supports one action, and it should be either `return`, or `limit-count` or `limit-conn`. When the action is configured to be `return`, you can configure an HTTP status code to return to the client when the condition is matched. When the action is configured to be `limit-count`, you can configure all options of the [`limit-count`](./limit-count.md) plugin, except for `group`. When the action is configured to be `limit-conn`, you can configure all options of the [`limit-conn`](./limit-conn.md) plugin. |\n\n## Examples\n\nThe examples below demonstrates how you can use the `workflow` Plugin for different scenarios.\n\n:::note\n\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Return Response HTTP Status Code Conditionally\n\nThe following example demonstrates a simple rule with one matching condition and one associated action to return HTTP status code conditionally.\n\nCreate a Route with the `workflow` Plugin to return HTTP status code 403 when the request's URI path is `/anything/rejected`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"workflow-route\",\n    \"uri\": \"/anything/*\",\n    \"plugins\": {\n      \"workflow\":{\n        \"rules\":[\n          {\n            \"case\":[\n              [\"uri\", \"==\", \"/anything/rejected\"]\n            ],\n            \"actions\":[\n              [\n                \"return\",\n                {\"code\": 403}\n              ]\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request that matches none of the rules:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nSend a request that matches the configured rule:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/rejected\"\n```\n\nYou should receive an `HTTP/1.1 403 Forbidden` response of following:\n\n```text\n{\"error_msg\":\"rejected by workflow\"}\n```\n\n### Apply Rate Limiting Conditionally by URI and Query Parameter\n\nThe following example demonstrates a rule with two matching conditions and one associated action to rate limit requests conditionally.\n\nCreate a Route with the `workflow` Plugin to apply rate limiting when the URI path is `/anything/rate-limit` and the query parameter `env` value is `v1`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"workflow-route\",\n    \"uri\": \"/anything/*\",\n    \"plugins\":{\n      \"workflow\":{\n        \"rules\":[\n          {\n            \"case\":[\n              [\"uri\", \"==\", \"/anything/rate-limit\"],\n              [\"arg_env\", \"==\", \"v1\"]\n            ],\n            \"actions\":[\n              [\n                \"limit-count\",\n                {\n                  \"count\":1,\n                  \"time_window\":60,\n                  \"rejected_code\":429\n                }\n              ]\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nGenerate two consecutive requests that matches the second rule:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/rate-limit?env=v1\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response and an `HTTP 429 Too Many Requests` response.\n\nGenerate requests that do not match the condition:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/anything?env=v1\"\n```\n\nYou should receive `HTTP/1.1 200 OK` responses for all requests, as they are not rate limited.\n\n### Apply Rate Limiting Conditionally by Consumers\n\nThe following example demonstrates how to configure the Plugin to perform rate limiting based on the following specifications:\n\n* Consumer `john` should have a quota of 5 requests within a 30-second window\n* Consumer `jane` should have a quota of 3 requests within a 30-second window\n* All other consumers should have a quota of 2 requests within a 30-second window\n\nWhile this example will be using [`key-auth`](./key-auth.md), you can easily replace it with other authentication Plugins.\n\nCreate a Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `key-auth` credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\nCreate a second Consumer `jane`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\nCreate `key-auth` credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\nCreate a third Consumer `jimmy`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jimmy\"\n  }'\n```\n\nCreate `key-auth` credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jimmy/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jimmy-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jimmy-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with the `workflow` and `key-auth` Plugins, with the desired rate limiting rules:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"workflow-route\",\n    \"uri\": \"/anything\",\n    \"plugins\":{\n      \"key-auth\": {},\n      \"workflow\":{\n        \"rules\":[\n          {\n            \"actions\": [\n              [\n                \"limit-count\",\n                {\n                  \"count\": 5,\n                  \"key\": \"consumer_john\",\n                  \"key_type\": \"constant\",\n                  \"rejected_code\": 429,\n                  \"time_window\": 30\n                }\n              ]\n            ],\n            \"case\": [\n              [\n                \"consumer_name\",\n                \"==\",\n                \"john\"\n              ]\n            ]\n          },\n          {\n            \"actions\": [\n              [\n                \"limit-count\",\n                {\n                  \"count\": 3,\n                  \"key\": \"consumer_jane\",\n                  \"key_type\": \"constant\",\n                  \"rejected_code\": 429,\n                  \"time_window\": 30\n                }\n              ]\n            ],\n            \"case\": [\n              [\n                \"consumer_name\",\n                \"==\",\n                \"jane\"\n              ]\n            ]\n          },\n          {\n            \"actions\": [\n              [\n                \"limit-count\",\n                {\n                  \"count\": 2,\n                  \"key\": \"$consumer_name\",\n                  \"key_type\": \"var\",\n                  \"rejected_code\": 429,\n                  \"time_window\": 30\n                }\n              ]\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send 6 consecutive requests with `john`'s key:\n\n```shell\nresp=$(seq 6 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: john-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 6 requests, 5 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    5, 429:    1\n```\n\nSend 6 consecutive requests with `jane`'s key:\n\n```shell\nresp=$(seq 6 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: jane-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 6 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    3, 429:    3\n```\n\nSend 3 consecutive requests with `jimmy`'s key:\n\n```shell\nresp=$(seq 3 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: jimmy-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\nYou should see the following response, showing that out of the 3 requests, 2 requests were successful (status code 200) while the others were rejected (status code 429).\n\n```text\n200:    2, 429:    1\n```\n"
  },
  {
    "path": "docs/en/latest/plugins/zipkin.md",
    "content": "---\ntitle: zipkin\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Plugin\n  - Zipkin\ndescription: Zipkin is an open-source distributed tracing system. The zipkin Plugin instruments APISIX and sends traces to Zipkin based on the Zipkin API specification.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements. See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/zipkin\" />\n</head>\n\n## Description\n\n[Zipkin](https://github.com/openzipkin/zipkin) is an open-source distributed tracing system. The `zipkin` Plugin instruments APISIX and sends traces to Zipkin based on the [Zipkin API specification](https://zipkin.io/pages/instrumenting.html).\n\nThe Plugin can also send traces to other compatible collectors, such as [Jaeger](https://www.jaegertracing.io/docs/1.51/getting-started/#migrating-from-zipkin) and [Apache SkyWalking](https://skywalking.apache.org/docs/main/latest/en/setup/backend/zipkin-trace/#zipkin-receiver), both of which support Zipkin [v1](https://zipkin.io/zipkin-api/zipkin-api.yaml) and [v2](https://zipkin.io/zipkin-api/zipkin2-api.yaml) APIs.\n\n## Static Configurations\n\nBy default, `zipkin` Plugin NGINX variables configuration is set to false in the [default configuration](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua):\n\nTo modify this value, add the updated configuration to `config.yaml`. For example:\n\n```yaml\nplugin_attr:\n  zipkin:\n    set_ngx_var: true\n```\n\nReload APISIX for changes to take effect.\n\n## Attributes\n\nSee the configuration file for configuration options available to all Plugins.\n\n| Name         | Type    | Required | Default        | Valid values | Description                                                                     |\n|--------------|---------|----------|----------------|--------------|---------------------------------------------------------------------------------|\n| endpoint     | string  | True     |                |              | Zipkin span endpoint to POST to, such as `http://127.0.0.1:9411/api/v2/spans`.        |\n|sample_ratio| number  | True     |                | [0.00001, 1] | Frequency to sample requests. Setting to `1` means sampling every request.      |\n|service_name| string  | False    | \"APISIX\"       |              | Service name for the Zipkin reporter to be displayed in Zipkin. |\n|server_addr | string  | False    |the value of `$server_addr` | IPv4 address | IPv4 address for the Zipkin reporter. For example, you can set this to your external IP address. |\n|span_version | integer | False    | 2             | [1, 2]       | Version of the span type. |\n\n## Examples\n\nThe examples below show different use cases of the `zipkin` Plugin.\n\n### Send Traces to Zipkin\n\nThe following example demonstrates how to trace requests to a Route and send traces to Zipkin using [Zipkin API v2](https://zipkin.io/zipkin-api/zipkin2-api.yaml). You will also understand the differences between span version 2 and span version 1.\n\nStart a Zipkin instance in Docker:\n\n```shell\ndocker run -d --name zipkin -p 9411:9411 openzipkin/zipkin\n```\n\nCreate a Route with `zipkin` and use the default span version 2. You should adjust the IP address as needed for the Zipkin HTTP endpoint, and configure the sample ratio to `1` to trace every request.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"zipkin-tracing-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"zipkin\": {\n        \"endpoint\": \"http://127.0.0.1:9411/api/v2/spans\",\n        \"sample_ratio\": 1,\n        \"span_version\": 2\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/7.64.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-65af2926-497590027bcdb09e34752b78\",\n    \"X-B3-Parentspanid\": \"347dddedf73ec176\",\n    \"X-B3-Sampled\": \"1\",\n    \"X-B3-Spanid\": \"429afa01d0b0067c\",\n    \"X-B3-Traceid\": \"aea58f4b490766eccb08275acd52a13a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  ...\n}\n```\n\nNavigate to the Zipkin web UI at [http://127.0.0.1:9411/zipkin](http://127.0.0.1:9411/zipkin) and click __Run Query__, you should see a trace corresponding to the request:\n\n![trace-from-request](https://static.api7.ai/uploads/2024/01/23/MaXhacYO_zipkin-run-query.png)\n\nClick __Show__ to see more tracing details:\n\n![v2-trace-spans](https://static.api7.ai/uploads/2024/01/23/3SmfFq9f_trace-details.png)\n\nNote that with span version 2, every traced request creates the following spans:\n\n```text\nrequest\n├── proxy\n└── response\n```\n\nwhere `proxy` represents the time from the beginning of the request to the beginning of `header_filter`, and `response` represents the time from the beginning of `header_filter` to the beginning of `log`.\n\nNow, update the Plugin on the Route to use span version 1:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/zipkin-tracing-route\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"zipkin\": {\n        \"span_version\": 1\n      }\n    }\n  }'\n```\n\nSend another request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\nIn the Zipkin web UI, you should see a new trace with details similar to the following:\n\n![v1-trace-spans](https://static.api7.ai/uploads/2024/01/23/OPw2sTPa_v1-trace-spans.png)\n\nNote that with the older span version 1, every traced request creates the following spans:\n\n```text\nrequest\n├── rewrite\n├── access\n└── proxy\n    └── body_filter\n```\n\n### Send Traces to Jaeger\n\nThe following example demonstrates how to trace requests to a Route and send traces to Jaeger.\n\nStart a Jaeger instance in Docker:\n\n```shell\ndocker run -d --name jaeger \\\n  -e COLLECTOR_ZIPKIN_HOST_PORT=9411 \\\n  -p 16686:16686 \\\n  -p 9411:9411 \\\n  jaegertracing/all-in-one\n```\n\nCreate a Route with `zipkin`. Please adjust the IP address as needed for the Zipkin HTTP endpoint, and configure the sample ratio to `1` to trace every request.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"kin-tracing-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"kin\": {\n        \"endpoint\": \"http://127.0.0.1:9411/api/v2/spans\",\n        \"sample_ratio\": 1\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\nSend a request to the Route:\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\nYou should receive an `HTTP/1.1 200 OK` response.\n\nNavigate to the Jaeger web UI at [http://127.0.0.1:16686](http://127.0.0.1:16686), select APISIX as the Service, and click __Find Traces__, you should see a trace corresponding to the request:\n\n![jaeger-traces](https://static.api7.ai/uploads/2024/01/23/X6QdLN3l_jaeger.png)\n\nSimilarly, you should find more span details once you click into a trace:\n\n![jaeger-details](https://static.api7.ai/uploads/2024/01/23/iP9fXI2A_jaeger-details.png)\n\n### Using Trace Variables in Logging\n\nThe following example demonstrates how to configure the `kin` Plugin to set the following built-in variables, which can be used in logger Plugins or access logs:\n\n- `kin_context_traceparent`: [trace parent](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format) ID\n- `kin_trace_id`: trace ID of the current span\n- `kin_span_id`: span ID of the current span\n\nUpdate the configuration file as below. You can customize the access log format to use the `zipkin` Plugin variables, and set `zipkin` variables in the `set_ngx_var` field.\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    enable_access_log: true\n    access_log_format: '{\"time\": \"$time_iso8601\",\"zipkin_context_traceparent\": \"$zipkin_context_traceparent\",\"zipkin_trace_id\": \"$zipkin_trace_id\",\"zipkin_span_id\": \"$zipkin_span_id\",\"remote_addr\": \"$remote_addr\"}'\n    access_log_format_escape: json\nplugin_attr:\n  zipkin:\n    set_ngx_var: true\n```\n\nReload APISIX for configuration changes to take effect.\n\nYou should see access log entries similar to the following when you generate requests:\n\n```text\n{\"time\": \"23/Jan/2024:06:28:00 +0000\",\"zipkin_context_traceparent\": \"00-61bce33055c56f5b9bec75227befd142-13ff3c7370b29925-01\",\"zipkin_trace_id\": \"61bce33055c56f5b9bec75227befd142\",\"zipkin_span_id\": \"13ff3c7370b29925\",\"remote_addr\": \"172.28.0.1\"}\n```\n"
  },
  {
    "path": "docs/en/latest/profile.md",
    "content": "---\ntitle: Configuration based on environments\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Configuration\n  - Environment\ndescription: This document describes how you can change APISIX configuration based on environments.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nExtracting configuration from the code makes APISIX adaptable to changes in the operating environments. For example, APISIX can be deployed in a development environment for testing and then moved to a production environment. The configuration for APISIX in these environments would be different.\n\nAPISIX supports managing multiple configurations through environment variables in two different ways:\n\n1. Using environment variables in the configuration file\n2. Using an environment variable to switch between multiple configuration profiles\n\n## Using environment variables in the configuration file\n\nThis is useful when you want to change some configurations based on the environment.\n\nTo use environment variables, you can use the syntax `key_name: ${{ENVIRONMENT_VARIABLE_NAME:=}}`. You can also set a default value to fall back to if no environment variables are set by adding it to the configuration as `key_name: ${{ENVIRONMENT_VARIABLE_NAME:=VALUE}}`. The example below shows how you can modify your configuration file to use environment variables to set the listening ports of APISIX:\n\n```yaml title=\"config.yaml\"\napisix:\n  node_listen:\n    - ${{APISIX_NODE_LISTEN:=}}\ndeployment:\n  admin:\n    admin_listen:\n      port: ${{DEPLOYMENT_ADMIN_ADMIN_LISTEN:=}}\n```\n\nWhen you run APISIX, you can set these environment variables dynamically:\n\n```shell\nexport APISIX_NODE_LISTEN=8132\nexport DEPLOYMENT_ADMIN_ADMIN_LISTEN=9232\n```\n\n:::caution\n\nYou should set these variables with `export`. If you do not export, APISIX will fail to resolve for these variables.\n\n:::\n\nNow when you start APISIX, it will listen on port `8132` and expose the Admin API on port `9232`.\n\nTo use default values if no environment variables are set, you can add it to your configuration file as shown below:\n\n```yaml title=\"config.yaml\"\napisix:\n  node_listen:\n    - ${{APISIX_NODE_LISTEN:=9080}}\ndeployment:\n  admin:\n    admin_listen:\n      port: ${{DEPLOYMENT_ADMIN_ADMIN_LISTEN:=9180}}\n```\n\nNow if you don't specify these environment variables when running APISIX, it will fall back to the default values and expose the Admin API on port `9180` and listen on port `9080`.\n\nSimilarly, you can also use environment variables in `apisix.yaml` when deploying APISIX in standalone mode.\n\nFor example, you can export the upstream address and port to environment variables:\n\n```shell\nexport HOST_ADDR=httpbin.org\nexport HOST_PORT=80\n```\n\nThen create a route as such:\n\n```yaml title=\"apisix.yaml\"\nroutes:\n  -\n    uri: \"/anything\"\n    upstream:\n      nodes:\n        \"${{HOST_ADDR}}:${{HOST_PORT}}\": 1\n      type: roundrobin\n#END\n```\n\nInitialize and start APISIX in standalone mode, requests to `/anything` should now be forwarded to `httpbin.org:80/anything`.\n\n*WARNING*: When using docker to deploy APISIX in standalone mode. New environment variables added to `apisix.yaml` while APISIX has been initialized will only take effect after a reload.\n\n## Using the `APISIX_PROFILE` environment variable\n\nIf you have multiple configuration changes for multiple environments, it might be better to have a different configuration file for each.\n\nAlthough this might increase the number of configuration files, you would be able to manage each independently and can even do version management.\n\nAPISIX uses the `APISIX_PROFILE` environment variable to switch between environments, i.e. to switch between different sets of configuration files. If the value of `APISIX_PROFILE` is `env`, then APISIX will look for the configuration files `conf/config-env.yaml`, `conf/apisix-env.yaml`, and `conf/debug-env.yaml`.\n\nFor example for the production environment, you can have:\n\n* conf/config-prod.yaml\n* conf/apisix-prod.yaml\n* conf/debug-prod.yaml\n\nAnd for the development environment:\n\n* conf/config-dev.yaml\n* conf/apisix-dev.yaml\n* conf/debug-dev.yaml\n\nAnd if no environment is specified, APISIX can use the default configuration files:\n\n* conf/config.yaml\n* conf/apisix.yaml\n* conf/debug.yaml\n\nTo use a particular configuration, you can specify it in the environment variable:\n\n```shell\nexport APISIX_PROFILE=prod\n```\n\nAPISIX will now use the `-prod.yaml` configuration files.\n"
  },
  {
    "path": "docs/en/latest/pubsub/kafka.md",
    "content": "---\ntitle: Apache Kafka\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - PubSub\n  - Kafka\ndescription: This document contains information about the Apache APISIX kafka pubsub scenario.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Connect to Apache Kafka\n\nConnecting to Apache Kafka in Apache APISIX is very simple.\n\nCurrently, we provide a simpler way to integrate by combining two APIs, ListOffsets and Fetch, to quickly implement the ability to pull Kafka messages. Still, they do not support Apache Kafka's consumer group feature for now and cannot be managed for offsets by Apache Kafka.\n\n### Limitations\n\n- Offsets need to be managed manually\n\nThey can be stored by a custom backend service or obtained via the list_offset command before starting to fetch the message, which can use timestamp to get the starting offset, or to get the initial and end offsets.\n\n- Unsupported batch data acquisition\n\nA single instruction can only obtain the data of a Topic Partition, does not support batch data acquisition through a single instruction\n\n### Prepare\n\nFirst, it is necessary to compile the [communication protocol](https://github.com/apache/apisix/blob/master/apisix/include/apisix/model/pubsub.proto) as a language-specific SDK using the `protoc`, which provides the command and response definitions to connect to Kafka via APISIX using the WebSocket.\n\nThe `sequence` field in the protocol is used to associate the request with the response, they will correspond one to one, the client can manage it in the way they want, APISIX will not modify it, only pass it back to the client through the response body.\n\nThe following commands are currently used by Apache Kafka connect：\n\n- CmdKafkaFetch\n- CmdKafkaListOffset\n\n> The `timestamp` field in the `CmdKafkaListOffset` command supports the following value:\n>\n> - `unix timestamp`: Offset of the first message after the specified timestamp\n> - `-1`：Offset of the last message of the current Partition\n> - `-2`：Offset of the first message of current Partition\n>\n> For more information, see [Apache Kafka Protocol Documentation](https://kafka.apache.org/protocol.html#The_Messages_ListOffsets)\n\nPossible response body: When an error occurs, `ErrorResp` will be returned, which includes the error string; the rest of the response will be returned after the execution of the particular command.\n\n- ErrorResp\n- KafkaFetchResp\n- KafkaListOffsetResp\n\n### How to use\n\n#### Create route\n\nCreate a route, set the upstream `scheme` field to `kafka`, and configure `nodes` to be the address of the Kafka broker.\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/kafka' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/kafka\",\n    \"upstream\": {\n        \"nodes\": {\n            \"kafka-server1:9092\": 1,\n            \"kafka-server2:9092\": 1,\n            \"kafka-server3:9092\": 1\n        },\n        \"type\": \"none\",\n        \"scheme\": \"kafka\"\n    }\n}'\n```\n\nAfter configuring the route, you can use this feature.\n\n#### Enabling TLS and SASL/PLAIN authentication\n\nSimply turn on the `kafka-proxy` plugin on the created route and enable the Kafka TLS handshake and SASL authentication through the configuration, which can be found in the [plugin documentation](../../../en/latest/plugins/kafka-proxy.md).\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/kafka' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/kafka\",\n    \"plugins\": {\n        \"kafka-proxy\": {\n            \"sasl\": {\n                \"username\": \"user\",\n                \"password\": \"pwd\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"kafka-server1:9092\": 1,\n            \"kafka-server2:9092\": 1,\n            \"kafka-server3:9092\": 1\n        },\n        \"type\": \"none\",\n        \"scheme\": \"kafka\",\n        \"tls\": {\n            \"verify\": true\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/pubsub.md",
    "content": "---\ntitle: PubSub\nkeywords:\n  - APISIX\n  - PubSub\ndescription: This document contains information about the Apache APISIX pubsub framework.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## What is PubSub\n\nPublish-subscribe is a messaging paradigm:\n\n- Producers send messages to specific brokers rather than directly to consumers.\n- Brokers cache messages sent by producers and then actively push them to subscribed consumers or pull them.\n\nThe system architectures use this pattern to decouple or handle high traffic scenarios.\n\nIn Apache APISIX, the most common scenario is handling north-south traffic from the server to the client. Combining it with a publish-subscribe system, we can achieve more robust features, such as real-time collaboration on online documents, online games, etc.\n\n## Architecture\n\n![pubsub architecture](../../assets/images/pubsub-architecture.svg)\n\nCurrently, Apache APISIX supports WebSocket communication with the client, which can be any application that supports WebSocket, with Protocol Buffer as the serialization mechanism, see the [protocol definition](https://github.com/apache/apisix/blob/master/apisix/include/apisix/model/pubsub.proto).\n\n## Supported messaging systems\n\n- [Apache Kafka](pubsub/kafka.md)\n\n## How to support other messaging systems\n\nApache APISIX implement an extensible pubsub module, which is responsible for starting the WebSocket server, coding and decoding communication protocols, handling client commands, and adding support for the new messaging system.\n\n### Basic Steps\n\n- Add new commands and response body definitions to `pubsub.proto`\n- Add a new option to the `scheme` configuration item in upstream\n- Add a new `scheme` judgment branch to `http_access_phase`\n- Implement the required message system instruction processing functions\n- Optional: Create plugins to support advanced configurations of this messaging system\n\n### Example of Apache Kafka\n\n#### Add new commands and response body definitions to `pubsub.proto`\n\nThe core of the protocol definition in `pubsub.proto` is the two parts `PubSubReq` and `PubSubResp`.\n\nFirst, create the `CmdKafkaFetch` command and add the required parameters. Then, register this command in the list of commands for `req` in `PubSubReq`, which is named `cmd_kafka_fetch`.\n\nThen create the corresponding response body `KafkaFetchResp` and register it in the `resp` of `PubSubResp`, named `kafka_fetch_resp`.\n\nThe protocol definition [pubsub.proto](https://github.com/apache/apisix/blob/master/apisix/include/apisix/model/pubsub.proto).\n\n#### Add a new option to the `scheme` configuration item in upstream\n\nAdd a new option `kafka` to the `scheme` field enumeration in the `upstream` of `apisix/schema_def.lua`.\n\nThe schema definition [schema_def.lua](https://github.com/apache/apisix/blob/master/apisix/schema_def.lua).\n\n#### Add a new `scheme` judgment branch to `http_access_phase`\n\nAdd a `scheme` judgment branch to the `http_access_phase` function in `apisix/init.lua` to support the processing of `kafka` type upstreams. Because Apache Kafka has its clustering and partition scheme, we do not need to use the Apache APISIX built-in load balancing algorithm, so we intercept and take over the processing flow before selecting the upstream node, using the `kafka_access_phase` function.\n\nThe APISIX init file [init.lua](https://github.com/apache/apisix/blob/master/apisix/init.lua).\n\n#### Implement the required message system commands processing functions\n\nFirst, create an instance of the `pubsub` module, which is provided in the `core` package.\n\nThen, an instance of the Apache Kafka client is created and omitted code here.\n\nNext, add the command registered in the protocol definition above to the `pubsub` instance, which will provide a callback function that provides the parameters parsed from the communication protocol, in which the developer needs to call the kafka client to get the data and return it to the `pubsub` module as the function return value.\n\n:::note Callback function prototype\n\nThe `params` is the data in the protocol definition; the first return value is the data, which needs to contain the fields in the response body definition, and returns the `nil` value when there is an error; the second return value is the error, and returns the error string when there is an error\n\n:::\n\nFinally, it enters the loop to wait for client commands, and when an error occurs, it returns the error and stops the processing flow.\n\nThe kafka pubsub implementation [kafka.lua](https://github.com/apache/apisix/blob/master/apisix/pubsub/kafka.lua).\n\n#### Optional: Create plugins to support advanced configurations of this messaging system\n\nAdd the required fields to the plugin schema definition and write them to the context of the current request in the `access` function.\n\nThe `kafka-proxy` plugin [kafka-proxy.lua](https://github.com/apache/apisix/blob/master/apisix/plugins/kafka-proxy.lua).\n\nAdd this plugin to [the existing list of plugins](https://github.com/apache/apisix/blob/master/apisix/cli/config.yaml.example) in the APISIX configuration file [`config.yaml`](https://github.com/apache/apisix/blob/master/conf/config.yaml). For instance:\n\n```yaml title=\"conf/config.yaml\"\nplugins:         # see `conf/config.yaml.example` for an example\n  - ...          # add existing plugins\n  - kafka-proxy\n```\n\n#### Results\n\nAfter this is done, create a route like the one below to connect to this messaging system via APISIX using the WebSocket.\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/kafka' \\\n    -H 'X-API-KEY: ${api-key}' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/kafka\",\n    \"plugins\": {\n        \"kafka-proxy\": {\n            \"sasl\": {\n              \"username\": \"user\",\n              \"password\": \"pwd\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"kafka-server1:9092\": 1,\n            \"kafka-server2:9092\": 1,\n            \"kafka-server3:9092\": 1\n        },\n        \"type\": \"none\",\n        \"scheme\": \"kafka\",\n        \"tls\": {\n            \"verify\": true\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/router-radixtree.md",
    "content": "---\nTitle: Router Radixtree\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### What is Libradixtree?\n\n[Libradixtree](https://github.com/api7/lua-resty-radixtree) is an adaptive radix tree that is implemented in Lua for OpenResty and it is based on FFI for [rax](https://github.com/antirez/rax). APISIX uses libradixtree as a route dispatching library.\n\n### How to use Libradixtree in APISIX?\n\nThere are several ways to use Libradixtree in APISIX. Let's take a look at a few examples and have an intuitive understanding.\n\n#### 1. Full match\n\n```\n/blog/foo\n```\n\nIt will only match the full path `/blog/foo`.\n\n#### 2. Prefix matching\n\n```\n/blog/bar*\n```\n\nIt will match the path with the prefix `/blog/bar`. For example, `/blog/bar/a`,\n`/blog/bar/b`, `/blog/bar/c/d/e`, `/blog/bar` etc.\n\n#### 3. Match priority\n\nFull match has a higher priority than deep prefix matching.\n\nHere are the rules:\n\n```\n/blog/foo/*\n/blog/foo/a/*\n/blog/foo/c/*\n/blog/foo/bar\n```\n\n| path | Match result |\n|------|--------------|\n|/blog/foo/bar | `/blog/foo/bar` |\n|/blog/foo/a/b/c | `/blog/foo/a/*` |\n|/blog/foo/c/d | `/blog/foo/c/*` |\n|/blog/foo/gloo | `/blog/foo/*` |\n|/blog/bar | not match |\n\n#### 4. Different routes have the same `uri`\n\nWhen different routes have the same `uri`, you can set the priority field of the route to determine which route to match first, or add other matching rules to distinguish different routes.\n\nNote: In the matching rules, the `priority` field takes precedence over other rules except `uri`.\n\n1. Different routes have the same `uri` but different `priority` field\n\nCreate two routes with different `priority` values ​​(the larger the value, the higher the priority).\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"priority\": 3,\n    \"uri\": \"/hello\"\n}'\n```\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1981\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"priority\": 2,\n    \"uri\": \"/hello\"\n}'\n```\n\nTest:\n\n```shell\ncurl http://127.0.0.1:1980/hello\n1980\n```\n\nAll requests will only hit the route of port `1980` because it has a priority of 3 while the route with the port of `1981` has a priority of 2.\n\n2. Different routes have the same `uri` but different matching conditions\n\nTo understand this, look at the example of setting host matching rules:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"hosts\": [\"localhost.com\"],\n    \"uri\": \"/hello\"\n}'\n```\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1981\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"hosts\": [\"test.com\"],\n    \"uri\": \"/hello\"\n}'\n```\n\nTest:\n\n```shell\n$ curl http://127.0.0.1:9080/hello -H 'host: localhost.com'\n1980\n```\n\n```shell\n$ curl http://127.0.0.1:9080/hello -H 'host: test.com'\n1981\n```\n\n```shell\n$ curl http://127.0.0.1:9080/hello\n{\"error_msg\":\"404 Route Not Found\"}\n```\n\nIf the `host` rule matches, the request hits the corresponding upstream, and if the `host` does not match, the request returns a 404 message.\n\n#### 5. Parameter match\n\nWhen `radixtree_uri_with_parameter` is used, we can match routes with parameters.\n\nFor example, with configuration:\n\n```yaml\napisix:\n    router:\n        http: 'radixtree_uri_with_parameter'\n```\n\nroute like\n\n```\n/blog/:name\n```\n\nwill match both `/blog/dog` and `/blog/cat`.\n\nFor more details, see https://github.com/api7/lua-resty-radixtree/#parameters-in-path.\n\n### How to filter route by Nginx built-in variable?\n\nNginx provides a variety of built-in variables that can be used to filter routes based on certain criteria. Here is an example of how to filter routes by Nginx built-in variables:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/index.html\",\n    \"vars\": [\n        [\"http_host\", \"==\", \"iresty.com\"],\n        [\"cookie_device_id\", \"==\", \"a66f0cdc4ba2df8c096f74c9110163a9\"],\n        [\"arg_name\", \"==\", \"json\"],\n        [\"arg_age\", \">\", \"18\"],\n        [\"arg_address\", \"~~\", \"China.*\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nThis route will require the request header `host` equal `iresty.com`, request cookie key `_device_id` equal `a66f0cdc4ba2df8c096f74c9110163a9` etc. You can learn more at [radixtree-new](https://github.com/api7/lua-resty-radixtree#new).\n\n### How to filter route by POST form attributes?\n\nAPISIX supports filtering route by POST form attributes with `Content-Type` = `application/x-www-form-urlencoded`.\n\nWe can define the following route:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/_post\",\n    \"vars\": [\n        [\"post_arg_name\", \"==\", \"json\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nThe route will be matched when the POST form contains `name=json`.\n\n### How to filter route by GraphQL attributes?\n\nAPISIX can handle HTTP GET and POST methods. At the same time, the request body can be a GraphQL query string or JSON-formatted content.\n\nAPISIX supports filtering routes by some attributes of GraphQL. Currently, we support:\n\n* graphql_operation\n* graphql_name\n* graphql_root_fields\n\nFor instance, with GraphQL like this:\n\n```graphql\nquery getRepo {\n    owner {\n        name\n    }\n    repo {\n        created\n    }\n}\n```\n\nWhere\n\n* The `graphql_operation` is `query`\n* The `graphql_name` is `getRepo`,\n* The `graphql_root_fields` is `[\"owner\", \"repo\"]`\n\nWe can filter such route with:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/graphql\",\n    \"vars\": [\n        [\"graphql_operation\", \"==\", \"query\"],\n        [\"graphql_name\", \"==\", \"getRepo\"],\n        [\"graphql_root_fields\", \"has\", \"owner\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nWe can verify GraphQL matches in the following three ways:\n\n1. GraphQL query strings\n\n```shell\n$ curl -H 'content-type: application/graphql' -X POST http://127.0.0.1:9080/graphql -d '\nquery getRepo {\n    owner {\n        name\n    }\n    repo {\n        created\n    }\n}'\n```\n\n2. JSON format\n\n```shell\n$ curl -H 'content-type: application/json' -X POST \\\nhttp://127.0.0.1:9080/graphql --data '{\"query\": \"query getRepo { owner {name } repo {created}}\"}'\n```\n\n3. Try `GET` request match\n\n```shell\n$ curl -H 'content-type: application/graphql' -X GET \\\n\"http://127.0.0.1:9080/graphql?query=query getRepo { owner {name } repo {created}}\" -g\n```\n\nTo prevent spending too much time reading invalid GraphQL request body, we only read the first 1 MiB\ndata from the request body. This limitation is configured via:\n\n```yaml\ngraphql:\n  max_size: 1048576\n\n```\n\nIf you need to pass a GraphQL body which is larger than the limitation, you can increase the value in `conf/config.yaml`.\n\n### How to filter route by POST request JSON body?\n\nAPISIX supports filtering route by POST form attributes with `Content-Type` = `application/json`.\n\nWe can define the following route:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"methods\": [\"POST\"],\n    \"uri\": \"/_post\",\n    \"vars\": [\n        [\"post_arg.name\", \"==\", \"xyz\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nIt will match the following POST request\n\n```shell\ncurl -X POST http://127.0.0.1:9180/_post \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"xyz\"}'\n```\n\nWe can also filter by complex queries like the example below:\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"methods\": [\"POST\"],\n    \"uri\": \"/_post\",\n    \"vars\": [\n         [\"post_arg.messages[*].content[*].type\",\"has\",\"image_url\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nIt will match the following POST request\n\n```shell\ncurl -X POST http://127.0.0.1:9180/_post \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"model\": \"deepseek\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": [\n        {\n          \"text\": \"You are a mathematician\",\n          \"type\": \"text\"\n        },\n        {\n          \"text\": \"You are a mathematician\",\n          \"type\": \"image_url\"\n        }\n      ]\n    }\n  ]\n}'\n\n```\n"
  },
  {
    "path": "docs/en/latest/ssl-protocol.md",
    "content": "---\ntitle: SSL Protocol\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n`APISIX` supports set TLS protocol and also supports dynamically specifying different TLS protocol versions for each [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication).\n\n**For security reasons, the encryption suite used by default in `APISIX` does not support TLSv1.1 and lower versions.**\n**If you need to enable the TLSv1.1 protocol, please add the encryption suite supported by the TLSv1.1 protocol to the configuration item `apisix.ssl.ssl_ciphers` in `config.yaml`.**\n\n## ssl_protocols Configuration\n\n### Static Configuration\n\nThe `ssl_protocols` parameter in the static configuration `config.yaml` applies to the entire APISIX, but cannot be dynamically modified. It only takes effect when the matching SSL resource does not set `ssl_protocols`.\n\n```yaml\napisix:\n  ssl:\n    ssl_protocols: TLSv1.2 TLSv1.3 # default TLSv1.2 TLSv1.3\n```\n\n### Dynamic Configuration\n\nUse the `ssl_protocols` field in the `ssl` resource to dynamically specify different TLS protocol versions for each SNI.\n\nSpecify the `test.com` domain uses the TLSv1.2 and TLSv1.3:\n\n```bash\n{\n    \"cert\": \"$cert\",\n    \"key\": \"$key\",\n    \"snis\": [\"test.com\"],\n    \"ssl_protocols\": [\n        \"TLSv1.2\",\n        \"TLSv1.3\"\n    ]\n}\n```\n\n### Notes\n\n- Dynamic configuration has a higher priority than static configuration. When the `ssl_protocols` configuration item in the ssl resource is not empty, the static configuration will be overridden.\n- The static configuration applies to the entire APISIX and requires a reload of APISIX to take effect.\n- Dynamic configuration can control the TLS protocol version of each SNI in a fine-grained manner and can be dynamically modified, which is more flexible than static configuration.\n\n## Examples\n\n### How to specify the TLSv1.1 protocol\n\nWhile newer products utilize higher security-level TLS protocol versions, there are still legacy clients that rely on the lower-level TLSv1.1 protocol. However, enabling TLSv1.1 for new products presents potential security risks. In order to maintain the security of the API, it is crucial to have the ability to seamlessly switch between different protocol versions based on specific requirements and circumstances.\nFor example, consider two domain names: `test.com`, utilized by legacy clients requiring TLSv1.1 configuration, and `test2.com`, associated with new products that support TLSv1.2 and TLSv1.3 protocols.\n\n1. `config.yaml` configuration.\n\n```yaml\napisix:\n  ssl:\n    ssl_protocols: TLSv1.3\n    # ssl_ciphers is for reference only\n    ssl_ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA\n```\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n2. Specify the TLSv1.1 protocol version for the test.com domain.\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server.crt)\"'\",\n     \"key\": \"'\"$(cat server.key)\"'\",\n     \"snis\": [\"test.com\"],\n     \"ssl_protocols\": [\n         \"TLSv1.1\"\n     ]\n}'\n```\n\n3. Create an SSL object for test.com without specifying the TLS protocol version, which will use the static configuration by default.\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server2.crt)\"'\",\n     \"key\": \"'\"$(cat server2.key)\"'\",\n     \"snis\": [\"test2.com\"]\n}'\n```\n\n4. Access Verification\n\nFailed, accessed test.com with TLSv1.3:\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n```\n\nSuccessfully, accessed test.com with TLSv1.1:\n\n```shell\n$ curl --tls-max 1.1 --tlsv1.1  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.1 (OUT), TLS handshake, Client hello (1):\n* TLSv1.1 (IN), TLS handshake, Server hello (2):\n* TLSv1.1 (IN), TLS handshake, Certificate (11):\n* TLSv1.1 (IN), TLS handshake, Server key exchange (12):\n* TLSv1.1 (IN), TLS handshake, Server finished (14):\n* TLSv1.1 (OUT), TLS handshake, Client key exchange (16):\n* TLSv1.1 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.1 (OUT), TLS handshake, Finished (20):\n* TLSv1.1 (IN), TLS handshake, Finished (20):\n* SSL connection using TLSv1.1 / ECDHE-RSA-AES256-SHA\n```\n\nSuccessfully, accessed test2.com with TLSv1.3:\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n```\n\nFailed, accessed test2.com with TLSv1.1:\n\n```shell\ncurl --tls-max 1.1 --tlsv1.1  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.1 (OUT), TLS handshake, Client hello (1):\n* TLSv1.1 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n```\n\n### Certificates are associated with multiple domains, but different TLS protocols are used between domains\n\nSometimes, we may encounter a situation where a certificate is associated with multiple domains, but they need to use different TLS protocols to ensure security. For example, the test.com domain needs to use the TLSv1.2 protocol, while the test2.com domain needs to use the TLSv1.3 protocol. In this case, we cannot simply create an SSL object for all domains, but need to create an SSL object for each domain separately and specify the appropriate protocol version. This way, we can perform the correct SSL handshake and encrypted communication based on different domains and protocol versions. The example is as follows:\n\n1. Create an SSL object for test.com using the certificate and specify the TLSv1.2 protocol.\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server.crt)\"'\",\n     \"key\": \"'\"$(cat server.key)\"'\",\n     \"snis\": [\"test.com\"],\n     \"ssl_protocols\": [\n         \"TLSv1.2\"\n     ]\n}'\n```\n\n2. Use the same certificate as test.com to create an SSL object for test2.com and specify the TLSv1.3 protocol.\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/2 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server.crt)\"'\",\n     \"key\": \"'\"$(cat server.key)\"'\",\n     \"snis\": [\"test2.com\"],\n     \"ssl_protocols\": [\n         \"TLSv1.3\"\n     ]\n}'\n```\n\n3. Access verification\n\nSuccessfully, accessed test.com with TLSv1.2:\n\n```shell\n$ curl --tls-max 1.2 --tlsv1.2  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.2 (OUT), TLS handshake, Client hello (1):\n* TLSv1.2 (IN), TLS handshake, Server hello (2):\n* TLSv1.2 (IN), TLS handshake, Certificate (11):\n* TLSv1.2 (IN), TLS handshake, Server key exchange (12):\n* TLSv1.2 (IN), TLS handshake, Server finished (14):\n* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):\n* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.2 (OUT), TLS handshake, Finished (20):\n* TLSv1.2 (IN), TLS handshake, Finished (20):\n* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test.com\n*  start date: Jul 20 15:50:08 2023 GMT\n*  expire date: Jul 17 15:50:08 2033 GMT\n*  issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test.com\n*  SSL certificate verify result: EE certificate key too weak (66), continuing anyway.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* Using Stream ID: 1 (easy handle 0x5608905ee2e0)\n> HEAD / HTTP/2\n> Host: test.com:9443\n> user-agent: curl/7.74.0\n> accept: */*\n\n```\n\nFailed, accessed test.com with TLSv1.3:\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n\n```\n\nSuccessfully, accessed test2.com with TLSv1.3:\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test2.com\n*  start date: Jul 20 16:05:47 2023 GMT\n*  expire date: Jul 17 16:05:47 2033 GMT\n*  issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test2.com\n*  SSL certificate verify result: EE certificate key too weak (66), continuing anyway.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* Using Stream ID: 1 (easy handle 0x55569cbe42e0)\n> HEAD / HTTP/2\n> Host: test2.com:9443\n> user-agent: curl/7.74.0\n> accept: */*\n>\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* old SSL session ID is stale, removing\n```\n\nFailed, accessed test2.com with TLSv1.2:\n\n```shell\n$ curl --tls-max 1.2 --tlsv1.2  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.2 (OUT), TLS handshake, Client hello (1):\n* TLSv1.2 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n```\n"
  },
  {
    "path": "docs/en/latest/status-api.md",
    "content": "---\ntitle: Status API\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nIn Apache APISIX, the status API is used to:\n\n* Check if APISIX has successfully started and running correctly.\n* Check if all of the workers have received and loaded the configuration.\n\nTo change the default endpoint (`127.0.0.1:7085`) of the Status API server, change the `ip` and `port` in the `status` section in your configuration file (`conf/config.yaml`):\n\n```yaml\napisix:\n  status:\n    ip: \"127.0.0.1\"\n    port: 7085\n```\n\nThis API can be used to perform readiness probes on APISIX before APISIX starts receiving user requests.\n\n### GET /status\n\nReturns a JSON reporting the status of APISIX workers. If APISIX is not running, the request will error out while establishing TCP connection. Otherwise this endpoint will always return ok if request reaches a running worker.\n\n```json\n{\n  \"status\": \"ok\"\n}\n```\n\n### GET /status/ready\n\nReturns `ok` when all workers have loaded the configuration, otherwise returns the specific error with `503` error code. Below are specific examples.\n\nWhen all workers have loaded the configuration:\n\n```json\n{\n  \"status\": \"ok\"\n}\n```\n\nWhen 1 workers has't been initialised:\n\n```json\n{\n  \"status\": \"error\",\n  \"error\": \"worker count: 16 but status report count: 15\"\n}\n```\n\nWhen a particular worker hasn't loaded the configuration:\n\n```json\n{\n  \"error\": \"worker id: 9 has not received configuration\",\n  \"status\": \"error\"\n}\n```\n"
  },
  {
    "path": "docs/en/latest/stream-proxy.md",
    "content": "---\ntitle: Stream Proxy\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nA stream proxy operates at the transport layer, handling stream-oriented traffic based on TCP and UDP protocols. TCP is used for many applications and services, such as LDAP, MySQL, and RTMP. UDP is used for many popular non-transactional applications, such as DNS, syslog, and RADIUS.\n\nAPISIX can serve as a stream proxy, in addition to being an application layer proxy.\n\n## How to enable stream proxy?\n\nBy default, stream proxy is disabled.\n\nTo enable this option, set `apisix.proxy_mode` to `stream` or `http&stream`, depending on whether you want stream proxy only or both http and stream. Then add the `apisix.stream_proxy` option in `conf/config.yaml` and specify the list of addresses where APISIX should act as a stream proxy and listen for incoming requests.\n\n```yaml\napisix:\n  proxy_mode: http&stream  # enable both http and stream proxies\n  stream_proxy:\n    tcp:\n      - 9100 # listen on 9100 ports of all network interfaces for TCP requests\n      - \"127.0.0.1:9101\"\n    udp:\n      - 9200 # listen on 9200 ports of all network interfaces for UDP requests\n      - \"127.0.0.1:9211\"\n```\n\nIf `apisix.stream_proxy` is undefined in `conf/config.yaml`, you will encounter an error similar to the following and not be able to add a stream route:\n\n```\n{\"error_msg\":\"stream mode is disabled, can not add stream routes\"}\n```\n\n## How to set a route?\n\nYou can create a stream route using the Admin API `/stream_routes` endpoint. For example:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"remote_addr\": \"192.168.5.3\",\n    \"upstream\": {\n        \"nodes\": {\n            \"192.168.4.10:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nWith this configuration, APISIX would only forward the request to the upstream service at `192.168.4.10:1995` if and only if the request is sent from `192.168.5.3`. See the next section to learn more about filtering options.\n\nMore examples can be found in [test cases](https://github.com/apache/apisix/blob/master/t/stream-node/sanity.t).\n\n## More stream route filtering options\n\nCurrently there are three attributes in stream routes that can be used for filtering requests:\n\n- `server_addr`: The address of the APISIX server that accepts the L4 stream connection.\n- `server_port`: The port of the APISIX server that accepts the L4 stream connection.\n- `remote_addr`: The address of client from which the request has been made.\n\nHere is an example:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"server_addr\": \"127.0.0.1\",\n    \"server_port\": 2000,\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nIt means APISIX will proxy the request to `127.0.0.1:1995` when the server address is `127.0.0.1` and the server port is equal to `2000`.\n\nHere is an example with MySQL:\n\n1. Put this config inside `config.yaml`\n\n   ```yaml\n   apisix:\n     proxy_mode: http&stream  # enable both http and stream proxies\n     stream_proxy: # TCP/UDP proxy\n       tcp: # TCP proxy address list\n         - 9100 # by default uses 0.0.0.0\n         - \"127.0.0.10:9101\"\n   ```\n\n2. Now run a mysql docker container and expose port 3306 to the host\n\n   ```shell\n   $ docker run --name mysql -e MYSQL_ROOT_PASSWORD=toor -p 3306:3306 -d mysql mysqld --default-authentication-plugin=mysql_native_password\n   # check it using a mysql client that it works\n   $ mysql --host=127.0.0.1 --port=3306 -u root -p\n   Enter password:\n   Welcome to the MySQL monitor.  Commands end with ; or \\g.\n   Your MySQL connection id is 25\n   ...\n   mysql>\n   ```\n\n3. Now we are going to create a stream route with server filtering:\n\n   ```shell\n   curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n   {\n       \"server_addr\": \"127.0.0.10\",\n       \"server_port\": 9101,\n       \"upstream\": {\n           \"nodes\": {\n               \"127.0.0.1:3306\": 1\n           },\n           \"type\": \"roundrobin\"\n       }\n   }'\n   ```\n\n   It only forwards the request to the mysql upstream whenever a connection is received at APISIX server `127.0.0.10` and port `9101`. Let's test that behaviour:\n\n4. Making a request to 9100 (stream proxy port enabled inside config.yaml), filter matching fails.\n\n   ```shell\n   $ mysql --host=127.0.0.1 --port=9100 -u root -p\n   Enter password:\n   ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 2\n\n   ```\n\n   Instead making a request to the APISIX host and port where the filter matching succeeds:\n\n   ```shell\n   mysql --host=127.0.0.10 --port=9101 -u root -p\n   Enter password:\n   Welcome to the MySQL monitor.  Commands end with ; or \\g.\n   Your MySQL connection id is 26\n   ...\n   mysql>\n   ```\n\nRead [Admin API's Stream Route section](./admin-api.md#stream-route) for the complete options list.\n\n## Accept TLS over TCP connection\n\nAPISIX can accept TLS over TCP connection.\n\nFirst of all, we need to enable TLS for the TCP address:\n\n```yaml\napisix:\n  proxy_mode: http&stream  # enable both http and stream proxies\n  stream_proxy: # TCP/UDP proxy\n    tcp: # TCP proxy address list\n      - addr: 9100\n        tls: true\n```\n\nSecond, we need to configure certificate for the given SNI.\nSee [Admin API's SSL section](./admin-api.md#ssl) for how to do.\nmTLS is also supported, see [Protect Route](./mtls.md#protect-route) for how to do.\n\nThird, we need to configure a stream route to match and proxy it to the upstream:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nWhen the connection is TLS over TCP, we can use the SNI to match a route, like:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"sni\": \"a.test.com\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:5991\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nIn this case, a connection handshaked with SNI `a.test.com` will be proxied to `127.0.0.1:5991`.\n\n## Proxy to TLS over TCP upstream\n\nAPISIX also supports proxying to TLS over TCP upstream.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"scheme\": \"tls\",\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\nBy setting the `scheme` to `tls`, APISIX will do TLS handshake with the upstream.\n\nWhen the client is also speaking TLS over TCP, the SNI from the client will pass through to the upstream. Otherwise, a dummy SNI `apisix_backend` will be used.\n"
  },
  {
    "path": "docs/en/latest/support-fips-in-apisix.md",
    "content": "---\nid: support-fips-in-apisix\ntitle: Support FIPS in APISIX\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Code Contribution\n  - Building APISIX\n  - OpenSSL 3.0 FIPS\ndescription: Compile apisix-runtime with OpenSSL 3.0 (FIPS enabled)\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nOpenSSL 3.0 [supports](https://www.openssl.org/blog/blog/2022/08/24/FIPS-validation-certificate-issued/) [FIPS](https://en.wikipedia.org/wiki/FIPS_140-2) mode. To support FIPS in APISIX, you can compile apisix-runtime with OpenSSL 3.0.\n\n## Compilation\n\nTo compile apisix-runtime with OpenSSL 3.0, run the commands below as root user:\n\n```bash\ncd $(mktemp -d)\nOPENSSL3_PREFIX=${OPENSSL3_PREFIX-/usr/local}\napt install -y build-essential\ngit clone https://github.com/openssl/openssl\ncd openssl\n./Configure --prefix=$OPENSSL3_PREFIX/openssl-3.0 enable-fips\nmake install\necho $OPENSSL3_PREFIX/openssl-3.0/lib64 > /etc/ld.so.conf.d/openssl3.conf\nldconfig\n$OPENSSL3_PREFIX/openssl-3.0/bin/openssl fipsinstall -out $OPENSSL3_PREFIX/openssl-3.0/ssl/fipsmodule.cnf -module $OPENSSL3_PREFIX/openssl-3.0/lib64/ossl-modules/fips.so\nsed -i 's@# .include fipsmodule.cnf@.include '\"$OPENSSL3_PREFIX\"'/openssl-3.0/ssl/fipsmodule.cnf@g; s/# \\(fips = fips_sect\\)/\\1\\nbase = base_sect\\n\\n[base_sect]\\nactivate=1\\n/g' $OPENSSL3_PREFIX/openssl-3.0/ssl/openssl.cnf\ncd ..\n\nexport cc_opt=\"-I$OPENSSL3_PREFIX/openssl-3.0/include\"\nexport ld_opt=\"-L$OPENSSL3_PREFIX/openssl-3.0/lib64 -Wl,-rpath,$OPENSSL3_PREFIX/openssl-3.0/lib64\"\n\nwget --no-check-certificate https://raw.githubusercontent.com/api7/apisix-build-tools/master/build-apisix-runtime.sh\nchmod +x build-apisix-runtime.sh\n./build-apisix-runtime.sh\n```\n\nThis will install apisix-runtime to `/usr/local/openresty`.\n"
  },
  {
    "path": "docs/en/latest/terminology/api-gateway.md",
    "content": "---\ntitle: API Gateway\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - Gateway\ndescription: This article mainly introduces the role of the API gateway and why it is needed.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nAn API gateway is a software pattern that sits in front of an application programming interface (API) or group of microservices, to facilitate requests and delivery of data and services. Its primary role is to act as a single entry point and standardized process for interactions between an organization's apps, data, and services and internal and external customers. The API gateway can also perform various other functions to support and manage API usage, from authentication to rate limiting to analytics.\n\nAn API gateway also acts as a gateway between the API and the underlying infrastructure. It can be used to route requests to different backends, such as a load balancer, or route requests to different services based on the request headers.\n\n## Why use an API gateway?\n\nAn API gateway comes with a lot of benefits over a traditional API microservice. The following are some of the benefits:\n\n- It is a single entry point for all API requests.\n- It can be used to route requests to different backends, such as a load balancer, or route requests to different services based on the request headers.\n- It can be used to perform authentication, authorization, and rate-limiting.\n- It can be used to support analytics, such as monitoring, logging, and tracing.\n- It can protect the API from malicious attack vectors such as SQL injections, DDOS attacks, and XSS.\n- It decreases the complexity of the API and microservices.\n"
  },
  {
    "path": "docs/en/latest/terminology/consumer-group.md",
    "content": "---\ntitle: Consumer Group\nkeywords:\n  - API gateway\n  - Apache APISIX\n  - Consumer Group\ndescription: Consumer Group in Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nConsumer Groups are used to extract commonly used [Plugin](./plugin.md) configurations and can be bound directly to a [Consumer](./consumer.md).\n\nWith consumer groups, you can define any number of plugins, e.g. rate limiting and apply them to a set of consumers,\ninstead of managing each consumer individually.\n\n## Example\n\nThe example below illustrates how to create a Consumer Group and bind it to a Consumer.\n\nCreate a Consumer Group which shares the same rate limiting quota:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumer_groups/company_a \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 200,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"group\": \"grp_company_a\"\n        }\n    }\n}'\n```\n\nCreate a Consumer within the Consumer Group:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    },\n    \"group_id\": \"company_a\"\n}'\n```\n\nWhen APISIX can't find the Consumer Group with the `group_id`, the Admin API is terminated with a status code of `400`.\n\n:::tip\n\n1. When the same plugin is configured in [consumer](./consumer.md), [routing](./route.md), [plugin config](./plugin-config.md) and [service](./service.md), only one configuration is in effect, and the consumer has the highest priority. Please refer to [Plugin](./plugin.md).\n2. If a Consumer already has the `plugins` field configured, the plugins in the Consumer Group will effectively be merged into it. The same plugin in the Consumer Group will not override the one configured directly in the Consumer.\n\n:::\n\nFor example, if we configure a Consumer Group as shown below:\n\n```json\n{\n    \"id\": \"bar\",\n    \"plugins\": {\n        \"response-rewrite\": {\n            \"body\": \"hello\"\n        }\n    }\n}\n```\n\nTo a Consumer as shown below.\n\n```json\n{\n    \"username\": \"foo\",\n    \"group_id\": \"bar\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\": \"foo\",\n            \"password\": \"bar\"\n        },\n        \"response-rewrite\": {\n            \"body\": \"world\"\n        }\n    }\n}\n```\n\nThen the `body` in `response-rewrite` keeps `world`.\n"
  },
  {
    "path": "docs/en/latest/terminology/consumer.md",
    "content": "---\ntitle: Consumer\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - APISIX Consumer\n  - Consumer\ndescription: This article describes the role of the Apache APISIX Consumer object and how to use the Consumer.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nFor an API gateway, it is usually possible to identify the type of the requester by using things like their request domain name and client IP address. A gateway like APISIX can then filter these requests using [Plugins](./plugin.md) and forward it to the specified [Upstream](./upstream.md).\n\nIt has the highest priority: Consumer > Route > Plugin Config > Service.\n\nBut this level of depth can be insufficient on some occasions.\n\n![consumer-who](../../../assets/images/consumer-who.png)\n\nAn API gateway should know who the consumer of the API is to configure different rules for different consumers. This is where the **Consumer** construct comes in APISIX.\n\n### Configuration options\n\nThe fields for defining a Consumer are defined as below.\n\n| Field      | Required | Description                                                                                                                                                                      |\n| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `username` | True      | Name of the consumer.                                                                                                                                                             |\n| `plugins`  | False       | Plugin configuration of the **Consumer**. For specific Plugin configurations, please refer the [Plugins](./plugin.md). |\n\n## Identifying a Consumer\n\nThe process of identifying a Consumer in APISIX is described below:\n\n![consumer-internal](../../../assets/images/consumer-internal.png)\n\n1. The first step is Authentication. This is achieved by Authentication Plugins like [key-auth](../plugins/key-auth.md) and [JWT](../plugins/jwt-auth.md).\n2. After authenticating, you can obtain the `id` of the Consumer. This `id` will be the unique identifier of a Consumer.\n3. The configurations like Plugins and Upstream bound to the Consumer are then executed.\n\nConsumers are useful when you have different consumers requesting the same API and you need to execute different Plugin and Upstream configurations based on the consumer. These need to be used in conjunction with the user authentication system.\n\nAuthentication plugins that can be configured with a Consumer include `basic-auth`, `hmac-auth`, `jwt-auth`, `key-auth`, `ldap-auth`, and `wolf-rbac`.\n\nRefer to the documentation for the [key-auth](../plugins/key-auth.md) authentication Plugin to further understand the concept of a Consumer.\n\n:::note\n\nFor more information about the Consumer object, you can refer to the [Admin API Consumer](../admin-api.md#consumer) object resource introduction.\n\n:::\n\n## Example\n\nThe example below shows how you can enable a Plugin for a specific Consumer.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. Create a Consumer, specify the authentication plugin `key-auth`, and enable the specific plugin `limit-count`.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\",\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-one\"\n            },\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n2. Create a Router, set routing rules and enable plugin configuration.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {}\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        },\n        \"uri\": \"/hello\"\n    }'\n    ```\n\n3. Send a test request, the first two return to normal, did not reach the speed limit threshold.\n\n    ```shell\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I\n    ```\n\n    The third test returns `503` and the request is restricted.\n\n    ```shell\n    HTTP/1.1 503 Service Temporarily Unavailable\n    ...\n    ```\n\nWe can use the [consumer-restriction](../plugins/consumer-restriction.md) Plugin to restrict our user \"Jack\" from accessing the API.\n\n1. Add Jack to the blacklist.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {},\n            \"consumer-restriction\": {\n                \"blacklist\": [\n                    \"jack\"\n                ]\n            }\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        },\n        \"uri\": \"/hello\"\n    }'\n    ```\n\n2. Repeated tests, all return `403`; Jack is forbidden to access this API.\n\n    ```shell\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I\n    ```\n\n    ```shell\n    HTTP/1.1 403\n    ...\n    ```\n"
  },
  {
    "path": "docs/en/latest/terminology/credential.md",
    "content": "---\ntitle: Credential\nkeywords:\n  - APISIX\n  - API Gateway\n  - Consumer\n  - Credential\ndescription: This article describes what the Apache APISIX Credential object does and how to use it.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nCredential is the object that holds the [Consumer](./consumer.md) credential configuration.\nA Consumer can use multiple credentials of different types.\nCredentials are used when you need to configure multiple credentials for a Consumer.\n\nCurrently, Credential can be configured with the authentication plugins `basic-auth`, `hmac-auth`, `jwt-auth`, and `key-auth`.\n\n### Configuration options\n\nThe fields for defining a Credential are defined as below.\n\n| Field      | Required | Description                                                                                             |\n|---------|----------|---------------------------------------------------------------------------------------------------------|\n| desc    | False    | Description of the Credential.                                                                          |\n| labels  | False    | Labels of the Credential.                                                                               |\n| plugins | False    | The plugin configuration corresponding to Credential. For more information, see [Plugins](./plugin.md). |\n\n:::note\n\nFor more information about the Credential object, you can refer to the [Admin API Credential](../admin-api.md#credential) resource guide.\n\n:::\n\n## Example\n\n[Consumer Example](./consumer.md#example) describes how to configure the auth plugin for Consumer and how to use it with other plugins.\nIn this example, the Consumer has only one credential of type key-auth.\nNow suppose the user needs to configure multiple credentials for that Consumer, you can use Credential to support this.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. Create the Consumer without specifying the auth plug-in, but use Credential to configure the auth plugin later.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\"\n    }'\n    ```\n\n2. Create 2 `key-auth` Credentials for the Consumer.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/key-auth-one \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-one\"\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/key-auth-two \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-two\"\n            }\n        }\n    }'\n    ```\n\n3. Create a route and enable `key-auth` plugin on it.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {}\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        },\n        \"uri\": \"/hello\"\n    }'\n    ```\n\n4. Test.\n\n    Test the request with the `auth-one` and `auth-two` keys, and they both respond correctly.\n\n    ```shell\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-two' -I\n    ```\n\n    Enable the `limit-count` plugin for the Consumer.\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\",\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n    Requesting the route more than 3 times in a row with each of the two keys, the test returns `503` and the request is restricted.\n"
  },
  {
    "path": "docs/en/latest/terminology/global-rule.md",
    "content": "---\ntitle: Global Rules\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Global Rules\ndescription: This article describes how to use global rules.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nA [Plugin](./plugin.md) configuration can be bound directly to a [Route](./route.md), a [Service](./service.md) or a [Consumer](./consumer.md). But what if we want a Plugin to work on all requests? This is where we register a global Plugin with Global Rule.\n\nCompared with the plugin configuration in Route, Service, Plugin Config, and Consumer, the plugin in the Global Rules is always executed first.\n\n## Example\n\nThe example below shows how you can use the `limit-count` Plugin on all requests:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -X PUT \\\n  http://{apisix_listen_address}/apisix/admin/global_rules/1 \\\n  -H 'Content-Type: application/json' \\\n  -H \"X-API-KEY: $admin_key\" \\\n  -d '{\n        \"plugins\": {\n            \"limit-count\": {\n                \"time_window\": 60,\n                \"policy\": \"local\",\n                \"count\": 2,\n                \"key\": \"remote_addr\",\n                \"rejected_code\": 503\n            }\n        }\n    }'\n```\n\nYou can also list all the Global rules by making this request with the Admin API:\n\n```shell\ncurl http://{apisix_listen_address}/apisix/admin/global_rules -H \"X-API-KEY: $admin_key\"\n```\n"
  },
  {
    "path": "docs/en/latest/terminology/plugin-config.md",
    "content": "---\ntitle: Plugin Config\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Plugin Config\ndescription: Plugin Config in Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nPlugin Configs are used to extract commonly used [Plugin](./plugin.md) configurations and can be bound directly to a [Route](./route.md).\n\nWhile configuring the same plugin, only one copy of the configuration is valid. Please read the [plugin execution order](../terminology/plugin.md#plugins-execution-order) and [plugin merging order](../terminology/plugin.md#plugins-merging-precedence).\n\n## Example\n\nThe example below illustrates how to create a Plugin Config and bind it to a Route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"desc\": \"blah\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503\n        }\n    }\n}'\n```\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H 'X-API-KEY:edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '\n{\n    \"uris\": [\"/index.html\"],\n    \"plugin_config_id\": 1,\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nWhen APISIX can't find the Plugin Config with the `id`, the requests reaching this Route are terminated with a status code of `503`.\n\n:::note\n\nIf a Route already has the `plugins` field configured, the plugins in the Plugin Config will effectively be merged to it.\n\nThe same plugin in the Plugin Config will not override the ones configured directly in the Route. For more information, see [Plugin](./plugin.md).\n\n:::\n\nFor example, if you configure a Plugin Config as shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \\\n -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"desc\": \"I am plugin_config 1\",\n    \"plugins\": {\n        \"ip-restriction\": {\n            \"whitelist\": [\n                \"127.0.0.0/24\",\n                \"113.74.26.106\"\n            ]\n        },\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503\n        }\n    }\n}'\n```\n\nto a Route as shown below,\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uris\": [\"/index.html\"],\n    \"plugin_config_id\": 1,\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n    \"plugins\": {\n        \"proxy-rewrite\": {\n            \"uri\": \"/test/add\",\n            \"host\": \"apisix.iresty.com\"\n        },\n        \"limit-count\": {\n            \"count\": 20,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    }\n}'\n```\n\nthe effective configuration will be as the one shown below:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uris\": [\"/index.html\"],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n    \"plugins\": {\n        \"ip-restriction\": {\n            \"whitelist\": [\n                \"127.0.0.0/24\",\n                \"113.74.26.106\"\n            ]\n        },\n        \"proxy-rewrite\": {\n            \"uri\": \"/test/add\",\n            \"host\": \"apisix.iresty.com\"\n        },\n        \"limit-count\": {\n            \"count\": 20,\n            \"time_window\": 60,\n            \"rejected_code\": 503\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/en/latest/terminology/plugin-metadata.md",
    "content": "---\ntitle: Plugin Metadata\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Plugin Metadata\ndescription: Plugin Metadata in Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nIn this document, you will learn the basic concept of plugin metadata in APISIX and why you may need them.\n\nExplore additional resources at the end of the document for more information on related topics.\n\n## Overview\n\nIn APISIX, a plugin metadata object is used to configure the common metadata field(s) of all plugin instances sharing the same plugin name. It is useful when a plugin is enabled across multiple objects and requires a universal update to their metadata fields.\n\nThe following diagram illustrates the concept of plugin metadata using two instances of [syslog](https://apisix.apache.org/docs/apisix/plugins/syslog/) plugins on two different routes, as well as a plugin metadata object setting a [global](https://apisix.apache.org/docs/apisix/plugins/syslog/) `log_format` for the syslog plugin:\n\n![plugin_metadata](https://static.apiseven.com/uploads/2023/04/17/Z0OFRQhV_plugin%20metadata.svg)\n\nWithout otherwise specified, the `log_format` on plugin metadata object should apply the same log format uniformly to both `syslog` plugins. However, since the `syslog` plugin on the `/orders` route has a different `log_format`, requests visiting this route will generate logs in the `log_format` specified by the plugin in route.\n\nMetadata properties set at the plugin level is more granular and has a higher priority over the \"global\" metadata object.\n\nPlugin metadata objects should only be used for plugins that have metadata fields. Check the specific plugin documentation to know more.\n\n## Example usage\n\nThe example below shows how you can configure through the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/http-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\"\n    }\n}'\n```\n\nWith this configuration, your logs would be formatted as shown below:\n\n```json\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"route_id\":\"1\"}\n```\n\n## Additional Resource(s)\n\nKey Concepts - [Plugins](https://apisix.apache.org/docs/apisix/terminology/plugin/)\n"
  },
  {
    "path": "docs/en/latest/terminology/plugin.md",
    "content": "---\ntitle: Plugin\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Plugin\n  - Filter\n  - Priority\ndescription: This article introduces the related information of the APISIX Plugin object and how to use it, and introduces how to customize the plugin priority, customize the error response, and dynamically control the execution status of the plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nAPISIX Plugins extend APISIX's functionalities to meet organization or user-specific requirements in traffic management, observability, security, request/response transformation, serverless computing, and more.\n\nA **Plugin** configuration can be bound directly to a [`Route`](route.md), [`Service`](service.md), [`Consumer`](consumer.md) or [`Plugin Config`](plugin-config.md). You can refer to [Admin API plugins](../admin-api.md#plugin) for how to use this resource.\n\nIf existing APISIX Plugins do not meet your needs, you can also write your own plugins in Lua or other languages such as Java, Python, Go, and Wasm.\n\n## Plugins installation\n\nBy default, most APISIX plugins are [installed](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua):\n\n```lua title=\"apisix/cli/config.lua\"\nlocal _M = {\n  ...\n  plugins = {\n    \"real-ip\",\n    \"ai\",\n    \"client-control\",\n    \"proxy-control\",\n    \"request-id\",\n    \"zipkin\",\n    \"ext-plugin-pre-req\",\n    \"fault-injection\",\n    \"mocking\",\n    \"serverless-pre-function\",\n    ...\n  },\n  ...\n}\n```\n\nIf you would like to make adjustments to plugins installation, add the customized `plugins` configuration to `config.yaml`. For example:\n\n```yaml\nplugins:\n  - real-ip                   # installed\n  - ai\n  - client-control\n  - proxy-control\n  - request-id\n  - zipkin\n  - ext-plugin-pre-req\n  - fault-injection\n  # - mocking                 # not install\n  - serverless-pre-function\n  ...                         # other plugins\n```\n\nSee `config.yaml.example`(https://github.com/apache/apisix/blob/master/conf/config.yaml.example) for a complete configuration reference.\n\nYou should reload APISIX for configuration changes to take effect.\n\n## Plugins execution lifecycle\n\nAn installed plugin is first initialized. The configuration of the plugin is then checked against the defined [JSON Schema](https://json-schema.org) to make sure the plugins configuration schema is correct.\n\nWhen a request goes through APISIX, the plugin's corresponding methods are executed in one or more of the following phases : `rewrite`, `access`, `before_proxy`, `header_filter`, `body_filter`, and `log`. These phases are largely influenced by the [OpenResty directives](https://openresty-reference.readthedocs.io/en/latest/Directives/).\n\n<br />\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.apiseven.com/uploads/2023/03/09/ZsH5C8Og_plugins-phases.png\" alt=\"Routes Diagram\" width=\"50%\"/>\n</div>\n<br />\n\n## Plugins execution order\n\nIn general, plugins are executed in the following order:\n\n1. Plugins in [global rules](./global-rule.md)\n   1. plugins in rewrite phase\n   2. plugins in access phase\n\n2. Plugins bound to other objects\n   1. plugins in rewrite phase\n   2. plugins in access phase\n\nWithin each phase, you can optionally define a new priority number in the `_meta.priority` field of the plugin, which takes precedence over the default plugins priority during execution. Plugins with higher priority numbers are executed first.\n\nFor example, if you want to have `limit-count` (priority 1002) run before `ip-restriction` (priority 3000) when requests hit a route, you can do so by passing a higher priority number to `_meta.priority` field of `limit-count`:\n\n```json\n{\n  ...,\n  \"plugins\": {\n    \"limit-count\": {\n      ...,\n      \"_meta\": {\n        \"priority\": 3010\n      }\n    }\n  }\n}\n```\n\nTo reset the priority of this plugin instance to the default, simply remove the `_meta.priority` field from your plugin configuration.\n\n## Plugins merging precedence\n\nWhen the same plugin is configured both globally in a global rule and locally in an object (e.g. a route), both plugin instances are executed sequentially.\n\nHowever, if the same plugin is configured locally on multiple objects, such as on [Route](./route.md), [Service](./service.md), [Consumer](./consumer.md), [Consumer Group](./consumer-group.md), or [Plugin Config](./plugin-config.md), only one copy of configuration is used as each non-global plugin is only executed once. This is because during execution, plugins configured in these objects are merged with respect to a specific order of precedence:\n\n`Consumer`  > `Consumer Group` > `Route` > `Plugin Config` > `Service`\n\nsuch that if the same plugin has different configurations in different objects, the plugin configuration with the highest order of precedence during merging will be used.\n\n## Plugin common configuration\n\nSome common configurations can be applied to plugins through the `_meta` configuration items, the specific configuration items are as follows:\n\n| Name           | Type           | Description |\n|----------------|--------------- |-------------|\n| disable        | boolean        | When set to `true`, the plugin is disabled. |\n| error_response | string/object  | Custom error response. |\n| priority       | integer        | Custom plugin priority. |\n| filter         | array          | Depending on the requested parameters, it is decided at runtime whether the plugin should be executed. Something like this: `{{var, operator, val}, {var, operator, val}, ...}}`. For example: `{\"arg_version\", \"==\", \"v2\"}`, indicating that the current request parameter `version` is `v2`. The variables here are consistent with NGINX internal variables. For details on supported operators, please see [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list). |\n\n### Disable the plugin\n\nThrough the `disable` configuration, you can add a new plugin with disabled status and the request will not go through the plugin.\n\n```json\n{\n    \"proxy-rewrite\": {\n        \"_meta\": {\n            \"disable\": true\n        }\n    }\n}\n```\n\n### Custom error response\n\nThrough the `error_response` configuration, you can configure the error response of any plugin to a fixed value to avoid troubles caused by the built-in error response information of the plugin.\n\nThe configuration below means to customize the error response of the `jwt-auth` plugin to `Missing credential in request`.\n\n```json\n{\n    \"jwt-auth\": {\n        \"_meta\": {\n            \"error_response\": {\n                \"message\": \"Missing credential in request\"\n            }\n        }\n    }\n}\n```\n\n### Custom plugin priority\n\nAll plugins have default priorities, but through the `priority` configuration item you can customize the plugin priority and change the plugin execution order.\n\n```json\n {\n    \"serverless-post-function\": {\n        \"_meta\": {\n            \"priority\": 10000\n        },\n        \"phase\": \"rewrite\",\n        \"functions\" : [\"return function(conf, ctx)\n                    ngx.say(\\\"serverless-post-function\\\");\n                    end\"]\n    },\n    \"serverless-pre-function\": {\n        \"_meta\": {\n            \"priority\": -2000\n        },\n        \"phase\": \"rewrite\",\n        \"functions\": [\"return function(conf, ctx)\n                    ngx.say(\\\"serverless-pre-function\\\");\n                    end\"]\n    }\n}\n```\n\nThe default priority of serverless-pre-function is 10000, and the default priority of serverless-post-function is -2000. By default, the serverless-pre-function plugin will be executed first, and serverless-post-function plugin will be executed next.\n\nThe above configuration means setting the priority of the serverless-pre-function plugin to -2000 and the priority of the serverless-post-function plugin to 10000. The serverless-post-function plugin will be executed first, and serverless-pre-function plugin will be executed next.\n\n:::note\n\n- Custom plugin priority only affects the current object(route, service ...) of the plugin instance binding, not all instances of that plugin. For example, if the above plugin configuration belongs to Route A, the order of execution of the plugins serverless-post-function and serverless-post-function on Route B will not be affected and the default priority will be used.\n- Custom plugin priority does not apply to the rewrite phase of some plugins configured on the consumer. The rewrite phase of plugins configured on the route will be executed first, and then the rewrite phase of plugins (exclude auth plugins) from the consumer will be executed.\n\n:::\n\n### Dynamically control whether the plugin is executed\n\nBy default, all plugins specified in the route will be executed. But we can add a filter to the plugin through the `filter` configuration item, and control whether the plugin is executed through the execution result of the filter.\n\nThe configuration below means that the `proxy-rewrite` plugin will only be executed if the `version` value in the request query parameters is `v2`.\n\n```json\n{\n    \"proxy-rewrite\": {\n        \"_meta\": {\n            \"filter\": [\n                [\"arg_version\", \"==\", \"v2\"]\n            ]\n        },\n        \"uri\": \"/anything\"\n    }\n}\n```\n\nCreate a complete route with the below configuration:\n\n```json\n{\n    \"uri\": \"/get\",\n    \"plugins\": {\n        \"proxy-rewrite\": {\n            \"_meta\": {\n                \"filter\": [\n                    [\"arg_version\", \"==\", \"v2\"]\n                ]\n            },\n            \"uri\": \"/anything\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}\n```\n\nWhen the request does not have any parameters, the `proxy-rewrite` plugin will not be executed, the request will be proxy to the upstream `/get`:\n\n```shell\ncurl -v /dev/null http://127.0.0.1:9080/get -H\"host:httpbin.org\"\n```\n\n```shell\n< HTTP/1.1 200 OK\n......\n< Server: APISIX/2.15.0\n<\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/7.79.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-62eb6eec-46c97e8a5d95141e621e07fe\",\n    \"X-Forwarded-Host\": \"httpbin.org\"\n  },\n  \"origin\": \"127.0.0.1, 117.152.66.200\",\n  \"url\": \"http://httpbin.org/get\"\n}\n```\n\nWhen the parameter `version=v2` is carried in the request, the `proxy-rewrite` plugin is executed, and the request will be proxy to the upstream `/anything`:\n\n```shell\ncurl -v /dev/null http://127.0.0.1:9080/get?version=v2 -H\"host:httpbin.org\"\n```\n\n```shell\n< HTTP/1.1 200 OK\n......\n< Server: APISIX/2.15.0\n<\n{\n  \"args\": {\n    \"version\": \"v2\"\n  },\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/7.79.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-62eb6f02-24a613b57b6587a076ef18b4\",\n    \"X-Forwarded-Host\": \"httpbin.org\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 117.152.66.200\",\n  \"url\": \"http://httpbin.org/anything?version=v2\"\n}\n```\n\n## Hot reload\n\nAPISIX Plugins are hot-loaded. This means that there is no need to restart the service if you add, delete, modify plugins, or even if you update the plugin code. To hot-reload, you can send an HTTP request through the [Admin API](../admin-api.md):\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n\n:::note\n\nIf a configured Plugin is disabled, then its execution will be skipped.\n\n:::\n\n### Hot reload in standalone mode\n\nFor hot-reloading in standalone mode, see the plugin related section in [stand alone mode](../deployment-modes.md#standalone).\n"
  },
  {
    "path": "docs/en/latest/terminology/route.md",
    "content": "---\ntitle: Route\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Route\ndescription: This article describes the concept of Route and how to use it.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nRoutes match the client's request based on defined rules, load and execute the corresponding [plugins](./plugin.md), and forwards the request to the specified [Upstream](./upstream.md).\n\nA Route mainly consists of three parts:\n\n1. Matching rules (`uri`, `host`, `remote address`)\n2. Plugin configuration (current-limit, rate-limit)\n3. Upstream information\n\nThe image below shows some example Route rules. Note that the values are of the same color if they are identical.\n\n![routes-example](../../../assets/images/routes-example.png)\n\nAll the parameters are configured directly in the Route. It is easy to set up, and each Route has a high degree of freedom.\n\nWhen Routes have repetitive configurations (say, enabling the same plugin configuration or Upstream information), to update it, we need to traverse all the Routes and modify them. This adds a lot of complexity, making it difficult to maintain.\n\nThese shortcomings are independently abstracted in APISIX by two concepts: [Service](service.md) and [Upstream](upstream.md).\n\n## Example\n\nThe Route example shown below proxies the request with the URL `/index.html` to the Upstream service with the address `127.0.0.1:1980`.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n```shell\nHTTP/1.1 201 Created\nDate: Sat, 31 Aug 2019 01:17:15 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\n{\"node\":{\"value\":{\"uri\":\"\\/index.html\",\"upstream\":{\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"}},\"createdIndex\":61925,\"key\":\"\\/apisix\\/routes\\/1\",\"modifiedIndex\":61925}}\n```\n\nA successful response indicates that the route was created.\n\n## Configuration\n\nFor specific options of Route, please refer to the [Admin API](../admin-api.md#route).\n"
  },
  {
    "path": "docs/en/latest/terminology/router.md",
    "content": "---\ntitle: Router\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Router\ndescription: This article describes how to choose a router for Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nA distinguishing feature of Apache APISIX from other API gateways is that it allows you to choose different Routers to better match free services, giving you the best choices for performance and freedom.\n\nYou can set the Router that best suits your needs in your configuration file `conf/config.yaml`.\n\n## Configuration\n\nA Router can have the following configurations:\n\n- `apisix.router.http`: The HTTP request route. It can take the following values:\n\n  - `radixtree_uri`: Only use the `uri` as the primary index. To learn more about the support for full and deep prefix matching, check [How to use router-radixtree](../router-radixtree.md).\n    - `Absolute match`: Match completely with the given `uri` (`/foo/bar`, `/foo/glo`).\n    - `Prefix match`: Match with the given prefix. Use `*` to represent the given `uri` for prefix matching. For example, `/foo*` can match with `/foo/`, `/foo/a` and `/foo/b`.\n    - `match priority`: First try an absolute match, if it didn't match, try prefix matching.\n    - `Any filter attribute`: This allows you to specify any Nginx built-in variable as a filter, such as URL request parameters, request headers, and cookies.\n  - `radixtree_uri_with_parameter`: Like `radixtree_uri` but also supports parameter match.\n  - `radixtree_host_uri`: (Default) Matches both host and URI of the request. Use `host + uri` as the primary index (based on the `radixtree` engine).\n\n:::note\n\nIn version 3.2 and earlier, APISIX used `radixtree_uri` as the default Router. `radixtree_uri` has better performance than `radixtree_host_uri`, so if you have higher performance requirements and can live with the fact that `radixtree_uri` only use the `uri` as the primary index, consider continuing to use `radixtree_uri` as the default Router.\n\n:::\n\n- `apisix.router.ssl`: SSL loads the matching route.\n  - `radixtree_sni`: (Default) Use `SNI` (Server Name Indication) as the primary index (based on the radixtree engine).\n"
  },
  {
    "path": "docs/en/latest/terminology/script.md",
    "content": "---\ntitle: Script\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nScripts lets you write arbitrary Lua code or directly call existing plugins and execute them during the HTTP request/response lifecycle.\n\nA Script configuration can be directly bound to a [Route](./route.md).\n\nScripts and [Plugins](./plugin.md) are mutually exclusive, and a Script is executed before a Plugin. This means that after configuring a Script, the Plugin configured on the Route will **not** be executed.\n\nScripts also have a concept of execution phase which supports the `access`, `header_filter`, `body_filter`, and the `log` phase. The corresponding phase will be executed automatically by the system in the Script.\n\n```json\n{\n    ...\n    \"script\": \"local _M = {} \\n function _M.access(api_ctx) \\n ngx.log(ngx.INFO,\\\"hit access phase\\\") \\n end \\nreturn _M\"\n}\n```\n"
  },
  {
    "path": "docs/en/latest/terminology/secret.md",
    "content": "---\ntitle: Secret\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nSecrets refer to any sensitive information required during the running process of APISIX, which may be part of the core configuration (such as the etcd's password) or some sensitive information in the plugin. Common types of Secrets in APISIX include:\n\n- username, the password for some components (etcd, Redis, Kafka, etc.)\n- the private key of the certificate\n- API key\n- Sensitive plugin configuration fields, typically used for authentication, hashing, signing, or encryption\n\nAPISIX Secret allows users to store secrets through some secrets management services (Vault, etc.) in APISIX, and read them according to the key when using them to ensure that **Secrets do not exist in plain text throughout the platform**.\n\nIts working principle is shown in the figure:\n![secret](../../../assets/images/secret.png)\n\nAPISIX currently supports storing secrets in the following ways:\n\n- [Environment Variables](#use-environment-variables-to-manage-secrets)\n- [HashiCorp Vault](#use-hashicorp-vault-to-manage-secrets)\n- [AWS Secrets Manager](#use-aws-secrets-manager-to-manage-secrets)\n- [GCP Secrets Manager](#use-gcp-secrets-manager-to-manage-secrets)\n\nYou can use APISIX Secret functions by specifying format variables in the consumer configuration of the following plugins, such as `key-auth`.\n\n:::note\n\nIf a key-value pair `key: \"$ENV://ABC\"` is configured in APISIX and the value of `$ENV://ABC` is unassigned in the environment variable, `$ENV://ABC` will be interpreted as a string literal, instead of `nil`.\n\n:::\n\n## Use environment variables to manage secrets\n\nUsing environment variables to manage secrets means that you can save key information in environment variables, and refer to environment variables through variables in a specific format when configuring plugins. APISIX supports referencing system environment variables and environment variables configured through the Nginx `env` directive.\n\n### Usage\n\n```\n$ENV://$env_name/$sub_key\n```\n\n- env_name: environment variable name\n- sub_key: get the value of a property when the value of the environment variable is a JSON string\n\n If the value of the environment variable is of type string, such as:\n\n```\nexport JACK_AUTH_KEY=abc\n```\n\nIt can be referenced as follows:\n\n```\n$ENV://JACK_AUTH_KEY\n```\n\nIf the value of the environment variable is a JSON string like:\n\n```\nexport JACK={\"auth-key\":\"abc\",\"openid-key\": \"def\"}\n```\n\nIt can be referenced as follows:\n\n```\n# Get the auth-key of the environment variable JACK\n$ENV://JACK/auth-key\n\n# Get the openid-key of the environment variable JACK\n$ENV://JACK/openid-key\n```\n\n### Example: use in key-auth plugin\n\nStep 1: Create environment variables before the APISIX instance starts\n\n```\nexport JACK_AUTH_KEY=abc\n```\n\nStep 2: Reference the environment variable in the `key-auth` plugin\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"$ENV://JACK_AUTH_KEY\"\n        }\n    }\n}'\n```\n\nThrough the above steps, the `key` configuration in the `key-auth` plugin can be saved in the environment variable instead of being displayed in plain text when configuring the plugin.\n\n## Use HashiCorp Vault to manage secrets\n\nUsing HashiCorp Vault to manage secrets means that you can store secrets information in the Vault service and refer to it through variables in a specific format when configuring plugins. APISIX currently supports [Vault KV engine version V1](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1).\n\n### Usage\n\n```\n$secret://$manager/$id/$secret_name/$key\n```\n\n- manager: secrets management service, could be the HashiCorp Vault, AWS, etc.\n- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource\n- secret_name: the secret name in the secrets management service\n- key: the key corresponding to the secret in the secrets management service\n\n### Example: use in key-auth plugin\n\nStep 1: Create the corresponding key in the Vault, you can use the following command:\n\n```shell\nvault kv put apisix/jack auth-key=value\n```\n\nStep 2: Add APISIX Secrets resources through the Admin API, configure the Vault address and other connection information:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/secrets/vault/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"https://127.0.0.1:8200\"，\n    \"prefix\": \"apisix\",\n    \"token\": \"root\"\n}'\n```\n\nIf you use APISIX Standalone mode, you can add the following configuration in `apisix.yaml` configuration file:\n\n```yaml\nsecrets:\n  - id: vault/1\n    prefix: apisix\n    token: root\n    uri: 127.0.0.1:8200\n```\n\n:::tip\n\nIt now supports the use of the [`namespace` field](../admin-api.md#request-body-parameters-11) to set the multi-tenant namespace concepts supported by [HashiCorp Vault Enterprise](https://developer.hashicorp.com/vault/docs/enterprise/namespaces#vault-api-and-namespaces) and HCP Vault.\n\n:::\n\nStep 3: Reference the APISIX Secrets resource in the `key-auth` plugin and fill in the key information:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"$secret://vault/1/jack/auth-key\"\n        }\n    }\n}'\n```\n\nThrough the above two steps, when the user request hits the `key-auth` plugin, the real value of the key in the Vault will be obtained through the APISIX Secret component.\n\n## Use AWS Secrets Manager to manage secrets\n\nManaging secrets with AWS Secrets Manager is a secure and convenient way to store and manage sensitive information. This method allows you to save secret information in AWS Secrets Manager and reference these secrets in a specific format when configuring APISIX plugins.\n\nAPISIX currently supports two authentication methods: using [long-term credentials](https://docs.aws.amazon.com/sdkref/latest/guide/access-iam-users.html) and [short-term credentials](https://docs.aws.amazon.com/sdkref/latest/guide/access-temp-idc.html).\n\n### Usage\n\n```\n$secret://$manager/$id/$secret_name/$key\n```\n\n- manager: secrets management service, could be the HashiCorp Vault, AWS, etc.\n- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource\n- secret_name: the secret name in the secrets management service\n- key: get the value of a property when the value of the secret is a JSON string\n\n### Required Parameters\n\n| Name | Required | Default Value | Description |\n| --- | --- | --- | --- |\n| access_key_id | True |  | AWS Access Key ID |\n| secret_access_key | True |  | AWS Secret Access Key |\n| session_token | False |  | Temporary access credential information |\n| region | False | us-east-1 | AWS Region |\n| endpoint_url | False | https://secretsmanager.{region}.amazonaws.com | AWS Secret Manager URL |\n\n### Example: use in key-auth plugin\n\nHere, we use the key-auth plugin as an example to demonstrate how to manage secrets through AWS Secrets Manager.\n\nStep 1: Create the corresponding key in the AWS secrets manager. Here, [localstack](https://www.localstack.cloud/) is used for as the example environment, and you can use the following command:\n\n```shell\ndocker exec -i localstack sh -c \"awslocal secretsmanager create-secret --name jack --description 'APISIX Secret' --secret-string '{\\\"auth-key\\\":\\\"value\\\"}'\"\n```\n\nStep 2: Add APISIX Secrets resources through the Admin API, configure the connection information such as the address of AWS Secrets Manager.\n\nYou can store the critical key information in environment variables to ensure the configuration information is secure, and reference it where it is used:\n\n```shell\nexport AWS_ACCESS_KEY_ID=<access_key_id>\nexport AWS_SECRET_ACCESS_KEY=<secrets_access_key>\nexport AWS_SESSION_TOKEN=<token>\nexport AWS_REGION=<aws-region>\n```\n\nAlternatively, you can also specify all the information directly in the configuration:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/secrets/aws/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"endpoint_url\": \"http://127.0.0.1:4566\",\n    \"region\": \"us-east-1\",\n    \"access_key_id\": \"access\",\n    \"secret_access_key\": \"secret\",\n    \"session_token\": \"token\"\n}'\n```\n\nIf you use APISIX Standalone mode, you can add the following configuration in `apisix.yaml` configuration file:\n\n```yaml\nsecrets:\n  - id: aws/1\n    endpoint_url: http://127.0.0.1:4566\n    region: us-east-1\n    access_key_id: access\n    secret_access_key: secret\n    session_token: token\n```\n\nStep 3: Reference the APISIX Secrets resource in the `key-auth` plugin and fill in the key information:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"$secret://aws/1/jack/auth-key\"\n        }\n    }\n}'\n```\n\nThrough the above two steps, when the user request hits the `key-auth` plugin, the real value of the key in the Vault will be obtained through the APISIX Secret component.\n\n### Verification\n\nYou can verify this with the following command:\n\n```shell\n#Replace the following your_route with the actual route path.\ncurl -i http://127.0.0.1:9080/your_route -H 'apikey: value'\n```\n\nThis will verify whether the `key-auth` plugin is correctly using the key from AWS Secrets Manager.\n\n## Use GCP Secrets Manager to manage secrets\n\nUsing the GCP Secrets Manager to manage secrets means you can store the secret information in the GCP service, and reference it using a specific format of variables when configuring plugins. APISIX currently supports integration with the GCP Secrets Manager, and the supported authentication method is [OAuth 2.0](https://developers.google.com/identity/protocols/oauth2).\n\n### Reference Format\n\n```\n$secret://$manager/$id/$secret_name/$key\n```\n\nThe reference format is the same as before:\n\n- manager: secrets management service, could be the HashiCorp Vault, AWS, GCP etc.\n- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource\n- secret_name: the secret name in the secrets management service\n- key: get the value of a property when the value of the secret is a JSON string\n\n### Required Parameters\n\n| Name                    | Required | Default                                                                                                                                                                                              | Description                                                                                                                                                        |\n|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| auth_config             | True     |                                                                                                                                                                                                      | Either `auth_config` or `auth_file` must be provided.                                                                                                              |\n| auth_config.client_email | True     |                                                                                                                                                                                                    | Email address of the Google Cloud service account.                                                                                                                   |\n| auth_config.private_key | True     |                                                                                                                                                                                                      | Private key of the Google Cloud service account.                                                                                                                   |\n| auth_config.project_id  | True     |                                                                                                                                                                                                      | Project ID in the Google Cloud service account.                                                                                                                    |\n| auth_config.token_uri   | False    | https://oauth2.googleapis.com/token                                                                                                                                                                    | Token URI of the Google Cloud service account.                                                                                                                     |\n| auth_config.entries_uri | False    | https://secretmanager.googleapis.com/v1                                                                                                                                                      | \tThe API access endpoint for the Google Secrets Manager.                                                                                                                                |\n| auth_config.scope      | False    | https://www.googleapis.com/auth/cloud-platform | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) |\n| auth_file               | True     |                                                                                                                                                                                                      | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided.                                           |\n| ssl_verify              | False    | true                                                                                                                                                                                                 | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).                  |\n\nYou need to configure the corresponding authentication parameters, or specify the authentication file through auth_file, where the content of auth_file is in JSON format.\n\n### Example\n\nHere is a correct configuration example:\n\n```\ncurl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"auth_config\" : {\n        \"client_email\": \"email@apisix.iam.gserviceaccount.com\",\n        \"private_key\": \"private_key\",\n        \"project_id\": \"apisix-project\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"entries_uri\": \"https://secretmanager.googleapis.com/v1\",\n        \"scope\": [\"https://www.googleapis.com/auth/cloud-platform\"]\n    }\n}'\n\n```\n"
  },
  {
    "path": "docs/en/latest/terminology/service.md",
    "content": "---\ntitle: Service\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nA Service is an abstraction of an API (which can also be understood as a set of [Route](./route.md) abstractions). It usually corresponds to an upstream service abstraction.\n\nThe relationship between Routes and a Service is usually N:1 as shown in the image below.\n\n![service-example](../../../assets/images/service-example.png)\n\nAs shown, different Routes could be bound to the same Service. This reduces redundancy as these bounded Routes will have the same [Upstream](./upstream.md) and [Plugin](./plugin.md) configurations.\n\nFor more information about Service, please refer to [Admin API Service object](../admin-api.md#service).\n\n## Examples\n\nThe following example creates a Service that enables the `limit-count` Plugin and then binds it to the Routes with the ids `100` and `101`.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. Create a Service.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/services/200 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n2. create new Route and reference the service by id `200`\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"methods\": [\"GET\"],\n        \"uri\": \"/index.html\",\n        \"service_id\": \"200\"\n    }'\n    ```\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/101 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"methods\": [\"GET\"],\n        \"uri\": \"/foo/index.html\",\n        \"service_id\": \"200\"\n    }'\n    ```\n\nWe can also specify different Plugins or Upstream for the Routes than the ones defined in the Service. The example below creates a Route with a `limit-count` Plugin. This Route will continue to use the other configurations defined in the Service (here, the Upstream configuration).\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/102 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/bar/index.html\",\n        \"id\": \"102\",\n        \"service_id\": \"200\",\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2000,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n:::note\n\nWhen a Route and a Service enable the same Plugin, the one defined in the Route is given the higher priority.\n\n:::\n"
  },
  {
    "path": "docs/en/latest/terminology/upstream.md",
    "content": "---\ntitle: Upstream\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - APISIX Upstream\n  - Upstream\ndescription: This article describes the role of the Apache APISIX Upstream object and how to use the Upstream.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nUpstream is a virtual host abstraction that performs load balancing on a given set of service nodes according to the configured rules.\n\nAlthough Upstream can be directly configured to the [Route](./route.md) or [Service](./service.md), using an Upstream object is recommended when there is duplication as shown below.\n\n![upstream-example](../../../assets/images/upstream-example.png)\n\nBy creating an Upstream object and referencing it by `upstream_id` in the Route, you can ensure that there is only a single value of the object that needs to be maintained.\n\nAn Upstream configuration can be directly bound to a Route or a Service, but the configuration in Route has a higher priority. This behavior is consistent with priority followed by the [Plugin](./plugin.md) object.\n\n## Configuration\n\nIn addition to the equalization algorithm selections, Upstream also supports passive health check and retry for the upstream. You can learn more about this [Admin API Upstream](../admin-api.md#upstream).\n\nTo create an Upstream object, you can use the Admin API as shown below.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/upstreams/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"type\": \"chash\",\n    \"key\": \"remote_addr\",\n    \"nodes\": {\n        \"127.0.0.1:80\": 1,\n        \"foo.com:80\": 2\n    }\n}'\n```\n\nAfter creating an Upstream object, it can be referenced by a specific Route or Service as shown below.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream_id\": 1\n}'\n```\n\nFor convenience, you can directly bind the upstream address to a Route or Service.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## Example\n\nThe example below shows how you can configure a health check.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n         \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n        \"type\": \"roundrobin\",\n        \"retries\": 2,\n        \"checks\": {\n            \"active\": {\n                \"http_path\": \"/status\",\n                \"host\": \"foo.com\",\n                \"healthy\": {\n                    \"interval\": 2,\n                    \"successes\": 1\n                },\n                \"unhealthy\": {\n                    \"interval\": 1,\n                    \"http_failures\": 2\n                }\n            }\n        }\n    }\n}'\n```\n\nYou can learn more about health checks [health-check](../tutorials/health-check.md).\n\nThe examples below show configurations that use different `hash_on` types.\n\n### Consumer\n\nCreating a Consumer object.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n       \"key-auth\": {\n            \"key\": \"auth-jack\"\n        }\n    }\n}'\n```\n\nCreating a Route object and enabling the `key-auth` authentication Plugin.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"key-auth\": {}\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1,\n            \"127.0.0.1:1981\": 1\n        },\n        \"type\": \"chash\",\n        \"hash_on\": \"consumer\"\n    },\n    \"uri\": \"/server_port\"\n}'\n```\n\nTo test the request, the `consumer_name` passed for authentication will be used as the hash value of the load balancing hash algorithm.\n\n```shell\ncurl http://127.0.0.1:9080/server_port -H \"apikey: auth-jack\"\n```\n\n### Cookie\n\nCreating a Route and an upstream object.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hash_on_cookie\",\n    \"upstream\": {\n        \"key\": \"sid\",\n        \"type\": \"chash\",\n        \"hash_on\": \"cookie\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1,\n            \"127.0.0.1:1981\": 1\n        }\n    }\n}'\n```\n\nThe client can then send a request with a cookie.\n\n```shell\n curl http://127.0.0.1:9080/hash_on_cookie \\\n -H \"Cookie: sid=3c183a30cffcda1408daf1c61d47b274\"\n```\n\n### Header\n\nCreating a Route and an upstream object.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hash_on_header\",\n    \"upstream\": {\n        \"key\": \"content-type\",\n        \"type\": \"chash\",\n        \"hash_on\": \"header\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1,\n            \"127.0.0.1:1981\": 1\n        }\n    }\n}'\n```\n\nThe client can now send requests with a header. The example below shows using the header `Content-Type`.\n\n```shell\n curl http://127.0.0.1:9080/hash_on_header \\\n -H \"X-API-KEY: $admin_key\" \\\n -H \"Content-Type: application/json\"\n```\n"
  },
  {
    "path": "docs/en/latest/tutorials/add-multiple-api-versions.md",
    "content": "---\ntitle: Add multiple API versions\nkeywords:\n  - API Versioning\n  - Apache APISIX\n  - API Gateway\n  - Multiple APIs\n  - Proxy rewrite\n  - Request redirect\n  - Route API requests\ndescription: In this tutorial, you will learn how to publish and manage multiple versions of your API with Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## What is API versioning?\n\n**API versioning** is the practice of managing changes to an API and ensuring that these changes are made without disrupting clients. A good API versioning strategy clearly communicates the changes made and allows API consumers to decide when to upgrade to the latest version at their own pace.\n\n## Types of API versioning\n\n#### URI Path\n\nThe most common way to version an API is in the URI path and is often done with the prefix \"v\". This method employs URI routing to direct requests to a specific version of the API.\n\n```shell\nhttp://apisix.apache.org/v1/hello\nhttp://apisix.apache.org/v2/hello\n```\n\n#### Query parameters\n\nIn this method, the version number is included in the URI, but as a query parameter instead of in the path.\n\n```shell\nhttp://apisix.apache.org/hello?version=1\nhttp://apisix.apache.org/hello?version=2\n```\n\n#### Custom request Header\n\nYou can also set the version number using custom headers in requests and responses. This leaves the URI of your resources unchanged.\n\n```shell\nhttp://apisix.apache.org/hello -H 'Version: 1'\nhttp://apisix.apache.org/hello -H 'Version: 2'\n```\n\nThe primary goal of versioning is to provide users of an API with the most functionality possible while causing minimal inconvenience. Keeping this goal in mind, let’s have a look in this tutorial at how to _publish and manage multiple versions of your API_ with Apache APISIX.\n\n**In this tutorial**, you learn how to:\n\n- Create a route and upstream for our sample API.\n- Add a new version to the existing API.\n- Use [proxy-rewrite](https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite/) plugin to rewrite the path in a plugin configuration.\n- Route API requests from the old version to the new one.\n\n## Prerequisites\n\nFor the demo case, we will leverage the sample repository [Evolve APIs](https://github.com/nfrankel/evolve-apis) on GitHub built on the Spring boot that demonstrates our API. You can see the complete source code there.\n\nTo execute and customize the example project per your need shown in this tutorial, here are the minimum requirements you need to install in your system:\n\n- [Docker](https://docs.docker.com/desktop/windows/install/) - you need [Docker](https://www.docker.com/products/docker-desktop/) installed locally to complete this tutorial. It is available for [Windows](https://desktop.docker.com/win/edge/Docker%20Desktop%20Installer.exe) or [macOS](https://desktop.docker.com/mac/edge/Docker.dmg).\n\nAlso, complete the following steps to run the sample project with Docker.\n\nUse [git](https://git-scm.com/downloads) to clone the repository:\n\n``` shell\ngit clone 'https://github.com/nfrankel/evolve-apis'\n```\n\nGo to root directory of _evolve-apis_\n\n``` shell\ncd evolve-apis\n```\n\nNow we can start our application by running `docker compose up` command from the root folder of the project:\n\n``` shell\ndocker compose up -d\n```\n\n### Create a route and upstream for the API.\n\nYou first need to [Route](https://apisix.apache.org/docs/apisix/terminology/route/) your HTTP requests from the gateway to an [Upstream](https://apisix.apache.org/docs/apisix/terminology/upstream/) (your API). With APISIX, you can create a route by sending an HTTP request to the gateway.\n\n```shell\ncurl http://apisix:9180/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PUT -d '\n{\n  \"name\": \"Direct Route to Old API\",\n  \"methods\": [\"GET\"],\n  \"uris\": [\"/hello\", \"/hello/\", \"/hello/*\"],\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"oldapi:8081\": 1\n    }\n  }\n}'\n```\n\nAt this stage, we do not have yet any version and you can query the gateway as below:\n\n```shell\ncurl http://apisix.apache.org/hello\n```\n\n```shell title=\"output\"\nHello world\n```\n\n```shell\ncurl http://apisix.apache.org/hello/Joe\n```\n\n```shell title=\"output\"\nHello Joe\n```\n\nIn the previous step, we created a route that wrapped an upstream inside its configuration. Also, APISIX allows us to create an upstream with a dedicated ID to reuse it across several routes.\n\nLet's create the shared upstream by running below curl cmd:\n\n```shell\ncurl http://apisix:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: xyz' -X PUT -d '\n{\n  \"name\": \"Old API\",\n  \"type\": \"roundrobin\",\n  \"nodes\": {\n    \"oldapi:8081\": 1\n  }\n}'\n```\n\n### Add a new version\n\nIn the scope of this tutorial, we will use _URI path-based versioning_ because it’s the most widespread. We are going to add `v1` version for our existing `oldapi` in this section.\n\n ![Apache APISIX Multiple API versions](https://static.apiseven.com/2022/12/13/639875780e094.png)\n\nBefore introducing the new version, we also need to rewrite the query that comes to the API gateway before forwarding it to the upstream. Because both the old and new versions should point to the same upstream and the upstream exposes endpoint `/hello`, not `/v1/hello`. Let’s create a plugin configuration to rewrite the path:\n\n```shell\ncurl http://apisix:9180/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PUT -d '\n{\n  \"plugins\": {\n    \"proxy-rewrite\": {\n      \"regex_uri\": [\"/v1/(.*)\", \"/$1\"]\n    }\n  }\n}'\n```\n\nWe can now create the second versioned route that references the existing  upstream and plugin config.\n\n> Note that we can create routes for different API versions.\n\n```shell\ncurl http://apisix:9180/apisix/admin/routes/2 -H 'X-API-KEY: xyz' -X PUT -d '\n{\n  \"name\": \"Versioned Route to Old API\",\n  \"methods\": [\"GET\"],\n  \"uris\": [\"/v1/hello\", \"/v1/hello/\", \"/v1/hello/*\"],\n  \"upstream_id\": 1,\n  \"plugin_config_id\": 1\n}'\n```\n\nAt this stage, we have configured two routes, one versioned and the other non-versioned:\n\n```shell\ncurl http://apisix.apache.org/hello\n```\n\n```shell title=\"output\"\nHello world\n```\n\n```shell\ncurl http://apisix.apache.org/v1/hello\n```\n\n```shell title=\"output\"\nHello world\n```\n\n## Route API requests from the old version to the new one\n\nWe have versioned our API, but our API consumers probably still use the legacy non-versioned API. We want them to migrate, but we cannot just delete the legacy route as our users are unaware of it. Fortunately, the `301 HTTP` status code is our friend: we can let users know that the resource has moved from `http://apisix.apache.org/hello` to `http://apisix.apache.org/v1/hello`. It requires configuring the [redirect plugin](https://apisix.apache.org/docs/apisix/plugins/redirect/) on the initial route:\n\n```shell\ncurl http://apisix:9180/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PATCH -d '\n{\n  \"plugins\": {\n    \"redirect\": {\n      \"uri\": \"/v1$uri\",\n      \"ret_code\": 301\n    }\n  }\n}'\n```\n\n![Apache APISIX Multiple API versions with two routes](https://static.apiseven.com/2022/12/13/63987577a9e66.png)\n\nNow when we try to request the first non-versioned API endpoint, you will get an expected output:\n\n```shell\ncurl http://apisix.apache.org/hello\n\n<html>\n<head><title>301 Moved Permanently</title></head>\n<body>\n<center><h1>301 Moved Permanently</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\nEither API consumers will transparently use the new endpoint because they will follow, or their integration breaks and they will notice the 301 status and the new API location to use.\n\n## Next steps\n\nAs you followed throughout the tutorial, it is very easy to publish multiple versions of your API with Apache APISIX and it does not require setting up actual API endpoints for each version of your API in the backend. It also allows your clients to switch between two versions without any downtime and save assets if there’s ever an update.\n\nLearn more about how to [manage](./manage-api-consumers.md) API consumers and [protect](./protect-api.md) your APIs.\n"
  },
  {
    "path": "docs/en/latest/tutorials/cache-api-responses.md",
    "content": "---\ntitle: Cache API responses\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Cache\n  - Performance\ndescription: This tutorial will focus primarily on handling caching at the API Gateway level by using Apache APISIX API Gateway and you will learn how to use proxy-caching plugin to improve response efficiency for your Web or Microservices API.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis tutorial will focus primarily on handling caching at the API Gateway level by using Apache APISIX API Gateway and you will learn how to use the proxy-cache plugin to improve response efficiency for your Web or Microservices API.\n\n**Here is an overview of what we cover in this walkthrough:**\n\n- Caching in API Gateway\n- About [Apache APISIX API Gateway](https://apisix.apache.org/docs/apisix/getting-started/)\n- Run the demo project [apisix-dotnet-docker](https://github.com/Boburmirzo/apisix-dotnet-docker)\n- Configure the [Proxy Cache](https://apisix.apache.org/docs/apisix/plugins/proxy-cache/) plugin\n- Validate Proxy Caching\n\n## Improve performance with caching\n\nWhen you are building an API, you want to keep it simple and fast. Once the concurrent need to read the same data increase, you'll face a few issues where you might be considering introducing **caching**:\n\n- There is latency on some API requests which is noticeably affecting the user's experience.\n- Fetching data from a database takes more time to respond.\n- Availability of your API is threatened by the API's high throughput.\n- There are some network failures in getting frequently accessed information from your API.\n\n## Caching in API Gateway\n\n[Caching](https://en.wikipedia.org/wiki/Cache_(computing)) is capable of storing and retrieving network requests and their corresponding responses. Caching happens at different levels in a web application:\n\n- Edge caching or CDN\n- Database caching\n- Server caching (API caching)\n- Browser caching\n\n**Reverse Proxy Caching** is yet another caching mechanism that is usually implemented inside **API Gateway**. It can reduce the number of calls made to your endpoint and also improve the latency of requests to your API by caching a response from the upstream. If the API Gateway cache has a fresh copy of the requested resource, it uses that copy to satisfy the request directly instead of making a request to the endpoint. If the cached data is not found, the request travels to the intended upstream services (backend services).\n\n## Apache APISIX API Gateway Proxy Caching\n\nWith the help of Apache APISIX, you can enable API caching with [proxy-cache](https://apisix.apache.org/docs/apisix/plugins/proxy-cache/) plugin to cache your API endpoint's responses and enhance the performance. It can be used together with other Plugins too and currently supports disk-based caching. The data to be cached can be filtered with _responseCodes_, _requestModes_, or more complex methods using the _noCache_ and _cacheByPass_ attributes. You can specify cache expiration time or a memory capacity in the plugin configuration as well. Please, refer to other `proxy-cache` plugin's [attributes](https://apisix.apache.org/docs/apisix/plugins/proxy-cache/).\n\nWith all this in mind, we'll look next at an example of using `proxy-cache` plugin offered by Apache APISIX and apply it for ASP.NET Core Web API with a single endpoint.\n\n## Run the demo project\n\nUntil now, I assume that you have the demo project [apisix-dotnet-docker](https://github.com/Boburmirzo/apisix-dotnet-docker) is up and running. You can see the complete source code on **Github** and the instruction on how to build a multi-container **APISIX** via **Docker CLI**.\n\nIn the **ASP.NET Core project**, there is a simple API to get all products list from the service layer in [ProductsController.cs](https://github.com/Boburmirzo/apisix-dotnet-docker/blob/main/ProductApi/Controllers/ProductsController.cs) file.\n\nLet's assume that this product list is usually updated only once a day and the endpoint receives repeated billions of requests every day to fetch the product list partially or all of them. In this scenario, using API caching technique with `proxy-cache` plugin might be really helpful. For the demo purpose, we only enable caching for `GET` method.\n\n> Ideally, `GET` requests should be cacheable by default - until a special condition arises.\n\n## Configure the Proxy Cache Plugin\n\nNow let's start with adding `proxy-cache` plugin to Apache APISIX declarative configuration file `config.yaml` in the project. Because in the current project, we have not registered yet the plugin we are going to use for this demo. We appended `proxy-cache` plugin's name to the end of plugins list:\n\n``` yaml\nplugins:\n - http-logger\n - ip-restriction\n …\n - proxy-cache\n```\n\nYou can add your cache configuration in the same file if you need to specify values like _disk_size, memory_size_ as shown below:\n\n``` yaml\nproxy_cache:\n cache_ttl: 10s # default caching time if the upstream doesn't specify the caching time\n zones:\n - name: disk_cache_one # name of the cache. Admin can specify which cache to use in the Admin API by name\n memory_size: 50m # size of shared memory, used to store the cache index\n disk_size: 1G # size of disk, used to store the cache data\n disk_path: \"/tmp/disk_cache_one\" # path to store the cache data\n cache_levels: \"1:2\" # hierarchy levels of the cache\n```\n\nNext, we can directly run `apisix reload` command to reload the latest plugin code without restarting Apache APISIX. See the command to reload the newly added plugin:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n\nThen, we run two more curl commands to configure an Upstream and Route for the `/api/products` endpoint. The following command creates a sample upstream (that's our API Server):\n\n``` shell\ncurl \"http://127.0.0.1:9180/apisix/admin/upstreams/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"type\": \"roundrobin\",\n  \"nodes\": {\n    \"productapi:80\": 1\n  }\n}'\n```\n\nNext, we will add a new route with caching ability by setting `proxy-cache` plugin in `plugins` property and giving a reference to the upstream service by its unique id to forward requests to the API server:\n\n``` shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '{\n  \"name\": \"Route for API Caching\",\n  \"methods\": [\n    \"GET\"\n  ],\n  \"uri\": \"/api/products\",\n  \"plugins\": {\n    \"proxy-cache\": {\n      \"cache_key\": [\n        \"$uri\",\n        \"-cache-id\"\n      ],\n      \"cache_bypass\": [\n        \"$arg_bypass\"\n      ],\n      \"cache_method\": [\n        \"GET\"\n      ],\n      \"cache_http_status\": [\n        200\n      ],\n      \"hide_cache_headers\": true,\n      \"no_cache\": [\n        \"$arg_test\"\n      ]\n    }\n  },\n  \"upstream_id\": 1\n}'\n```\n\nAs you can see in the above configuration, we defined some plugin attributes that we want to cache only successful responses from the `GET` method of API.\n\n## Validate Proxy Caching\n\nFinally, we can test the proxy caching if it is working as it is expected.\n\nWe will send multiple requests to the `/api/products` path and we should receive `HTTP 200 OK` response each time. However, the `Apisix-Cache-Status` in the response shows _MISS_ meaning that the response has not cached yet when the request hits the route for the first time. Now, if you make another request, you will see that you get a cached response with the caching indicator as _HIT_.\n\nNow we can make an initial request:\n\n``` shell\ncurl http://localhost:9080/api/products -i\n```\n\nThe response looks like as below:\n\n``` shell\nHTTP/1.1 200 OK\n…\nApisix-Cache-Status: MISS\n```\n\nWhen you do the next call to the service, the route responds to the request with a cached response since it has already cached in the previous request:\n\n``` shell\nHTTP/1.1 200 OK\n…\nApisix-Cache-Status: HIT\n```\n\nOr if you try again to hit the endpoint after the time-to-live (TTL) period for the cache ends, you will get:\n\n``` shell\nHTTP/1.1 200 OK\n…\nApisix-Cache-Status: EXPIRED\n```\n\nExcellent! We enabled caching for our API endpoint.\n\n### Additional test case\n\nOptionally, you can also add some delay in the Product controller code and measure response time properly with and without cache:\n\n``` c#\n [HttpGet]\n public IActionResult GetAll()\n {\n Console.Write(\"The delay starts.\\n\");\n System.Threading.Thread.Sleep(5000);\n Console.Write(\"The delay ends.\");\n return Ok(_productsService.GetAll());\n }\n```\n\nThe `curl` command to check response time would be:\n\n```shell\ncurl -i 'http://localhost:9080/api/products' -s -o /dev/null -w \"Response time: %{time_starttransfer} seconds\\n\"\n```\n\n## What's next\n\nAs we learned, it is easy to configure and quick to set up API response caching for our ASP.NET Core WEB API with the help of Apache APISIX. It can reduce significantly the number of calls made to your endpoint and also improve the latency of requests to your API. There are other numerous built-in plugins available in Apache APISIX, you can check them on [Plugin Hub page](https://apisix.apache.org/plugins) and use them per your need.\n\n## Recommended content\n\nYou can refer to [Expose API](./protect-api.md) to learn about how to expose your first API.\n\nYou can refer to [Protect API](./protect-api.md) to protect your API.\n"
  },
  {
    "path": "docs/en/latest/tutorials/client-to-apisix-mtls.md",
    "content": "---\ntitle: Configure mTLS for client to APISIX\nkeywords:\n  - mTLS\n  - API Gateway\n  - Apache APISIX\ndescription: This article describes how to configure mutual authentication (mTLS) between the client and Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nmTLS is a method for mutual authentication. Suppose in your network environment, only trusted clients are required to access the server. In that case, you can enable mTLS to verify the client's identity and ensure the server API's security. This article mainly introduces how to configure mutual authentication (mTLS) between the client and Apache APISIX.\n\n## Configuration\n\nThis example includes the following procedures:\n\n1. Generate certificates;\n2. Configure the certificate in APISIX;\n3. Create and configure routes in APISIX;\n4. Test verification.\n\nTo make the test results clearer, the examples mentioned in this article pass some information about the client credentials upstream, including: `serial`, `fingerprint` and `common name`.\n\n### Generate certificates\n\nWe need to generate three test certificates: the root, server, and client. Just use the following command to generate the test certificates we need via `OpenSSL`.\n\n```shell\n# For ROOT CA\nopenssl genrsa -out ca.key 2048\nopenssl req -new -sha256 -key ca.key -out ca.csr -subj \"/CN=ROOTCA\"\nopenssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.cer\n\n# For server certificate\nopenssl genrsa -out server.key 2048\n# Note: The `test.com` in the CN value is the domain name/hostname we want to test\nopenssl req -new -sha256 -key server.key -out server.csr -subj \"/CN=test.com\"\nopenssl x509 -req -days 36500 -sha256 -extensions v3_req  -CA  ca.cer -CAkey ca.key  -CAserial ca.srl  -CAcreateserial -in server.csr -out server.cer\n\n# For client certificate\nopenssl genrsa -out client.key 2048\nopenssl req -new -sha256 -key client.key  -out client.csr -subj \"/CN=CLIENT\"\nopenssl x509 -req -days 36500 -sha256 -extensions v3_req  -CA  ca.cer -CAkey ca.key  -CAserial ca.srl  -CAcreateserial -in client.csr -out client.cer\n\n# Convert client certificate to pkcs12 for Windows usage (optional)\nopenssl pkcs12 -export -clcerts -in client.cer -inkey client.key -out client.p12\n```\n\n### Configure the certificate in APISIX\n\nUse the `curl` command to request APISIX Admin API to set up SSL for specific SNI.\n\n:::note\n\nNote that the newline character in the certificate needs to be replaced with its escape character `\\n`.\n\n:::\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/ssls/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"sni\": \"test.com\",\n    \"cert\": \"<content of server.cer>\",\n    \"key\": \"<content of server.key>\",\n    \"client\": {\n        \"ca\": \"<content of ca.cer>\"\n    }\n}'\n```\n\n- `sni`: Specify the domain name (CN) of the certificate. When the client tries to handshake with APISIX via TLS, APISIX will match the SNI data in `ClientHello` with this field and find the corresponding server certificate for handshaking.\n- `cert`: The server certificate.\n- `key`: The private key of the server certificate.\n- `client.ca`: The CA (certificate authority) file to verfiy the client certificate. For demonstration purposes, the same `CA` is used here.\n\n### Configure the route in APISIX\n\nUse the `curl` command to request the APISIX Admin API to create a route.\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"uri\": \"/anything\",\n    \"plugins\": {\n        \"proxy-rewrite\": {\n            \"headers\": {\n                \"X-Ssl-Client-Fingerprint\": \"$ssl_client_fingerprint\",\n                \"X-Ssl-Client-Serial\": \"$ssl_client_serial\",\n                \"X-Ssl-Client-S-DN\": \"$ssl_client_s_dn\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org\":1\n        },\n        \"type\":\"roundrobin\"\n    }\n}'\n```\n\nAPISIX automatically handles the TLS handshake based on the SNI and the SSL resource created in the previous step, so we do not need to specify the hostname in the route (but it is possible to specify the hostname if you need it).\n\nAlso, in the `curl` command above, we enabled the [proxy-rewrite](../plugins/proxy-rewrite.md) plugin, which will dynamically update the request header information. The source of the variable values in the example are the `NGINX` variables, and you can find them here: [http://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables).\n\n### Test\n\nSince we are using the domain `test.com` as the test domain, we have to add the test domain to your DNS or local `hosts` file before we can start the verification.\n\n1. If we don't use `hosts` and just want to test the results, then you can do so directly using the following command.\n\n```\ncurl --resolve \"test.com:9443:127.0.0.1\" https://test.com:9443/anything -k --cert ./client.cer --key ./client.key\n```\n\n2. If you need to modify `hosts`, please read the following example (for Ubuntu).\n\n- Modify the `/etc/hosts` file\n\n  ```shell\n  # 127.0.0.1 localhost\n  127.0.0.1 test.com\n  ```\n\n- Verify that the test domain name is valid\n\n  ```shell\n  ping test.com\n\n  PING test.com (127.0.0.1) 56(84) bytes of data.\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=64 time=0.028 ms\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=64 time=0.037 ms\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=3 ttl=64 time=0.036 ms\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=4 ttl=64 time=0.031 ms\n  ^C\n  --- test.com ping statistics ---\n  4 packets transmitted, 4 received, 0% packet loss, time 3080ms\n  rtt min/avg/max/mdev = 0.028/0.033/0.037/0.003 ms\n  ```\n\n- Test results\n\n  ```shell\n  curl https://test.com:9443/anything -k --cert ./client.cer --key ./client.key\n  ```\n\n  You will then receive the following response body.\n\n  ```shell\n  {\n    \"args\": {},\n    \"data\": \"\",\n    \"files\": {},\n    \"form\": {},\n    \"headers\": {\n      \"Accept\": \"*/*\",\n      \"Host\": \"test.com\",\n      \"User-Agent\": \"curl/7.81.0\",\n      \"X-Amzn-Trace-Id\": \"Root=1-63256343-17e870ca1d8f72dc40b2c5a9\",\n      \"X-Forwarded-Host\": \"test.com\",\n      \"X-Ssl-Client-Fingerprint\": \"c1626ce3bca723f187d04e3757f1d000ca62d651\",\n      \"X-Ssl-Client-S-Dn\": \"CN=CLIENT\",\n      \"X-Ssl-Client-Serial\": \"5141CC6F5E2B4BA31746D7DBFE9BA81F069CF970\"\n    },\n    \"json\": null,\n    \"method\": \"GET\",\n    \"origin\": \"127.0.0.1\",\n    \"url\": \"http://test.com/anything\"\n  }\n  ```\n\nSince we configured the [proxy-rewrite](../plugins/proxy-rewrite.md) plugin in the example, we can see that the response body contains the request body received upstream, containing the correct data.\n\n## MTLS bypass based on regular expression matching against URI\n\nAPISIX allows configuring an URI whitelist to bypass MTLS.\nIf the URI of a request is in the whitelist, then the client certificate will not be checked.\nNote that other URIs of the associated SNI will get HTTP 400 response\ninstead of alert error in the SSL handshake phase, if the client certificate is missing or invalid.\n\n### Timing diagram\n\n![skip mtls](../../../assets/images/skip-mtls.png)\n\n### Example\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. Configure route and ssl via admin API\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\": \"'\"$(<t/certs/mtls_server.crt)\"'\",\n    \"key\": \"'\"$(<t/certs/mtls_server.key)\"'\",\n    \"snis\": [\n        \"*.apisix.dev\"\n    ],\n    \"client\": {\n        \"ca\": \"'\"$(<t/certs/mtls_ca.crt)\"'\",\n        \"depth\": 10,\n        \"skip_mtls_uri_regex\": [\n            \"/anything.*\"\n        ]\n    }\n}'\n```\n\n2. If the client certificate is missing and the URI is not in the whitelist,\nthen you'll get HTTP 400 response.\n\n```bash\ncurl https://admin.apisix.dev:9443/uuid -v \\\n--resolve 'admin.apisix.dev:9443:127.0.0.1' --cacert t/certs/mtls_ca.crt\n* Added admin.apisix.dev:9443:127.0.0.1 to DNS cache\n* Hostname admin.apisix.dev was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* TCP_NODELAY set\n* Connected to admin.apisix.dev (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*   CAfile: t/certs/mtls_ca.crt\n  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=cn; ST=GuangDong; L=ZhuHai; CN=admin.apisix.dev; OU=ops\n*  start date: Dec  1 10:17:24 2022 GMT\n*  expire date: Aug 18 10:17:24 2042 GMT\n*  subjectAltName: host \"admin.apisix.dev\" matched cert's \"admin.apisix.dev\"\n*  issuer: C=cn; ST=GuangDong; L=ZhuHai; CN=ca.apisix.dev; OU=ops\n*  SSL certificate verify ok.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* Using Stream ID: 1 (easy handle 0x56246de24e30)\n> GET /uuid HTTP/2\n> Host: admin.apisix.dev:9443\n> user-agent: curl/7.68.0\n> accept: */*\n>\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* old SSL session ID is stale, removing\n* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!\n< HTTP/2 400\n< date: Fri, 21 Apr 2023 07:53:23 GMT\n< content-type: text/html; charset=utf-8\n< content-length: 229\n< server: APISIX/3.2.0\n<\n<html>\n<head><title>400 Bad Request</title></head>\n<body>\n<center><h1>400 Bad Request</h1></center>\n<hr><center>openresty</center>\n<p><em>Powered by <a href=\"https://apisix.apache.org/\">APISIX</a>.</em></p></body>\n</html>\n* Connection #0 to host admin.apisix.dev left intact\n```\n\n3. Although the client certificate is missing, but the URI is in the whitelist,\nyou get successful response.\n\n```bash\ncurl https://admin.apisix.dev:9443/anything/foobar -i \\\n--resolve 'admin.apisix.dev:9443:127.0.0.1' --cacert t/certs/mtls_ca.crt\nHTTP/2 200\ncontent-type: application/json\ncontent-length: 416\ndate: Fri, 21 Apr 2023 07:58:28 GMT\naccess-control-allow-origin: *\naccess-control-allow-credentials: true\nserver: APISIX/3.2.0\n...\n```\n\n## Conclusion\n\nIf you don't want to use curl or test on windows, you can read this gist for more details. [APISIX mTLS for client to APISIX](https://gist.github.com/bzp2010/6ce0bf7c15c191029ed54724547195b4).\n\nFor more information about the mTLS feature of Apache APISIX, you can read [Mutual TLS Authentication](../mtls.md).\n"
  },
  {
    "path": "docs/en/latest/tutorials/expose-api.md",
    "content": "---\ntitle: Expose API\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Expose Service\ndescription: This article describes how to publish services through the API Gateway Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis article will guide you through APISIX's upstream, routing, and service concepts and introduce how to publish your services through APISIX.\n\n## Concept introduction\n\n### Upstream\n\n[Upstream](../terminology/upstream.md) is a virtual host abstraction that performs load balancing on a given set of service nodes according to the configured rules.\n\nThe role of the Upstream is to load balance the service nodes according to the configuration rules, and Upstream information can be directly configured to the Route or Service.\n\nWhen multiple routes or services refer to the same upstream, you can create an upstream object and use the upstream ID in the Route or Service to reference the upstream to reduce maintenance pressure.\n\n### Route\n\n[Routes](../terminology/route.md) match the client's request based on defined rules, load and execute the corresponding plugins, and forwards the request to the specified Upstream.\n\n### Service\n\nA [Service](../terminology/service.md) is an abstraction of an API (which can also be understood as a set of Route abstractions). It usually corresponds to an upstream service abstraction.\n\n## Prerequisites\n\nPlease make sure you have [installed Apache APISIX](../installation-guide.md) before doing the following.\n\n## Expose your service\n\n1. Create an Upstream.\n\nCreate an Upstream service containing `httpbin.org` that you can use for testing. This is a return service that will return the parameters we passed in the request.\n\n```\ncurl \"http://127.0.0.1:9180/apisix/admin/upstreams/1\" \\\n-H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"type\": \"roundrobin\",\n  \"nodes\": {\n    \"httpbin.org:80\": 1\n  }\n}'\n```\n\nIn this command, we specify the Admin API Key of Apache APISIX as `edd1c9f034335f136f87ad84b625c8f1`, use `roundrobin` as the load balancing mechanism, and set `httpbin.org:80` as the upstream service. To bind this upstream to a route, `upstream_id` needs to be set to `1` here. Here you can specify multiple upstreams under `nodes` to achieve load balancing.\n\nFor more information, please refer to [Upstream](../terminology/upstream.md).\n\n2. Create a Route.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" \\\n-H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"host\": \"example.com\",\n  \"uri\": \"/anything/*\",\n  \"upstream_id\": \"1\"\n}'\n```\n\n:::note\n\nAdding an `upstream` object to your route can achieve the above effect.\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" \\\n-H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"host\": \"example.com\",\n  \"uri\": \"/anything/*\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n:::\n\n3. Test\n\nAfter creating the Route, you can test the Service with the following command:\n\n```\ncurl -i -X GET \"http://127.0.0.1:9080/anything/get?foo1=bar1&foo2=bar2\" -H \"Host: example.com\"\n```\n\nAPISIX will forward the request to `http://httpbin.org:80/anything/get?foo1=bar1&foo2=bar2`.\n\n## More Tutorials\n\nYou can refer to [Protect API](./protect-api.md) to protect your API.\n\nYou can also use APISIX's [Plugin](../terminology/plugin.md) to achieve more functions.\n"
  },
  {
    "path": "docs/en/latest/tutorials/health-check.md",
    "content": "---\ntitle: Health Check\nkeywords:\n  - APISIX\n  - API Gateway\n  - Health Check\ndescription: This article describes how to use the health check feature of API Gateway Apache APISIX to check the health status of upstream nodes.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThis article mainly introduces the health check function of Apache APISIX. The health check function can proxy requests to healthy nodes when the upstream node fails or migrates, avoiding the problem of service unavailability to the greatest extent. The health check function of APISIX is implemented using [lua-resty-healthcheck](https://github.com/api7/lua-resty-healthcheck), which is divided into active check and passive check.\n\n## Active check\n\nActive health check mainly means that APISIX actively detects the survivability of upstream nodes through preset probe types. APISIX supports three probe types: `HTTP`, `HTTPS`, and `TCP`.\n\nWhen N consecutive probes sent to healthy node `A` fail, the node will be marked as unhealthy, and the unhealthy node will be ignored by APISIX's load balancer and cannot receive requests; if For an unhealthy node, if M consecutive probes are successful, the node will be re-marked as healthy and can be proxied.\n\n## Passive check\n\nPassive health check refers to judging whether the corresponding upstream node is healthy by judging the response status of the request forwarded from APISIX to the upstream node. Compared with the active health check, the passive health check method does not need to initiate additional probes, but it cannot sense the node status in advance, and there may be a certain amount of failed requests.\n\nIf `N` consecutive requests to a healthy node A fail, the node will be marked as unhealthy.\n\n:::note\n\nSince unhealthy nodes cannot receive requests, nodes cannot be re-marked as healthy using the passive health check strategy alone, so combining the active health check strategy is usually necessary.\n\n:::\n\n:::tip\n\n- We only start the health check when the upstream is hit by a request. There won't be any health check if an upstream is configured but isn't in used.\n- If there is no healthy node can be chosen, we will continue to access the upstream.\n\n:::\n\n### Configuration instructions\n\n| Name                                      | Configuration type              | Value type | Valid values         | Default                                                                                      | Description                                                                                                          |\n| ----------------------------------------------- | ------------------------------- | ---------- | -------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| upstream.checks.active.type                     | Active check                    | string     | `http` `https` `tcp` | http                                                                                          | The type of active check.                                                                                            |\n| upstream.checks.active.timeout                  | Active check                    | integer    |                      | 1                                                                                             | The timeout period of the active check (unit: second).                                                               |\n| upstream.checks.active.concurrency              | Active check                    | integer    |                      | 10                                                                                            | The number of targets to be checked at the same time during the active check.                                        |\n| upstream.checks.active.http_path                | Active check                    | string     |                      | /                                                                                             | The HTTP request path that is actively checked.                                                                      |\n| upstream.checks.active.host                     | Active check                    | string     |                      | ${upstream.node.host}                                                                         | The hostname of the HTTP request actively checked.                                                                   |\n| upstream.checks.active.port                     | Active check                    | integer    | `1` to `65535`       | ${upstream.node.port}                                                                         | The host port of the HTTP request that is actively checked.                                                          |\n| upstream.checks.active.https_verify_certificate | Active check                    | boolean    |                      | true                                                                                          | Active check whether to check the SSL certificate of the remote host when HTTPS type checking is used.               |\n| upstream.checks.active.req_headers              | Active check                    | array      |                      | []                                                                                            | Active check When using HTTP or HTTPS type checking, set additional request header information.                      |\n| upstream.checks.active.healthy.interval         | Active check (healthy node)    | integer    | `>= 1`               | 1                                                                                             | Active check (healthy node) check interval (unit: second)                                                            |\n| upstream.checks.active.healthy.http_statuses    | Active check (healthy node)    | array      | `200` to `599`       | [200, 302]                                                                                    | Active check (healthy node) HTTP or HTTPS type check, the HTTP status code of the healthy node.                      |\n| upstream.checks.active.healthy.successes        | Active check (healthy node)    | integer    | `1` to `254`         | 2                                                                                             | Active check (healthy node) determine the number of times a node is healthy.                                         |\n| upstream.checks.active.unhealthy.interval       | Active check (unhealthy node)  | integer    | `>= 1`               | 1                                                                                             | Active check (unhealthy node) check interval (unit: second)                                                          |\n| upstream.checks.active.unhealthy.http_statuses  | Active check (unhealthy node)  | array      | `200` to `599`       | [429, 404, 500, 501, 502, 503, 504, 505]                                                      | Active check (unhealthy node) HTTP or HTTPS type check, the HTTP status code of the non-healthy node.                |\n| upstream.checks.active.unhealthy.http_failures  | Active check (unhealthy node)  | integer    | `1` to `254`         | 5                                                                                             | Active check (unhealthy node) HTTP or HTTPS type check, determine the number of times that the node is not healthy.  |\n| upstream.checks.active.unhealthy.tcp_failures   | Active check (unhealthy node)  | integer    | `1` to `254`         | 2                                                                                             | Active check (unhealthy node) TCP type check, determine the number of times that the node is not healthy.            |\n| upstream.checks.active.unhealthy.timeouts       | Active check (unhealthy node)  | integer    | `1` to `254`         | 3                                                                                             | Active check (unhealthy node) to determine the number of timeouts for unhealthy nodes.                              |\n| upstream.checks.passive.type      | Passive check  | string    | `http` `https` `tcp`    | http                                                                                             | The type of passive check.                             |\n| upstream.checks.passive.healthy.http_statuses   | Passive check (healthy node)   | array      | `200` to `599`       | [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] | Passive check (healthy node) HTTP or HTTPS type check, the HTTP status code of the healthy node.                     |\n| upstream.checks.passive.healthy.successes       | Passive check (healthy node)   | integer    | `0` to `254`         | 5                                                                                             | Passive checks (healthy node) determine the number of times a node is healthy.                                       |\n| upstream.checks.passive.unhealthy.http_statuses | Passive check (unhealthy node) | array      | `200` to `599`       | [429, 500, 503]                                                                               | Passive check (unhealthy node) HTTP or HTTPS type check, the HTTP status code of the non-healthy node.               |\n| upstream.checks.passive.unhealthy.tcp_failures  | Passive check (unhealthy node) | integer    | `0` to `254`         | 2                                                                                             | Passive check (unhealthy node) When TCP type is checked, determine the number of times that the node is not healthy. |\n| upstream.checks.passive.unhealthy.timeouts      | Passive check (unhealthy node) | integer    | `0` to `254`         | 7                                                                                             | Passive checks (unhealthy node) determine the number of timeouts for unhealthy nodes.                                |\n| upstream.checks.passive.unhealthy.http_failures | Passive check (unhealthy node) | integer    | `0` to `254`         | 5                                                                                                           | Passive check (unhealthy node) The number of times that the node is not healthy during HTTP or HTTPS type checking.  |\n\n### Configuration example\n\nYou can enable health checks in routes via the Admin API:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n         \"nodes\": {\n            \"127.0.0.1:1980\": 1,\n            \"127.0.0.1:1970\": 1\n        },\n        \"type\": \"roundrobin\",\n        \"retries\": 2,\n        \"checks\": {\n            \"active\": {\n                \"timeout\": 5,\n                \"http_path\": \"/status\",\n                \"host\": \"foo.com\",\n                \"healthy\": {\n                    \"interval\": 2,\n                    \"successes\": 1\n                },\n                \"unhealthy\": {\n                    \"interval\": 1,\n                    \"http_failures\": 2\n                },\n                \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n            },\n            \"passive\": {\n                \"healthy\": {\n                    \"http_statuses\": [200, 201],\n                    \"successes\": 3\n                },\n                \"unhealthy\": {\n                    \"http_statuses\": [500],\n                    \"http_failures\": 3,\n                    \"tcp_failures\": 3\n                }\n            }\n        }\n    }\n}'\n```\n\nIf APISIX detects an unhealthy node, the following logs will be output in the error log:\n\n```shell\nenabled healthcheck passive while logging request\nfailed to receive status line from 'nil (127.0.0.1:1980)': closed\nunhealthy TCP increment (1/2) for '(127.0.0.1:1980)'\nfailed to receive status line from 'nil (127.0.0.1:1980)': closed\nunhealthy TCP increment (2/2) for '(127.0.0.1:1980'\n```\n\n:::tip\n\nTo observe the above log information, you need to adjust the error log level to `info`.\n\n:::\n\nThe health check status can be fetched via `GET /v1/healthcheck` in [Control API](../control-api.md).\n\n```shell\n\ncurl http://127.0.0.1:9090/v1/healthcheck/upstreams/healthycheck -s | jq .\n\n```\n\n## Health Check Status\n\nAPISIX provides comprehensive health check information, with particular emphasis on the `status` and `counter` parameters for effective health monitoring. In the APISIX context, nodes exhibit four states: `healthy`, `unhealthy`, `mostly_unhealthy`, and `mostly_healthy`. The `mostly_healthy` status indicates that the current node is considered healthy, but during health checks, the node's health status is not consistently successful. The `mostly_unhealthy` status indicates that the current node is considered unhealthy, but during health checks, the node's health detection is not consistently unsuccessful. The transition of a node's state depends on the success or failure of the current health check, along with the recording of four key metrics in the `counter`: `tcp_failure`, `http_failure`, `success`, and `timeout_failure`.\n\nTo retrieve health check information, you can use the following curl command:\n\n```shell\n curl -i http://127.0.0.1:9090/v1/healthcheck\n```\n\nResponse Example:\n\n```json\n[\n  {\n    \"nodes\": {},\n    \"name\": \"/apisix/routes/1\",\n    \"type\": \"http\"\n  },\n  {\n    \"nodes\": [\n      {\n        \"port\": 1970,\n        \"hostname\": \"127.0.0.1\",\n        \"status\": \"healthy\",\n        \"ip\": \"127.0.0.1\",\n        \"counter\": {\n          \"tcp_failure\": 0,\n          \"http_failure\": 0,\n          \"success\": 0,\n          \"timeout_failure\": 0\n        }\n      },\n      {\n        \"port\": 1980,\n        \"hostname\": \"127.0.0.1\",\n        \"status\": \"healthy\",\n        \"ip\": \"127.0.0.1\",\n        \"counter\": {\n          \"tcp_failure\": 0,\n          \"http_failure\": 0,\n          \"success\": 0,\n          \"timeout_failure\": 0\n        }\n      }\n    ],\n    \"name\": \"/apisix/routes/example-hc-route\",\n    \"type\": \"http\"\n  }\n]\n```\n\n### State Transition Diagram\n\n![image](../../../assets/images/health_check_node_state_diagram.png)\n\nNote that all nodes start with the `healthy` status without any initial probes, and the counter only resets and updates with a state change. Hence, when nodes are `healthy` and all subsequent checks are successful, the `success` counter is not updated and remains zero.\n\n### Counter Information\n\nIn the event of a health check failure, the `success` count in the counter will be reset to zero. Upon a successful health check, the `tcp_failure`, `http_failure`, and `timeout_failure` data will be reset to zero.\n\n| Name            | Description                            | Purpose                                                                                                                  |\n|----------------|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------|\n| success        | Number of successful health checks     | When `success` exceeds the configured `healthy.successes` value, the node transitions to a `healthy` state.              |\n| tcp_failure    | Number of TCP health check failures    | When `tcp_failure` exceeds the configured `unhealthy.tcp_failures` value, the node transitions to an `unhealthy` state.  |\n| http_failure   | Number of HTTP health check failures   | When `http_failure` exceeds the configured `unhealthy.http_failures` value, the node transitions to an `unhealthy` state. |\n| timeout_failure | Number of health check timeouts        | When `timeout_failure` exceeds the configured `unhealthy.timeouts` value, the node transitions to an `unhealthy` state.  |\n"
  },
  {
    "path": "docs/en/latest/tutorials/keycloak-oidc.md",
    "content": "---\ntitle: Set Up SSO with Keycloak (OIDC)\nkeywords:\n  - APISIX\n  - API Gateway\n  - OIDC\n  - Keycloak\ndescription: This article describes how to integrate APISIX with Keycloak using the authorization code grant, client credentials grant, and password grant, using the openid-connect Plugin.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n[OpenID Connect (OIDC)](https://openid.net/connect/) is a simple identity layer on top of the [OAuth 2.0 protocol](https://www.rfc-editor.org/rfc/rfc6749). It allows clients to verify the identity of end users based on the authentication performed by the identity provider, as well as to obtain basic profile information about end users in an interoperable and REST-like manner. With APISIX and [Keycloak](https://www.keycloak.org/), you can implement OIDC-based authentication processes to protect your APIs and enable single sign-on (SSO).\n\n[Keycloak](https://www.keycloak.org/) is an open-source identity and access management solution for modern applications and services. Keycloak supports single sign-on (SSO), which enables services to interface with Keycloak through protocols such as OIDC and OAuth 2.0. In addition, Keycloak also supports delegating authentication to third party identity providers such as Facebook and Google.\n\nThis tutorial will show you how to integrate APISIX with Keycloak using [authorization code grant](#implement-authorization-code-grant), [client credentials grant](#implement-client-credentials-grant), and [password grant](#implement-password-grant), using the [`openid-connect`](/hub/openid-connect) Plugin.\n\n## Configure Keycloak\n\nStart a Keycloak instance named `apisix-quickstart-keycloak` with the administrator name `quickstart-admin` and password `quickstart-admin-pass` in [development mode](https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode) in Docker. The exposed port is mapped to `8080` on the host machine:\n\n```shell\ndocker run -d --name \"apisix-quickstart-keycloak\" \\\n  -e 'KEYCLOAK_ADMIN=quickstart-admin' \\\n  -e 'KEYCLOAK_ADMIN_PASSWORD=quickstart-admin-pass' \\\n  -p 8080:8080 \\\n  quay.io/keycloak/keycloak:18.0.2 start-dev\n```\n\nKeycloak provides an easy-to-use web UI to help the administrator manage all resources, such as clients, roles, and users.\n\nNavigate to `http://localhost:8080` in browser to access the Keycloak web page, then click __Administration Console__:\n\n![web-ui](https://static.api7.ai/uploads/2023/03/30/ItcwYPIx_web-ui.png)\n\nEnter the administrator’s username `quickstart-admin` and password `quickstart-admin-pass` and sign in:\n\n![admin-signin](https://static.api7.ai/uploads/2023/03/30/6W3pjzE1_admin-signin.png)\n\nYou need to maintain the login status to configure Keycloak during the following steps.\n\n### Create a Realm\n\n_Realms_ in Keycloak are workspaces to manage resources such as users, credentials, and roles. The resources in different realms are isolated from each other. You need to create a realm named `quickstart-realm` for APISIX.\n\nIn the left menu, hover over **Master**, and select __Add realm__ in the dropdown:\n\n![create-realm](https://static.api7.ai/uploads/2023/03/30/S1Xvqliv_create-realm.png)\n\nEnter the realm name `quickstart-realm` and click __Create__ to create it:\n\n![add-realm](https://static.api7.ai/uploads/2023/03/30/jwb7QU8k_add-realm.png)\n\n### Create a Client\n\n_Clients_ in Keycloak are entities that request Keycloak to authenticate a user. More often, clients are applications that want to use Keycloak to secure themselves and provide a single sign-on solution. APISIX is equivalent to a client that is responsible for initiating authentication requests to Keycloak, so you need to create its corresponding client named `apisix-quickstart-client`.\n\nClick __Clients__ > __Create__ to open the __Add Client__ page:\n\n![create-client](https://static.api7.ai/uploads/2023/03/30/qLom0axN_create-client.png)\n\nEnter __Client ID__ as `apisix-quickstart-client`, then select __Client Protocol__ as `openid-connect` and __Save__:\n\n![add-client](https://static.api7.ai/uploads/2023/03/30/X5on2r7x_add-client.png)\n\nThe client `apisix-quickstart-client` is created. After redirecting to the detailed page, select `confidential` as the __Access Type__:\n\n![config-client](https://static.api7.ai/uploads/2023/03/30/v70c8y9F_config-client.png)\n\nWhen the user login is successful during the SSO, Keycloak will carry the state and code to redirect the client to the addresses in __Valid Redirect URIs__. To simplify the operation, enter wildcard `*` to consider any URI valid:\n\n![client-redirect](https://static.api7.ai/uploads/2023/03/30/xLxcyVkn_client-redirect.png)\n\nIf you are implementing the [authorization code grant with PKCE](#implement-authorization-code-grant), configure the PKCE challenge method in the client's advanced settings:\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/11/04/xvnCNb20_pkce-keycloak-revised.jpeg\" alt=\"PKCE keycloak configuration\" style={{width: '70%'}} />\n</div>\n\nIf you are implementing [client credentials grant](#implement-client-credentials-grant), enable service accounts for the client:\n\n![enable-service-account](https://static.api7.ai/uploads/2023/12/29/h1uNtghd_sa.png)\n\nSelect __Save__ to apply custom configurations.\n\n### Create a User\n\nUsers in Keycloak are entities that are able to log into the system. They can have attributes associated with themselves, such as username, email, and address.\n\nIf you are only implementing [client credentials grant](#implement-client-credentials-grant), you can [skip this section](#obtain-the-oidc-configuration).\n\nClick __Users__ > __Add user__ to open the __Add user__ page:\n\n![create-user](https://static.api7.ai/uploads/2023/03/30/onQEp23L_create-user.png)\n\nEnter the __Username__ as `quickstart-user` and select __Save__:\n\n![add-user](https://static.api7.ai/uploads/2023/03/30/EKhuhgML_add-user.png)\n\nClick on __Credentials__, then set the __Password__ as `quickstart-user-pass`. Switch __Temporary__ to `OFF` to turn off the restriction, so that you need not to change password the first time you log in:\n\n![user-pass](https://static.api7.ai/uploads/2023/03/30/rQKEAEnh_user-pass.png)\n\n## Obtain the OIDC Configuration\n\nIn this section, you will obtain the key OIDC configuration from Keycloak and define them as shell variables. Steps after this section will use these variables to configure the OIDC by shell commands.\n\n:::info\n\nOpen a separate terminal to follow the steps and define related shell variables. Then steps after this section could use the defined variables directly.\n\n:::\n\n### Get Discovery Endpoint\n\nClick __Realm Settings__, then right click __OpenID Endpoints Configuration__ and copy the link.\n\n![get-discovery](https://static.api7.ai/uploads/2023/03/30/526lbJbg_get-discovery.png)\n\nThe link should be the same as the following:\n\n```text\nhttp://localhost:8080/realms/quickstart-realm/.well-known/openid-configuration\n```\n\nConfiguration values exposed with this endpoint are required during OIDC authentication. Update the address with your host IP and save to environment variables:\n\n```shell\nexport KEYCLOAK_IP=192.168.42.145    # replace with your host IP\nexport OIDC_DISCOVERY=http://${KEYCLOAK_IP}:8080/realms/quickstart-realm/.well-known/openid-configuration\n```\n\n### Get Client ID and Secret\n\nClick on __Clients__ > `apisix-quickstart-client` > __Credentials__, and copy the client secret from __Secret__:\n\n![client-ID](https://static.api7.ai/uploads/2023/03/30/MwYmU20v_client-id.png)\n\n![client-secret](https://static.api7.ai/uploads/2023/03/30/f9iOG8aN_client-secret.png)\n\nSave the OIDC client ID and secret to environment variables:\n\n```shell\nexport OIDC_CLIENT_ID=apisix-quickstart-client\nexport OIDC_CLIENT_SECRET=bSaIN3MV1YynmtXvU8lKkfeY0iwpr9cH  # replace with your value\n```\n\n## Implement Authorization Code Grant\n\nThe authorization code grant is used by web and mobile applications. The flow starts by authorization server displaying a login page in browser where users could key in their credentials. During the process, a short-lived authorization code is exchanged for an access token, which APISIX stores in browser session cookies and will be sent with every request visiting the upstream resource server.\n\nTo implement authorization code grant, create a Route with `openid-connect` Plugin as such:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"bearer_only\": false,\n      \"session\": {\n        \"secret\": \"change_to_whatever_secret_you_want\"\n      },\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\nAlternatively, if you would like to implement authorization code grant with PKCE, create a Route with `openid-connect` Plugin as such:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"bearer_only\": false,\n      \"session\": {\n        \"secret\": \"change_to_whatever_secret_you_want\"\n      },\n      \"use_pkce\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n### Verify with Valid Credentials\n\nNavigate to `http://127.0.0.1:9080/anything/test` in browser. The request will be redirected to a login page:\n\n![test-sign-on](https://static.api7.ai/uploads/2023/03/30/i38u1x9a_validate-sign.png)\n\nLog in with the correct username `quickstart-user` and password `quickstart-user-pass`. If successful, the request will be forwarded to `httpbin.org` and you should see a response similar to the following:\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"text/html...\"\n    ...\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 59.71.244.81\",\n  \"url\": \"http://127.0.0.1/anything/test\"\n}\n```\n\n### Verify with Invalid Credentials\n\nSign in with the wrong credentials. You should see an authentication failure:\n\n![test-sign-failed](https://static.api7.ai/uploads/2023/03/31/YOuSYX1r_validate-sign-failed.png)\n\n## Implement Client Credentials Grant\n\nIn client credentials grant, clients obtain access tokens without any users involved. It is typically used in machine-to-machine (M2M) communications.\n\nTo implement client credentials grant, create a Route with `openid-connect` Plugin to use the JWKS endpoint of the identity provider to verify the token. The endpoint would be obtained from the discovery document.\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"use_jwks\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\nAlternatively, if you would like to use the introspection endpoint to verify the token, create the Route as such:\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"bearer_only\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\nThe introspection endpoint will be obtained from the discovery document.\n\n### Verify With Valid Access Token\n\nObtain an access token for the Keycloak server at the [token endpoint](https://www.keycloak.org/docs/latest/securing_apps/#token-endpoint):\n\n```shell\ncurl -i \"http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token\" -X POST \\\n  -d 'grant_type=client_credentials' \\\n  -d 'client_id='$OIDC_CLIENT_ID'' \\\n  -d 'client_secret='$OIDC_CLIENT_SECRET''\n```\n\nThe expected response is similar to the following:\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoT3ludlBPY2d6Y3VWWnYtTU42bXZKMUczb0dOX2d6MFo3WFl6S2FSa1NBIn0.eyJleHAiOjE3MDM4MjU1NjQsImlhdCI6MTcwMzgyNTI2NCwianRpIjoiMWQ4NWE4N2UtZDFhMC00NThmLThiMTItNGZiYWM2ODA5YmYwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjE1OGUzOWFlLTk0YjAtNDI3Zi04ZGU3LTU3MTRhYWYwOGYzOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJjbGllbnRJZCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1hcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.TltzSXqrJuVID7aGrb35jn-oc07U_-jugSn-3jKz4A44LwtAsME_8b3qkmR4boMOIht_5pF6bnnp70MFAlg6JKu4_yIQDxF_GAHjnZXEO8OCKhtIKwXm2w-hnnJVIhIdGkIVkbPP0HfILuar_m0hpa53VpPBGYR-OS4pyh0KTUs8MB22xAEqyz9zjCm6SX9vXCqgeVkSpRW2E8NaGEbAdY25uY-ZC4dI_pON87Ey5e8GdD6HQLXQlGIOdCDi3N7k0HDoD9TZRv2bMRPfy4zVYm1ZlClIuF79A-ZBwr0c-XYuq7t6EY0gPGEXB-s0SaKlrIU5S9JBeVXRzYvqAih41g\",\"expires_in\":300,\"refresh_expires_in\":0,\"token_type\":\"Bearer\",\"not-before-policy\":0,\"scope\":\"email profile\"}\n```\n\nSave the access token to an environment variable:\n\n```shell\n# replace with your access token\nexport ACCESS_TOKEN=\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoT3ludlBPY2d6Y3VWWnYtTU42bXZKMUczb0dOX2d6MFo3WFl6S2FSa1NBIn0.eyJleHAiOjE3MDM4MjU1NjQsImlhdCI6MTcwMzgyNTI2NCwianRpIjoiMWQ4NWE4N2UtZDFhMC00NThmLThiMTItNGZiYWM2ODA5YmYwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjE1OGUzOWFlLTk0YjAtNDI3Zi04ZGU3LTU3MTRhYWYwOGYzOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJjbGllbnRJZCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1hcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.TltzSXqrJuVID7aGrb35jn-oc07U_-jugSn-3jKz4A44LwtAsME_8b3qkmR4boMOIht_5pF6bnnp70MFAlg6JKu4_yIQDxF_GAHjnZXEO8OCKhtIKwXm2w-hnnJVIhIdGkIVkbPP0HfILuar_m0hpa53VpPBGYR-OS4pyh0KTUs8MB22xAEqyz9zjCm6SX9vXCqgeVkSpRW2E8NaGEbAdY25uY-ZC4dI_pON87Ey5e8GdD6HQLXQlGIOdCDi3N7k0HDoD9TZRv2bMRPfy4zVYm1ZlClIuF79A-ZBwr0c-XYuq7t6EY0gPGEXB-s0SaKlrIU5S9JBeVXRzYvqAih41g\"\n```\n\nSend a request to the route with the valid access token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer $ACCESS_TOKEN\"\n```\n\nAn `HTTP/1.1 200 OK` response verifies that the request to the upstream resource was authorized.\n\n### Verify With Invalid Access Token\n\nSend a request to the Route with invalid access token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer invalid-access-token\"\n```\n\nAn `HTTP/1.1 401 Unauthorized` response verifies that the OIDC Plugin rejects requests with invalid access token.\n\n### Verify without Access Token\n\nSend a request to the Route without access token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\"\n```\n\nAn `HTTP/1.1 401 Unauthorized` response verifies that the OIDC Plugin rejects requests without access token.\n\n## Implement Password Grant\n\nPassword grant is a legacy approach to exchange user credentials for an access token.\n\nTo implement password grant, create a Route with `openid-connect` Plugin to use the JWKS endpoint of the identity provider to verify the token. The endpoint would be obtained from the discovery document.\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"use_jwks\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n### Verify With Valid Access Token\n\nObtain an access token for the Keycloak server at the [token endpoint](https://www.keycloak.org/docs/latest/securing_apps/#token-endpoint):\n\n```shell\nOIDC_USER=quickstart-user\nOIDC_PASSWORD=quickstart-user-pass\ncurl -i \"http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token\" -X POST \\\n  -d 'grant_type=password' \\\n  -d 'client_id='$OIDC_CLIENT_ID'' \\\n  -d 'client_secret='$OIDC_CLIENT_SECRET'' \\\n  -d 'username='$OIDC_USER'' \\\n  -d 'password='$OIDC_PASSWORD''\n```\n\nThe expected response is similar to the following:\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ6U3FFaXN6VlpuYi1sRWMzZkp0UHNpU1ZZcGs4RGN3dXI1Mkx5V05aQTR3In0.eyJleHAiOjE2ODAxNjA5NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiMzQ5MTc4YjQtYmExZC00ZWZjLWFlYTUtZGY2MzJiMDJhNWY5IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTg4MTVjM2EtNmQwNy00YTY2LWJjZjItYWQ5NjdmMmIwMTFmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBpc2l4LXF1aWNrc3RhcnQtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoicXVpY2tzdGFydC11c2VyIn0.uD_7zfZv5182aLXu9-YBzBDK0nr2mE4FWb_4saTog2JTqFTPZZa99Gm8AIDJx2ZUcZ_ElkATqNUZ4OpWmL2Se5NecMw3slJReewjD6xgpZ3-WvQuTGpoHdW5wN9-Rjy8ungilrnAsnDA3tzctsxm2w6i9KISxvZrzn5Rbk-GN6fxH01VC5eekkPUQJcJgwuJiEiu70SjGnm21xDN4VGkNRC6jrURoclv3j6AeOqDDIV95kA_MTfBswDFMCr2PQlj5U0RTndZqgSoxwFklpjGV09Azp_jnU7L32_Sq-8coZd0nj5mSdbkJLJ8ZDQDV_PP3HjCP7EHdy4P6TyZ7oGvjw\",\"expires_in\":300,\"refresh_expires_in\":1800,\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0YjFiNTQ3Yi0zZmZjLTQ5YzQtYjE2Ni03YjdhNzIxMjk1ODcifQ.eyJleHAiOjE2ODAxNjI0NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiYzRjNjNlMTEtZTdlZS00ZmEzLWJlNGYtNDMyZWQ4ZmY5OTQwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJodHRwOi8vMTkyLjE2OC40Mi4xNDU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsInN1YiI6IjE4ODE1YzNhLTZkMDctNGE2Ni1iY2YyLWFkOTY3ZjJiMDExZiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2Iiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2In0.8xYP4bhDg1U9B5cTaEVD7B4oxNp8wwAYEynUne_Jm78\",\"token_type\":\"Bearer\",\"not-before-policy\":0,\"session_state\":\"b16b262e-1056-4515-a455-f25e077ccb76\",\"scope\":\"profile email\"}\n```\n\nSave the access token and refresh token to environment variables. The refresh token will be used in the [refresh token step](#refresh-token).\n\n```shell\n# replace with your access token\nexport ACCESS_TOKEN=\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ6U3FFaXN6VlpuYi1sRWMzZkp0UHNpU1ZZcGs4RGN3dXI1Mkx5V05aQTR3In0.eyJleHAiOjE2ODAxNjA5NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiMzQ5MTc4YjQtYmExZC00ZWZjLWFlYTUtZGY2MzJiMDJhNWY5IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTg4MTVjM2EtNmQwNy00YTY2LWJjZjItYWQ5NjdmMmIwMTFmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBpc2l4LXF1aWNrc3RhcnQtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoicXVpY2tzdGFydC11c2VyIn0.uD_7zfZv5182aLXu9-YBzBDK0nr2mE4FWb_4saTog2JTqFTPZZa99Gm8AIDJx2ZUcZ_ElkATqNUZ4OpWmL2Se5NecMw3slJReewjD6xgpZ3-WvQuTGpoHdW5wN9-Rjy8ungilrnAsnDA3tzctsxm2w6i9KISxvZrzn5Rbk-GN6fxH01VC5eekkPUQJcJgwuJiEiu70SjGnm21xDN4VGkNRC6jrURoclv3j6AeOqDDIV95kA_MTfBswDFMCr2PQlj5U0RTndZqgSoxwFklpjGV09Azp_jnU7L32_Sq-8coZd0nj5mSdbkJLJ8ZDQDV_PP3HjCP7EHdy4P6TyZ7oGvjw\"\nexport REFRESH_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0YjFiNTQ3Yi0zZmZjLTQ5YzQtYjE2Ni03YjdhNzIxMjk1ODcifQ.eyJleHAiOjE2ODAxNjI0NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiYzRjNjNlMTEtZTdlZS00ZmEzLWJlNGYtNDMyZWQ4ZmY5OTQwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJodHRwOi8vMTkyLjE2OC40Mi4xNDU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsInN1YiI6IjE4ODE1YzNhLTZkMDctNGE2Ni1iY2YyLWFkOTY3ZjJiMDExZiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2Iiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2In0.8xYP4bhDg1U9B5cTaEVD7B4oxNp8wwAYEynUne_Jm78\"\n```\n\nSend a request to the route with the valid access token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer $ACCESS_TOKEN\"\n```\n\nAn `HTTP/1.1 200 OK` response verifies that the request to the upstream resource was authorized.\n\n### Verify With Invalid Access Token\n\nSend a request to the Route with invalid access token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer invalid-access-token\"\n```\n\nAn `HTTP/1.1 401 Unauthorized` response verifies that the OIDC Plugin rejects requests with invalid access token.\n\n### Verify without Access Token\n\nSend a request to the Route without access token:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\"\n```\n\nAn `HTTP/1.1 401 Unauthorized` response verifies that the OIDC Plugin rejects requests without access token.\n\n### Refresh Token\n\nTo refresh the access token, send a request to the Keycloak token endpoint as such:\n\n```shell\ncurl -i \"http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token\" -X POST \\\n  -d 'grant_type=refresh_token' \\\n  -d 'client_id='$OIDC_CLIENT_ID'' \\\n  -d 'client_secret='$OIDC_CLIENT_SECRET'' \\\n  -d 'refresh_token='$REFRESH_TOKEN''\n```\n\nYou should see a response similar to the following, with the new access token and refresh token, which you can use for subsequent requests and token refreshes:\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJTdnVwLXlPMHhDdTJBVi1za2pCZ0h6SHZNaG1mcDVDQWc0NHpYb2QxVTlNIn0.eyJleHAiOjE3MzAyNzQ3NDUsImlhdCI6MTczMDI3NDQ0NSwianRpIjoiMjk2Mjk5MWUtM2ExOC00YWFiLWE0NzAtODgxNWEzNjZjZmM4IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMTUyLjU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2ZWI0ZTg0Yy00NmJmLTRkYzUtOTNkMC01YWM5YzE5MWU0OTciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiNTU2ZTQyYjktMjE2Yi00NTEyLWE5ZjAtNzE3ZTAyYTQ4MjZhIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLXF1aWNrc3RhcnQtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiNTU2ZTQyYjktMjE2Yi00NTEyLWE5ZjAtNzE3ZTAyYTQ4MjZhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJxdWlja3N0YXJ0LXVzZXIifQ.KLqn1LQdazoPBqLLR856C35XpqbMO9I7WFt3KrDxZF1N8vwv4AvZYWI_2rsbdjCakh9JmPgyYRgEGufYLiDBsqy9CrMVejAIJPYsJIonIXBCp5Ysu92ODJuqtTKuuJ6K7dam7fisBFfCBbVvGspnZ3p0caedpOaF_kSd-F8ARHKVsmkuX3_ucDrP3UctjEXHezefTY4YHjNMB9wuMDPXX2vXt2BsOasnznsIHHHX-ZH8JY6eEfWPtfx0qAED6lVZICT6Rqj_j5-Cf9ogzFtLyy_XvtG9BbHME2B8AXYpxdzqxOxmVVbZdrB8elfmFjs1R3vUn2r3xA9hO_znZo_IoQ\",\"expires_in\":300,\"refresh_expires_in\":1800,\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwYWYwZTAwYy0xMThjLTRkNDktYmIwMS1iMDIwNDE3MmFjMzIifQ.eyJleHAiOjE3MzAyNzYyNDUsImlhdCI6MTczMDI3NDQ0NSwianRpIjoiZGQyZTJmYTktN2Y3Zi00MjM5LWEwODAtNWQyZDFiZTdjNzk4IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMTUyLjU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsImF1ZCI6Imh0dHA6Ly8xOTIuMTY4LjE1Mi41OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJzdWIiOiI2ZWI0ZTg0Yy00NmJmLTRkYzUtOTNkMC01YWM5YzE5MWU0OTciLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiYXBpc2l4LXF1aWNrc3RhcnQtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjU1NmU0MmI5LTIxNmItNDUxMi1hOWYwLTcxN2UwMmE0ODI2YSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjU1NmU0MmI5LTIxNmItNDUxMi1hOWYwLTcxN2UwMmE0ODI2YSJ9.Uad4BVuojHfyxqedFT5BHliWjIqVDbjM-Xeme0G2AAg\",\"token_type\":\"Bearer\",\"not-before-policy\":0,\"session_state\":\"556e42b9-216b-4512-a9f0-717e02a4826a\",\"scope\":\"email profile\"}\n```\n"
  },
  {
    "path": "docs/en/latest/tutorials/manage-api-consumers.md",
    "content": "---\ntitle: Manage API Consumers\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Rate Limit\n  - Consumer\n  - Consumer Group\ndescription: This tutorial explains how to manage your single or multiple API consumers with Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis tutorial explains how to manage your single or multiple API consumers with Apache APISIX.\n\nNowadays [APIs](https://en.wikipedia.org/wiki/API) connect multiple systems, internal services, and third-party applications easily and securely. _API consumers_ are probably the most important stakeholders for API providers because they interact the most with the APIs and the developer portal. This post explains how to manage your single or multiple API consumers with an open-source API Management solution such as [Apache APISIX](https://apisix.apache.org/).\n\n![Manage API Consumers](https://static.apiseven.com/2022/11/29/6385b565b4c11.png)\n\n## API Consumers\n\nAPI consumers use an API without integrating it into an APP developed for it. In other words, API consumers are _the users of APIs_. This means, for example, a marketing department uses a [Facebook API](https://developers.facebook.com/docs/) to analyze social media responses to specific actions. It does this with individual, irregular requests to the API provided, as needed.\n\nAn [API Management](https://en.wikipedia.org/wiki/API_management) solution should know who the consumer of the API is to configure different rules for different consumers.\n\n## Apache APISIX Consumers\n\nIn Apache APISIX, the [Consumer object](https://apisix.apache.org/docs/apisix/terminology/consumer/) is the most common way for API consumers to access APIs published through its [API Gateway](https://apisix.apache.org/docs/apisix/terminology/api-gateway/). Consumer concept is extremely useful when you have different consumers requesting the same API and you need to execute various [Plugins](https://apisix.apache.org/docs/apisix/terminology/plugin/) and [Upstream](https://apisix.apache.org/docs/apisix/terminology/upstream/) configurations based on the consumer.\n\nBy publishing APIs through **Apache APISIX API Gateway**, you can easily secure API access using consumer keys or sometimes it can be referred to as subscription keys. Developers who need to consume the published APIs must include a valid subscription key in `HTTP` requests when calling those APIs. Without a valid subscription key, the calls are rejected immediately by the API gateway and not forwarded to the back-end services.\n\nConsumers can be associated with various scopes: per Plugin, all APIs, or an individual API. There are many use cases for consumer objects in the API Gateway that you get with the combination of its plugins:\n\n1. Enable different authentication methods for different consumers. It can be useful when consumers are trying to access the API by using various authentication mechanisms such as [API key](https://apisix.apache.org/docs/apisix/plugins/key-auth/), [Basic](https://apisix.apache.org/docs/apisix/plugins/basic-auth/), or [JWT](https://apisix.apache.org/docs/apisix/plugins/jwt-auth/)-based auth.\n2. Restrict access to API resources for specific consumers.\n3. Route requests to the corresponding backend service based on the consumer.\n4. Define rate limiting on the number of data clients can consume.\n5. Analyze data usage for an individual and a subset of consumers.\n\n## Apache APISIX Consumer example\n\nLet's look at some examples of configuring the rate-limiting policy for a single consumer and a group of consumers with the help of [key-auth](https://apisix.apache.org/docs/apisix/plugins/key-auth/) authentication key (API Key) and [limit-count](https://apisix.apache.org/docs/apisix/plugins/limit-count/) plugins. For the demo case,  we can leverage [the sample project](https://github.com/Boburmirzo/apisix-api-consumers-management) built on [ASP.NET Core WEB API](https://learn.microsoft.com/en-us/aspnet/core/?view=aspnetcore-7.0) with a single `GET` endpoint (retrieves all products list). You can find in [README file](https://github.com/Boburmirzo/apisix-api-consumers-management#readme) all instructions on how to run the sample app.\n\n### Enable rate-limiting for a single consumer\n\nUp to now, I assume that the sample project is up and running. To use consumer object along with the other two plugins we need to follow easy steps:\n\n- Create a new Consumer.\n- Specify the authentication plugin key-auth and limit count for the consumer.\n- Create a new Route, and set a routing rule (If necessary).\n- Enable key-auth plugin configuration for the created route.\n\nThe above steps can be achieved by running simple two [curl commands](https://en.wikipedia.org/wiki/CURL) against APISIX [Admin API](https://apisix.apache.org/docs/apisix/admin-api/).\n\nThe first `cmd` creates a **new Consumer** with API Key based authentication enabled where the API consumer can only make 2 requests against the Product API within 60 seconds.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n   \"username\":\"consumer1\",\n   \"plugins\":{\n      \"key-auth\":{\n         \"key\":\"auth-one\"\n      },\n      \"limit-count\":{\n         \"count\":2,\n         \"time_window\":60,\n         \"rejected_code\":403,\n         \"rejected_msg\":\"Requests are too many, please try again later or upgrade your subscription plan.\",\n         \"key\":\"remote_addr\"\n      }\n   }\n}'\n```\n\nThen, we define our **new Route and Upstream** so that all incoming requests to the gateway endpoint `/api/products` will be forwarded to our example product backend service after a successful authentication process.\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"name\": \"Route for consumer request rate limiting\",\n  \"methods\": [\n    \"GET\"\n  ],\n  \"uri\": \"/api/products\",\n    \"plugins\": {\n        \"key-auth\": {}\n    },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"productapi:80\": 1\n    }\n  }\n}'\n```\n\nApache APISIX will handle the first two requests as usual, but a third request in the same period will return a `403` HTTP code.\n\n``` shell\ncurl http://127.0.0.1:9080/api/products -H 'apikey: auth-one' -i\n```\n\nSample output after calling the API 3 times within 60 sec:\n\n``` shell\nHTTP/1.1 403 Forbidden\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/2.13.1\n\n{\"error_msg\":\"Requests are too many, please try again later or upgrade your subscription plan.\"}\n```\n\nIndeed, after reaching the threshold, the subsequent requests are not allowed by APISIX.\n\n### Enable rate-limiting for consumer groups\n\nIn Apache APISIX, [Consumer group](https://apisix.apache.org/docs/apisix/terminology/consumer-group/) object is used to manage the visibility of backend services to developers. Backend services are first made visible to groups, and then developers in those groups can view and subscribe to the products that are associated with the groups.\n\nWith consumer groups, you can specify any number of rate-limiting tiers and apply them to a group of consumers, instead of managing each consumer individually.\n\nTypical scenarios can be different pricing models for your API Monetization like API Consumers with the basic plan are allowed to make 50 API calls per minute or in another use case, you can enable specific APIs for Admins, Developers, and Guests based on their roles in the system.\n\nYou can create, update, delete and manage your groups using the Apache APISIX Admin REST API [Consumer Group entity](https://apisix.apache.org/docs/apisix/admin-api/#consumer-group).\n\n#### Consumer groups example\n\nFor the sake of the demo, let’s create two consumer groups for the basic and premium plans respectively. We can add one or two consumers for each group and control the traffic from consumer groups with the help of the `rate-limiting` plugin.\n\nTo use consumer groups with rate limiting, you need to:\n\n- Create one or more consumer groups with a limit-count plugin enabled.\n- Create consumers and assign consumers to groups.\n\nBelow two curl cmds create consumer groups named `basic_plan` and `premium_plan`:\n\nCreate a Consumer Group Basic Plan.\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/consumer_groups/basic_plan -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 403,\n            \"group\": \"basic_plan\"\n        }\n    }\n}'\n```\n\nCreate a Consumer Group Premium Plan.\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/consumer_groups/premium_plan -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 200,\n            \"time_window\": 60,\n            \"rejected_code\": 403,\n            \"group\": \"premium_plan\"\n        }\n    }\n}'\n```\n\nIn the above steps, we set up the rate limiting config for Basic plan to have only 2 requests per 60secs, and the Premium plan has 200 allowed API requests within the the same time window.\n\nCreate and add first consumer to the Basic group.\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"consumer1\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    },\n    \"group_id\": \"basic_plan\"\n}'\n```\n\nCreate and add second consumer to the Premium group.\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"consumer2\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-two\"\n        }\n    },\n    \"group_id\": \"premium_plan\"\n}'\n```\n\nCreate and add third consumer to the Premium group.\n\n``` shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"consumer3\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-three\"\n        }\n    },\n    \"group_id\": \"premium_plan\"\n}'\n```\n\nAfterward, we can easily check that the first consumer `Consumer1` in the Basic Plan group will get a `403 HTTP status error` after hitting the 2 API calls per a minute, while the other two consumers in the Premium Plan group can request as many times as until they reach the limit.\n\nYou can run below cmds by changing auth key for each consumer in the request header:\n\n``` shell\ncurl -i http://127.0.0.1:9080/api/products -H 'apikey: auth-one'\n```\n\n``` shell\ncurl -i http://127.0.0.1:9080/api/products -H 'apikey: auth-two'\n```\n\n``` shell\ncurl -i http://127.0.0.1:9080/api/products -H 'apikey: auth-three'\n```\n\nNote that you can also add or remove a consumer from any consumer group and enable other built-in plugins.\n\n## More Tutorials\n\nRead our other [tutorials](./expose-api.md) to learn more about API Management.\n"
  },
  {
    "path": "docs/en/latest/tutorials/monitor-api-health-check.md",
    "content": "---\ntitle: Monitor API Health Check with Prometheus\nkeywords:\n  - API Health Check\n  - Monitoring with Prometheus\n  - API Gateway\ndescription: In this tutorial, we'll guide you on how to enable and monitor API health checks using APISIX and Prometheus.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n[APISIX](https://apisix.apache.org/) has a [health check](https://apisix.apache.org/docs/apisix/tutorials/health-check/) mechanism, which proactively checks the health status of the upstream nodes in your system. Also, APISIX integrates with [Prometheus](https://prometheus.io/) through its [plugin](https://apisix.apache.org/docs/apisix/plugins/prometheus/) that exposes upstream nodes (multiple instances of a backend API service that APISIX manages) health check metrics on the Prometheus metrics endpoint typically, on URL path **`/apisix/prometheus/metrics`**.\n\nIn this tutorial, we'll guide you on how to **enable and monitor API health checks** using APISIX and Prometheus.\n\n## Prerequisite(s)\n\n- Before you start, it is good to have a basic understanding of APISIX. Familiarity with [API gateway](https://apisix.apache.org/docs/apisix/terminology/api-gateway/), and its key concepts such as [routes](https://docs.api7.ai/apisix/key-concepts/routes), [upstream](https://docs.api7.ai/apisix/key-concepts/upstreams), [Admin API](https://apisix.apache.org/docs/apisix/admin-api/), [plugins](https://docs.api7.ai/apisix/key-concepts/plugins), and HTTP protocol will also be beneficial.\n- [Docker](https://docs.docker.com/get-docker/) is used to install the containerized etcd and APISIX.\n- Install [cURL](https://curl.se/) to send requests to the services for validation.\n\n## Start the APISIX demo project\n\nThis project leverages the pre-defined [Docker Compose configuration](https://github.com/apache/apisix-docker/blob/master/example/docker-compose.yml) file to set up, deploy and run APISIX, etcd, Prometheus, and other services with a single command. First, clone the [apisix-docker](https://github.com/apache/apisix-docker) repo on GitHub and open it in your favorite editor, navigate to `/example` folder, and start the project by simply running `docker compose up` from the folder.\n\nWhen you start the project, Docker downloads any images it needs to run. You can see the full list of services in [docker-compose.yaml](https://github.com/apache/apisix-docker/blob/master/example/docker-compose.yml) file.\n\n## Add health check API endpoints in upstream\n\nTo check API health periodically, APISIX needs an HTTP path of the health endpoint of the upstream service. So, you need first to add `/health` endpoint for your backend service.  From there, you inspect the most relevant metrics for that service such as memory usage, database connectivity, response duration, and more.  Assume that we have two backend REST API services web1 and web2 running using the demo project and each has its **own health check** endpoint at URL path `/health`. At this point, you do not need to make additional configurations. In reality, you can replace them with your backend services.\n\n> The simplest and standardized way to validate the status of a service is to define a new [health check](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check) endpoint like `/health` or `/status`\n\n## Setting Up Health Checks in APISIX\n\nThis process involves checking the operational status of the 'upstream' nodes. APISIX provides two types of health checks: **Active checks** and **Passive Checks** respectively. Read more about Health Checks and how to enable them [here](https://apisix.apache.org/docs/apisix/tutorials/health-check/). Use the [Admin API](https://apisix.apache.org/docs/apisix/admin-api/) to create an Upstream object. Here is an example of creating an [Upstream](https://apisix.apache.org/docs/apisix/terminology/upstream/) object with two nodes (Per each backend service we defined) and configuring the health check parameters in the upstream object:\n\n```bash\ncurl \"http://127.0.0.1:9180/apisix/admin/upstreams/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n   \"nodes\":{\n      \"web1:80\":1,\n      \"web2:80\":1\n   },\n   \"checks\":{\n      \"active\":{\n         \"timeout\":5,\n         \"type\":\"http\",\n         \"http_path\":\"/health\",\n         \"healthy\":{\n            \"interval\":2,\n            \"successes\":1\n         },\n         \"unhealthy\":{\n            \"interval\":1,\n            \"http_failures\":2\n         }\n      }\n   }\n}'\n```\n\nThis example configures an active health check on the **`/health`** endpoint of the node. It considers the node healthy after **one successful health check** and unhealthy **after two failed health checks**.\n\n> Note that sometimes you might need the IP addresses of upstream nodes, not their domains (`web1` and `web2`) if you are running services outside docker network. Health check will be started only if the number of nodes (resolved IPs) is bigger than 1.\n\n## Enable the Prometheus Plugin\n\nCreate a global rule to enable the `prometheus` plugin on all routes by adding `\"prometheus\": {}` in the plugins option. APISIX gathers internal runtime metrics and exposes them through port `9091` and URI path `/apisix/prometheus/metrics` by default that Prometheus can scrape. It is also possible to customize the export port and **URI path**, **add** **extra labels, the frequency of these scrapes, and other parameters** by configuring them in the Prometheus configuration `/prometheus_conf/prometheus.yml`file.\n\n```bash\ncurl \"http://127.0.0.1:9180/apisix/admin/global_rules\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n   \"id\":\"rule-for-metrics\",\n   \"plugins\":{\n      \"prometheus\":{\n      }\n   }\n}'\n```\n\n## Create a Route\n\nCreate a [Route](https://apisix.apache.org/docs/apisix/terminology/route/) object to route incoming requests to upstream nodes:\n\n```bash\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n   \"name\":\"backend-service-route\",\n   \"methods\":[\n      \"GET\"\n   ],\n   \"uri\":\"/\",\n   \"upstream_id\":\"1\"\n}'\n```\n\n## Send validation requests to the route\n\nTo generate some metrics, you try to send few requests to the route we created in the previous step:\n\n```bash\ncurl -i -X GET \"http://localhost:9080/\"\n```\n\nIf you run the above requests a couple of times, you can see from responses that APISIX routes some requests to `node1` and others to `node2`. That’s how Gateway load balancing works!\n\n```bash\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=utf-8\nContent-Length: 10\nConnection: keep-alive\nDate: Sat, 22 Jul 2023 10:16:38 GMT\nServer: APISIX/3.3.0\n\nhello web2\n\n...\n\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=utf-8\nContent-Length: 10\nConnection: keep-alive\nDate: Sat, 22 Jul 2023 10:16:39 GMT\nServer: APISIX/3.3.0\n\nhello web1\n```\n\n## Collecting health check data with the Prometheus plugin\n\nOnce the health checks and route are configured in APISIX, you can employ Prometheus to monitor health checks. APISIX **automatically exposes health check metrics data** for your APIs if the health check parameter is enabled for upstream nodes. You will see metrics in the response after fetching them from APISIX:\n\n```bash\ncurl -i http://127.0.0.1:9091/apisix/prometheus/metrics\n```\n\nExample Output:\n\n```bash\n# HELP apisix_http_requests_total The total number of client requests since APISIX started\n# TYPE apisix_http_requests_total gauge\napisix_http_requests_total 119740\n# HELP apisix_http_status HTTP status codes per service in APISIX\n# TYPE apisix_http_status counter\napisix_http_status{code=\"200\",route=\"1\",matched_uri=\"/\",matched_host=\"\",service=\"\",consumer=\"\",node=\"172.27.0.5\"} 29\napisix_http_status{code=\"200\",route=\"1\",matched_uri=\"/\",matched_host=\"\",service=\"\",consumer=\"\",node=\"172.27.0.7\"} 12\n# HELP apisix_upstream_status Upstream status from health check\n# TYPE apisix_upstream_status gauge\napisix_upstream_status{name=\"/apisix/upstreams/1\",ip=\"172.27.0.5\",port=\"443\"} 0\napisix_upstream_status{name=\"/apisix/upstreams/1\",ip=\"172.27.0.5\",port=\"80\"} 1\napisix_upstream_status{name=\"/apisix/upstreams/1\",ip=\"172.27.0.7\",port=\"443\"} 0\napisix_upstream_status{name=\"/apisix/upstreams/1\",ip=\"172.27.0.7\",port=\"80\"} 1\n```\n\nHealth check data is represented with metrics label `apisix_upstream_status`. It has attributes like upstream `name`, `ip` and `port`. A value of 1 represents healthy and 0 means the upstream node is unhealthy.\n\n## Visualize the data in the Prometheus dashboard\n\nNavigate to http://localhost:9090/ where the Prometheus instance is running in Docker and type **Expression** `apisix_upstream_status` in the search bar. You can also see the output of the health check statuses of upstream nodes on the **Prometheus dashboard** in the table or graph view:\n\n![Visualize the data in Prometheus dashboard](https://static.apiseven.com/uploads/2023/07/20/OGBtqbDq_output.png)\n\n## Next Steps\n\nYou have now learned how to set up and monitor API health checks with Prometheus and APISIX.  APISIX Prometheus plugin is configured to connect [Grafana](https://grafana.com/) automatically to visualize metrics. Keep exploring the data and customize the [Grafana dashboard](https://grafana.com/grafana/dashboards/11719-apache-apisix/) by adding a panel that shows the number of active health checks.\n\n### Related resources\n\n- [Monitoring API Metrics: How to Ensure Optimal Performance of Your API?](https://api7.ai/blog/api7-portal-monitor-api-metrics)\n- [Monitoring Microservices with Prometheus and Grafana](https://api7.ai/blog/introduction-to-monitoring-microservices)\n\n### Recommended content\n\n- [Implementing resilient applications with API Gateway (Health Check)](https://dev.to/apisix/implementing-resilient-applications-with-api-gateway-health-check-338c)\n"
  },
  {
    "path": "docs/en/latest/tutorials/observe-your-api.md",
    "content": "---\ntitle: Observe APIs\nkeywords:\n  - API gateway\n  - Apache APISIX\n  - Observability\n  - Monitor\n  - Plugins\ndescription: Apache APISIX Observability Plugins and take a look at how to set up these plugins.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nIn this guide, we can leverage the power of some [Apache APISIX](https://apisix.apache.org/) Observability Plugins and take a look at how to set up these plugins, how to use them to understand API behavior, and later solve problems that impact our users.\n\n## API Observability\n\nNowadays **API Observability** is already a part of every API development as it addresses many problems related to API consistency, reliability, and the ability to quickly iterate on new API features. When you design for full-stack observability, you get everything you need to find issues and catch breaking changes.\n\nAPI observability can help every team in your organization:\n\n- Sales and growth teams to monitor your API usage, free trials, observe expansion opportunities and ensure that API serves the correct data.\n\n- Engineering teams to monitor and troubleshoot API issues.\n\n- Product teams to understand API usage and business value.\n\n- Security teams to detect and protect from API threats.\n\n![API observability in every team](https://static.apiseven.com/2022/09/14/6321ceff5548e.jpg)\n\n## A central point for observation\n\nWe know that **an API gateway** offers a central control point for incoming traffic to a variety of destinations but it can also be a central point for observation as well since it is uniquely qualified to know about all the traffic moving between clients and our service networks.\n\nThe core of observability breaks down into _three key areas_: structured logs, metrics, and traces. Let’s break down each pillar of API observability and learn how with Apache APISIX Plugins we can simplify these tasks and provides a solution that you can use to better understand API usage.\n\n![Observability of three key areas](https://static.apiseven.com/2022/09/14/6321cf14c555a.jpg)\n\n## Prerequisites\n\nBefore enabling our plugins we need to install Apache APISIX, create a route, an upstream, and map the route to the upstream. You can simply follow [getting started guide](https://apisix.apache.org/docs/apisix/getting-started) provided on the website.\n\n## Logs\n\n**Logs** are also easy to instrument and trivial steps of API observability, they can be used to inspect API calls in real-time for debugging, auditing, and recording time-stamped events that happened over time. There are several logger plugins Apache APISIX provides such as:\n\n- [http-logger](https://apisix.apache.org/docs/apisix/plugins/http-logger/)\n\n- [skywalking-logger](https://apisix.apache.org/docs/apisix/plugins/skywalking-logger/)\n\n- [tcp-logger](https://apisix.apache.org/docs/apisix/plugins/tcp-logger)\n\n- [kafka-logger](https://apisix.apache.org/docs/apisix/plugins/kafka-logger)\n\n- [rocketmq-logger](https://apisix.apache.org/docs/apisix/plugins/rocketmq-logger)\n\n- [udp-logger](https://apisix.apache.org/docs/apisix/plugins/udp-logger)\n\n- [clickhouse-logger](https://apisix.apache.org/docs/apisix/plugins/clickhouse-logger)\n\n- [error-logger](https://apisix.apache.org/docs/apisix/plugins/error-log-logger)\n\n- [google-cloud-logging](https://apisix.apache.org/docs/apisix/plugins/google-cloud-logging)\n\nAnd you can see the [full list](../plugins/http-logger.md) on the official website of Apache APISIX. Now for demo purposes, let's choose a simple but mostly used _http-logger_ plugin that is capable of sending API Log data requests to HTTP/HTTPS servers or sends as JSON objects to Monitoring tools. We can assume that a route and an upstream are created.  You can learn how to set up them in the **[Getting started with Apache APISIX](https://youtu.be/dUOjJkb61so)** video tutorial. Also, you can find all command-line examples on the GitHub page [apisix-observability-plugins](https://boburmirzo.github.io/apisix-observability-plugins/)\n\nYou can generate a mock HTTP server at [mockbin.com](https://mockbin.org/) to record and view the logs. Note that we also bind the route to an upstream (You can refer to this documentation to learn about more [core concepts of Apache APISIX](https://apisix.apache.org/docs/apisix/architecture-design/apisix)).\n\nThe following is an example of how to enable the http-logger for a specific route.\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"plugins\": {\n    \"http-logger\": {\n      \"uri\": \"http://mockbin.org/bin/5451b7cd-af27-41b8-8df1-282ffea13a61\"\n    }\n  },\n  \"upstream_id\": \"1\",\n  \"uri\": \"/get\"\n}'\n\n```\n\n:::note\n\nTo `http-logger` plugin settings, your can just put your mock server URI address like below:\n\n```json\n{\n  \"uri\": \"http://mockbin.org/bin/5451b7cd-af27-41b8-8df1-282ffea13a61\"\n}\n```\n\n:::\n\nOnce we get a successful response from APISIX server, we can send a request to this _get_ endpoint to generate logs.\n\n```shell\n\ncurl -i http://127.0.0.1:9080/get\n\n```\n\nThen if you click and navigate to the following our [mock server link](http://mockbin.org/bin/5451b7cd-af27-41b8-8df1-282ffea13a61/log) some recent logs are sent and we can see them:\n\n![http-logger-plugin-test-screenshot](https://static.apiseven.com/2022/09/14/6321d1d83eb7a.png)\n\n## Metrics\n\n**Metrics** are a numeric representation of data measured over intervals of time. You can also aggregate this data into daily or weekly frequency and run queries against a distributed system like [Elasticsearch](https://www.elastic.co/). Or sometimes based on metrics you trigger alerts to take any action later. Once API metrics are collected, you can track them with metrics tracking tools such as [Prometheus](https://prometheus.io/).\n\nApache APISIX API Gateway also offers [prometheus-plugin](https://apisix.apache.org/docs/apisix/plugins/prometheus/) to fetch your API metrics and expose them in Prometheus. Behind the scene, Apache APISIX downloads the Grafana dashboard meta, imports it to [Grafana](https://grafana.com/), and fetches real-time metrics from the Prometheus plugin.\n\nLet’s enable prometheus-plugin for our route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"uri\": \"/get\",\n  \"plugins\": {\n    \"prometheus\": {}\n  },\n  \"upstream_id\": \"1\"\n}'\n```\n\nWe fetch the metric data from the specified URL `/apisix/prometheus/metrics`.\n\n```shell\ncurl -i http://127.0.0.1:9091/apisix/prometheus/metrics\n```\n\nYou will get a response with Prometheus metrics something like below:\n\n```text\nHTTP/1.1 200 OK\nServer: openresty\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\n\n# HELP apisix_batch_process_entries batch process remaining entries\n# TYPE apisix_batch_process_entries gauge\napisix_batch_process_entries{name=\"http logger\",route_id=\"1\",server_addr=\"172.19.0.8\"} 0\n# HELP apisix_etcd_modify_indexes Etcd modify index for APISIX keys\n# TYPE apisix_etcd_modify_indexes gauge\napisix_etcd_modify_indexes{key=\"consumers\"} 17819\napisix_etcd_modify_indexes{key=\"global_rules\"} 17832\napisix_etcd_modify_indexes{key=\"max_modify_index\"} 20028\napisix_etcd_modify_indexes{key=\"prev_index\"} 18963\napisix_etcd_modify_indexes{key=\"protos\"} 0\napisix_etcd_modify_indexes{key=\"routes\"} 20028\n...\n```\n\nAnd we can also check the status of our endpoint at the Prometheus dashboard by pointing to this URL `http://localhost:9090/targets`\n\n![plugin-orchestration-configure-rule-screenshot](https://static.apiseven.com/2022/09/14/6321d30b32024.png)\n\nAs you can see, Apache APISIX exposed metrics endpoint is upon and running.\n\nNow you can query metrics for `apisix_http_status` to see what HTTP requests are handled by API Gateway and what was the outcome.\n\n![prometheus-plugin-dashboard-query-http-status-screenshot](https://static.apiseven.com/2022/09/14/6321d30aed3b2.png)\n\nIn addition to this, you can view the Grafana dashboard running in your local instance. Go to `http://localhost:3000/`\n\n![prometheus-plugin-grafana-dashboard-screenshot](https://static.apiseven.com/2022/09/14/6321d30bba97c.png)\n\nYou can also check two other plugins for metrics:\n\n- [Node status Plugin](../plugins/node-status.md)\n\n- [Datadog Plugin](../plugins/datadog.md)\n\n## Tracing\n\nThe third is **tracing** or distributed tracing allows you to understand the life of a request as it traverses your service network and allows you to answer questions like what service has this request touched and how much latency was introduced. Traces enable you to further explore which logs to look at for a particular session or related set of API calls.\n\n[Zipkin](https://zipkin.io/) an open-source distributed tracing system. [APISIX plugin](https://apisix.apache.org/docs/apisix/plugins/zipkin) is supported to collect tracing and report to Zipkin Collector based on [Zipkin API specification](https://zipkin.io/pages/instrumenting.html).\n\nHere’s an example to enable the `zipkin` plugin on the specified route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"methods\": [\n    \"GET\"\n  ],\n  \"uri\": \"/get\",\n  \"plugins\": {\n    \"zipkin\": {\n      \"endpoint\": \"http://127.0.0.1:9411/api/v2/spans\",\n      \"sample_ratio\": 1\n    }\n  },\n  \"upstream_id\": \"1\"\n}'\n```\n\nWe can test our example by simply running the following curl command:\n\n```shell\ncurl -i http://127.0.0.1:9080/get\n```\n\nAs you can see, there are some additional trace identifiers (like traceId, spanId, parentId) were appended to the headers:\n\n```text\n\"X-B3-Parentspanid\": \"61bd3f4046a800e7\",\n\"X-B3-Sampled\": \"1\",\n\"X-B3-Spanid\": \"855cd5465957f414\",\n\"X-B3-Traceid\": \"e18985df47dab632d62083fd96626692\",\n```\n\nThen you can use a browser to access `http://127.0.0.1:9411/zipkin`, see traces on the Web UI of Zipkin.\n\n> Note that you need to run the Zipkin instance in order to install Zipkin Web UI. For example, by using docker you can simply run it:\n>`docker run -d -p 9411:9411 openzipkin/zipkin`\n\n![Zipkin plugin output 1](https://static.apiseven.com/2022/09/14/6321dc27f3d33.png)\n\n![Zipkin plugin output 2](https://static.apiseven.com/2022/09/14/6321dc284049c.png)\n\nAs you noticed, the recent traces were exposed in the above pictures.\n\nYou can also check two other plugins for tracing:\n\n- [Skywalking-plugin](../plugins/skywalking.md)\n\n- [Opentelemetry-plugin](../plugins/opentelemetry.md)\n\n## Summary\n\nAs we learned, API Observability is a sort of framework for managing your applications in an API world and Apache APISIX API Gateway plugins can help when observing modern API-driven applications by integrating to several observability platforms. So, you can make your development work focused on core business features instead of building a custom integration for observability tools.\n"
  },
  {
    "path": "docs/en/latest/tutorials/protect-api.md",
    "content": "---\ntitle: Protect API\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - Rate Limit\n  - Protect API\ndescription: This article describes how to secure your API with the rate limiting plugin for API Gateway Apache APISIX.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis article describes secure your API with the rate limiting plugin for API Gateway Apache APISIX.\n\n## Concept introduction\n\n### Plugin\n\nThis represents the configuration of the plugins that are executed during the HTTP request/response lifecycle. A [Plugin](../terminology/plugin.md) configuration can be bound directly to a Route, a Service, a Consumer or a Plugin Config.\n\n:::note\n\nIf [Route](../terminology/route.md), [Service](../terminology/service.md), [Plugin Config](../terminology/plugin-config.md) or [Consumer](../terminology/consumer.md) are all bound to the same for plugins, only one plugin configuration will take effect. The priority of plugin configurations is described in [plugin execution order](../terminology/plugin.md#plugins-execution-order). At the same time, there are various stages involved in the plugin execution process. See [plugin execution lifecycle](../terminology/plugin.md#plugins-execution-order).\n\n:::\n\n## Preconditions\n\nBefore following this tutorial, ensure you have [exposed the service](./expose-api.md).\n\n## Protect your API\n\nWe can use rate limits to limit our API services to ensure the stable operation of API services and avoid system crashes caused by some sudden traffic. We can restrict as follows:\n\n1. Limit the request rate;\n2. Limit the number of requests per unit time;\n3. Delay request;\n4. Reject client requests;\n5. Limit the rate of response data.\n\nAPISIX provides several plugins for limiting current and speed, including [limit-conn](../plugins/limit-conn.md), [limit-count](../plugins/limit-count.md), [limit- req](../plugins/limit-req.md) and other plugins.\n\n- The `limit-conn` Plugin limits the number of concurrent requests to your services.\n- The `limit-req` Plugin limits the number of requests to your service using the leaky bucket algorithm.\n- The `limit-count` Plugin limits the number of requests to your service by a given count per time.\n\nNext, we will use the `limit-count` plugin as an example to show you how to protect your API with a rate limit plugin:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. Create a Route.\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key_type\": \"var\",\n            \"key\": \"remote_addr\"\n        }\n    },\n  \"upstream_id\": \"1\"\n}'\n```\n\nIn the above configuration, a Route with ID `1` is created using the upstream made in [Expose Service](./expose-api.md), and the `limit-count` plugin is enabled. The plugin only allows the client to access the upstream service `2` times within `60` seconds. If more than two times, the `503` error code will be returned.\n\n2. Test\n\n```shell\ncurl http://127.0.0.1:9080/index.html\n```\n\nAfter using the above command to access three times in a row, the following error will appear:\n\n```\n<html>\n<head><title>503 Service Temporarily Unavailable</title></head>\n<body>\n<center><h1>503 Service Temporarily Unavailable</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\nIf the above result is returned, the `limit-count` plugin has taken effect and protected your API.\n\n## More Traffic plugins\n\nIn addition to providing plugins for limiting current and speed, APISIX also offers many other plugins to meet the needs of actual scenarios:\n\n- [proxy-cache](../plugins/proxy-cache.md): This plugin provides the ability to cache backend response data. It can be used with other plugins. The plugin supports both disk and memory-based caching. Currently, the data to be cached can be specified according to the response code and request mode, and more complex caching strategies can also be configured through the no_cache and cache_bypass attributes.\n- [request-validation](../plugins/request-validation.md): This plugin is used to validate requests forwarded to upstream services in advance.\n- [proxy-mirror](../plugins/proxy-mirror.md): This plugin provides the ability to mirror client requests. Traffic mirroring is copying the real online traffic to the mirroring service, so that the online traffic or request content can be analyzed in detail without affecting the online service.\n- [api-breaker](../plugins/api-breaker.md): This plugin implements an API circuit breaker to help us protect upstream business services.\n- [traffic-split](../plugins/traffic-split.md): You can use this plugin to gradually guide the percentage of traffic between upstreams to achieve blue-green release and grayscale release.\n- [request-id](../plugins/request-id.md): The plugin adds a `unique` ID to each request proxy through APISIX for tracking API requests.\n- [proxy-control](../plugins/proxy-control.md): This plugin can dynamically control the behavior of NGINX proxy.\n- [client-control](../plugins/client-control.md): This plugin can dynamically control how NGINX handles client requests by setting an upper limit on the client request body size.\n\n## More Tutorials\n\nYou can refer to the [Observe API](./observe-your-api.md) document to monitor APISIX, collect logs, and track.\n"
  },
  {
    "path": "docs/en/latest/tutorials/websocket-authentication.md",
    "content": "---\ntitle: WebSocket Authentication\nkeywords:\n  - API Gateway\n  - Apache APISIX\n  - WebSocket\n  - Authentication\ndescription: This article is a guide on how to configure authentication for WebSocket connections.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nApache APISIX supports [WebSocket](https://en.wikipedia.org/wiki/WebSocket) traffic, but the WebSocket protocol doesn't handle authentication. This article guides you on how to configure authentication for WebSocket connections using Apache APISIX.\n\n## WebSocket Protocol\n\nTo establish a WebSocket connection, the client sends a WebSocket handshake request, for which the server returns a WebSocket handshake response as shown below:\n\n```text title=\"Client request\"\nGET /chat HTTP/1.1\nHost: server.example.com\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\nSec-WebSocket-Protocol: chat, superchat\nSec-WebSocket-Version: 13\nOrigin: http://example.com\n```\n\n```text title=\"Server response\"\nHTTP/1.1 101 Switching Protocols\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\nSec-WebSocket-Protocol: chat\n```\n\nThe handshake workflow is shown below:\n\n![Websocket Handshake Workflow](https://static.apiseven.com/2022/12/06/638eda2e2415f.png)\n\n## WebSocket Authentication\n\nAPISIX supports several authentication methods like [basic-auth](https://apisix.apache.org/docs/apisix/plugins/basic-auth/), [key-auth](https://apisix.apache.org/docs/apisix/plugins/key-auth/), and [jwt-auth](https://apisix.apache.org/docs/apisix/plugins/jwt-auth/).\n\nWhile establishing connections from the client to server in the _handshake_ phase, APISIX first checks its authentication information before choosing to forward the request or deny it.\n\n## Prerequisites\n\nBefore you move on, make sure you have:\n\n1. A WebSocket server as the Upstream. This article uses [Postman's public echo service](https://blog.postman.com/introducing-postman-websocket-echo-service/): `wss://ws.postman-echo.com/raw`.\n2. APISIX 3.0 installed.\n\n## Configuring Authentication\n\n### Create a Route\n\nFirst we will create a Route to the Upstream echo service.\n\nSince the Upstream uses wss protocol, the scheme is set to `https`. We should also set `enable_websocket` to `true`.\n\nIn this tutorial, we will use the [key-auth](https://apisix.apache.org/docs/apisix/plugins/key-auth/) Plugin. This would work similarly for other authentication methods:\n\n```shell\ncurl --location --request PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"uri\": \"/*\",\n    \"methods\": [\"GET\"],\n    \"enable_websocket\": true,\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"ws.postman-echo.com:443\": 1\n        },\n        \"scheme\": \"https\"\n    },\n    \"plugins\": {\n        \"key-auth\": {}\n    }\n}'\n```\n\n### Create a Consumer\n\nWe will now create a [Consumer](https://apisix.apache.org/docs/apisix/terminology/consumer/) and add a key `this_is_the_key`. A user would now need to use this key configured in the Consumer object to access the API.\n\n```sh\ncurl --location --request PUT 'http://127.0.0.1:9180/apisix/admin/consumers/jack' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"this_is_the_key\"\n        }\n    }\n}'\n```\n\n## Testing the Route\n\nNow, if you try to connect `ws://127.0.0.1:9080/raw` without the `apikey` header or an incorrect key, APISIX will return a `401 Unauthorized`.\n\n![Connect without Key](https://static.apiseven.com/2022/12/06/638ef6db9dd4b.png)\n\nTo authenticate, you can add the header `apikey` with the value `this_is_the_key`:\n\n![Connect with key](https://static.apiseven.com/2022/12/06/638efac7c42b6.png)\n"
  },
  {
    "path": "docs/en/latest/upgrade-guide-from-2.15.x-to-3.0.0.md",
    "content": "---\ntitle: Upgrade Guide\nkeywords:\n  - APISIX\n  - APISIX Upgrade Guide\n  - APISIX Version Upgrade\ndescription: Guide for upgrading APISIX from version 2.15.x to 3.0.0.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis document guides you in upgrading APISIX from version 2.15.x to 3.0.0.\n\n:::note\n\nUpgrading to version 3.0.0 is a major change and it is recommended that you first upgrade to version 2.15.x before you upgrade to 3.0.0.\n\n:::\n\n## Changelog\n\nPlease refer to the [3.0.0-beta](https://github.com/apache/apisix/blob/master/CHANGELOG.md#300-beta) and [3.0.0](https://github.com/apache/apisix/blob/master/CHANGELOG.md#300) changelogs for a complete list of incompatible changes and major updates.\n\n## Deployments\n\nFrom 3.0.0, we no longer support the Alpine-based images of APISIX. You can use the [Debian or CentOS-based images](https://hub.docker.com/r/apache/apisix/tags?page=1&ordering=last_updated) instead.\n\nIn addition to the Docker images, we also provide:\n\n1. RPM packages for CentOS 7 and CentOS 8 supporting both AMD64 and ARM64 architectures.\n2. DEB packages for Debian 11 (bullseye) supporting both AMD64 and ARM64 architectures.\n\nSee the [installation guide](/installation-guide.md) for more details.\n\n3.0.0 also introduces multiple deployment modes. The following modes are supported:\n\n1. [Traditional](./deployment-modes.md#traditional): As the name implies, this is the original deployment mode where one instance of APISIX acts as the control plane and the data plane. Use this deployment mode to keep your deployment similar to older versions.\n2. [Decoupled](./deployment-modes.md#decoupled): In this mode, the data plane and the control plane are separated. You can deploy an instance of APISIX either as a control plane or a data plane.\n3. [Standalone](./deployment-modes.md#standalone): Using this mode will disable etcd as the configuration center and use a static configuration file instead. You can use this to manage APISIX configuration decaratively or for using other configuration centers.\n\n## Dependencies\n\nAll Docker images and binary packages (RPM, DEB) already come with all the necessary dependencies for APISIX.\n\nSome features might require additional Nginx modules in OpenResty and requires you to [build a custom OpenResty distribution (APISIX-Base)](https://github.com/api7/apisix-build-tools).\n\nTo run APISIX on a native OpenResty instance use [OpenResty version 1.19.3.2](https://openresty.org/en/download.html#legacy-releases) and above.\n\n## Configurations\n\nThere are some major changes to the configuration file in APISIX. You need to update your configuration file (`conf/config.yaml`) to reflect these changes. See the `conf/config-default.yaml` file for the complete changes.\n\nThe following attributes in the configuration have been moved:\n\n1. `config_center` is replaced by `config_provider` and moved under `deployment`.\n2. `etcd` is moved under `deployment`.\n3. The following Admin API configuration attributes are moved to the `admin` attribute under `deployment`:\n   1. `admin_key`\n   2. `enable_admin_cors`\n   3. `allow_admin`\n   4. `admin_listen`\n   5. `https_admin`\n   6. `admin_api_mtls`\n   7. `admin_api_version`\n\nThe following attributes in the configuration have been replaced:\n\n1. `enable_http2` and `listen_port` under `apisix.ssl` are replaced by `apisix.ssl.listen`. i.e., the below configuration:\n\n   ```yaml title=\"conf/config.yaml\"\n   ssl:\n     enable_http2: true\n     listen_port: 9443\n   ```\n\n   changes to:\n\n   ```yaml title=\"conf/config.yaml\"\n   ssl:\n     listen:\n       - port: 9443\n         enable_http2: true\n   ```\n\n2. `nginx_config.http.lua_shared_dicts` is replaced by `nginx_config.http.custom_lua_shared_dict`. i.e., the below configuration:\n\n   ```yaml title=\"conf/config.yaml\"\n   nginx_config:\n     http:\n       lua_shared_dicts:\n         my_dict: 1m\n   ```\n\n   changes to:\n\n   ```yaml title=\"conf/config.yaml\"\n   nginx_config:\n     http:\n       custom_lua_shared_dict:\n       my_dict: 1m\n   ```\n\n   This attribute declares custom shared memory blocks.\n\n3. `etcd.health_check_retry` is replaced by `deployment.etcd.startup_retry`. So this configuration:\n\n   ```yaml title=\"conf/config.yaml\"\n   etcd:\n     health_check_retry: 2\n   ```\n\n   changes to:\n\n   ```yaml title=\"conf/config.yaml\"\n   deployment:\n     etcd:\n       startup_retry: 2\n   ```\n\n   This attribute is to configure the number of retries when APISIX tries to connect to etcd.\n\n4. `apisix.port_admin` is replaced by `deployment.admin.admin_listen`. So your previous configuration:\n\n   ```yaml title=\"conf/config.yaml\"\n   apisix:\n     port_admin: 9180\n   ```\n\n   Should be changed to:\n\n   ```yaml title=\"conf/config.yaml\"\n   deployment:\n     apisix:\n       admin_listen:\n         ip: 127.0.0.1 # replace with the actual IP exposed\n         port: 9180\n   ```\n\n   This attribute configures the Admin API listening port.\n\n5. `apisix.real_ip_header` is replaced by `nginx_config.http.real_ip_header`.\n\n6. `enable_cpu_affinity` is set to `false` by default instead of `true`. This is because Nginx's `worker_cpu_affinity` does not count against the cgroup when APISIX is deployed in containers. In such scenarios, it can affect APISIX's behavior when multiple instances are bound to a single CPU.\n\n## Data Compatibility\n\nIn 3.0.0, the data structures holding route, upstream, and plugin configuration have been modified and is not fully compatible with 2.15.x. You won't be able to connect an instance of APISIX 3.0.0 to an etcd cluster used by APISIX 2.15.x.\n\nTo ensure compatibility, you can try one of the two ways mentioned below:\n\n1. Backup the incompatible data (see [etcdctl snapshot](https://etcd.io/docs/v3.5/op-guide/maintenance/#snapshot-backup)) in etcd and clear it. Convert the backed up data to be compatible with 3.0.0 as mentioned in the below examples and reconfigure it through the Admin API of 3.0.0 instance.\n2. Use custom scripts to convert the data structure in etcd to be compatible with 3.0.0.\n\nThe following changes have been made in version 3.0.0:\n\n1. `disable` attribute of a plugin has been moved under `_meta`. It enables or disables the plugin. For example, this configuration to disable the `limit-count` plugin:\n\n   ```json\n   {\n    \"plugins\":{\n        \"limit-count\":{\n            ... // plugin configuration\n            \"disable\":true\n        }\n    }\n   }\n   ```\n\n   should be changed to:\n\n   ```json\n   {\n    \"plugins\":{\n        \"limit-count\":{\n            ... // plugin configuration\n            \"_meta\":{\n                \"disable\":true\n            }\n        }\n    }\n   }\n   ```\n\n2. `service_protocol` in route has been replaced with `upstream.scheme`. For example, this configuration:\n\n   ```json\n   {\n     \"uri\": \"/hello\",\n     \"service_protocol\": \"grpc\",\n     \"upstream\": {\n       \"type\": \"roundrobin\",\n       \"nodes\": {\n         \"127.0.0.1:1980\": 1\n       }\n     }\n   }\n   ```\n\n   Should be changed to:\n\n   ```json\n   {\n     \"uri\": \"/hello\",\n     \"upstream\": {\n       \"type\": \"roundrobin\",\n       \"scheme\": \"grpc\",\n       \"nodes\": {\n         \"127.0.0.1:1980\": 1\n       }\n     }\n   }\n   ```\n\n3. `audience` field from the [authz-keycloak](./plugins/authz-keycloak.md) plugin has been replaced with `client_id`. So this configuration:\n\n   ```json\n   {\n    \"plugins\":{\n        \"authz-keycloak\":{\n            ... // plugin configuration\n            \"audience\":\"Client ID\"\n        }\n    }\n   }\n   ```\n\n   should be changed to:\n\n   ```json\n   {\n    \"plugins\":{\n        \"authz-keycloak\":{\n            ... // plugin configuration\n            \"client_id\":\"Client ID\"\n        }\n    }\n   }\n   ```\n\n4. `upstream` attribute from the [mqtt-proxy](./plugins/mqtt-proxy.md) plugin has been moved outside the plugin conference and referenced in the plugin. The configuration below:\n\n   ```json\n   {\n     \"remote_addr\": \"127.0.0.1\",\n     \"plugins\": {\n       \"mqtt-proxy\": {\n         \"protocol_name\": \"MQTT\",\n         \"protocol_level\": 4,\n         \"upstream\": {\n           \"ip\": \"127.0.0.1\",\n           \"port\": 1980\n         }\n       }\n     }\n   }\n   ```\n\n   changes to:\n\n   ```json\n   {\n     \"remote_addr\": \"127.0.0.1\",\n     \"plugins\": {\n       \"mqtt-proxy\": {\n         \"protocol_name\": \"MQTT\",\n         \"protocol_level\": 4\n       }\n     },\n     \"upstream\": {\n       \"type\": \"chash\",\n       \"key\": \"mqtt_client_id\",\n       \"nodes\": [\n         {\n           \"host\": \"127.0.0.1\",\n           \"port\": 1980,\n           \"weight\": 1\n         }\n       ]\n     }\n   }\n   ```\n\n5. `max_retry_times` and `retry_interval` fields from the [syslog](./plugins/syslog.md) plugin are replaced `max_retry_count` and `retry_delay` respectively. The configuration below:\n\n   ```json\n   {\n    \"plugins\":{\n        \"syslog\":{\n            \"max_retry_times\":1,\n            \"retry_interval\":1,\n            ... // other configuration\n        }\n    }\n   }\n   ```\n\n   changes to:\n\n   ```json\n   {\n    \"plugins\":{\n        \"syslog\":{\n            \"max_retry_count\":1,\n            \"retry_delay\":1,\n            ... // other configuration\n        }\n    }\n   }\n   ```\n\n6. `scheme` attribute has been removed from the [proxy-rewrite](./plugins/proxy-rewrite.md) plugin and has been added to the upstream. The configuration below:\n\n   ```json\n   {\n    \"plugins\":{\n        \"proxy-rewrite\":{\n            \"scheme\":\"https\",\n            ... // other configuration\n        }\n    },\n    \"upstream\":{\n        \"nodes\":{\n            \"127.0.0.1:1983\":1\n        },\n        \"type\":\"roundrobin\"\n    },\n    \"uri\":\"/hello\"\n   }\n   ```\n\n   changes to:\n\n   ```json\n   {\n   \"plugins\":{\n      \"proxy-rewrite\":{\n          ... // other configuration\n      }\n   },\n   \"upstream\":{\n      \"scheme\":\"https\",\n      \"nodes\":{\n          \"127.0.0.1:1983\":1\n      },\n      \"type\":\"roundrobin\"\n   },\n   \"uri\":\"/hello\"\n   }\n   ```\n\n## API\n\nChanges have been made to the Admin API to make it easier to use and be more RESTful.\n\nThe following changes have been made:\n\n1. The `count`, `action`, and `node` fields in the response body when querying resources (single and list) are removed and the fields in `node` are moved up to the root of the response body. For example, if you query the `/apisix/admin/routes/1` endpoint of the Admin API in version 2.15.x, you get the response:\n\n   ```json\n   {\n   \"count\":1,\n   \"action\":\"get\",\n   \"node\":{\n      \"key\":\"\\/apisix\\/routes\\/1\",\n      \"value\":{\n          ... // content\n      }\n   }\n   }\n   ```\n\n   In 3.0.0, this response body is changes to:\n\n   ```json\n   {\n   \"key\":\"\\/apisix\\/routes\\/1\",\n   \"value\":{\n      ... // content\n   }\n   }\n   ```\n\n2. When querying list resources, the `dir` field is removed from the response body, a `list` field to store the data of the list resources and a `total` field to show the total number of list resources are added. For example, if you query the `/apisix/admin/routes` endpoint of the Admin API in version 2.15.x, you get the response:\n\n   ```json\n   {\n   \"action\":\"get\",\n   \"count\":2,\n   \"node\":{\n      \"key\":\"\\/apisix\\/routes\",\n      \"nodes\":[\n          {\n              \"key\":\"\\/apisix\\/routes\\/1\",\n              \"value\":{\n                  ... // content\n              }\n          },\n          {\n              \"key\":\"\\/apisix\\/routes\\/2\",\n              \"value\":{\n                  ... // content\n              }\n          }\n      ],\n      \"dir\":true\n   }\n   }\n   ```\n\n   In 3.0.0, the response body is:\n\n   ```json\n   {\n   \"list\":[\n      {\n          \"key\":\"\\/apisix\\/routes\\/1\",\n          \"value\":{\n              ... // content\n          }\n\n      },\n      {\n          \"key\":\"\\/apisix\\/routes\\/2\",\n          \"value\":{\n              ... // content\n          }\n      }\n   ],\n   \"total\":2\n   }\n   ```\n\n3. The endpoint to SSL resource is changed from `/apisix/admin/ssl/{id}` to `/apisix/admin/ssls/{id}`.\n\n4. The endpoint to Proto resource is changed from `/apisix/admin/proto/{id}` to `/apisix/admin/protos/{id}`.\n\n5. Admin API port is set to `9180` by default.\n"
  },
  {
    "path": "docs/en/latest/wasm.md",
    "content": "---\ntitle: Wasm\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nAPISIX supports Wasm plugins written with [Proxy Wasm SDK](https://github.com/proxy-wasm/spec#sdks).\n\nCurrently, only a few APIs are implemented. Please follow [wasm-nginx-module](https://github.com/api7/wasm-nginx-module) to know the progress.\n\n## Programming model\n\nThe plugin supports the following concepts from Proxy Wasm:\n\n```\n                    Wasm Virtual Machine\n┌────────────────────────────────────────────────────────────────┐\n│      Your Plugin                                               │\n│          │                                                     │\n│          │ 1: 1                                                │\n│          │         1: N                                        │\n│      VMContext  ──────────  PluginContext                      │\n│                                           ╲ 1: N               │\n│                                            ╲                   │\n│                                             ╲  HttpContext     │\n│                                               (Http stream)    │\n└────────────────────────────────────────────────────────────────┘\n```\n\n* All plugins run in the same Wasm VM, like the Lua plugin in the Lua VM\n* Each plugin has its own VMContext (the root ctx)\n* Each configured route/global rules has its own PluginContext (the plugin ctx).\nFor example, if we have a service configuring with Wasm plugin, and two routes inherit from it,\nthere will be two plugin ctxs.\n* Each HTTP request which hits the configuration will have its own HttpContext (the HTTP ctx).\nFor example, if we configure both global rules and route, the HTTP request will\nhave two HTTP ctxs, one for the plugin ctx from global rules and the other for the\nplugin ctx from route.\n\n## How to use\n\nFirst of all, we need to define the plugin in `config.yaml`:\n\n```yaml\nwasm:\n  plugins:\n    - name: wasm_log # the name of the plugin\n      priority: 7999 # priority\n      file: t/wasm/log/main.go.wasm # the path of `.wasm` file\n      http_request_phase: access # default to \"access\", can be one of [\"access\", \"rewrite\"]\n```\n\nThat's all. Now you can use the wasm plugin as a regular plugin.\n\nFor example, enable this plugin on the specified route:\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n         \"wasm_log\": {\n             \"conf\": \"blahblah\"\n         }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\nAttributes below can be configured in the plugin:\n\n| Name           | Type                 | Requirement | Default        | Valid                                                                      | Description                                                                                                                                         |\n| --------------------------------------| ------------| -------------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  conf         | string or object | required |   |  != \"\" and != {}     | the plugin ctx configuration which can be fetched via Proxy Wasm SDK |\n\nHere is the mapping between Proxy Wasm callbacks and APISIX's phases:\n\n* `proxy_on_configure`: run once there is not PluginContext for the new configuration.\nFor example, when the first request hits the route which has Wasm plugin configured.\n* `proxy_on_http_request_headers`: run in the access/rewrite phase, depends on the configuration of `http_request_phase`.\n* `proxy_on_http_request_body`: run in the same phase of `proxy_on_http_request_headers`. To run this callback, we need to set property `wasm_process_req_body` to non-empty value in `proxy_on_http_request_headers`. See `t/wasm/request-body/main.go` as an example.\n* `proxy_on_http_response_headers`: run in the header_filter phase.\n* `proxy_on_http_response_body`: run in the body_filter phase. To run this callback, we need to set property `wasm_process_resp_body` to non-empty value in `proxy_on_http_response_headers`. See `t/wasm/response-rewrite/main.go` as an example.\n\n## Example\n\nWe have reimplemented some Lua plugin via Wasm, under `t/wasm/` of this repo:\n\n* fault-injection\n* forward-auth\n* response-rewrite\n"
  },
  {
    "path": "docs/en/latest/xrpc/redis.md",
    "content": "---\ntitle: redis\nkeywords:\n  - Apache APISIX\n  - API Gateway\n  - xRPC\n  - redis\ndescription: This document contains information about the Apache APISIX xRPC implementation for Redis.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Description\n\nThe Redis protocol support allows APISIX to proxy Redis commands, and provide various features according to the content of the commands, including:\n\n* [Redis protocol](https://redis.io/docs/reference/protocol-spec/) codec\n* Fault injection according to the commands and key\n\n:::note\n\nThis feature requires APISIX to be run on [APISIX-Runtime](../FAQ.md#how-do-i-build-the-apisix-runtime-environment).\n\nIt also requires the data sent from clients are well-formed and sane. Therefore, it should only be used in deployments where both the downstream and upstream are trusted.\n\n:::\n\n## Granularity of the request\n\nLike other protocols based on the xRPC framework, the Redis implementation here also has the concept of `request`.\n\nEach Redis command is considered a request. However, the message subscribed from the server won't be considered a request.\n\nFor example, when a Redis client subscribes to channel `foo` and receives the message `bar`, then it unsubscribes the `foo` channel, there are two requests: `subscribe foo` and `unsubscribe foo`.\n\n## Attributes\n\n| Name | Type          | Required | Default                                       | Valid values                                                       | Description                                                                                                                                                                                                                                           |\n|----------------------------------------------|---------------|----------|-----------------------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| faults | array[object]        | False    |                                               |  | Fault injections which can be applied based on the commands and keys |\n\nFields under an entry of `faults`:\n\n| Name | Type          | Required | Default                                       | Valid values                                                       | Description                                                                                                                                                                                                                                           |\n|----------------------------------------------|---------------|----------|-----------------------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| commands | array[string]        | True    |                                               | [\"get\", \"mget\"]  | Commands fault is restricted to |\n| key | string        | False    |                                               | \"blahblah\"  | Key fault is restricted to |\n| delay | number        | True    |                                               | 0.1  | Duration of the delay in seconds |\n\n## Metrics\n\n* `apisix_redis_commands_total`: Total number of requests for a specific Redis command.\n\n    | Labels        | Description             |\n    | ------------- | --------------------    |\n    | route         | matched stream route ID |\n    | command       | the Redis command       |\n\n* `apisix_redis_commands_latency_seconds`: Latency of requests for a specific Redis command.\n\n    | Labels        | Description             |\n    | ------------- | --------------------    |\n    | route         | matched stream route ID |\n    | command       | the Redis command       |\n\n## Example usage\n\n:::note\nYou can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command:\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\nAssumed the APISIX is proxying TCP on port `9101`, and the Redis is listening on port `6379`.\n\nLet's create a Stream Route:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"type\": \"none\",\n        \"nodes\": {\n            \"127.0.0.1:6379\": 1\n        }\n    },\n    \"protocol\": {\n        \"name\": \"redis\",\n        \"conf\": {\n            \"faults\": [{\n                \"commands\": [\"get\", \"ping\"],\n                \"delay\": 5\n            }]\n        }\n    }\n}\n'\n```\n\nOnce you have configured the stream route, as shown above, you can make a request to it:\n\n```shell\nredis-cli -p 9101\n```\n\n```\n127.0.0.1:9101> ping\nPONG\n(5.00s)\n```\n\nYou can notice that there is a 5 seconds delay for the ping command.\n"
  },
  {
    "path": "docs/en/latest/xrpc.md",
    "content": "---\ntitle: xRPC\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## What is xRPC\n\nAPISIX supports proxy TCP protocols, but there are times when a pure TCP protocol proxy is not enough. It would be helpful if you had an application-specific proxy, such as Redis Proxy, Kafka Proxy, etc. In addition, some features must be coded and decoded for that protocol before they can be implemented.\n\nTherefore, Apache APISIX implements an L4 protocol extension framework called xRPC that allows developers to customize application-specific protocols. Based on xRPC, developers can codec requests and responses through Lua code and then implement fault injection, log reporting, dynamic routing, and other functions based on understanding the protocol content.\n\nBased on the xRPC framework, APISIX can provide a proxy implementation of several major application protocols. In addition, users can also support their own private TCP-based application protocols based on this framework, giving them precise granularity and higher-level 7-layer control similar to HTTP protocol proxies.\n\n## How to use\n\nCurrently, the steps for users to use xRPC are relatively simple and can be handled quickly in just two steps.\n\n1. First, enable the corresponding protocol in `conf/config.yaml`.\n\n```yaml\nxrpc:\n  protocols:\n    - name: redis\n```\n\n2. Then specify the protocol in the relevant `stream_routes`.\n\n```json\n{\n    ...\n    \"protocol\": {\n        \"name\": \"redis\",\n        \"conf\": {\n            \"faults\": [\n                { \"delay\": 5, \"key\": \"bogus_key\", \"commands\":[\"GET\", \"MGET\"]}\n            ]\n        }\n    }\n}\n```\n\nThe TCP connection that hits that `stream_route` is then handled according to that protocol.\n\n## Configuration\n\n| Name        | Type   | Required | Default | Description                                     |\n|-------------|--------|----------|---------|-------------------------------------------------|\n| name        | string | True     |         | the protocol name                               |\n| conf        |        | False    |         | the application-specific protocol configuration |\n| superior_id | ID     | False    |         | the ID of the superior stream route             |\n\n## Scenarios\n\n### Fault injection\n\nTaking Redis protocol as an example, after decoding the RESP protocol of Redis, we can know the command and parameters of the current request and then get the corresponding content according to the configuration, encode it using RESP protocol, and return it to the client.\n\nSuppose the user uses the following routing configuration.\n\n```json\n{\n    ...\n    \"protocol\": {\n        \"name\": \"redis\",\n        \"conf\": {\n            \"faults\": [\n                { \"delay\": 5, \"key\": \"bogus_key\", \"commands\":[\"GET\", \"MGET\"]}\n            ]\n        }\n    }\n}\n```\n\nThen when the command is \"GET\" or \"MGET\", and the operation key contains \"bogus_key\", it will get \"delay\" according to the configuration: \"5\" parameter, and the corresponding operation will be performed with a delay of 5 seconds.\n\nSince xRPC requires developers to codec the protocol when customizing it, the same operation can be applied to other protocols.\n\n### Dynamic Routing\n\nIn the process of proxy RPC protocol, there are often different RPC calls that need to be forwarded to different upstream requirements. Therefore, the xRPC framework has built-in support for dynamic routing.\n\nTo solve this problem, the concept of superior and subordinate is used in xRPC routing, as shown in the following two examples.\n\n```json\n# /stream_routes/1\n{\n    \"sni\": \"a.test.com\",\n    \"protocol\": {\n        \"name\": \"xx\",\n        \"conf\": {\n            ...\n        }\n    },\n    \"upstream_id\": \"1\"\n}\n```\n\n```json\n# /stream_routes/2\n{\n    \"protocol\": {\n        \"name\": \"xx\",\n        \"superior_id\": \"1\",\n        \"conf\": {\n            ...\n        }\n    },\n    \"upstream_id\": \"2\"\n}\n```\n\nOne specifies the `superior_id`, whose corresponding value is the ID of another route; the other specifies that the route with the `superior_id` is a subordinate route, subordinate to the one with the `superior_id`. Only the superior route is involved in matching at the entry point. The subordinate route is then matched by the specific protocol when the request is decoded.\n\nFor example, for the Dubbo RPC protocol, the subordinate route is matched based on the service_name and other parameters configured in the route and the actual service_name brought in the request. If the match is successful, the configuration above the subordinate route is used, otherwise, the configuration of the superior route is still used. In the above example, if the match for route 2 is successful, it will be forwarded to upstream 2; otherwise, it will still be forwarded to upstream 1.\n\n### Log Reporting\n\nxRPC supports logging-related functions. You can use this feature to filter requests that require attention, such as high latency, excessive transfer content, etc.\n\nEach logger item configuration parameter will contain\n\n- name: the Logger plugin name,\n- filter: the prerequisites for the execution of the logger plugin(e.g., request processing time exceeding a given value),\n- conf: the configuration of the logger plugin itself.\n\n The following configuration is an example:\n\n```json\n{\n    ...\n    \"protocol\": {\n        \"name\": \"redis\",\n        \"logger\": {\n            {\n                \"name\": \"syslog\",\n                \"filter\": [\n                    [\"rpc_time\", \">=\", 0.01]\n                ],\n                \"conf\": {\n                    \"host\": \"127.0.0.1\",\n                    \"port\": 8125,\n                }\n            }\n        }\n    }\n}\n```\n\nThis configuration means that when the `rpc_time` is greater than 0.01 seconds, xRPC reports the request log to the log server via the `syslog` plugin. `conf` is the configuration of the logging server required by the `syslog` plugin.\n\nUnlike standard TCP proxies, which only execute a logger when the connection is closed, xRPC executes a logger at the end of each 'request'.\n\nThe protocol itself defines the granularity of the specific request, and the xRPC extension code implements the request's granularity.\n\nFor example, in the Redis protocol, the execution of a command is considered a request.\n\n### Dynamic metrics\n\nxRPC also supports gathering metrics on the fly and exposing them via Prometheus.\n\nTo know how to enable Prometheus metrics for TCP and collect them, please refer to [prometheus](./plugins/prometheus.md).\n\nTo get the protocol-specific metrics, you need to:\n\n1. Make sure the Prometheus is enabled for TCP\n2. Add the metric field to the specific route and ensure the `enable` is true:\n\n```json\n{\n    ...\n    \"protocol\": {\n        \"name\": \"redis\",\n        \"metric\": {\n            \"enable\": true\n        }\n    }\n}\n```\n\nDifferent protocols will have different metrics. Please refer to the `Metrics` section of their own documentation.\n\n## How to write your own protocol\n\nAssuming that your protocol is named `my_proto`, you need to create a directory that can be introduced by `require \"apisix.stream.xrpc.protocols.my_proto\"`.\nInside this directory you need to have two files, `init.lua`, which implements the methods required by the xRPC framework, and `schema.lua`, which implements the schema checks for the protocol configuration.\n\nFor a concrete implementation, you can refer to the existing protocols at:\n\n* https://github.com/apache/apisix/tree/master/apisix/stream/xrpc/protocols\n* https://github.com/apache/apisix/tree/master/t/xrpc/apisix/stream/xrpc/protocols\n\nTo know what methods are required to be implemented and how the xRPC framework works, please refer to:\nhttps://github.com/apache/apisix/tree/master/apisix/stream/xrpc/runner.lua\n"
  },
  {
    "path": "docs/zh/latest/CHANGELOG.md",
    "content": "---\ntitle: 版本发布\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Table of Contents\n\n- [3.8.0](#380)\n- [3.7.0](#370)\n- [3.6.0](#360)\n- [3.5.0](#350)\n- [3.4.0](#340)\n- [3.3.0](#330)\n- [3.2.1](#321)\n- [3.2.0](#320)\n- [3.1.0](#310)\n- [3.0.0](#300)\n- [3.0.0-beta](#300-beta)\n- [2.15.3](#2153)\n- [2.15.2](#2152)\n- [2.15.1](#2151)\n- [2.15.0](#2150)\n- [2.14.1](#2141)\n- [2.14.0](#2140)\n- [2.13.3](#2133)\n- [2.13.2](#2132)\n- [2.13.1](#2131)\n- [2.13.0](#2130)\n- [2.12.1](#2121)\n- [2.12.0](#2120)\n- [2.11.0](#2110)\n- [2.10.5](#2105)\n- [2.10.4](#2104)\n- [2.10.3](#2103)\n- [2.10.2](#2102)\n- [2.10.1](#2101)\n- [2.10.0](#2100)\n- [2.9.0](#290)\n- [2.8.0](#280)\n- [2.7.0](#270)\n- [2.6.0](#260)\n- [2.5.0](#250)\n- [2.4.0](#240)\n- [2.3.0](#230)\n- [2.2.0](#220)\n- [2.1.0](#210)\n- [2.0.0](#200)\n- [1.5.0](#150)\n- [1.4.1](#141)\n- [1.4.0](#140)\n- [1.3.0](#130)\n- [1.2.0](#120)\n- [1.1.0](#110)\n- [1.0.0](#100)\n- [0.9.0](#090)\n- [0.8.0](#080)\n- [0.7.0](#070)\n- [0.6.0](#060)\n\n## 3.8.0\n\n### Core\n\n- :sunrise: 支持使用 lua-resty-events 模块以提高性能：\n  - [#10550](https://github.com/apache/apisix/pull/10550)\n  - [#10558](https://github.com/apache/apisix/pull/10558)\n- :sunrise: 将 OpenSSL 1.1.1 升级到 OpenSSL 3：[#10724](https://github.com/apache/apisix/pull/10724)\n\n### Plugins\n\n- :sunrise: 添加 jwe-decryp 插件：[#10252](https://github.com/apache/apisix/pull/10252)\n- :sunrise: response-rewrite 插件使用 filters.regex 选项时支持 brotli：[#10733](https://github.com/apache/apisix/pull/10733)\n- :sunrise: 添加多重认证插件：[#10482](https://github.com/apache/apisix/pull/10482)\n- :sunrise: 在 `openid-connect` 插件中添加 `required scopes` 配置属性：[#10493](https://github.com/apache/apisix/pull/10493)\n- :sunrise: cors 插件支持 Timing-Allow-Origin 头：[#9365](https://github.com/apache/apisix/pull/9365)\n- :sunrise: 添加 brotli 插件：[#10515](https://github.com/apache/apisix/pull/10515)\n- :sunrise: body-transformer 插件增强：[#10496](https://github.com/apache/apisix/pull/10496)\n- :sunrise: limit-count 插件设置 redis_cluster_nodes 的最小长度为 1：[#10612](https://github.com/apache/apisix/pull/10612)\n- :sunrise: 允许通过环境变量配置 limit-count 插件：[#10607](https://github.com/apache/apisix/pull/10607)\n\n### Bugfixes\n\n- 修复：upstream nodes 为数组类型时，port 应为可选字段：[#10477](https://github.com/apache/apisix/pull/10477)\n- 修复：fault-injection 插件中变量提取不正确：[#10485](https://github.com/apache/apisix/pull/10485)\n- 修复：所有消费者应共享同一计数器 (limit-count)：[#10541](https://github.com/apache/apisix/pull/10541)\n- 修复：在向 opa 插件发送路由时安全地删除上游：[#10552](https://github.com/apache/apisix/pull/10552)\n- 修复：缺少 etcd init_dir 和无法列出资源：[#10569](https://github.com/apache/apisix/pull/10569)\n- 修复：Forward-auth 请求体过大：[#10589](https://github.com/apache/apisix/pull/10589)\n- 修复：永不退出的定时器导致的内存泄漏：[#10614](https://github.com/apache/apisix/pull/10614)\n- 修复：如果在 proxy-rewrite 插件中解析的值为 nil，则不调用 add_header：[#10619](https://github.com/apache/apisix/pull/10619)\n- 修复：频繁遍历 etcd 所有的键，导致 cpu 使用率高：[#10671](https://github.com/apache/apisix/pull/10671)\n- 修复：对于 prometheus 的 upstream_status 指标，mostly_healthy 是健康的：[#10639](https://github.com/apache/apisix/pull/10639)\n- 修复：在 zipkin 中避免在日志阶段获取 nil 值：[#10666](https://github.com/apache/apisix/pull/10666)\n- 修复：启用 openid-connect 插件而没有 redirect_uri 导致 500 错误：[#7690](https://github.com/apache/apisix/pull/7690)\n- 修复：为没有 end_session_endpoint 的 ODIC 添加 redirect_after_logout_uri：[#10653](https://github.com/apache/apisix/pull/10653)\n- 修复：当 content-encoding 为 gzip 时，response-rewrite 的 filters.regex 不适用：[#10637](https://github.com/apache/apisix/pull/10637)\n- 修复：prometheus 指标的泄漏：[#10655](https://github.com/apache/apisix/pull/10655)\n- 修复：Authz-keycloak 添加返回详细错误：[#10691](https://github.com/apache/apisix/pull/10691)\n- 修复：服务发现未正确更新上游节点：[#10722](https://github.com/apache/apisix/pull/10722)\n- 修复：apisix 重启失败：[#10696](https://github.com/apache/apisix/pull/10696)\n\n## 3.7.0\n\n### Change\n\n- :warning: 创建核心资源时不允许传入 `create_time` 和 `update_time`：[#10232](https://github.com/apache/apisix/pull/10232)\n- :warning: 从 SSL schema 中移除自包含的信息字段 `exptime`、`validity_start` 和 `validity_end`：[10323](https://github.com/apache/apisix/pull/10323)\n- :warning: 在 opentelemetry 插件的属性中，将 `route` 替换为 `apisix.route_name`，将 `service` 替换为 `apisix.service_name`，以遵循 span 名称和属性的标准：[#10393](https://github.com/apache/apisix/pull/10393)\n\n### Core\n\n- :sunrise: 添加令牌以支持 Consul 的访问控制：[#10278](https://github.com/apache/apisix/pull/10278)\n- :sunrise: 支持在 stream_route 中配置 `service_id` 引用 service 资源：[#10298](https://github.com/apache/apisix/pull/10298)\n- :sunrise: 使用 `apisix-runtime` 作为 apisix 运行时：\n  - [#10415](https://github.com/apache/apisix/pull/10415)\n  - [#10427](https://github.com/apache/apisix/pull/10427)\n\n### Plugins\n\n- :sunrise: 为 authz-keycloak 添加测试，使用 apisix secrets：[#10353](https://github.com/apache/apisix/pull/10353)\n- :sunrise: 向 openid-connect 插件添加授权参数：[#10058](https://github.com/apache/apisix/pull/10058)\n- :sunrise: 支持在 zipkin 插件中设置变量：[#10361](https://github.com/apache/apisix/pull/10361)\n- :sunrise: 支持 Nacos ak/sk 认证：[#10445](https://github.com/apache/apisix/pull/10445)\n\n### Bugfixes\n\n- 修复：获取健康检查目标状态失败时使用警告日志：\n  - [#10156](https://github.com/apache/apisix/pull/10156)\n- 修复：更新上游时应保留健康检查的状态：\n  - [#10312](https://github.com/apache/apisix/pull/10312)\n  - [#10307](https://github.com/apache/apisix/pull/10307)\n- 修复：在插件配置模式中添加 name 字段以保持一致性：[#10315](https://github.com/apache/apisix/pull/10315)\n- 修复：优化 upstream_schema 中的 tls 定义和错误的变量：[#10269](https://github.com/apache/apisix/pull/10269)\n- 修复（consul）：无法正常退出：[#10342](https://github.com/apache/apisix/pull/10342)\n- 修复：请求头 `Content-Type: application/x-www-form-urlencoded;charset=utf-8` 会导致 var 条件 `post_arg_xxx` 匹配失败：[#10372](https://github.com/apache/apisix/pull/10372)\n- 修复：在 Mac 上安装失败：[#10403](https://github.com/apache/apisix/pull/10403)\n- 修复（log-rotate）：日志压缩超时导致数据丢失：[#8620](https://github.com/apache/apisix/pull/8620)\n- 修复（kafka-logger）：从 required_acks 枚举值中移除 0：[#10469](https://github.com/apache/apisix/pull/10469)\n\n## 3.6.0\n\n### Change\n\n- :warning: 移除 `etcd.use_grpc`，不再支持使用 gRPC 协议与 etcd 进行通信：[#10015](https://github.com/apache/apisix/pull/10015)\n- :warning: 移除 conf server，数据平面不再支持与控制平面进行通信，需要从 `config_provider: control_plane` 调整为 `config_provider: etcd`：[#10012](https://github.com/apache/apisix/pull/10012)\n- :warning: 严格验证核心资源的输入：[#10233](https://github.com/apache/apisix/pull/10233)\n\n### Core\n\n- :sunrise: 支持配置访问日志的缓冲区大小：[#10225](https://github.com/apache/apisix/pull/10225)\n- :sunrise: 支持在 DNS 发现服务中允许配置 `resolv_conf` 来使用本地 DNS 解析器：[#9770](https://github.com/apache/apisix/pull/9770)\n- :sunrise: 安装不再依赖 Rust：[#10121](https://github.com/apache/apisix/pull/10121)\n- :sunrise: 在 xRPC 中添加 Dubbo 协议支持：[#9660](https://github.com/apache/apisix/pull/9660)\n\n### Plugins\n\n- :sunrise: 在 `traffic-split` 插件中支持 HTTPS：[#9115](https://github.com/apache/apisix/pull/9115)\n- :sunrise: 在 `ext-plugin` 插件中支持重写请求体：[#9990](https://github.com/apache/apisix/pull/9990)\n- :sunrise: 在 `opentelemetry` 插件中支持设置 NGINX 变量：[#8871](https://github.com/apache/apisix/pull/8871)\n- :sunrise: 在 `chaitin-waf` 插件中支持 UNIX sock 主机模式：[#10161](https://github.com/apache/apisix/pull/10161)\n\n### Bugfixes\n\n- 修复 GraphQL POST 请求路由匹配异常：[#10198](https://github.com/apache/apisix/pull/10198)\n- 修复 `apisix.yaml` 中多行字符串数组的错误：[#10193](https://github.com/apache/apisix/pull/10193)\n- 修复在 proxy-cache 插件中缺少 cache_zone 时提供错误而不是 nil panic：[#10138](https://github.com/apache/apisix/pull/10138)\n\n## 3.5.0\n\n### Change\n\n- :warning: request-id 插件移除雪花算法：[#9715](https://github.com/apache/apisix/pull/9715)\n- :warning: 不再兼容 OpenResty 1.19 版本，需要将其升级到 1.21+ 版本：[#9913](https://github.com/apache/apisix/pull/9913)\n- :warning: 删除配置项 `apisix.stream_proxy.only`，L4/L7 代理需要通过配置项 `apesix.proxy_mode` 来启用：[#9607](https://github.com/apache/apisix/pull/9607)\n- :warning: admin-api 的 `/apisix/admin/plugins?all=true` 接口标记为弃用：[#9580](https://github.com/apache/apisix/pull/9580)\n- :warning: ua-restriction 插件不允许同时启用黑名单和白名单：[#9841](https://github.com/apache/apisix/pull/9841)\n\n### Core\n\n- :sunrise: 支持根据 host 级别动态设置 TLS 协议版本：[#9903](https://github.com/apache/apisix/pull/9903)\n- :sunrise: 支持强制删除资源：[#9810](https://github.com/apache/apisix/pull/9810)\n- :sunrise: 支持从 yaml 中提取环境变量：[#9855](https://github.com/apache/apisix/pull/9855)\n- :sunrise: admin-api 新增 schema validate API 校验资源配置：[#10065](https://github.com/apache/apisix/pull/10065)\n\n### Plugins\n\n- :sunrise: 新增 chaitin-waf 插件：[#9838](https://github.com/apache/apisix/pull/9838)\n- :sunrise: file-logger 支持设置 var 变量：[#9712](https://github.com/apache/apisix/pull/9712)\n- :sunrise: mock 插件支持添加响应头：[#9720](https://github.com/apache/apisix/pull/9720)\n- :sunrise: proxy-rewrite 插件支持正则匹配 URL 编码：[#9813](https://github.com/apache/apisix/pull/9813)\n- :sunrise: google-cloud-logging 插件支持 client_email 配置：[#9813](https://github.com/apache/apisix/pull/9813)\n- :sunrise: opa 插件支持向上游发送 OPA server 返回的头：[#9710](https://github.com/apache/apisix/pull/9710)\n- :sunrise: openid-connect 插件支持配置代理服务器：[#9948](https://github.com/apache/apisix/pull/9948)\n\n### Bugfixes\n\n- 修复 log-rotate 插件使用自定义名称时，max_kept 配置不起作用：[#9749](https://github.com/apache/apisix/pull/9749)\n- 修复 limit_conn 在 stream 模式下非法使用 http 变量：[#9816](https://github.com/apache/apisix/pull/9816)\n- 修复 loki-logger 插件在获取 log_labels 时会索引空值：[#9850](https://github.com/apache/apisix/pull/9850)\n- 修复使用 limit-count 插件时，当请求被拒绝后，X-RateLimit-Reset 不应设置为 0：[#9978](https://github.com/apache/apisix/pull/9978)\n- 修复 nacos 插件在运行时索引一个空值：[#9960](https://github.com/apache/apisix/pull/9960)\n- 修复 etcd 在同步数据时，如果密钥有特殊字符，则同步异常：[#9967](https://github.com/apache/apisix/pull/9967)\n- 修复 tencent-cloud-cls 插件 DNS 解析失败：[#9843](https://github.com/apache/apisix/pull/9843)\n- 修复执行 reload 或 quit 命令时 worker 未退出：[#9909](https://github.com/apache/apisix/pull/9909)\n- 修复在 traffic-split 插件中 upstream_id 有效性验证：[#10008](https://github.com/apache/apisix/pull/10008)\n\n## 3.4.0\n\n### Core\n\n- :sunrise: 支持路由级别的 MTLS [#9322](https://github.com/apache/apisix/pull/9322)\n- :sunrise: 支持全局规则的 id schema [#9517](https://github.com/apache/apisix/pull/9517)\n- :sunrise: 支持使用单个长连接来监视 etcd 的所有资源 [#9456](https://github.com/apache/apisix/pull/9456)\n- :sunrise: 支持 ssl 标签的最大长度为 256 [#9301](https://github.com/apache/apisix/pull/9301)\n\n### Plugins\n\n- :sunrise: 支持 proxy_rewrite 插件的多个正则表达式匹配 [#9194](https://github.com/apache/apisix/pull/9194)\n- :sunrise: 添加 loki-logger 插件 [#9399](https://github.com/apache/apisix/pull/9399)\n- :sunrise: 允许用户为 prometheus 插件配置 DEFAULT_BUCKETS [#9673](https://github.com/apache/apisix/pull/9673)\n\n### Bugfixes\n\n- 修复 (body-transformer)：xml2lua 将空表替换为空字符串 [#9669](https://github.com/apache/apisix/pull/9669)\n- 修复：opentelemetry 和 grpc-transcode 插件无法同时启用 [#9606](https://github.com/apache/apisix/pull/9606)\n- 修复 (skywalking-logger, error-log-logger)：支持在 skywalking service_instance_name 中使用 $hostname [#9401](https://github.com/apache/apisix/pull/9401)\n- 修复 (admin)：修复 secrets 不支持通过 PATCH 更新属性 [#9510](https://github.com/apache/apisix/pull/9510)\n- 修复 (http-logger)：默认请求路径应为'/' [#9472](https://github.com/apache/apisix/pull/9472)\n- 修复：syslog 插件不起作用 [#9425](https://github.com/apache/apisix/pull/9425)\n- 修复：splunk-hec-logging 的日志格式错误 [#9478](https://github.com/apache/apisix/pull/9478)\n- 修复：etcd 复用 cli 并启用 keepalive [#9420](https://github.com/apache/apisix/pull/9420)\n- 修复：upstream key 添加 mqtt_client_id 支持 [#9450](https://github.com/apache/apisix/pull/9450)\n- 修复：body-transformer 插件总是返回原始 body [#9446](https://github.com/apache/apisix/pull/9446)\n- 修复：当 consumer 使用 wolf-rbac 插件时，consumer 中的其他插件无效 [#9298](https://github.com/apache/apisix/pull/9298)\n- 修复：当 host 是域名时，总是解析域名 [#9332](https://github.com/apache/apisix/pull/9332)\n- 修复：response-rewrite 插件不能只添加一个字符 [#9372](https://github.com/apache/apisix/pull/9372)\n- 修复：consul 支持只获取 health endpoint [#9204](https://github.com/apache/apisix/pull/9204)\n\n## 3.3.0\n\n### Change\n\n- 默认路由从 `radixtree_uri` 修改为 `radixtree_host_uri`: [#9047](https://github.com/apache/apisix/pull/9047)\n- CORS 插件将会在 `allow_origin` 不为 `*` 时默认添加 `Vary: Origin` 响应头：[#9010](https://github.com/apache/apisix/pull/9010)\n\n### Core\n\n- :sunrise: 支持将路由证书存储在 secrets manager 中：[#9247](https://github.com/apache/apisix/pull/9247)\n- :sunrise: 支持通过配置绕过 Admin API 身份验证：[#9147](https://github.com/apache/apisix/pull/9147)\n\n### Plugins\n\n- :sunrise: fault-injection 插件支持请求头注入：[#9039](https://github.com/apache/apisix/pull/9039)\n- :sunrise: 提供在其他插件中引用 proxy-rewrite 插件中路由改写捕捉到的变量支持：[#9112](https://github.com/apache/apisix/pull/9112)\n- :sunrise: limit-count 插件提供 `username` 与 `ssl` redis 认证方式：[#9185](https://github.com/apache/apisix/pull/9185)\n\n### Bugfixes\n\n- 修复 etcd 数据同步异常：[#8493](https://github.com/apache/apisix/pull/8493)\n- 修复在 `core.request.add_header` 中的无效缓存：[#8824](https://github.com/apache/apisix/pull/8824)\n- 修复由健康检查引起的高 CPU 和内存占用：[#9015](https://github.com/apache/apisix/pull/9015)\n- 仅当 `allow_origins_by_regex` 不为 `nil` 时生效：[#9028](https://github.com/apache/apisix/pull/9028)\n- 在删除 upstream 时，检查 `traffic-split` 插件中的引用：[#9044](https://github.com/apache/apisix/pull/9044)\n- 修复启动时无法连接到 etcd 的问题：[#9077](https://github.com/apache/apisix/pull/9077)\n- 修复域节点的健康检查泄漏问题：[#9090](https://github.com/apache/apisix/pull/9090)\n- 禁止非 `127.0.0.0/24` 的用户在没有 admin_key 的情况下访问 Admin API: [#9146](https://github.com/apache/apisix/pull/9146)\n- 确保 hold_body_chunk 函数对每个插件设置独立缓冲区，避免数据污染：[#9266](https://github.com/apache/apisix/pull/9266)\n- 确保 batch-requests 插件能够在尾部响应头存在时能够正确读取：[#9289](https://github.com/apache/apisix/pull/9289)\n- 确保 `proxy-rewrite` 改写 `ngx.var.uri`: [#9309](https://github.com/apache/apisix/pull/9309)\n\n## 3.2.1\n\n**这是一个 LTS 维护版本，您可以在 `release/3.2` 分支中看到 CHANGELOG。**\n\n## 3.2.0\n\n### Change\n\n- 废弃了 jwt-auth 内单独的 Vault 配置。用户能用密钥来实现同样的功能：[#8660](https://github.com/apache/apisix/pull/8660)\n\n### Core\n\n- :sunrise: 支持通过环境变量来配置密钥的 Vault token：[#8866](https://github.com/apache/apisix/pull/8866)\n- :sunrise: 支持四层上的服务发现：\n    - [#8583](https://github.com/apache/apisix/pull/8583)\n    - [#8593](https://github.com/apache/apisix/pull/8593)\n    - [#8584](https://github.com/apache/apisix/pull/8584)\n    - [#8640](https://github.com/apache/apisix/pull/8640)\n    - [#8633](https://github.com/apache/apisix/pull/8633)\n    - [#8696](https://github.com/apache/apisix/pull/8696)\n    - [#8826](https://github.com/apache/apisix/pull/8826)\n\n### Plugin\n\n- :sunrise: 新增 RESTful 请求转 graphQL 的插件：[#8959](https://github.com/apache/apisix/pull/8959)\n- :sunrise: 支持在每个日志插件上设置日志格式：\n    - [#8806](https://github.com/apache/apisix/pull/8806)\n    - [#8643](https://github.com/apache/apisix/pull/8643)\n- :sunrise: 新增请求体/响应体转换插件：[#8766](https://github.com/apache/apisix/pull/8766)\n- :sunrise: 支持发送错误日志到 Kafka：[#8693](https://github.com/apache/apisix/pull/8693)\n- :sunrise: limit-count 插件支持 X-RateLimit-Reset：[#8578](https://github.com/apache/apisix/pull/8578)\n- :sunrise: limit-count 插件支持设置 TLS 来访问 Redis 集群：[#8558](https://github.com/apache/apisix/pull/8558)\n- :sunrise: consumer-restriction 插件支持通过 consumer_group_id 来做权限控制：[#8567](https://github.com/apache/apisix/pull/8567)\n\n### Bugfix\n\n- 修复 Host 和 SNI 不匹配时，mTLS 失效的问题：[#8967](https://github.com/apache/apisix/pull/8967)\n- 如果 URI 参数部分不来自于用户配置，proxy-rewrite 插件应当对其转义：[#8888](https://github.com/apache/apisix/pull/8888)\n- Admin API PATCH 操作成功后应返回 200 状态码：[#8855](https://github.com/apache/apisix/pull/8855)\n- 修复特定条件下，etcd 同步失败之后的 reload 不生效：[#8736](https://github.com/apache/apisix/pull/8736)\n- 修复 Consul 服务发现得到的节点不全的问题：[#8651](https://github.com/apache/apisix/pull/8651)\n- 修复 grpc-transcode 插件对 Map 数据的转换问题：[#8731](https://github.com/apache/apisix/pull/8731)\n- 外部插件应当可以设置 content-type 响应头：[#8588](https://github.com/apache/apisix/pull/8588)\n- 插件热加载时，如果 request-id 插件中初始化 snowflake 生成器出错，可能遗留多余的计时器：[#8556](https://github.com/apache/apisix/pull/8556)\n- 插件热加载时，关闭 grpc-transcode 的 proto 同步器：[#8557](https://github.com/apache/apisix/pull/8557)\n\n## 3.1.0\n\n### Core\n\n- :sunrise: 支持通过 gRPC 来同步 etcd 的配置：\n    - [#8485](https://github.com/apache/apisix/pull/8485)\n    - [#8450](https://github.com/apache/apisix/pull/8450)\n    - [#8411](https://github.com/apache/apisix/pull/8411)\n- :sunrise: 支持在插件中配置加密字段：\n    - [#8487](https://github.com/apache/apisix/pull/8487)\n    - [#8403](https://github.com/apache/apisix/pull/8403)\n- :sunrise: 支持使用 secret 资源将部分字段放到 Vault 或环境变量中：\n    - [#8448](https://github.com/apache/apisix/pull/8448)\n    - [#8421](https://github.com/apache/apisix/pull/8421)\n    - [#8412](https://github.com/apache/apisix/pull/8412)\n    - [#8394](https://github.com/apache/apisix/pull/8394)\n    - [#8390](https://github.com/apache/apisix/pull/8390)\n- :sunrise: 允许在 stream 子系统中以域名的形式配置上游：[#8500](https://github.com/apache/apisix/pull/8500)\n- :sunrise: 支持 Consul 服务发现：[#8380](https://github.com/apache/apisix/pull/8380)\n\n### Plugin\n\n- :sunrise: 优化 prometheus 采集的资源占用：[#8434](https://github.com/apache/apisix/pull/8434)\n- :sunrise: 增加便于调试的 inspect 插件： [#8400](https://github.com/apache/apisix/pull/8400)\n- :sunrise: jwt-auth 插件支持对上游隐蔽认证的参数：[#8206](https://github.com/apache/apisix/pull/8206)\n- :sunrise: proxy-rewrite 插件支持新增请求头的同时不覆盖现有同名请求头：[#8336](https://github.com/apache/apisix/pull/8336)\n- :sunrise: grpc-transcode 插件支持将 grpc-status-details-bin 响应头设置到响应体中：[#7639](https://github.com/apache/apisix/pull/7639)\n- :sunrise: proxy-mirror 插件支持设置前缀：[#8261](https://github.com/apache/apisix/pull/8261)\n\n### Bugfix\n\n- 修复某些情况下，配置在 service 对象下的插件无法及时生效的问题：[#8482](https://github.com/apache/apisix/pull/8482)\n- 修复因连接池复用，http 和 grpc 共用同一个上游节点时偶发 502 的问题：[#8364](https://github.com/apache/apisix/pull/8364)\n- file-logger 在写日志时，应避免缓冲区造成的日志截断：[#7884](https://github.com/apache/apisix/pull/7884)\n- log-rotate 插件的 max_kept 参数应对压缩文件生效：[#8366](https://github.com/apache/apisix/pull/8366)\n- 修复 openid-connect 插件中当 use_jwks 为 true 时没有设置 userinfo 的问题：[#8347](https://github.com/apache/apisix/pull/8347)\n- 修复无法在 proxy-rewrite 插件中修改 x-forwarded-host 的问题：[#8200](https://github.com/apache/apisix/pull/8200)\n- 修复某些情况下，禁用 v3 admin API 导致响应体丢失：[#8349](https://github.com/apache/apisix/pull/8349)\n- zipkin 插件中，即使存在 reject 的 sampling decision，也要传递 trace ID：[#8099](https://github.com/apache/apisix/pull/8099)\n- 修复插件配置中的 `_meta.filter` 无法使用上游响应后才赋值的变量和 APISIX 中自定义变量的问题：\n    - [#8162](https://github.com/apache/apisix/pull/8162)\n    - [#8256](https://github.com/apache/apisix/pull/8256)\n\n## 3.0.0\n\n### Change\n\n- 默认关闭 `enable_cpu_affinity`，避免在容器部署场景中该配置影响 APSISIX 的行为：[#8074](https://github.com/apache/apisix/pull/8074)\n\n### Core\n\n- :sunrise: 新增 Consumer Group 实体，用于管理多个 Consumer：[#7980](https://github.com/apache/apisix/pull/7980)\n- :sunrise: 支持配置 DNS 解析域名类型的顺序：[#7935](https://github.com/apache/apisix/pull/7935)\n- :sunrise: 支持配置多个 `key_encrypt_salt` 进行轮转：[#7925](https://github.com/apache/apisix/pull/7925)\n\n### Plugin\n\n- :sunrise: 新增 ai 插件，根据场景动态优化 APISIX 的执行路径：\n    - [#8102](https://github.com/apache/apisix/pull/8102)\n    - [#8113](https://github.com/apache/apisix/pull/8113)\n    - [#8120](https://github.com/apache/apisix/pull/8120)\n    - [#8128](https://github.com/apache/apisix/pull/8128)\n    - [#8130](https://github.com/apache/apisix/pull/8130)\n    - [#8149](https://github.com/apache/apisix/pull/8149)\n    - [#8157](https://github.com/apache/apisix/pull/8157)\n- :sunrise: openid-connect 插件支持设置 `session_secret`，解决多个 worker 间 `session_secret` 不一致的问题：[#8068](https://github.com/apache/apisix/pull/8068)\n- :sunrise: kafka-logger 插件支持设置 sasl 相关配置：[#8050](https://github.com/apache/apisix/pull/8050)\n- :sunrise: proxy-mirror 插件支持设置域名作为 host：[#7861](https://github.com/apache/apisix/pull/7861)\n- :sunrise: kafka-logger 插件新增 brokers 属性，支持不同 broker 设置相同 host：[#7999](https://github.com/apache/apisix/pull/7999)\n- :sunrise: ext-plugin-post-resp 插件支持获取上游响应体：[#7947](https://github.com/apache/apisix/pull/7947)\n- :sunrise: 新增 cas-auth 插件，支持 CAS 认证：[#7932](https://github.com/apache/apisix/pull/7932)\n\n### Bugfix\n\n- workflow 插件的条件表达式应该支持操作符：[#8121](https://github.com/apache/apisix/pull/8121)\n- 修复禁用 prometheus 插件时 batch processor 加载问题：[#8079](https://github.com/apache/apisix/pull/8079)\n- APISIX 启动时，如果存在旧的 conf server 的 sock 文件则删除：[#8022](https://github.com/apache/apisix/pull/8022)\n- 没有编译 gRPC-client-nginx-module 模块时禁用 core.grpc：[#8007](https://github.com/apache/apisix/pull/8007)\n\n## 3.0.0-beta\n\n这里我们使用 `2.99.0` 作为源代码中的版本号，而不是代码名称\n`3.0.0-beta`，有两个原因。\n\n1. 避免在一些程序试图比较版本时出现意外的错误，因为 `3.0.0-beta` 包含 `3.0.0` 并且比它长。\n2. 一些软件包系统可能不允许在版本号后面有一个后缀。\n\n### Change\n\n#### 移动 config_center、etcd 和 Admin API 的配置到 deployment 下面\n\n我们调整了下静态配置文件里面的配置，所以你需要同步更新下 config.yaml 里面的配置了：\n\n- `config_center` 功能改由 `deployment` 下面的 `config_provider` 实现： [#7901](https://github.com/apache/apisix/pull/7901)\n- `etcd` 字段整体搬迁到 `deployment` 下面： [#7860](https://github.com/apache/apisix/pull/7860)\n- 以下的 Admin API 配置移动到 `deployment` 下面的 `admin` 字段：[#7823](https://github.com/apache/apisix/pull/7823)\n    - admin_key\n    - enable_admin_cors\n    - allow_admin\n    - admin_listen\n    - https_admin\n    - admin_api_mtls\n    - admin_api_version\n\n具体可以参考最新的 config-default.yaml。\n\n#### 移除多个已废弃的配置\n\n借着 3.0 新版本的机会，我们把许多之前标记为 deprecated 的配置清理出去。\n\n在静态配置中，我们移除了以下若干字段：\n\n- 移除 `apisix.ssl` 中的 `enable_http2` 和 `listen_port`：[#7717](https://github.com/apache/apisix/pull/7717)\n- 移除 `apisix.port_admin`： [#7716](https://github.com/apache/apisix/pull/7716)\n- 移除 `etcd.health_check_retry`： [#7676](https://github.com/apache/apisix/pull/7676)\n- 移除 `nginx_config.http.lua_shared_dicts`： [#7677](https://github.com/apache/apisix/pull/7677)\n- 移除 `apisix.real_ip_header`: [#7696](https://github.com/apache/apisix/pull/7696)\n\n在动态配置中，我们做了以下调整：\n\n- 将插件配置的 `disable` 移到 `_meta` 下面：[#7707](https://github.com/apache/apisix/pull/7707)\n- 从 Route 里面移除了 `service_protocol`：[#7701](https://github.com/apache/apisix/pull/7701)\n\n此外还有具体插件级别上的改动：\n\n- authz-keycloak 中移除了 `audience` 字段： [#7683](https://github.com/apache/apisix/pull/7683)\n- mqtt-proxy 中移除了 `upstream` 字段：[#7694](https://github.com/apache/apisix/pull/7694)\n- error-log-logger 中把 tcp 相关配置放到 `tcp` 字段下面：[#7700](https://github.com/apache/apisix/pull/7700)\n- syslog 中移除了 `max_retry_times` 和 `retry_interval` 字段： [#7699](https://github.com/apache/apisix/pull/7699)\n- proxy-rewrite 中移除了 `scheme` 字段： [#7695](https://github.com/apache/apisix/pull/7695)\n\n#### 新的 Admin API 响应格式\n\n我们在以下若干个 PR 中调整了 Admin API 的响应格式：\n\n- [#7630](https://github.com/apache/apisix/pull/7630)\n- [#7622](https://github.com/apache/apisix/pull/7622)\n\n新的响应格式展示如下：\n\n返回单个配置：\n\n```json\n{\n  \"modifiedIndex\": 2685183,\n  \"value\": {\n    \"id\": \"1\",\n    ...\n  },\n  \"key\": \"/apisix/routes/1\",\n  \"createdIndex\": 2684956\n}\n```\n\n返回多个配置：\n\n```json\n{\n  \"list\": [\n    {\n      \"modifiedIndex\": 2685183,\n      \"value\": {\n        \"id\": \"1\",\n        ...\n      },\n      \"key\": \"/apisix/routes/1\",\n      \"createdIndex\": 2684956\n    },\n    {\n      \"modifiedIndex\": 2685163,\n      \"value\": {\n        \"id\": \"2\",\n        ...\n      },\n      \"key\": \"/apisix/routes/2\",\n      \"createdIndex\": 2685163\n    }\n  ],\n  \"total\": 2\n}\n```\n\n#### 其他\n\n- Admin API 的端口改为 9180：[#7806](https://github.com/apache/apisix/pull/7806)\n- 我们只支持 OpenResty 1.19.3.2 及以上的版本：[#7625](https://github.com/apache/apisix/pull/7625)\n- 调整了 Plugin Config 对象的优先级，同名插件配置的优先级由 Consumer > Plugin Config > Route > Service 变成 Consumer > Route > Plugin Config > Service： [#7614](https://github.com/apache/apisix/pull/7614)\n\n### Core\n\n- 集成 grpc-client-nginx-module 到 APISIX： [#7917](https://github.com/apache/apisix/pull/7917)\n- k8s 服务发现支持配置多个集群：[#7895](https://github.com/apache/apisix/pull/7895)\n\n### Plugin\n\n- 支持在 opentelemetry 插件里注入指定前缀的 header：[#7822](https://github.com/apache/apisix/pull/7822)\n- 新增 openfunction 插件：[#7634](https://github.com/apache/apisix/pull/7634)\n- 新增 elasticsearch-logger 插件：[#7643](https://github.com/apache/apisix/pull/7643)\n- response-rewrite 插件支持增加响应体：[#7794](https://github.com/apache/apisix/pull/7794)\n- log-rorate 支持指定最大大小来切割日志：[#7749](https://github.com/apache/apisix/pull/7749)\n- 新增 workflow 插件：\n    - [#7760](https://github.com/apache/apisix/pull/7760)\n    - [#7771](https://github.com/apache/apisix/pull/7771)\n- 新增 Tencent Cloud Log Service 插件：[#7593](https://github.com/apache/apisix/pull/7593)\n- jwt-auth 支持 ES256 算法： [#7627](https://github.com/apache/apisix/pull/7627)\n- ldap-auth 内部实现，由 lualdap 换成 lua-resty-ldap：[#7590](https://github.com/apache/apisix/pull/7590)\n- prometheus 插件内的 http request metrics 支持通过变量来设置额外的 labels：[#7549](https://github.com/apache/apisix/pull/7549)\n- clickhouse-logger 插件支持指定多个 clickhouse endpoints: [#7517](https://github.com/apache/apisix/pull/7517)\n\n### Bugfix\n\n- gRPC 代理设置 :authority 请求头为配置的上游 Host： [#7939](https://github.com/apache/apisix/pull/7939)\n- response-rewrite 写入空 body 时有可能导致 AIPSIX 无法响应该请求：[#7836](https://github.com/apache/apisix/pull/7836)\n- 修复同时使用 Plugin Config 和 Consumer，有一定概率发生插件配置没有更新的问题：[#7965](https://github.com/apache/apisix/pull/7965)\n- 日志切割时，只 reopen 一次日志文件：[#7869](https://github.com/apache/apisix/pull/7869)\n- 默认不应开启被动健康检查： [#7850](https://github.com/apache/apisix/pull/7850)\n- zipkin 插件即使不进行 sample，也要向上游传递 trace IDs： [#7833](https://github.com/apache/apisix/pull/7833)\n- 将 opentelemetry 的 span kind 更正为 server: [#7830](https://github.com/apache/apisix/pull/7830)\n- limit-count 插件中，同样配置的不同路由不应该共享同一个计数器：[#7750](https://github.com/apache/apisix/pull/7750)\n- 修复偶发的移除 clean_handler 时抛异常的问题： [#7648](https://github.com/apache/apisix/pull/7648)\n- 允许配置上游节点时直接使用 IPv6 字面量： [#7594](https://github.com/apache/apisix/pull/7594)\n- wolf-rbac 插件调整对错误的响应方式：\n    - [#7561](https://github.com/apache/apisix/pull/7561)\n    - [#7497](https://github.com/apache/apisix/pull/7497)\n- 当代理到上游之前发生 500 错误时，代理到上游之后运行的插件不应被跳过 [#7703](https://github.com/apache/apisix/pull/7703)\n- 当 consumer 上绑定了多个插件且该插件定义了 rewrite 方法时，避免抛出异常 [#7531](https://github.com/apache/apisix/pull/7531)\n- 升级 lua-resty-etcd 到 1.8.3。该版本修复了若干问题。 [#7565](https://github.com/apache/apisix/pull/7565)\n\n## 2.15.3\n\n**这是一个 LTS 维护版本，您可以在 `release/2.15` 分支中看到 CHANGELOG。**\n\n## 2.15.2\n\n**这是一个 LTS 维护版本，您可以在 `release/2.15` 分支中看到 CHANGELOG。**\n\n## 2.15.1\n\n**这是一个 LTS 维护版本，您可以在 `release/2.15` 分支中看到 CHANGELOG。**\n\n## 2.15.0\n\n### Change\n\n- grpc 状态码 OUT_OF_RANGE 如今会在 grpc-transcode 插件中作为 http 状态码 400: [#7419](https://github.com/apache/apisix/pull/7419)\n- 重命名 `etcd.health_check_retry` 配置项为 `startup_retry`。 [#7304](https://github.com/apache/apisix/pull/7304)\n- 移除 `upstream.enable_websocket`。该配置已于 2020 年标记成已过时。 [#7222](https://github.com/apache/apisix/pull/7222)\n\n### Core\n\n- 支持动态启用插件 [#7453](https://github.com/apache/apisix/pull/7453)\n- 支持动态指定插件执行顺序 [#7273](https://github.com/apache/apisix/pull/7273)\n- 支持 Upstream 对象从 SSL 对象中引用证书 [#7221](https://github.com/apache/apisix/pull/7221)\n- 允许在插件中使用自定义错误 [#7128](https://github.com/apache/apisix/pull/7128)\n- xRPC Redis 代理增加 metrics: [#7183](https://github.com/apache/apisix/pull/7183)\n- 引入 deployment role 概念来简化 APISIX 的部署：\n    - [#7405](https://github.com/apache/apisix/pull/7405)\n    - [#7417](https://github.com/apache/apisix/pull/7417)\n    - [#7392](https://github.com/apache/apisix/pull/7392)\n    - [#7365](https://github.com/apache/apisix/pull/7365)\n    - [#7249](https://github.com/apache/apisix/pull/7249)\n\n### Plugin\n\n- prometheus 指标中提供 ngx.shared.dict 统计信息 [#7412](https://github.com/apache/apisix/pull/7412)\n- 允许在 proxy-rewrite 插件中使用客户端发过来的原始 URL [#7401](https://github.com/apache/apisix/pull/7401)\n- openid-connect 插件支持 PKCE： [#7370](https://github.com/apache/apisix/pull/7370)\n- sls-logger 插件支持自定义日志格式 [#7328](https://github.com/apache/apisix/pull/7328)\n- kafka-logger 插件支持更多的 Kafka 客户端配置 [#7266](https://github.com/apache/apisix/pull/7266)\n- openid-connect 插件支持暴露 refresh token [#7220](https://github.com/apache/apisix/pull/7220)\n- 移植 prometheus 插件到 stream 子系统 [#7174](https://github.com/apache/apisix/pull/7174)\n\n### Bugfix\n\n- Kubernetes 服务发现在重试时应当清除上一次尝试时遗留的状态 [#7506](https://github.com/apache/apisix/pull/7506)\n- redirect 插件禁止同时启用冲突的 http_to_https 和 append_query_string 配置 [#7433](https://github.com/apache/apisix/pull/7433)\n- 默认配置下，http-logger 不再发送空 Authorization 头 [#7444](https://github.com/apache/apisix/pull/7444)\n- 修复 limit-count 插件不能同时配置 group 和 disable 的问题 [#7384](https://github.com/apache/apisix/pull/7384)\n- 让 request-id 插件优先执行，这样 tracing 插件可以用到 request id [#7281](https://github.com/apache/apisix/pull/7281)\n- 更正 grpc-transcode 插件中对 repeated Message 的处理。 [#7231](https://github.com/apache/apisix/pull/7231)\n- 允许 proxy-cache 插件 cache key 出现缺少的值。 [#7168](https://github.com/apache/apisix/pull/7168)\n- 减少 chash 负载均衡节点权重过大时额外的内存消耗。 [#7103](https://github.com/apache/apisix/pull/7103)\n- proxy-cache 插件 method 不匹配时不应该返回缓存结果。 [#7111](https://github.com/apache/apisix/pull/7111)\n- 上游 keepalive 应考虑 TLS 参数：\n    - [#7054](https://github.com/apache/apisix/pull/7054)\n    - [#7466](https://github.com/apache/apisix/pull/7466)\n- 重定向插件在将 HTTP 重定向到 HTTPS 时设置了正确的端口。\n    - [#7065](https://github.com/apache/apisix/pull/7065)\n\n## 2.14.1\n\n### Bugfix\n\n- `real_ip_from` 中配置 \"unix: \" 不应该导致 batch-requests 插件无法使用 [#7106](https://github.com/apache/apisix/pull/7106)\n\n## 2.14.0\n\n### Change\n\n- 为了适应 OpenTelemetry 规范的变化，OTLP/HTTP 的默认端口改为 4318: [#7007](https://github.com/apache/apisix/pull/7007)\n\n### Core\n\n- 引入一个实验性功能，允许通过 APISIX 订阅 Kafka 消息。这个功能是基于 websocket 上面运行的 pubsub 框架。\n    - [#7028](https://github.com/apache/apisix/pull/7028)\n    - [#7032](https://github.com/apache/apisix/pull/7032)\n- 引入一个名为 xRPC 的实验性框架来管理非 HTTP 的 L7 流量。\n    - [#6885](https://github.com/apache/apisix/pull/6885)\n    - [#6901](https://github.com/apache/apisix/pull/6901)\n    - [#6919](https://github.com/apache/apisix/pull/6919)\n    - [#6960](https://github.com/apache/apisix/pull/6960)\n    - [#6965](https://github.com/apache/apisix/pull/6965)\n    - [#7040](https://github.com/apache/apisix/pull/7040)\n- 现在我们支持在代理 Redis traffic 过程中根据命令和键添加延迟，它建立在 xRPC 之上。\n    - [#6999](https://github.com/apache/apisix/pull/6999)\n- 引入实验性支持，通过 xDS 配置 APISIX。\n    - [#6614](https://github.com/apache/apisix/pull/6614)\n    - [#6759](https://github.com/apache/apisix/pull/6759)\n- 增加 `normalize_uri_like_servlet` 配置选项，像 servlet 一样规范化 URI。[#6984](https://github.com/apache/apisix/pull/6984)\n- 通过 apisix-seed 实现 Zookeeper 服务发现：[#6751](https://github.com/apache/apisix/pull/6751)\n\n### Plugin\n\n- real-ip 插件支持像 `real_ip_recursive` 那样的递归 IP 搜索。[#6988](https://github.com/apache/apisix/pull/6988)\n- api-breaker 插件允许配置响应。[#6949](https://github.com/apache/apisix/pull/6949)\n- response-rewrite 插件支持正文过滤器。[#6750](https://github.com/apache/apisix/pull/6750)\n- request-id 插件增加了 nanoid 算法来生成 ID：[#6779](https://github.com/apache/apisix/pull/6779)\n- file-logger 插件可以缓存和重开 file handler。[#6721](https://github.com/apache/apisix/pull/6721)\n- 增加 casdoor 插件。[#6382](https://github.com/apache/apisix/pull/6382)\n- authz-keycloak 插件支持 password grant：[#6586](https://github.com/apache/apisix/pull/6586)\n\n### Bugfix\n\n- 上游 keepalive 应考虑 TLS 参数：[#7054](https://github.com/apache/apisix/pull/7054)\n- 不要将内部错误信息暴露给客户端。\n    - [#6982](https://github.com/apache/apisix/pull/6982)\n    - [#6859](https://github.com/apache/apisix/pull/6859)\n    - [#6854](https://github.com/apache/apisix/pull/6854)\n    - [#6853](https://github.com/apache/apisix/pull/6853)\n    - [#6846](https://github.com/apache/apisix/pull/6846)\n- DNS 支持端口为 0 的 SRV 记录：[#6739](https://github.com/apache/apisix/pull/6739)\n- 修复客户端 mTLS 在 TLS 会话重用中有时不生效的问题：[#6906](https://github.com/apache/apisix/pull/6906)\n- grpc-web 插件不会在响应中覆盖 Access-Control-Allow-Origin 头。[#6842](https://github.com/apache/apisix/pull/6842)\n- syslog 插件的默认超时已被纠正。[#6807](https://github.com/apache/apisix/pull/6807)\n- 修复 authz-keycloak 插件的 `access_denied_redirect_uri` 的设置有时不生效的问题。[#6794](https://github.com/apache/apisix/pull/6794)\n- 正确处理 `USR2` 信号。[#6758](https://github.com/apache/apisix/pull/6758)\n- 重定向插件在将 HTTP 重定向到 HTTPS 时设置了正确的端口。\n    - [#7065](https://github.com/apache/apisix/pull/7065)\n    - [#6686](https://github.com/apache/apisix/pull/6686)\n- Admin API 拒绝未知的 stream 插件。[#6813](https://github.com/apache/apisix/pull/6813)\n\n## 2.13.3\n\n**这是一个 LTS 维护版本，您可以在 `release/2.13` 分支中看到 CHANGELOG。**\n\n## 2.13.2\n\n**这是一个 LTS 维护版本，您可以在 `release/2.13` 分支中看到 CHANGELOG。**\n\n## 2.13.1\n\n**这是一个 LTS 维护版本，您可以在 `release/2.13` 分支中看到 CHANGELOG。**\n\n## 2.13.0\n\n### Change\n\n- 更正 syslog 插件的配置 [#6551](https://github.com/apache/apisix/pull/6551)\n- server-info 插件使用新方法来上报 DP 面信息 [#6202](https://github.com/apache/apisix/pull/6202)\n- Admin API 返回的空 nodes 应当被编码为数组 [#6384](https://github.com/apache/apisix/pull/6384)\n- 更正 prometheus 统计指标 apisix_nginx_http_current_connections{state=\"total\"} [#6327](https://github.com/apache/apisix/pull/6327)\n- 不再默认暴露 public API 并移除 plugin interceptor [#6196](https://github.com/apache/apisix/pull/6196)\n\n### Core\n\n- :sunrise: 新增 delayed_body_filter 阶段 [#6605](https://github.com/apache/apisix/pull/6605)\n- :sunrise: standalone 模式的配置支持环境变量 [#6505](https://github.com/apache/apisix/pull/6505)\n- :sunrise: consumer 新增的插件都能被执行 [#6502](https://github.com/apache/apisix/pull/6502)\n- :sunrise: 添加配置项来控制是否在 x-upsream-apisix-status 中记录所有状态码 [#6392](https://github.com/apache/apisix/pull/6392)\n- :sunrise: 新增 kubernetes 服务发现 [#4880](https://github.com/apache/apisix/pull/4880)\n- :sunrise: graphql 路由支持 JSON 类型和 GET 方法 [#6343](https://github.com/apache/apisix/pull/6343)\n\n### Plugin\n\n- :sunrise: jwt-auth 支持自定义参数名 [#6561](https://github.com/apache/apisix/pull/6561)\n- :sunrise: cors 参数支持通过 plugin metadata 配置 [#6546](https://github.com/apache/apisix/pull/6546)\n- :sunrise: openid-connect 支持 post_logout_redirect_uri [#6455](https://github.com/apache/apisix/pull/6455)\n- :sunrise: mocking 插件 [#5940](https://github.com/apache/apisix/pull/5940)\n- :sunrise: error-log-logger 新增 clickhouse 支持 [#6256](https://github.com/apache/apisix/pull/6256)\n- :sunrise: clickhouse 日志插件 [#6215](https://github.com/apache/apisix/pull/6215)\n- :sunrise: grpc-transcode 支持处理 .pb 文件 [#6264](https://github.com/apache/apisix/pull/6264)\n- :sunrise: loggly 日志插件 [#6113](https://github.com/apache/apisix/pull/6113)\n- :sunrise: opentelemetry 日志插件 [#6119](https://github.com/apache/apisix/pull/6119)\n- :sunrise: public api 插件 [#6145](https://github.com/apache/apisix/pull/6145)\n- :sunrise: CSRF 插件 [#5727](https://github.com/apache/apisix/pull/5727)\n\n### Bugfix\n\n- 修复 skywalking,opentelemetry 没有追踪认证失败的问题 [#6617](https://github.com/apache/apisix/pull/6617)\n- log-rotate 切割日志时按整点完成 [#6521](https://github.com/apache/apisix/pull/6521)\n- deepcopy 没有复制 metatable [#6623](https://github.com/apache/apisix/pull/6623)\n- request-validate 修复对 JSON 里面重复键的处理 [#6625](https://github.com/apache/apisix/pull/6625)\n- prometheus 避免重复计算指标 [#6579](https://github.com/apache/apisix/pull/6579)\n- 修复 proxy-rewrite 中，当 conf.headers 缺失时，conf.method 不生效的问题 [#6300](https://github.com/apache/apisix/pull/6300)\n- 修复 traffic-split 首条规则失败时无法匹配的问题 [#6292](https://github.com/apache/apisix/pull/6292)\n- etcd 超时不应触发 resync_delay [#6259](https://github.com/apache/apisix/pull/6259)\n- 解决 proto 定义冲突 [#6199](https://github.com/apache/apisix/pull/6199)\n- limit-count 配置不变，不应重置计数器 [#6151](https://github.com/apache/apisix/pull/6151)\n- Admin API 的 plugin-metadata 和 global-rule 计数有误 [#6155](https://github.com/apache/apisix/pull/6155)\n- 解决合并 route 和 service 时 labels 丢失问题 [#6177](https://github.com/apache/apisix/pull/6177)\n\n## 2.12.1\n\n**这是一个 LTS 维护版本，您可以在 `release/2.12` 分支中看到 CHANGELOG。**\n\n## 2.12.0\n\n### Change\n\n- 重命名 serverless 插件的 \"balancer\" phase 为 \"before_proxy\" [#5992](https://github.com/apache/apisix/pull/5992)\n- 不再承诺支持 Tengine [#5961](https://github.com/apache/apisix/pull/5961)\n- 当 L4 支持 和 Admin API 都启用时，自动开启 HTTP 支持 [#5867](https://github.com/apache/apisix/pull/5867)\n\n### Core\n\n- :sunrise: 支持 TLS over TCP upstream [#6030](https://github.com/apache/apisix/pull/6030)\n- :sunrise: 支持自定义 APISIX variable [#5941](https://github.com/apache/apisix/pull/5941)\n- :sunrise: 支持集成 Vault [#5745](https://github.com/apache/apisix/pull/5745)\n- :sunrise: 支持 L4 的 access log [#5768](https://github.com/apache/apisix/pull/5768)\n- :sunrise: 支持自定义 http_server_location_configuration_snippet 配置 [#5740](https://github.com/apache/apisix/pull/5740)\n- :sunrise: 支持配置文件环境变量中设置默认值 [#5675](https://github.com/apache/apisix/pull/5675)\n- :sunrise: 支持在 header_filter 阶段运行 Wasm 代码 [#5544](https://github.com/apache/apisix/pull/5544)\n\n### Plugin\n\n- :sunrise: 支持在 basic-auth 中隐藏 Authorization 请求头 [#6039](https://github.com/apache/apisix/pull/6039)\n- :sunrise: 支持动态设置 proxy_request_buffering [#6075](https://github.com/apache/apisix/pull/6075)\n- :sunrise: mqtt 支持通过 client id 负载均衡 [#6079](https://github.com/apache/apisix/pull/6079)\n- :sunrise: 添加 forward-auth 插件 [#6037](https://github.com/apache/apisix/pull/6037)\n- :sunrise: 支持 gRPC-Web 代理 [#5964](https://github.com/apache/apisix/pull/5964)\n- :sunrise: limit-count 支持请求间共享计数器 [#5984](https://github.com/apache/apisix/pull/5984)\n- :sunrise: limit-count 支持在路由间共享计数器 [#5881](https://github.com/apache/apisix/pull/5881)\n- :sunrise: 新增 splunk hec logging 插件 [#5819](https://github.com/apache/apisix/pull/5819)\n- :sunrise: 新增 OPA 插件 [#5734](https://github.com/apache/apisix/pull/5734)\n- :sunrise: 新增 rocketmq logger 插件 [#5653](https://github.com/apache/apisix/pull/5653)\n- :sunrise: mqtt 支持直接使用 route 上配置的 upstream [#5666](https://github.com/apache/apisix/pull/5666)\n- :sunrise: ext-plugin 支持获取请求体 [#5600](https://github.com/apache/apisix/pull/5600)\n- :sunrise: 新增 aws lambda 插件 [#5594](https://github.com/apache/apisix/pull/5594)\n- :sunrise: http/kafka-logger 插件支持记录响应体 [#5550](https://github.com/apache/apisix/pull/5550)\n- :sunrise: 新增 Apache OpenWhisk 插件 [#5518](https://github.com/apache/apisix/pull/5518)\n- :sunrise: 支持 google cloud logging service [#5538](https://github.com/apache/apisix/pull/5538)\n\n### Bugfix\n\n- 同时启用 error-log-logger 和 prometheusis 时报告 labels inconsistent 的问题 [#6055](https://github.com/apache/apisix/pull/6055)\n- 支持禁止 IPv6 IP 解析 [#6023](https://github.com/apache/apisix/pull/6023)\n- 正确处理 MQTT 5 中的 properties [#5916](https://github.com/apache/apisix/pull/5916)\n- sls-logger 上报的 timestamp 补上毫秒部分 [#5820](https://github.com/apache/apisix/pull/5820)\n- MQTT 中的 client id 可以为空 [#5816](https://github.com/apache/apisix/pull/5816)\n- ext-plugin 避免使用过期的 key [#5782](https://github.com/apache/apisix/pull/5782)\n- 解决 log-rotate 中 reopen log 和压缩中的 race [#5715](https://github.com/apache/apisix/pull/5715)\n- 释放 batch-processor 中过期对象 [#5700](https://github.com/apache/apisix/pull/5700)\n- 解决被动健康检查时配置被污染的问题 [#5589](https://github.com/apache/apisix/pull/5589)\n\n## 2.11.0\n\n### Change\n\n- wolf-rbac 插件变更默认端口，并在文档中增加 authType 参数 [#5477](https://github.com/apache/apisix/pull/5477)\n\n### Core\n\n- :sunrise: 支持基于 POST 表单的高级路由匹配 [#5409](https://github.com/apache/apisix/pull/5409)\n- :sunrise: 初步的 WASM 支持 [#5288](https://github.com/apache/apisix/pull/5288)\n- :sunrise: control API 暴露 service 配置 [#5271](https://github.com/apache/apisix/pull/5271)\n- :sunrise: control API 暴露 upstream 配置 [#5259](https://github.com/apache/apisix/pull/5259)\n- :sunrise: 支持在 etcd 少于半数节点不可用时成功启动 [#5158](https://github.com/apache/apisix/pull/5158)\n- :sunrise: 支持 etcd 配置里面自定义 SNI [#5206](https://github.com/apache/apisix/pull/5206)\n\n### Plugin\n\n- :sunrise: 新增 Azure-functions 插件 [#5479](https://github.com/apache/apisix/pull/5479)\n- :sunrise: kafka-logger 支持动态记录请求体 [#5501](https://github.com/apache/apisix/pull/5501)\n- :sunrise: 新增 skywalking-logger 插件 [#5478](https://github.com/apache/apisix/pull/5478)\n- :sunrise: 新增 datadog 插件 [#5372](https://github.com/apache/apisix/pull/5372)\n- :sunrise: limit-* 系列插件，在 key 对应的值不存在时，回退到用客户端地址作为限流的 key [#5422](https://github.com/apache/apisix/pull/5422)\n- :sunrise: limit-count 支持使用多个变量作为 key [#5378](https://github.com/apache/apisix/pull/5378)\n- :sunrise: limit-conn 支持使用多个变量作为 key [#5354](https://github.com/apache/apisix/pull/5354)\n- :sunrise: proxy-rewrite 支持改写 HTTP method [#5292](https://github.com/apache/apisix/pull/5292)\n- :sunrise: limit-req 支持使用多个变量作为 key [#5302](https://github.com/apache/apisix/pull/5302)\n- :sunrise: proxy-cache 支持基于内存的缓存机制 [#5028](https://github.com/apache/apisix/pull/5028)\n- :sunrise: ext-plugin 避免发送重复的 conf 请求 [#5183](https://github.com/apache/apisix/pull/5183)\n- :sunrise: 新增 ldap-auth 插件 [#3894](https://github.com/apache/apisix/pull/3894)\n\n## 2.10.5\n\n**这是一个 LTS 维护版本，您可以在 `release/2.10` 分支中看到 CHANGELOG。**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2105](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2105)\n\n## 2.10.4\n\n**这是一个 LTS 维护版本，您可以在 `release/2.10` 分支中看到 CHANGELOG。**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2104](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2104)\n\n## 2.10.3\n\n**这是一个 LTS 维护版本，您可以在 `release/2.10` 分支中看到 CHANGELOG。**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103)\n\n## 2.10.2\n\n**这是一个 LTS 维护版本，您可以在 `release/2.10` 分支中看到 CHANGELOG。**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102)\n\n## 2.10.1\n\n**这是一个 LTS 维护版本，您可以在 `release/2.10` 分支中看到 CHANGELOG。**\n\n[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101)\n\n## 2.10.0\n\n### Change\n\n- 将 enable_debug 配置从 config.yaml 移到 debug.yaml [#5046](https://github.com/apache/apisix/pull/5046)\n- 更改自定义 lua_shared_dict 配置的名称 [#5030](https://github.com/apache/apisix/pull/5030)\n- 不再提供 APISIX 安装 shell 脚本 [#4985](https://github.com/apache/apisix/pull/4985)\n\n### Core\n\n- :sunrise: debug-mode 支持动态请求过滤 [#5012](https://github.com/apache/apisix/pull/5012)\n- :sunrise: 支持注入逻辑到 APISIX 方法中 [#5068](https://github.com/apache/apisix/pull/5068)\n- :sunrise: 支持配置 fallback SNI [#5000](https://github.com/apache/apisix/pull/5000)\n- :sunrise: stream_route 支持在 IP 匹配中使用 CIDR [#4980](https://github.com/apache/apisix/pull/4980)\n- :sunrise: 支持 route 从 service 中继承 hosts [#4977](https://github.com/apache/apisix/pull/4977)\n- :sunrise: 改善数据面监听地址的配置 [#4856](https://github.com/apache/apisix/pull/4856)\n\n### Plugin\n\n- :sunrise: hmac-auth 支持校验请求体 [#5038](https://github.com/apache/apisix/pull/5038)\n- :sunrise: proxy-mirror 支持控制镜像流量的比例 [#4965](https://github.com/apache/apisix/pull/4965)\n- :sunrise: referer-restriction 增加黑名单和自定义信息 [#4916](https://github.com/apache/apisix/pull/4916)\n- :sunrise: kafka-logger 增加 cluster 支持 [#4876](https://github.com/apache/apisix/pull/4876)\n- :sunrise: kafka-logger 增加 required_acks 选项 [#4878](https://github.com/apache/apisix/pull/4878)\n- :sunrise: uri-blocker 支持大小写无关的匹配 [#4868](https://github.com/apache/apisix/pull/4868)\n\n### Bugfix\n\n- radixtree_host_uri 路由更正匹配结果的 host [#5124](https://github.com/apache/apisix/pull/5124)\n- radixtree_host_uri 路由更正匹配结果的 path [#5104](https://github.com/apache/apisix/pull/5104)\n- Nacos 服务发现，区分处于不同 group/namespace 的同名 service [#5083](https://github.com/apache/apisix/pull/5083)\n- Nacos 服务发现，当一个服务的地址获取失败后，继续处理剩下的服务 [#5112](https://github.com/apache/apisix/pull/5112)\n- 匹配 SNI 时需要大小写无关 [#5074](https://github.com/apache/apisix/pull/5074)\n- upstream 的 keepalive_pool 配置，缺省时不应覆盖默认的 keepalive 配置 [#5054](https://github.com/apache/apisix/pull/5054)\n- DNS 服务发现，优先查询 SRV 记录 [#4992](https://github.com/apache/apisix/pull/4992)\n- Consul 服务发现，重试前需等待一段时间 [#4979](https://github.com/apache/apisix/pull/4979)\n- 当 upstream domain 背后的 IP 改变时，避免复制多余数据 [#4952](https://github.com/apache/apisix/pull/4952)\n- 当 plugin_config 变化时，恢复之前被覆盖的配置 [#4888](https://github.com/apache/apisix/pull/4888)\n\n## 2.9.0\n\n### Change\n\n- 为避免误解，将插件中的 balancer 方法改成 before_proxy [#4697](https://github.com/apache/apisix/pull/4697)\n\n### Core\n\n- :sunrise: 增大总 timer 数的限制 [#4843](https://github.com/apache/apisix/pull/4843)\n- :sunrise: 移除禁止额外字段的检验，方便给 APISIX 做 A/B 测试 [#4797](https://github.com/apache/apisix/pull/4797)\n- :sunrise: 支持在 arg 变量中使用 '-' (#4519) [#4676](https://github.com/apache/apisix/pull/4676)\n- :sunrise: Admin API 拒绝错误的 proto 文件内容 [#4750](https://github.com/apache/apisix/pull/4750)\n\n### Plugin\n\n- :sunrise: ext-plugin 插件允许 Runner 查询请求信息 [#4835](https://github.com/apache/apisix/pull/4835)\n- :sunrise: gzip 插件支持通过 * 匹配任意类型 [#4817](https://github.com/apache/apisix/pull/4817)\n- :sunrise: 增加 real-ip 插件 [#4813](https://github.com/apache/apisix/pull/4813)\n- :sunrise: limit-* 系列插件允许自定义请求拒绝信息 [#4808](https://github.com/apache/apisix/pull/4808)\n- :sunrise: request-id 插件增加 snowflake 算法支持 [#4559](https://github.com/apache/apisix/pull/4559)\n- :sunrise: 增加 authz-casbin 插件 [#4710](https://github.com/apache/apisix/pull/4710)\n- :sunrise: error-log-logger 插件增加 skywalking 后端 [#4633](https://github.com/apache/apisix/pull/4633)\n- :sunrise: ext-plugin 插件在发送配置时会额外发送一个 idempotent key [#4736](https://github.com/apache/apisix/pull/4736)\n\n### Bugfix\n\n- 避免特定条件下缓存过期的全局规则 [#4867](https://github.com/apache/apisix/pull/4867)\n- grpc-transcode 插件支持嵌套信息 [#4859](https://github.com/apache/apisix/pull/4859)\n- authz-keycloak 插件避免当 lazy_load_path 为 false 且没有配置 permissions 时出错 [#4845](https://github.com/apache/apisix/pull/4845)\n- proxy-cache 插件保持 cache_method 配置和 nginx's proxy_cache_methods 一致 [#4814](https://github.com/apache/apisix/pull/4814)\n- Admin API 确保 PATCH with sub path 时也能注入 updatetime [#4765](https://github.com/apache/apisix/pull/4765)\n- Admin API 更新 consumer 时校验 username [#4756](https://github.com/apache/apisix/pull/4756)\n- error-log-logger 插件避免发送过期的错误日志 [#4690](https://github.com/apache/apisix/pull/4690)\n- grpc-transcode 插件支持 enum 类型 [#4706](https://github.com/apache/apisix/pull/4706)\n- 当非 HEAD/GET 请求触发 500 错误时，会被错误转成 405 [#4696](https://github.com/apache/apisix/pull/4696)\n\n## 2.8.0\n\n### Change\n\n- 如果启用 stream proxy，默认将不再一并启用 HTTP proxy 功能 [#4580](https://github.com/apache/apisix/pull/4580)\n\n### Core\n\n- :sunrise: 允许用户自定义 balancer [#4605](https://github.com/apache/apisix/pull/4605)\n- :sunrise: upstream 中添加 retry_timeout，类似于 Nginx 的 proxy_next_upstream_timeout [#4574](https://github.com/apache/apisix/pull/4574)\n- :sunrise: 允许在 balancer_by_lua 中运行插件 [#4549](https://github.com/apache/apisix/pull/4549)\n- :sunrise: 允许给 upstream 指定单独的连接池 [#4506](https://github.com/apache/apisix/pull/4506)\n- :sunrise: etcd 连接开启健康检查 [#4191](https://github.com/apache/apisix/pull/4191)\n\n### Plugin\n\n- :sunrise: 增加 gzip 插件 [#4640](https://github.com/apache/apisix/pull/4640)\n- :sunrise: 增加 ua-restriction 插件来拒绝爬虫请求 [#4587](https://github.com/apache/apisix/pull/4587)\n- :sunrise: stream 模块增加 ip-restriction 插件 [#4602](https://github.com/apache/apisix/pull/4602)\n- :sunrise: stream 模块增加 limit-conn 插件 [#4515](https://github.com/apache/apisix/pull/4515)\n- :sunrise: 将 ext-plugin 的超时提升到 60s [#4557](https://github.com/apache/apisix/pull/4557)\n- :sunrise: key-auth 支持从 query string 中获取 key [#4490](https://github.com/apache/apisix/pull/4490)\n- :sunrise: kafka-logger 支持通过 admin API 设置日志格式 [#4483](https://github.com/apache/apisix/pull/4483)\n\n### Bugfix\n\n- 修复 stream proxy 的 SNI router 在 session 复用中不可用的问题 [#4607](https://github.com/apache/apisix/pull/4607)\n- 修复 limit-conn 同时在全局和 route 中指定会出错的问题 [#4585](https://github.com/apache/apisix/pull/4585)\n- 修复 Admin API 中检查 proto 引用关系的错误 [#4575](https://github.com/apache/apisix/pull/4575)\n- 修复 skywalking 同时在全局和 route 中指定会出错的问题 [#4589](https://github.com/apache/apisix/pull/4589)\n- 调用 `ctx.var.cookie_*` 时如果没有找到 cookie 不再报错 [#4564](https://github.com/apache/apisix/pull/4564)\n- 修复 request-id 同时在全局和 route 中指定会出错的问题 [#4479](https://github.com/apache/apisix/pull/4479)\n\n## 2.7.0\n\n### Change\n\n- 修改 metadata_schema 校验方式，让它跟其他 schema 一致 [#4381](https://github.com/apache/apisix/pull/4381)\n- 移除 echo 插件的 auth_value 字段 [#4055](https://github.com/apache/apisix/pull/4055)\n- 更正 Admin API count 字段的计算，并把它的类型变成 integer [#4385](https://github.com/apache/apisix/pull/4385)\n\n### Core\n\n- :sunrise: TCP 代理支持客户端证书校验 [#4445](https://github.com/apache/apisix/pull/4445)\n- :sunrise: TCP 代理支持接收 TLS over TCP 连接 [#4409](https://github.com/apache/apisix/pull/4409)\n- :sunrise: TCP/UDP 代理上游配置支持用域名 [#4386](https://github.com/apache/apisix/pull/4386)\n- :sunrise: CLI 中封装 nginx quit 操作 [#4360](https://github.com/apache/apisix/pull/4360)\n- :sunrise: 允许在 route 配置上游超时时间 [#4340](https://github.com/apache/apisix/pull/4340)\n- :sunrise: Nacos 服务发现支持 group 参数 [#4325](https://github.com/apache/apisix/pull/4325)\n- :sunrise: Nacos 服务发现支持 namespace 参数 [#4313](https://github.com/apache/apisix/pull/4313)\n\n### Plugin\n\n- :sunrise: client-control 允许动态设置 client_max_body_size [#4423](https://github.com/apache/apisix/pull/4423)\n- :sunrise: ext-plugin 使用 SIGTERM 结束 runner [#4367](https://github.com/apache/apisix/pull/4367)\n- :sunrise: limit-req 增加 nodelay 参数 [#4395](https://github.com/apache/apisix/pull/4395)\n- :sunrise: mqtt-proxy 允许配置域名 [#4391](https://github.com/apache/apisix/pull/4391)\n- :sunrise: redirect 支持带上 query string [#4298](https://github.com/apache/apisix/pull/4298)\n\n### Bugfix\n\n- 修复客户端断开连接导致的内存泄漏 [#4405](https://github.com/apache/apisix/pull/4405)\n- 修复处理 etcd 响应时有一个地方没有检查 res.body.error 的问题 [#4371](https://github.com/apache/apisix/pull/4371)\n- 修复 ext-plugin 插件 token 过期后没有刷新 token 的问题 [#4345](https://github.com/apache/apisix/pull/4345)\n- 修复 ext-plugin 插件没有传递环境变量的问题 [#4349](https://github.com/apache/apisix/pull/4349)\n- 修复插件热加载时，插件可能不会重新加载的问题 [#4319](https://github.com/apache/apisix/pull/4319)\n\n## 2.6.0\n\n### Change\n\n- 更改 prometheus 里面关于 latency 的指标的 label [#3993](https://github.com/apache/apisix/pull/3993)\n- 修改 prometheus 默认端口，不再暴露到数据面的端口上 [#3994](https://github.com/apache/apisix/pull/3994)\n- limit-count 里面如果使用 redis cluster，需要指定名称 [#3910](https://github.com/apache/apisix/pull/3910)\n- 不再支持 OpenResty 1.15 [#3960](https://github.com/apache/apisix/pull/3960)\n\n### Core\n\n- :sunrise: 允许 pass_host 为 node 时，upstream 配置多个节点 [#4208](https://github.com/apache/apisix/pull/4208)\n- :sunrise: 自定义 500 错误页 [#4164](https://github.com/apache/apisix/pull/4164)\n- :sunrise: stream_route 中支持 upstream_id [#4121](https://github.com/apache/apisix/pull/4121)\n- :sunrise: 支持客户端证书认证 [#4034](https://github.com/apache/apisix/pull/4034)\n- :sunrise: 实验性支持 nacos 服务发现 [#3820](https://github.com/apache/apisix/pull/3820)\n- :sunrise: 给 tcp.sock.connect 打补丁，采用配置的 DNS resolver [#4114](https://github.com/apache/apisix/pull/4114)\n\n### Plugin\n\n- :sunrise: redirect 插件，支持编码 uri [#4244](https://github.com/apache/apisix/pull/4244)\n- :sunrise: key-auth 插件：支持自定义鉴权头 [#4013](https://github.com/apache/apisix/pull/4013)\n- :sunrise: response-rewrite 插件：允许在 header 里面使用变量 [#4194](https://github.com/apache/apisix/pull/4194)\n- :sunrise: 实现 ext-plugin 第一版，APISIX 现在支持使用其他语言编写自定义插件 [#4183](https://github.com/apache/apisix/pull/4183)\n\n### Bugfix\n\n- 支持 IPv6 DNS resolver [#4242](https://github.com/apache/apisix/pull/4242)\n- 修复被动健康检查可能重复报告的问题 [#4116](https://github.com/apache/apisix/pull/4116)\n- 修复 traffic-split 中偶发的规则紊乱 [#4092](https://github.com/apache/apisix/pull/4092)\n- 修复带域名的 upstream 配置的访问问题 [#4061](https://github.com/apache/apisix/pull/4061)\n- 修复 2.5 版本的 APISIX 无法识别之前版本的 route 配置的问题 [#4056](https://github.com/apache/apisix/pull/4056)\n- standalone 模式下，启动程序时应该可以读取配置 [#4027](https://github.com/apache/apisix/pull/4027)\n- limit-count 插件 redis 模式下原子化计数操作 [#3991](https://github.com/apache/apisix/pull/3991)\n\n## 2.5.0\n\n### Change\n\n- 更改 zipkin 插件的 span 类型 [#3877](https://github.com/apache/apisix/pull/3877)\n\n### Core\n\n- :sunrise: 支持 etcd 客户端证书校验 [#3905](https://github.com/apache/apisix/pull/3905)\n- :sunrise: 支持表达式使用“或”和“非”的逻辑 [#3809](https://github.com/apache/apisix/pull/3809)\n- :sunrise: 默认启动时会同步 etcd 配置 [#3799](https://github.com/apache/apisix/pull/3799)\n- :sunrise: 负载均衡支持节点优先级 [#3755](https://github.com/apache/apisix/pull/3755)\n- :sunrise: 服务发现提供了一系列 control API [#3742](https://github.com/apache/apisix/pull/3742)\n\n### Plugin\n\n- :sunrise: 允许热更新 skywalking 插件配置，并允许配置上报间隔 [#3925](https://github.com/apache/apisix/pull/3925)\n- :sunrise: consumer-restriction 支持 HTTP method 级别的白名单配置 [#3691](https://github.com/apache/apisix/pull/3691)\n- :sunrise: cors 插件支持通过正则表达式匹配 Origin [#3839](https://github.com/apache/apisix/pull/3839)\n- :sunrise: response-rewrite 插件支持条件改写 [#3577](https://github.com/apache/apisix/pull/3577)\n\n### Bugfix\n\n- error-log-logger 插件需要在每个进程中上报日志 [#3912](https://github.com/apache/apisix/pull/3912)\n- 当使用 snippet 引入 Nginx server 段配置时，确保内置 server 是默认 server [#3907](https://github.com/apache/apisix/pull/3907)\n- 修复 traffic-split 插件通过 upstream_id 绑定上游的问题 [#3842](https://github.com/apache/apisix/pull/3842)\n- 修复 ssl_trusted_certificate 配置项的校验 [#3832](https://github.com/apache/apisix/pull/3832)\n- 启用 proxy-cache 时，避免覆盖到其他路由缓存相关的响应头 [#3789](https://github.com/apache/apisix/pull/3789)\n- 解决 macOS 下无法 `make deps` 的问题 [#3718](https://github.com/apache/apisix/pull/3718)\n\n## 2.4.0\n\n### Change\n\n- 插件暴露的公共 API 将默认不再执行全局插件 [#3396](https://github.com/apache/apisix/pull/3396)\n- DNS 记录缓存时间默认按 TTL 设置 [#3530](https://github.com/apache/apisix/pull/3530)\n\n### Core\n\n- :sunrise: 支持 DNS SRV 记录 [#3686](https://github.com/apache/apisix/pull/3686)\n- :sunrise: 新的 DNS 服务发现模块 [#3629](https://github.com/apache/apisix/pull/3629)\n- :sunrise: 支持 Consul HTTP 接口服务发现模块 [#3615](https://github.com/apache/apisix/pull/3615)\n- :sunrise: 支持插件复用 [#3567](https://github.com/apache/apisix/pull/3567)\n- :sunrise: 支持 plaintext HTTP2 [#3547](https://github.com/apache/apisix/pull/3547)\n- :sunrise: 支持 DNS AAAA 记录 [#3484](https://github.com/apache/apisix/pull/3484)\n\n### Plugin\n\n- :sunrise: traffic-split 插件支持 upstream_id [#3512](https://github.com/apache/apisix/pull/3512)\n- :sunrise: zipkin 插件 b3 请求头 [#3551](https://github.com/apache/apisix/pull/3551)\n\n### Bugfix\n\n- 一致性 hash 负载均衡确保重试所有节点 [#3651](https://github.com/apache/apisix/pull/3651)\n- 当 route 绑定 service 后仍能执行 script [#3678](https://github.com/apache/apisix/pull/3678)\n- 应当依赖 openssl111 [#3603](https://github.com/apache/apisix/pull/3603)\n- zipkin 避免缓存请求特定的数据 [#3522](https://github.com/apache/apisix/pull/3522)\n\n更多的变动可以参考[里程碑](https://github.com/apache/apisix/milestone/13)\n\n## 2.3.0\n\n### Change\n\n- 默认使用 LuaJIT 运行命令行 [#3335](https://github.com/apache/apisix/pull/3335)\n- 命令行采用 luasocket 而不是 curl 访问 etcd [#2965](https://github.com/apache/apisix/pull/2965)\n\n### Core\n\n- :sunrise: 命令行中访问 etcd 可以禁用 HTTPS 检验 [#3415](https://github.com/apache/apisix/pull/3415)\n- :sunrise: 添加 etcd 无法连接时的 Chaos 测试 [#3404](https://github.com/apache/apisix/pull/3404)\n- :sunrise: ewma 负载均衡算法更新 [#3300](https://github.com/apache/apisix/pull/3300)\n- :sunrise: 允许在 Upstream 中配置 HTTPS scheme 来跟 HTTPS 后端通信 [#3430](https://github.com/apache/apisix/pull/3430)\n- :sunrise: 允许自定义 lua_package_path & lua_package_cpath [#3417](https://github.com/apache/apisix/pull/3417)\n- :sunrise: HTTPS 代理时传递 SNI [#3420](https://github.com/apache/apisix/pull/3420)\n- :sunrise: 支持 gRPCS [#3411](https://github.com/apache/apisix/pull/3411)\n- :sunrise: 支持通过 control API 获得健康检查状态 [#3345](https://github.com/apache/apisix/pull/3345)\n- :sunrise: 支持代理 HTTP 到 dubbo 后端 [#3224](https://github.com/apache/apisix/pull/3224)\n- :sunrise: 支持最少连接负载均衡算法 [#3304](https://github.com/apache/apisix/pull/3304)\n\n### Plugin\n\n- :sunrise: kafka-logger 支持复用 kafka 生产者对象 [#3429](https://github.com/apache/apisix/pull/3429)\n- :sunrise: authz-keycloak 支持动态 scope & resource 映射 [#3308](https://github.com/apache/apisix/pull/3308)\n- :sunrise: proxy-rewrite 支持在域名中带端口 [#3428](https://github.com/apache/apisix/pull/3428)\n- :sunrise: fault-injection 支持通过变量条件动态做错误注入 [#3363](https://github.com/apache/apisix/pull/3363)\n\n### Bugfix\n\n- 修复 standalone 下 consumer 的 id 跟 username 可以不一致的问题 [#3394](https://github.com/apache/apisix/pull/3394)\n- gRPC 中可以用 upstream_id & consumer [#3387](https://github.com/apache/apisix/pull/3387)\n- 修复没有匹配规则时命中 global rule 报错的问题 [#3332](https://github.com/apache/apisix/pull/3332)\n- 避免缓存过期的服务发现得到的节点 [#3295](https://github.com/apache/apisix/pull/3295)\n- 应该在 access 阶段创建 health checker [#3240](https://github.com/apache/apisix/pull/3240)\n- 修复 chash 负载均衡算法时重试的问题 [#2676](https://github.com/apache/apisix/pull/2676)\n\n更多的变动可以参考[里程碑](https://github.com/apache/apisix/milestone/12)\n\n## 2.2.0\n\n### Change\n\n- 默认不启用 node-status 插件 [#2968](https://github.com/apache/apisix/pull/2968)\n- upstreeam 配置中不再允许使用 k8s_deployment_info [#3098](https://github.com/apache/apisix/pull/3098)\n- 默认不再匹配路由中以 ':' 开头的参数变量 [#3154](https://github.com/apache/apisix/pull/3154)\n\n### Core\n\n- :sunrise: 允许一个 consumer 关联多个认证插件 [#2898](https://github.com/apache/apisix/pull/2898)\n- :sunrise: 增加 etcd 重试间隔，并允许配置 [#2977](https://github.com/apache/apisix/pull/2977)\n- :sunrise: 允许启用或禁用 route [#2943](https://github.com/apache/apisix/pull/2943)\n- :sunrise: 允许通过 graphql 属性进行路由 [#2964](https://github.com/apache/apisix/pull/2964)\n- :sunrise: 共享 etcd 鉴权 token [#2932](https://github.com/apache/apisix/pull/2932)\n- :sunrise: 新增 control API [#3048](https://github.com/apache/apisix/pull/3048)\n\n### Plugin\n\n- :sunrise: limt-count 中使用 'remote_addr' 作为默认 key [#2927](https://github.com/apache/apisix/pull/2927)\n- :sunrise: 支持在 fault-injection 的 abort.body 中使用变量 [#2986](https://github.com/apache/apisix/pull/2986)\n- :sunrise: 新增插件 `server-info` [#2926](https://github.com/apache/apisix/pull/2926)\n- :sunrise: 增加 batch process 指标 [#3070](https://github.com/apache/apisix/pull/3070)\n- :sunrise: 新增 traffic-split 插件 [#2935](https://github.com/apache/apisix/pull/2935)\n- :sunrise: proxy-rewrite 支持在 header 中使用变量 [#3144](https://github.com/apache/apisix/pull/3144)\n- :sunrise: openid-connect 插件增加更多配置项 [#2903](https://github.com/apache/apisix/pull/2903)\n- :sunrise: proxy-rewrite 支持在 upstream_uri 中使用变量 [#3139](https://github.com/apache/apisix/pull/3139)\n\n### Bugfix\n\n- basic-auth 应该在 rewrite phase 执行 [#2905](https://github.com/apache/apisix/pull/2905)\n- http/udp-logger 中插件配置运行时变更没有生效 [#2901](https://github.com/apache/apisix/pull/2901)\n- 修复 limit-conn 对象没有被正确释放的问题 [#2465](https://github.com/apache/apisix/pull/2465)\n- 修复自动生成的 id 可能重复的问题 [#3003](https://github.com/apache/apisix/pull/3003)\n- 修复 OpenResty 1.19 下 ctx 互相影响的问题。**对于使用 OpenResty 1.19 的用户，请尽快升级到该版本。** [#3105](https://github.com/apache/apisix/pull/3105)\n- 修复 route.vars 字段的校验 [#3124](https://github.com/apache/apisix/pull/3124)\n\n更多的变动可以参考[里程碑](https://github.com/apache/apisix/milestone/10)\n\n## 2.1.0\n\n### Core\n\n- :sunrise: **支持使用环境变量来配置参数。** [#2743](https://github.com/apache/apisix/pull/2743)\n- :sunrise: **支持使用 TLS 来连接 etcd.** [#2548](https://github.com/apache/apisix/pull/2548)\n- 自动生成对象的创建和更新时间。[#2740](https://github.com/apache/apisix/pull/2740)\n- 在上游中开启 websocket 时，增加日志来提示此功能即将废弃。[#2691](https://github.com/apache/apisix/pull/2691)\n- 增加日志来提示 consumer id 即将废弃。[#2829](https://github.com/apache/apisix/pull/2829)\n- 增加 `X-APISIX-Upstream-Status` 头来区分 5xx 错误来自上游还是 APISIX 自身。[#2817](https://github.com/apache/apisix/pull/2817)\n- 支持 Nginx 配置片段。[#2803](https://github.com/apache/apisix/pull/2803)\n\n### Plugin\n\n- :sunrise: **升级协议来 Apache Skywalking 8.0**[#2389](https://github.com/apache/apisix/pull/2389). 这个版本只支持 skywalking 8.0 协议。此插件默认关闭，需要修改 config.yaml 来开启。这是不向下兼容的修改。\n- :sunrise: 新增阿里云 sls 日志服务插件。[#2169](https://github.com/apache/apisix/issues/2169)\n- proxy-cache: cache_zone 字段改为可选。[#2776](https://github.com/apache/apisix/pull/2776)\n- 在数据平面校验插件的配置。[#2856](https://github.com/apache/apisix/pull/2856)\n\n### Bugfix\n\n- :bug: fix(etcd): 处理 etcd compaction.[#2687](https://github.com/apache/apisix/pull/2687)\n- 将 `conf/cert` 中的测试证书移动到 `t/certs` 目录中，并且默认关闭 SSL。这是不向下兼容的修改。 [#2112](https://github.com/apache/apisix/pull/2112)\n- 检查 decrypt key 来阻止 lua thread 中断。 [#2815](https://github.com/apache/apisix/pull/2815)\n\n### 不向下兼容特性预告\n\n- 在 2.3 发布版本中，consumer 将只支持用户名，废弃 id，consumer 需要在 etcd 中手工清理掉 id 字段，不然使用时 schema 校验会报错\n- 在 2.3 发布版本中，将不再支持在 upstream 上开启 websocket\n- 在 3.0 版本中，数据平面和控制平面将分开为两个独立的端口，即现在的 9080 端口将只处理数据平面的请求，不再处理 admin API 的请求\n\n更多的变动可以参考[里程碑](https://github.com/apache/apisix/milestone/8)\n\n## 2.0.0\n\n这是一个 release candidate。\n\n### Core\n\n- :sunrise: **从 etcd v2 协议迁移到 v3，这是不向下兼容的修改。Apache APISIX 只支持 etcd 3.4 以及后续的版本。** [#2036](https://github.com/apache/apisix/pull/2036)\n- 支持为上游对象增加标签。[#2279](https://github.com/apache/apisix/pull/2279)\n- 为上游、路由等资源增加更多字段，比如 create_time 和 update_time。[#2444](https://github.com/apache/apisix/pull/2444)\n- 使用拦截器来保护插件的路由。[#2416](https://github.com/apache/apisix/pull/2416)\n- 支持 http 和 https 监听多个端口。[#2409](https://github.com/apache/apisix/pull/2409)\n- 实现 `core.sleep` 函数。[#2397](https://github.com/apache/apisix/pull/2397)\n\n### Plugin\n\n- :sunrise: **增加 AK/SK(HMAC) 认证插件。**[#2192](https://github.com/apache/apisix/pull/2192)\n- :sunrise: 增加 referer-restriction 插件。[#2352](https://github.com/apache/apisix/pull/2352)\n- `limit-count` 插件支持 `redis` cluster。[#2406](https://github.com/apache/apisix/pull/2406)\n- proxy-cache 插件支持存储临时文件。[#2317](https://github.com/apache/apisix/pull/2317)\n- http-logger 插件支持通过 admin API 来指定文件格式。[#2309](https://github.com/apache/apisix/pull/2309)\n\n### Bugfix\n\n- :bug: **`高优先级`** 当数据平面接收到删除某一个资源 (路由、上游等) 的指令时，没有正确的清理缓存，导致存在的资源也会找不到。这个问题在长时间、频繁删除操作的情况下才会出现。[#2168](https://github.com/apache/apisix/pull/2168)\n- 修复路由优先级不生效的问题。[#2447](https://github.com/apache/apisix/pull/2447)\n- 在 `init_worker` 阶段设置随机数，而不是 `init` 阶段。[#2357](https://github.com/apache/apisix/pull/2357)\n- 删除 jwt 插件中不支持的算法。[#2356](https://github.com/apache/apisix/pull/2356)\n- 当重定向插件的 `http_to_https` 开启时，返回正确的响应码。[#2311](https://github.com/apache/apisix/pull/2311)\n\n更多的变动可以参考[里程碑](https://github.com/apache/apisix/milestone/7)\n\n### CVE\n\n- 修复 Admin API 默认访问令牌漏洞\n\n## 1.5.0\n\n### Core\n\n- Admin API：支持使用 SSL 证书进行身份验证。[1747](https://github.com/apache/apisix/pull/1747)\n- Admin API：同时支持标准的 PATCH 和子路径 PATCH。[1930](https://github.com/apache/apisix/pull/1930)\n- HealthCheck：支持自定义检查端口。[1914](https://github.com/apache/apisix/pull/1914)\n- Upstream：支持禁用 `Nginx` 默认重试机制。[1919](https://github.com/apache/apisix/pull/1919)\n- URI：支持以配置方式删除 `URI` 末尾的 `/` 符号。[1766](https://github.com/apache/apisix/pull/1766)\n\n### New Plugin\n\n- :sunrise: **新增 请求验证器 插件** [1709](https://github.com/apache/apisix/pull/1709)\n\n### Improvements\n\n- 变更：nginx `worker_shutdown_timeout` 配置默认值由 `3s` 变更为推荐值 `240s`。[1883](https://github.com/apache/apisix/pull/1883)\n- 变更：`healthcheck` 超时时间类型 由 `integer` 变更为 `number`。[1892](https://github.com/apache/apisix/pull/1892)\n- 变更：`request-validation` 插件输入参数支持 `JsonSchema` 验证。[1920](https://github.com/apache/apisix/pull/1920)\n- 变更：为 Makefile `install` 命令添加注释。[1912](https://github.com/apache/apisix/pull/1912)\n- 变更：更新 config.yaml `etcd.timeout` 默认配置的注释。[1929](https://github.com/apache/apisix/pull/1929)\n- 变更：为 `prometheus` 添加更多度量指标，以更好地了解 `APISIX` 节点的情况。[1888](https://github.com/apache/apisix/pull/1888)\n- 变更：为 `cors` 插件添加更多配置选项。[1963](https://github.com/apache/apisix/pull/1963)\n\n### Bugfix\n\n- 修复：`healthcheck` 获取 `host` 配置失败。 [1871](https://github.com/apache/apisix/pull/1871)\n- 修复：插件运行时数据保存到 `etcd`。 [1910](https://github.com/apache/apisix/pull/1910)\n- 修复：多次运行 `apisix start` 将启动多个 `Nginx` 进程。[1913](https://github.com/apache/apisix/pull/1913)\n- 修复：从临时文件读取请求正文（如果已缓存）。[1863](https://github.com/apache/apisix/pull/1863)\n- 修复：批处理器名称和错误返回类型。[1927](https://github.com/apache/apisix/pull/1927)\n- 修复：`limit-count` 插件 `redis.ttl` 读取异常。[1928](https://github.com/apache/apisix/pull/1928)\n- 修复：被动健康检查不能提供健康报告。[1918](https://github.com/apache/apisix/pull/1918)\n- 修复：避免插件中直接修改或使用原始配置数据。[1958](https://github.com/apache/apisix/pull/1958)\n- 修复：`invalid-upstream` 测试用例稳定性问题。[1925](https://github.com/apache/apisix/pull/1925)\n\n### Doc\n\n- 文档：添加 `APISIX Lua` 代码风格指南。[1874](https://github.com/apache/apisix/pull/1874)\n- 文档：修正 `README` 中语法错误。[1894](https://github.com/apache/apisix/pull/1894)\n- 文档：修正 `benchmark` 文档中图片链接错误。[1896](https://github.com/apache/apisix/pull/1896)\n- 文档：修正 `FAQ`、`admin-api`、`architecture-design`、`discovery`、`prometheus`、`proxy-rewrite`、`redirect`、`http-logger` 文档中错别字。[1916](https://github.com/apache/apisix/pull/1916)\n- 文档：更新 `request-validation` 插件示例。[1926](https://github.com/apache/apisix/pull/1926)\n- 文档：修正 `architecture-design` 文档中错别字。[1938](https://github.com/apache/apisix/pull/1938)\n- 文档：添加 `how-to-build` 文档中在 `Linux` 和 `macOS` 系统中单元测试 `Nginx` 的默认引入路径。[1936](https://github.com/apache/apisix/pull/1936)\n- 文档：添加 `request-validation` 插件中文文档。[1932](https://github.com/apache/apisix/pull/1932)\n- 文档：修正 `README` 中 `gRPC transcoding` 文档路径。[1945](https://github.com/apache/apisix/pull/1945)\n- 文档：修正 `README` 中 `uri-blocker` 文档路径。[1950](https://github.com/apache/apisix/pull/1950)\n- 文档：修正 `README` 中 `grpc-transcode` 文档路径。[1946](https://github.com/apache/apisix/pull/1946)\n- 文档：删除 `k8s` 文档中不必要的配置。[1891](https://github.com/apache/apisix/pull/1891)\n\n## 1.4.1\n\n### Bugfix\n\n- 修复在配置了多个 SSL 证书的情况下，只有一个证书生效的问题。 [1818](https://github.com/apache/incubator-apisix/pull/1818)\n\n## 1.4.0\n\n### Core\n\n- Admin API: 路由支持唯一 name 字段 [1655](https://github.com/apache/incubator-apisix/pull/1655)\n- 优化 log 缓冲区大小和刷新时间 [1570](https://github.com/apache/incubator-apisix/pull/1570)\n\n### New plugins\n\n- :sunrise: **Apache Skywalking plugin** [1241](https://github.com/apache/incubator-apisix/pull/1241)\n- :sunrise: **Keycloak Identity Server Plugin** [1701](https://github.com/apache/incubator-apisix/pull/1701)\n- :sunrise: **Echo Plugin** [1632](https://github.com/apache/incubator-apisix/pull/1632)\n- :sunrise: **Consume Restriction Plugin** [1437](https://github.com/apache/incubator-apisix/pull/1437)\n\n### Improvements\n\n- Batch Request : 对每个请求拷贝头 [1697](https://github.com/apache/incubator-apisix/pull/1697)\n- SSL 私钥加密 [1678](https://github.com/apache/incubator-apisix/pull/1678)\n- 众多插件文档改善\n\n## 1.3.0\n\n1.3 版本主要带来安全更新。\n\n## Security\n\n- 拒绝无效的 header [#1462](https://github.com/apache/incubator-apisix/pull/1462) 并对 uri 进行安全编码 [#1461](https://github.com/apache/incubator-apisix/pull/1461)\n- 默认只允许本地环回地址 127.0.0.1 访问 admin API 和 dashboard. [#1458](https://github.com/apache/incubator-apisix/pull/1458)\n\n### Plugin\n\n- :sunrise: **新增 batch request 插件**. [#1388](https://github.com/apache/incubator-apisix/pull/1388)\n- 实现完成 `sys logger` 插件。[#1414](https://github.com/apache/incubator-apisix/pull/1414)\n\n## 1.2.0\n\n1.2 版本在内核以及插件上带来了非常多的更新。\n\n### Core\n\n- :sunrise: **支持 etcd 集群**. [#1283](https://github.com/apache/incubator-apisix/pull/1283)\n- 默认使用本地 DNS resolver，这对于 k8s 环境更加友好。[#1387](https://github.com/apache/incubator-apisix/pull/1387)\n- 支持在 `header_filter`、`body_filter` 和 `log` 阶段运行全局插件。[#1364](https://github.com/apache/incubator-apisix/pull/1364)\n- 将目录 `lua/apisix` 修改为 `apisix`(**不向下兼容**). [#1351](https://github.com/apache/incubator-apisix/pull/1351)\n- 增加 dashboard 子模块。[#1360](https://github.com/apache/incubator-apisix/pull/1360)\n- 允许自定义共享字典。[#1367](https://github.com/apache/incubator-apisix/pull/1367)\n\n### Plugin\n\n- :sunrise: **新增 Apache Kafka 插件**. [#1312](https://github.com/apache/incubator-apisix/pull/1312)\n- :sunrise: **新增 CORS 插件**. [#1327](https://github.com/apache/incubator-apisix/pull/1327)\n- :sunrise: **新增 TCP logger 插件**. [#1221](https://github.com/apache/incubator-apisix/pull/1221)\n- :sunrise: **新增 UDP logger 插件**. [1070](https://github.com/apache/incubator-apisix/pull/1070)\n- :sunrise: **新增 proxy mirror 插件**. [#1288](https://github.com/apache/incubator-apisix/pull/1288)\n- :sunrise: **新增 proxy cache 插件**. [#1153](https://github.com/apache/incubator-apisix/pull/1153)\n- 在 proxy-rewrite 插件中废弃 websocket 开关 (**不向下兼容**). [1332](https://github.com/apache/incubator-apisix/pull/1332)\n- OAuth 插件中增加基于公钥的自省支持。[#1266](https://github.com/apache/incubator-apisix/pull/1266)\n- response-rewrite 插件通过 base64 来支持传输二进制数据。[#1381](https://github.com/apache/incubator-apisix/pull/1381)\n- gRPC 转码插件支持 `deadline`. [#1149](https://github.com/apache/incubator-apisix/pull/1149)\n- limit count 插件支持 redis 权限认证。[#1150](https://github.com/apache/incubator-apisix/pull/1150)\n- Zipkin 插件支持名字和本地服务器 ip 的记录。[#1386](https://github.com/apache/incubator-apisix/pull/1386)\n- Wolf-Rbac 插件增加 `change_pwd` 和 `user_info` 参数。[#1204](https://github.com/apache/incubator-apisix/pull/1204)\n\n### Admin API\n\n- :sunrise: 对调用 Admin API 增加 key-auth 权限认证 (**not backward compatible**). [#1169](https://github.com/apache/incubator-apisix/pull/1169)\n- 隐藏 SSL 私钥的返回值。[#1240](https://github.com/apache/incubator-apisix/pull/1240)\n\n### Bugfix\n\n- 在复用 table 之前遗漏了对数据的清理 (**会引发内存泄漏**). [#1134](https://github.com/apache/incubator-apisix/pull/1134)\n- 如果 yaml 中路由非法就打印警告信息。[#1141](https://github.com/apache/incubator-apisix/pull/1141)\n- 使用空字符串替代空的 balancer IP. [#1166](https://github.com/apache/incubator-apisix/pull/1166)\n- 修改 node-status 和 heartbeat 插件没有 schema 的问题。[#1249](https://github.com/apache/incubator-apisix/pull/1249)\n- basic-auth 增加 required 字段。[#1251](https://github.com/apache/incubator-apisix/pull/1251)\n- 检查上游合法节点的个数。[#1292](https://github.com/apache/incubator-apisix/pull/1292)\n\n## 1.1.0\n\n这个版本主要是加强代码的稳定性，以及增加更多的文档。\n\n### Core\n\n- 每次跑测试用例都指定 perl 包含路径。 [#1097](https://github.com/apache/incubator-apisix/pull/1097)\n- 增加对代理协议的支持。 [#1113](https://github.com/apache/incubator-apisix/pull/1113)\n- 增加用于校验 nginx.conf 的命令。 [#1112](https://github.com/apache/incubator-apisix/pull/1112)\n- 支持「nginx 最多可以打开文件数」可配置，并增大其默认配置。[#1105](https://github.com/apache/incubator-apisix/pull/1105) [#1098](https://github.com/apache/incubator-apisix/pull/1098)\n- 优化日志模块。 [#1093](https://github.com/apache/incubator-apisix/pull/1093)\n- 支持 SO_REUSEPORT。 [#1085](https://github.com/apache/incubator-apisix/pull/1085)\n\n### Doc\n\n- 增加 Grafana 元数据下载链接。[#1119](https://github.com/apache/incubator-apisix/pull/1119)\n- 更新 README.md。 [#1118](https://github.com/apache/incubator-apisix/pull/1118)\n- 增加 wolf-rbac 插件说明文档 [#1116](https://github.com/apache/incubator-apisix/pull/1116)\n- 更新 rpm 下载链接。 [#1108](https://github.com/apache/incubator-apisix/pull/1108)\n- 增加更多英文文章链接。 [#1092](https://github.com/apache/incubator-apisix/pull/1092)\n- 增加文档贡献指引。 [#1086](https://github.com/apache/incubator-apisix/pull/1086)\n- 检查更新「快速上手」文档。 [#1084](https://github.com/apache/incubator-apisix/pull/1084)\n- 检查更新「插件开发指南」。 [#1078](https://github.com/apache/incubator-apisix/pull/1078)\n- 更新 admin-api-cn.md。 [#1067](https://github.com/apache/incubator-apisix/pull/1067)\n- 更新 architecture-design-cn.md。 [#1065](https://github.com/apache/incubator-apisix/pull/1065)\n\n### CI\n\n- 移除不再必须的补丁。 [#1090](https://github.com/apache/incubator-apisix/pull/1090)\n- 修复使用 luarocks 安装时路径错误问题。[#1068](https://github.com/apache/incubator-apisix/pull/1068)\n- 为 luarocks 安装专门配置一个 travis 进行回归测试。 [#1063](https://github.com/apache/incubator-apisix/pull/1063)\n\n### Plugins\n\n- 在「节点状态」插件使用 nginx 内部请求替换原来的外部请求。 [#1109](https://github.com/apache/incubator-apisix/pull/1109)\n- 增加 wolf-rbac 插件。 [#1095](https://github.com/apache/incubator-apisix/pull/1095)\n- 增加 udp-logger 插件。 [#1070](https://github.com/apache/incubator-apisix/pull/1070)\n\n## 1.0.0\n\n这个版本主要是加强代码的稳定性，以及增加更多的文档。\n\n### Core\n\n- :sunrise: 支持路由的优先级。可以在 URI 相同的条件下，根据 header、args、优先级等条件，来匹配到不同的上游服务。 [#998](https://github.com/apache/incubator-apisix/pull/998)\n- 在没有匹配到任何路由的时候，返回错误信息。以便和其他的 404 请求区分开。[#1013](https://github.com/apache/incubator-apisix/pull/1013)\n- dashboard 的地址 `/apisix/admin` 支持 CORS。[#982](https://github.com/apache/incubator-apisix/pull/982)\n- jsonschema 校验器返回更清晰的错误提示。[#1011](https://github.com/apache/incubator-apisix/pull/1011)\n- 升级 `ngx_var` 模块到 0.5 版本。[#1005](https://github.com/apache/incubator-apisix/pull/1005)\n- 升级 `lua-resty-etcd` 模块到 0.8 版本。[#980](https://github.com/apache/incubator-apisix/pull/980)\n- 在开发模式下，自动把 worker 数调整为 1。[#926](https://github.com/apache/incubator-apisix/pull/926)\n- 从代码仓库中移除 nginx.conf 文件，它每次都会自动生成，不可手工修改。[#974](https://github.com/apache/incubator-apisix/pull/974)\n\n### Doc\n\n- 增加如何自定义开发插件的文档。[#909](https://github.com/apache/incubator-apisix/pull/909)\n- 修复 serverless 插件文档中错误的示例。[#1006](https://github.com/apache/incubator-apisix/pull/1006)\n- 增加 Oauth 插件的使用文档。[#987](https://github.com/apache/incubator-apisix/pull/987)\n- 增加 dashboard 编译的文档。[#985](https://github.com/apache/incubator-apisix/pull/985)\n- 增加如何进行 a/b 测试的文档。[#957](https://github.com/apache/incubator-apisix/pull/957)\n- 增加如何开启 MQTT 插件的文档。[#916](https://github.com/apache/incubator-apisix/pull/916)\n\n### Test case\n\n- 增加 key-auth 插件正常情况下的测试案例。[#964](https://github.com/apache/incubator-apisix/pull/964/)\n- 增加 grpc transcode pb 选项的测试。[#920](https://github.com/apache/incubator-apisix/pull/920)\n\n## 0.9.0\n\n这个版本带来很多新特性，比如支持使用 Tengine 运行 APISIX，增加了对开发人员更友好的高级调试模式，还有新的 URI 重定向插件等。\n\n### Core\n\n- :sunrise: 支持使用 Tengine 运行 APISIX。 [#683](https://github.com/apache/incubator-apisix/pull/683)\n- :sunrise: 启用 HTTP2 并支持设置 ssl_protocols。 [#663](https://github.com/apache/incubator-apisix/pull/663)\n- :sunrise: 增加高级调试模式，可在不重启的服务的情况下动态打印指定模块方法的请求参数或返回值。[#614](https://github.com/apache/incubator-apisix/pull/641)\n- 安装程序增加了仪表盘开关，支持用户自主选择是否安装仪表板程序。 [#686](https://github.com/apache/incubator-apisix/pull/686)\n- 取消对 R3 路由的支持，并移除 R3 路由模块。 [#725](https://github.com/apache/incubator-apisix/pull/725)\n\n### Plugins\n\n- :sunrise: **[Redirect URI](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/redirect.md)**：URI 重定向插件。 [#732](https://github.com/apache/incubator-apisix/pull/732)\n- [Proxy Rewrite](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/proxy-rewrite.md)：支持 `header` 删除功能。 [#658](https://github.com/apache/incubator-apisix/pull/658)\n- [Limit Count](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/limit-count.md)：通过 `Redis Server` 聚合 `APISIX` 节点之间将共享流量限速结果，实现集群流量限速。[#624](https://github.com/apache/incubator-apisix/pull/624)\n\n### lua-resty-*\n\n- lua-resty-radixtree\n    - 支持将`host + uri`作为索引。\n- lua-resty-jsonschema\n    - 该扩展作用是 JSON 数据验证器，用于替换现有的 `lua-rapidjson` 扩展。\n\n### Bugfix\n\n- 在多个使用者的情况下，`key-auth` 插件无法正确运行。 [#826](https://github.com/apache/incubator-apisix/pull/826)\n- 无法在 `API Server` 中获取 `serverless`插件配置。 [#787](https://github.com/apache/incubator-apisix/pull/787)\n- 解决使用 `proxy-write` 重写 URI 时 GET 参数丢失问题。 [#642](https://github.com/apache/incubator-apisix/pull/642)\n- `Zipkin` 插件未将跟踪数据设置为请求头。[#715](https://github.com/apache/incubator-apisix/pull/715)\n- 使用本地文件作为配置中心时，跳过 etcd 初始化。 [#737](https://github.com/apache/incubator-apisix/pull/737)\n- 在 APISIX CLI 中跳过 luajit 环境的`check cjson`。[#652](https://github.com/apache/incubator-apisix/pull/652)\n- 配置 `Upstream` 时，选择 `balancer` 类型为 `chash` 时，支持更多 Nginx 内置变量作为计算 key。 [#775](https://github.com/apache/incubator-apisix/pull/775)\n\n### Dependencies\n\n- 使用 `lua-resty-jsonschema` 全局替换 `lua-rapidjson` 扩展，`lua-resty-jsonschema` 解析速度更快，更容易编译。\n\n## 0.8.0\n\n> Released on 2019/09/30\n\n这个版本带来很多新的特性，比如四层协议的代理，支持 MQTT 协议代理，以及对 ARM 平台的支持，和代理改写插件等。\n\n### Core\n\n- :sunrise: **[增加单机模式](https://github.com/apache/incubator-apisix/blob/master/docs/en/latest/deployment-modes.md#Standalone)**: 使用 yaml 配置文件来更新 APISIX 的配置，这对于 kubernetes 更加友好。 [#464](https://github.com/apache/incubator-apisix/pull/464)\n- :sunrise: **[支持 stream 代理](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest/stream-proxy.md)**. [#513](https://github.com/apache/incubator-apisix/pull/513)\n- :sunrise: 支持[在 consumer 上绑定插件](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest/terminology/consumer.md). [#544](https://github.com/apache/incubator-apisix/pull/544)\n- 上游增加对域名的支持，而不仅是 IP。[#522](https://github.com/apache/incubator-apisix/pull/522)\n- 当上游节点的权重为 0 时自动忽略。[#536](https://github.com/apache/incubator-apisix/pull/536)\n\n### Plugins\n\n- :sunrise: **[MQTT 代理](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/mqtt-proxy.md)**: 支持用 `client_id` 对 MQTT 进行负载均衡，同时支持 MQTT 3.1 和 5.0 两个协议标准。 [#513](https://github.com/apache/incubator-apisix/pull/513)\n- [proxy-rewrite](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/proxy-rewrite.md): 对代理到上游的请求进行改写，包括 host, uri 和 schema。 [#594](https://github.com/apache/incubator-apisix/pull/594)\n\n### ARM\n\n- :sunrise: **APISIX 可以在基于 ARM64 架构的 Ubuntu 18.04 系统中正常运行**, 搭配上 MQTT 插件，你可以把它当做 IoT 网关来使用。\n\n### lua-resty-*\n\n- lua-resty-ipmatcher\n    - 支持 IPv6。\n    - 支持 IP 黑白名单和路由。\n- lua-resty-radixtree\n    - 允许指定多个 host, remote_addr 和 uri。\n    - 允许设置用户自定义函数来做额外的过滤。\n    - 使用 `lua-resty-ipmatcher` 替代 `lua-resty-iputils`, `lua-resty-ipmatcher` 支持 IPv6 并且速度更快。\n\n### Bugfix\n\n- 健康检查：修复在多 worker 下运行时健康检查 checker 的名字错误。 [#568](https://github.com/apache/incubator-apisix/issues/568)\n\n### Dependencies\n\n- 把 `lua-tinyyaml` 从源码中移除，通过 Luarocks 来安装。\n\n## 0.7.0\n\n> Released on 2019/09/06\n\n这个版本带来很多新的特性，比如 IP 黑白名单、gPRC 协议转换、支持 IPv6、对接 IdP（身份认证提供商）服务、serverless、默认路由修改为 radix tree（**不向下兼容**）等。\n\n### Core\n\n- :sunrise: **[gRPC 协议转换](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/grpc-transcode.md)**: 支持 gRPC 协议的转换，这样客户端可以通过 HTTP/JSON 来访问你的 gRPC API. [#395](https://github.com/apache/incubator-apisix/issues/395)\n- :sunrise: **[radix tree 路由](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//router-radixtree.md)**: 默认的路由器更改为 radix tree，支持把 uri、host、cookie、请求头、请求参数、Nginx 内置变量等作为路由的条件，并支持等于、大于、小于等常见操作符，更加强大和灵活。**需要注意的是，这个改动不向下兼容，所有使用历史版本的用户，需要手动修改路由才能正常使用**。[#414](https://github.com/apache/incubator-apisix/issues/414)\n- 动态上游支持更多的参数，可以指定上游的 uri 和 host，以及是否开启 websocket. [#451](https://github.com/apache/incubator-apisix/pull/451)\n- 支持从 `ctx.var` 中直接获取 cookie 中的值。[#449](https://github.com/apache/incubator-apisix/pull/449)\n- 路由支持 IPv6. [#331](https://github.com/apache/incubator-apisix/issues/331)\n\n### Plugins\n\n- :sunrise: **[serverless](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/serverless.md)**: 支持 serverless，用户可以把任意 Lua 函数动态的在网关节点上运行。用户也可以把这个功能当做是轻量级的插件来使用。[#86](https://github.com/apache/incubator-apisix/pull/86)\n- :sunrise: **IdP 支持**: 支持外部的身份认证服务，比如 Auth0，okta 等，用户可以借此来对接 Oauth2.0 等认证方式。 [#447](https://github.com/apache/incubator-apisix/pull/447)\n- [限流限速](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/limit-conn.md)支持更多的限制 key，比如 X-Forwarded-For 和 X-Real-IP，并且允许用户把 Nginx 变量、请求头和请求参数作为 key. [#228](https://github.com/apache/incubator-apisix/issues/228)\n- [IP 黑白名单](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/ip-restriction.md) 支持 IP 黑白名单，提供更高的安全性。[#398](https://github.com/apache/incubator-apisix/pull/398)\n\n### CLI\n\n- 增加 `version` 指令，获取 APISIX 的版本号。[#420](https://github.com/apache/incubator-apisix/issues/420)\n\n### Admin\n\n- 支持 `PATCH` API，可以针对某个配置单独修改，而不再用提交整段配置。[#365](https://github.com/apache/incubator-apisix/pull/365)\n\n### Dashboard\n\n- :sunrise: **增加在线版本的 dashboard**，用户不用安装即可[体验 APISIX](http://apisix.iresty.com/). [#374](https://github.com/apache/incubator-apisix/issues/374)\n\n[Back to TOC](#table-of-contents)\n\n## 0.6.0\n\n> Released on 2019/08/05\n\n这个版本带来很多新的特性，比如健康检查、服务熔断、debug 模式，分布式追踪、JWT\n认证等，以及**内置的 dashboard**.\n\n### Core\n\n- :sunrise: **[健康检查和服务熔断](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest/tutorials/health-check.md)**: 对上游节点开启健康检查，智能判断服务状态进行熔断和连接。[#249](https://github.com/apache/incubator-apisix/pull/249)\n- 阻止 ReDoS(Regular expression Denial of Service). [#252](https://github.com/apache/incubator-apisix/pull/250)\n- 支持 debug 模式。[#319](https://github.com/apache/incubator-apisix/pull/319)\n- 允许自定义路由。[#364](https://github.com/apache/incubator-apisix/pull/364)\n- 路由支持 host 和 uri 的组合。[#325](https://github.com/apache/incubator-apisix/pull/325)\n- 允许在 balance 阶段注入插件。[#299](https://github.com/apache/incubator-apisix/pull/299)\n- 为 upstream 和 service 在 schema 中增加描述信息。[#289](https://github.com/apache/incubator-apisix/pull/289)\n\n### Plugins\n\n- :sunrise: **[分布式追踪 OpenTracing](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/zipkin.md)**: 支持 Zipkin 和 Apache SkyWalking. [#304](https://github.com/apache/incubator-apisix/pull/304)\n- [JWT 认证](https://github.com/apache/incubator-apisix/blob/master/docs/zh/latest//plugins/jwt-auth.md). [#303](https://github.com/apache/incubator-apisix/pull/303)\n\n### CLI\n\n- `allow` 指令中支持多个 ip 地址。[#340](https://github.com/apache/incubator-apisix/pull/340)\n- 支持在 nginx.conf 中配置 real_ip 指令，以及增加函数来获取 ip. [#236](https://github.com/apache/incubator-apisix/pull/236)\n\n### Dashboard\n\n- :sunrise: **增加内置的 dashboard**. [#327](https://github.com/apache/incubator-apisix/pull/327)\n\n### Test\n\n- 在 Travis CI 中支持 OSX. [#217](https://github.com/apache/incubator-apisix/pull/217)\n- 把所有依赖安装到 `deps` 目录。[#248](https://github.com/apache/incubator-apisix/pull/248)\n\n[Back to TOC](#table-of-contents)\n"
  },
  {
    "path": "docs/zh/latest/CODE_STYLE.md",
    "content": "---\ntitle: APISIX Lua 编码风格指南\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 缩进\n\n使用 4 个空格作为缩进的标记：\n\n```lua\n--No\nif a then\nngx.say(\"hello\")\nend\n```\n\n```lua\n--Yes\nif a then\n    ngx.say(\"hello\")\nend\n```\n\n你可以在使用的编辑器中把 tab 改为 4 个空格来简化操作。\n\n## 空格\n\n在操作符的两边，都需要用一个空格来做分隔：\n\n```lua\n--No\nlocal i=1\nlocal s    =    \"apisix\"\n```\n\n```lua\n--Yes\nlocal i = 1\nlocal s = \"apisix\"\n```\n\n## 空行\n\n不少开发者会在行尾增加一个分号：\n\n```lua\n--No\nif a then\n    ngx.say(\"hello\");\nend;\n```\n\n增加分号会让 Lua 代码显得非常丑陋，也是没有必要的。\n\n另外，不要为了显得“简洁”节省代码行数，而把多行代码变为一行。这样会在定位错误的时候不知道到底哪一段代码出了问题：\n\n```lua\n--No\nif a then ngx.say(\"hello\") end\n```\n\n```lua\n--Yes\nif a then\n    ngx.say(\"hello\")\nend\n```\n\n函数之间需要用两个空行来做分隔：\n\n```lua\n--No\nlocal function foo()\nend\nlocal function bar()\nend\n```\n\n```lua\n--Yes\nlocal function foo()\nend\n\n\nlocal function bar()\nend\n```\n\n如果有多个 if elseif 的分支，它们之间需要一个空行来做分隔：\n\n```lua\n--No\nif a == 1 then\n    foo()\nelseif a== 2 then\n    bar()\nelseif a == 3 then\n    run()\nelse\n    error()\nend\n```\n\n```lua\n--Yes\nif a == 1 then\n    foo()\n\nelseif a== 2 then\n    bar()\n\nelseif a == 3 then\n    run()\n\nelse\n    error()\nend\n```\n\n## 每行最大长度\n\n每行不能超过 80 个字符，超过的话，需要换行并对齐：\n\n```lua\n--No\nreturn limit_conn_new(\"plugin-limit-conn\", conf.conn, conf.burst, conf.default_conn_delay)\n```\n\n```lua\n--Yes\nreturn limit_conn_new(\"plugin-limit-conn\", conf.conn, conf.burst,\n                      conf.default_conn_delay)\n```\n\n在换行对齐的时候，要体现出上下两行的对应关系。\n\n就上面示例而言，第二行函数的参数，要在第一行左括号的右边。\n\n如果是字符串拼接的对齐，需要把 `..` 放到下一行中：\n\n```lua\n--No\nreturn limit_conn_new(\"plugin-limit-conn\" ..  \"plugin-limit-conn\" ..\n                      \"plugin-limit-conn\")\n```\n\n```lua\n--Yes\nreturn limit_conn_new(\"plugin-limit-conn\" .. \"plugin-limit-conn\"\n                      .. \"plugin-limit-conn\")\n```\n\n```lua\n--Yes\nreturn \"param1\", \"plugin-limit-conn\"\n                 .. \"plugin-limit-conn\")\n```\n\n## 变量\n\n应该永远使用局部变量，不要使用全局变量：\n\n```lua\n--No\ni = 1\ns = \"apisix\"\n```\n\n```lua\n--Yes\nlocal i = 1\nlocal s = \"apisix\"\n```\n\n变量命名使用 `snake_case`（蛇形命名法）风格：\n\n```lua\n--No\nlocal IndexArr = 1\nlocal str_Name = \"apisix\"\n```\n\n```lua\n--Yes\nlocal index_arr = 1\nlocal str_name = \"apisix\"\n```\n\n对于常量要使用全部大写：\n\n```lua\n--No\nlocal max_int = 65535\nlocal server_name = \"apisix\"\n```\n\n```lua\n--Yes\nlocal MAX_INT = 65535\nlocal SERVER_NAME = \"apisix\"\n```\n\n## 表格/数组\n\n使用 `table.new` 来预先分配数组：\n\n```lua\n--No\nlocal t = {}\nfor i = 1, 100 do\n    t[i] = i\nend\n```\n\n```lua\n--Yes\nlocal new_tab = require \"table.new\"\nlocal t = new_tab(100, 0)\nfor i = 1, 100 do\n    t[i] = i\nend\n```\n\n不要在数组中使用 `nil`：\n\n```lua\n--No\nlocal t = {1, 2, nil, 3}\n```\n\n如果一定要使用空值，请用 `ngx.null` 来表示：\n\n```lua\n--Yes\nlocal t = {1, 2, ngx.null, 3}\n```\n\n## 字符串\n\n不要在热代码路径上拼接字符串：\n\n```lua\n--No\nlocal s = \"\"\nfor i = 1, 100000 do\n    s = s .. \"a\"\nend\n```\n\n```lua\n--Yes\nlocal new_tab = require \"table.new\"\nlocal t = new_tab(100000, 0)\nfor i = 1, 100000 do\n    t[i] = \"a\"\nend\nlocal s = table.concat(t, \"\")\n```\n\n## 函数\n\n函数的命名也同样遵循 `snake_case`（蛇形命名法）:\n\n```lua\n--No\nlocal function testNginx()\nend\n```\n\n```lua\n--Yes\nlocal function test_nginx()\nend\n```\n\n函数应该尽可能早的返回：\n\n```lua\n--No\nlocal function check(age, name)\n    local ret = true\n    if age < 20 then\n        ret = false\n    end\n\n    if name == \"a\" then\n        ret = false\n    end\n    -- do something else\n    return ret\nend\n```\n\n```lua\n--Yes\nlocal function check(age, name)\n    if age < 20 then\n        return false\n    end\n\n    if name == \"a\" then\n        return false\n    end\n    -- do something else\n    return true\nend\n```\n\n## 模块\n\n所有 `require` 的库都要 `local` 化：\n\n```lua\n--No\nlocal function foo()\n    local ok, err = ngx.timer.at(delay, handler)\nend\n```\n\n```lua\n--Yes\nlocal timer_at = ngx.timer.at\n\nlocal function foo()\n    local ok, err = timer_at(delay, handler)\nend\n```\n\n为了风格的统一，`require` 和 `ngx` 也需要 `local` 化：\n\n```lua\n--No\nlocal core = require(\"apisix.core\")\nlocal timer_at = ngx.timer.at\n\nlocal function foo()\n    local ok, err = timer_at(delay, handler)\nend\n```\n\n```lua\n--Yes\nlocal ngx = ngx\nlocal require = require\nlocal core = require(\"apisix.core\")\nlocal timer_at = ngx.timer.at\n\nlocal function foo()\n    local ok, err = timer_at(delay, handler)\nend\n```\n\n## 错误处理\n\n对于有错误信息返回的函数，必须对错误信息进行判断和处理：\n\n```lua\n--No\nlocal sock = ngx.socket.tcp()\nlocal ok = sock:connect(\"www.google.com\", 80)\nngx.say(\"successfully connected to google!\")\n```\n\n```lua\n--Yes\nlocal sock = ngx.socket.tcp()\nlocal ok, err = sock:connect(\"www.google.com\", 80)\nif not ok then\n    ngx.say(\"failed to connect to google: \", err)\n    return\nend\nngx.say(\"successfully connected to google!\")\n```\n\n自己编写的函数，错误信息要作为第二个参数，用字符串的格式返回：\n\n```lua\n--No\nlocal function foo()\n    local ok, err = func()\n    if not ok then\n        return false\n    end\n    return true\nend\n```\n\n```lua\n--No\nlocal function foo()\n    local ok, err = func()\n    if not ok then\n        return false, {msg = err}\n    end\n    return true\nend\n```\n\n```lua\n--Yes\nlocal function foo()\n    local ok, err = func()\n    if not ok then\n        return false, \"failed to call func(): \" .. err\n    end\n    return true\nend\n```\n"
  },
  {
    "path": "docs/zh/latest/FAQ.md",
    "content": "---\ntitle: 常见问题\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 常见问题\n  - FAQ\ndescription: 本文列举了使用 Apache APISIX 时常见问题解决方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 为什么需要一个新的 API 网关？不是已经有其他的开源网关了吗？\n\n随着企业向云本地微服务的发展，企业对高性能、灵活、安全、可扩展的 API 网关的需求越来越大。\n\nAPISIX 在这些指标表现上优于其它 API 网关，同时具有平台无关性和完全动态的特性，如支持多种协议、细粒度路由和多语言支持。\n\n## APISIX 和其他的 API 网关有什么不同之处？\n\nApache APISIX 在以下方面有所不同：\n\n— 它使用 etcd 来保存和同步配置，而不是使用如 PostgreSQL 或 MySQL 这类的关系数据库。etcd 中的实时事件通知系统比这些替代方案更容易扩展。这允许 APISIX 实时同步配置，使代码简洁，并避免单点故障。\n\n- 完全动态\n- 支持[热加载插件](./terminology/plugin.md#热加载)。\n\n## APISIX 所展现出的性能如何？\n\n与其它 API 网关相比，Apache APISIX 提供了更好的性能，其单核 QPS 高达 18,000，平均延迟仅为 0.2 ms。\n\n如果您想获取性能基准测试的具体结果，请查看 [benchmark](benchmark.md)。\n\n## Apache APISIX 支持哪些平台？\n\nApache APISIX 是一个开源的云原生 API 网关，它支持在裸金属服务器上运行，也支持在 Kubernetes 上使用，甚至也可以运行在 Apple Silicon ARM 芯片上。\n\n## 如何理解“Apache APISIX 是全动态的”？\n\nApache APISIX 是全动态的 API 网关，意味着当你在更改一个配置后，只需要重新加载配置文件就可以使其生效。\n\nAPISIX 可以动态处理以下行为：\n\n- 重新加载插件\n- 代理重写\n- 对请求进⾏镜像复制\n- 对请求进⾏修改\n- 健康状态的检查\n- 动态控制指向不同上游服务的流量⽐\n\n## APISIX 是否有控制台界面？\n\nAPISIX 内置功能强大的 Dashboard [APISIX Dashboard](https://github.com/apache/apisix-dashboard)。你可以通过 [APISIX Dashboard](https://github.com/apache/apisix-dashboard) 用户操作界面来管理 APISIX 配置。\n\n## 我可以为 Apache APISIX 开发适合自身业务的插件吗？\n\n当然可以，APISIX 提供了灵活的自定义插件，方便开发者和企业编写自己的逻辑。\n\n如果你想开发符合自身业务逻辑的插件，请参考：[如何开发插件](plugin-develop.md)。\n\n## 为什么 Apache APISIX 选择 etcd 作为配置中心？\n\n对于配置中心，配置存储只是最基本功能，APISIX 还需要下面几个特性：\n\n1. 集群中的分布式部署\n2. 通过比较来监视业务\n3. 多版本并发控制\n4. 变化通知\n5. 高性能和最小的读/写延迟\n\netcd 提供了这些特性，并且使它比 PostgreSQL 和 MySQL 等其他数据库更理想。\n\n如果你想了解更多关于 etcd 与其他替代方案的比较，请参考[对比图表](https://etcd.io/docs/latest/learning/why/#comparison-chart)。\n\n## 使用 LuaRocks 安装 Apache APISIX 依赖项时，为什么会导致超时、安装缓慢或安装失败？\n\n可能是因为使用的 LuaRocks 服务器延迟过高。\n\n为了解决这个问题，你可以使用 https_proxy 或者使用 `--server` 参数指定一个更快的 LuaRocks 服务器。\n\n你可以运行如下命令来查看可用的服务器（需要 LuaRocks 3.0+）：\n\n```shell\nluarocks config rocks_servers\n```\n\n中国大陆用户可以使用 `luarocks.cn` 作为 LuaRocks 的服务器。\n\n以下命令可以帮助你更快速的安装依赖：\n\n```bash\nmake deps ENV_LUAROCKS_SERVER=https://luarocks.cn\n```\n\n如果通过上述操作仍然无法解决问题，可以尝试使用 `--verbose` 或 `-v` 参数获取详细的日志来诊断问题。\n\n## 如何构建 APISIX-Runtime 环境？\n\n有些功能需要引入额外的 NGINX 模块，这就要求 APISIX 需要运行在 APISIX-Runtime 上。如果你需要这些功能，你可以参考 [api7/apisix-build-tools](https://github.com/api7/apisix-build-tools) 中的代码，构建自己的 APISIX-Runtime 环境。\n\n## 我该如何使用 Apache APISIX 进行灰度发布？\n\n举个例子，比如：`foo.com/product/index.html?id=204&page=2`，并考虑您需要根据查询字符串中的 `id` 在此条件下进行灰度发布：\n\n1. Group A:`id <= 1000`\n2. Group B:`id > 1000`\n\n在 Apache APISIX 中有两种不同的方法来实现这一点：\n\n1. 创建一个[Route](terminology/route.md)并配置 `vars` 字段：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"vars\": [\n        [\"arg_id\", \"<=\", \"1000\"]\n    ],\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"/test?group_id=1\"\n        }\n    }\n}'\n\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"vars\": [\n        [\"arg_id\", \">\", \"1000\"]\n    ],\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"/test?group_id=2\"\n        }\n    }\n}'\n```\n\n更多 `lua-resty-radixtree` 匹配操作，请参考：[lua-resty-radixtree](https://github.com/api7/lua-resty-radixtree#operator-list)。\n\n2、通过 [traffic-split](plugins/traffic-split.md) 插件来实现。\n\n## 我如何通过 Apache APISIX 实现从 HTTP 自动跳转到 HTTPS？\n\n比如，将 `http://foo.com` 重定向到 `https://foo.com`。\n\nApache APISIX 提供了几种不同的方法来实现：\n\n1. 在 [redirect](plugins/redirect.md) 插件中将 `http_to_https` 设置为 `true`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"host\": \"foo.com\",\n    \"plugins\": {\n        \"redirect\": {\n            \"http_to_https\": true\n        }\n    }\n}'\n```\n\n2. 结合高级路由规则 `vars` 和 `redirect` 插件一起使用：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"host\": \"foo.com\",\n    \"vars\": [\n        [\n            \"scheme\",\n            \"==\",\n            \"http\"\n        ]\n    ],\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"https://$host$request_uri\",\n            \"ret_code\": 301\n        }\n    }\n}'\n```\n\n3. 使用 `serverless` 插件：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\"return function() if ngx.var.scheme == \\\"http\\\" and ngx.var.host == \\\"foo.com\\\" then ngx.header[\\\"Location\\\"] = \\\"https://foo.com\\\" .. ngx.var.request_uri; ngx.exit(ngx.HTTP_MOVED_PERMANENTLY); end; end\"]\n        }\n    }\n}'\n```\n\n然后测试下是否生效：\n\n```shell\ncurl -i -H 'Host: foo.com' http://127.0.0.1:9080/hello\n```\n\n响应信息应该是：\n\n```\nHTTP/1.1 301 Moved Permanently\nDate: Mon, 18 May 2020 02:56:04 GMT\nContent-Type: text/html\nContent-Length: 166\nConnection: keep-alive\nLocation: https://foo.com/hello\nServer: APISIX web server\n\n<html>\n<head><title>301 Moved Permanently</title></head>\n<body>\n<center><h1>301 Moved Permanently</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n## 我应该如何更改 Apache APISIX 的日志等级？\n\nApache APISIX 默认的日志等级为 `warn`，你需要将日志等级调整为 `info` 来查看 `core.log.info` 的打印结果。\n\n你需要将 `./conf/config.yaml` 中的 `nginx_config` 配置参数 `error_log_level: \"warn\"` 修改为 `error_log_level: \"info\"`，然后重新加载 Apache APISIX 使其生效。\n\n```yaml\nnginx_config:\n  error_log_level: \"info\"\n```\n\n## 我应该如何重新加载 Apache APISIX 的自定义插件？\n\n所有的 Apache APISIX 的插件都支持热加载的方式。\n\n如果你想了解更多关于热加载的内容，请参考[热加载](./terminology/plugin.md#热加载)。\n\n## 在处理 HTTP 或 HTTPS 请求时，我该如何配置 Apache APISIX 来监听多个端口？\n\n默认情况下，APISIX 在处理 HTTP 请求时只监听 9080 端口。\n\n要配置 Apache APISIX 监听多个端口，你可以：\n\n1. 修改 `conf/config.yaml` 中 HTTP 端口监听的参数 `node_listen`，示例：\n\n   ```\n   apisix:\n     node_listen:\n       - 9080\n       - 9081\n       - 9082\n   ```\n\n   处理 HTTPS 请求也类似，修改 `conf/config.yaml` 中 HTTPS 端口监听的参数 `ssl.listen`，示例：\n\n   ```\n   apisix:\n     ssl:\n       enable: true\n       listen:\n         - port: 9443\n         - port: 9444\n         - port: 9445\n   ```\n\n2. 重启或者重新加载 APISIX。\n\n## 启用 SSL 证书后，为什么无法通过 HTTPS + IP 访问对应的路由？\n\n如果直接使用 HTTPS + IP 地址访问服务器，服务器将会使用 IP 地址与绑定的 SNI 进行比对，由于 SSL 证书是和域名进行绑定的，无法在 SNI 中找到对应的资源，因此证书就会校验失败，进而导致用户无法通过 HTTPS + IP 访问网关。\n\n此时你可以通过在配置文件中设置 `fallback_sni` 参数，并配置域名，实现该功能。当用户使用 HTTPS + IP 访问网关时，SNI 为空时，则 fallback 到默认 SNI，从而实现 HTTPS + IP 访问网关。\n\n```yaml title=\"./conf/config.yaml\"\napisix\n  ssl：\n    fallback_sni: \"${your sni}\"\n```\n\n## APISIX 如何利用 etcd 如何实现毫秒级别的配置同步？\n\nApache APISIX 使用 etcd 作为它的配置中心。etcd 提供以下订阅功能（比如：[watch](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watch)、[watchdir](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watchdir)）。它可以监视对特定关键字或目录的更改。\n\nAPISIX 主要使用 [etcd.watchdir](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watchdir) 监视目录内容变更：\n\n- 如果监听目录没有数据更新：则该调用会被阻塞，直到超时或其他错误返回。\n\n- 如果监听目录有数据更新：etcd 将立刻返回订阅（毫秒级）到的新数据，APISIX 将它更新到内存缓存。\n\n## 我应该如何自定义 APISIX 实例 id？\n\n默认情况下，APISIX 从 `conf/apisix.uid` 中读取实例 id。如果找不到，且没有配置 id，APISIX 会生成一个 `uuid` 作为实例 id。\n\n要指定一个有意义的 id 来绑定 Apache APISIX 到你的内部系统，请在你的 `./conf/config.yaml` 中设置 id：\n\n```yaml\napisix:\n  id: \"your-id\"\n```\n\n## 为什么 `error.log` 中会出现 \"failed to fetch data from etcd, failed to read etcd dir, etcd key: xxxxxx\" 的错误？\n\n请按照以下步骤进行故障排除：\n\n1. 确保 Apache APISIX 和集群中的 etcd 部署之间没有任何网络问题。\n2. 如果网络正常，请检查是否为 etcd 启用了[gRPC gateway](https://etcd.io/docs/v3.4.0/dev-guide/api_grpc_gateway/)。默认状态取决于你是使用命令行还是配置文件来启动 etcd 服务器。\n\n- 如果使用命令行选项，默认启用 gRPC 网关。可以手动启用，如下所示：\n\n```shell\netcd --enable-grpc-gateway --data-dir=/path/to/data\n```\n\n**注意**：当运行 `etcd --help` 时，这个参数不会显示。\n\n- 如果使用配置文件，默认关闭 gRPC 网关。你可以手动启用，如下所示：\n\n  在 `etcd.json` 配置：\n\n```json\n{\n    \"enable-grpc-gateway\": true,\n    \"data-dir\": \"/path/to/data\"\n}\n```\n\n  在 `etcd.conf.yml` 配置\n\n```yml\nenable-grpc-gateway: true\n```\n\n**注意**：事实上这种差别已经在 etcd 的 master 分支中消除，但并没有向后兼容到已经发布的版本中，所以在部署 etcd 集群时，依然需要小心。\n\n## 我应该如何创建高可用的 Apache APISIX 集群？\n\nApache APISIX 可以通过在其前面添加一个负载均衡器来实现高可用性，因为 APISIX 的数据面是无状态的，并且可以在需要时进行扩展。\n\nApache APISIX 的控制平面是依赖于 `etcd cluster` 的高可用实现的，它只依赖于 etcd 集群。\n\n## 为什么使用源码安装 Apache APISIX 时，执行 `make deps` 命令会失败？\n\n当使用源代码安装 Apache APISIX 时，执行 `make deps` 命令可能会出现如下错误：\n\n```shell\n$ make deps\n......\nError: Failed installing dependency: https://luarocks.org/luasec-0.9-1.src.rock - Could not find header file for OPENSSL\n  No file openssl/ssl.h in /usr/local/include\nYou may have to install OPENSSL in your system and/or pass OPENSSL_DIR or OPENSSL_INCDIR to the luarocks command.\nExample: luarocks install luasec OPENSSL_DIR=/usr/local\nmake: *** [deps] Error 1\n```\n\n这是由于缺少 OpenResty openssl 开发工具包。要安装它，请参考[installation dependencies](install-dependencies.md)。\n\n## 如何使用正则表达式 (regex) 匹配 Route 中的 `uri`？\n\n你可以在 Route 中使用 `vars` 字段来匹配正则表达式：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"vars\": [\n        [\"uri\", \"~~\", \"^/[a-z]+$\"]\n    ],\n    \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n    }\n}'\n```\n\n测试请求：\n\n```shell\n# uri 匹配成功\n$ curl http://127.0.0.1:9080/hello -i\nHTTP/1.1 200 OK\n...\n\n# uri 匹配失败\n$ curl http://127.0.0.1:9080/12ab -i\nHTTP/1.1 404 Not Found\n...\n```\n\n如果你想了解 `vars` 字段的更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。\n\n## Upstream 节点是否支持配置 [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) 地址？\n\n这是支持的，下面是一个 `FQDN` 为 `httpbin.default.svc.cluster.local`（一个 Kubernetes Service）的示例：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/ip\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.default.svc.cluster.local\": 1\n        }\n    }\n}'\n```\n\n使用如下命令测试路由：\n\n```shell\ncurl http://127.0.0.1:9080/ip -i\n```\n\n## Admin API 的 `X-API-KEY` 指的是什么？是否可以修改？\n\nAdmin API 的 `X-API-KEY` 指的是 `./conf/config.yaml` 文件中的 `deployment.admin.admin_key.key`，默认值是 `edd1c9f034335f136f87ad84b625c8f1`。它是 Admin API 的访问 token。\n\n默认情况下，它被设置为 `edd1c9f034335f136f87ad84b625c8f1`，也可以通过修改 `./conf/conf/config` 中的参数来修改，如下示例：\n\n```yaml\ndeployment:\n  admin:\n    admin_key\n      - name: \"admin\"\n        key: newkey\n        role: admin\n```\n\n然后访问 Admin API：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H 'X-API-KEY: newkey' -X PUT -d '\n{\n    \"uris\":[ \"/*\" ],\n    \"name\":\"admin-token-test\",\n    \"upstream\":{\n        \"nodes\":[\n            {\n                \"host\":\"127.0.0.1\",\n                \"port\":1980,\n                \"weight\":1\n            }\n        ],\n        \"type\":\"roundrobin\"\n    }\n}'\n\nHTTP/1.1 200 OK\n......\n```\n\n**注意**：通过使用默认令牌，可能会面临安全风险。在将其部署到生产环境时，需要对其进行更新。\n\n## 如何允许所有 IP 访问 Apache APISIX 的 Admin API？\n\nApache APISIX 默认只允许 `127.0.0.0/24` 的 IP 段范围访问 `Admin API`，\n\n如果你想允许所有的 IP 访问，只需在 `./conf/config.yaml` 配置文件中添加如下的配置，然后重启或重新加载 APISIX 就可以让所有 IP 访问 `Admin API`。\n\n```yaml\ndeployment:\n  admin:\n    allow_admin:\n      - 0.0.0.0/0\n```\n\n**注意**：你可以在非生产环境中使用此方法，以允许所有客户端从任何地方访问 Apache APISIX 实例，但是在生产环境中该设置并不安全。在生产环境中，请仅授权特定的 IP 地址或地址范围访问 Apache APISIX 实例。\n\n## 如何基于 acme.sh 自动更新 APISIX SSL 证书？\n\n你可以运行以下命令来实现这一点：\n\n```bash\ncurl --output /root/.acme.sh/renew-hook-update-apisix.sh --silent https://gist.githubusercontent.com/anjia0532/9ebf8011322f43e3f5037bc2af3aeaa6/raw/65b359a4eed0ae990f9188c2afa22bacd8471652/renew-hook-update-apisix.sh\n```\n\n```bash\nchmod +x /root/.acme.sh/renew-hook-update-apisix.sh\n```\n\n```bash\nacme.sh  --issue  --staging  -d demo.domain --renew-hook \"/root/.acme.sh/renew-hook-update-apisix.sh  -h http://apisix-admin:port -p /root/.acme.sh/demo.domain/demo.domain.cer -k /root/.acme.sh/demo.domain/demo.domain.key -a xxxxxxxxxxxxx\"\n```\n\n```bash\nacme.sh --renew --domain demo.domain\n```\n\n详细步骤，请参考 [APISIX 基于 acme.sh 自动更新 HTTPS 证书](https://juejin.cn/post/6965778290619449351)。\n\n## 在 Apache APISIX 中，我如何在转发到上游之前从路径中删除一个前缀？\n\n在转发至上游之前移除请求路径中的前缀，比如说从 `/foo/get` 改成 `/get`，可以通过 `[proxy-rewrite](plugins/proxy-rewrite.md)` 插件来实现：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/foo/*\",\n    \"plugins\": {\n        \"proxy-rewrite\": {\n            \"regex_uri\": [\"^/foo/(.*)\",\"/$1\"]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n\n测试这个配置：\n\n```shell\ncurl http://127.0.0.1:9080/foo/get -i\nHTTP/1.1 200 OK\n...\n{\n  ...\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n## 我应该如何解决 `unable to get local issuer certificate` 这个错误？\n\n你可以手动设置证书的路径，将其添加到 `./conf/config.yaml` 文件中，具体操作如下所示：\n\n```yaml\napisix:\n  ssl:\n    ssl_trusted_certificate: /path/to/certs/ca-certificates.crt\n```\n\n**注意：**当你尝试使用 cosocket 连接任何 TLS 服务时，如果 APISIX 不信任对端 TLS 服务证书，都需要配置 `apisix.ssl.ssl_trusted_certificate`。\n\n例如：如果在 APISIX 中使用 Nacos 作为服务发现时，Nacos 开启了 TLS 协议，即 Nacos 配置的 `host` 是 `https://` 开头，就需要配置 `apisix.ssl.ssl_trusted_certificate`，并且使用和 Nacos 相同的 CA 证书。\n\n## 我应该如何解决 `module 'resty.worker.events' not found` 这个错误？\n\n引起这个错误的原因是在 `/root` 目录下安装了 APISIX。因为 worker 进程的用户是 nobody，无权访问 `/root` 目录下的文件。\n\n解决办法是改变 APISIX 的安装目录，推荐安装在 `/usr/local` 目录下。\n\n## 在 Apache APISIX 中，`plugin-metadata` 和 `plugin-configs` 有什么区别？\n\n两者之间的差异如下：\n\n| `plugin-metadata`                                                                                                | `plugin-config`                                                                                                                                     |\n| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 当更改该 Plugin 属性后，需要应用到配置该插件的所有路由上时使用。 | 当你需要复用一组通用的插件配置时使用，可以把 Plugin 配置提取到一个 `plugin-config` 并绑定到不同的路由。 |\n| 对绑定到 Plugin 的配置实例的所有实体生效。                           | 对绑定到 `plugin-config` 的路由生效。                                                                                               |\n| 对绑定到 Plugin 的配置实例的所有实体生效。                           | 对绑定到 `plugin-config` 的路由生效。                                                                                               |\n\n## 部署了 Apache APISIX 之后，如何检测 APISIX 数据平面的存活情况（如何探活）?\n\n可以创建一个名为 `health-info` 的路由，并开启 [fault-injection](https://apisix.apache.org/zh/docs/apisix/plugins/fault-injection/) 插件（其中 YOUR-TOKEN 是用户自己的 token；127.0.0.1 是控制平面的 IP 地址，可以自行修改）:\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/health-info \\\n-H 'X-API-KEY: YOUR-TOKEN' -X PUT -d '\n{\n  \"plugins\": {\n    \"fault-injection\": {\n      \"abort\": {\n       \"http_status\": 200,\n       \"body\": \"fine\"\n      }\n    }\n  },\n  \"uri\": \"/status\"\n}'\n```\n\n验证方式：\n\n访问 Apache APISIX 数据平面的 `/status` 来探测 APISIX，如果 response code 是 200 就代表 APISIX 存活。\n\n:::note\n\n这个方式只是探测 APISIX 数据平面是否存活，并不代表 APISIX 的路由和其他功能是正常的，这些需要更多路由级别的探测。\n\n:::\n\n## APISIX 与 [etcd](https://etcd.io/) 相关的延迟较高的问题有哪些，如何修复？\n\netcd 作为 APISIX 的数据存储组件，它的稳定性关乎 APISIX 的稳定性。在实际场景中，如果 APISIX 使用证书通过 HTTPS 的方式连接 etcd，可能会出现以下 2 种数据查询或写入延迟较高的问题：\n\n1. 通过接口操作 APISIX Admin API 进行数据的查询或写入，延迟较高。\n2. 在监控系统中，Prometheus 抓取 APISIX 数据面 Metrics 接口超时。\n\n这些延迟问题，严重影响了 APISIX 的服务稳定性，而之所以会出现这类问题，主要是因为 etcd 对外提供了 2 种操作方式：HTTP（HTTPS）、gRPC。而 APISIX 默认是基于 HTTP（HTTPS）协议来操作 etcd 的。\n\n在这个场景中，etcd 存在一个关于 HTTP/2 的 BUG：如果通过 HTTPS 操作 etcd（HTTP 不受影响），HTTP/2 的连接数上限为 Golang 默认的 `250` 个。\n\n所以，当 APISIX 数据面节点数较多时，一旦所有 APISIX 节点与 etcd 连接数超过这个上限，则 APISIX 的接口响应会非常的慢。\n\nGolang 中，默认的 HTTP/2 上限为 `250`，代码如下：\n\n```go\npackage http2\n\nimport ...\n\nconst (\n    prefaceTimeout         = 10 * time.Second\n    firstSettingsTimeout   = 2 * time.Second // should be in-flight with preface anyway\n    handlerChunkWriteSize  = 4 << 10\n    defaultMaxStreams      = 250 // TODO: make this 100 as the GFE seems to?\n    maxQueuedControlFrames = 10000\n)\n\n```\n\n目前，etcd 官方主要维护了 `3.4` 和 `3.5` 这两个主要版本。在 `3.4` 系列中，近期发布的 `3.4.20` 版本已修复了这个问题。至于 `3.5` 版本，其实，官方很早之前就在筹备发布 `3.5.5` 版本了，但截止目前（2022.09.13）仍尚未发布。所以，如果你使用的是 etcd 的版本小于 `3.5.5`，可以参考以下几种方式解决这个问题：\n\n1. 将 APISIX 与 etcd 的通讯方式由 HTTPS 改为 HTTP。\n2. 将 etcd 版本回退到 `3.4.20`。\n3. 将 etcd 源码克隆下来，直接编译 `release-3.5` 分支（此分支已修复，只是尚未发布新版本而已）。\n\n重新编译 etcd 的方式如下：\n\n```shell\ngit checkout release-3.5\nmake GOOS=linux GOARCH=amd64\n```\n\n编译的二进制在 `bin` 目录下，将其替换掉你服务器环境的 etcd 二进制后，然后重启 etcd 即可。\n\n更多信息，请参考：\n\n- [when etcd node have many http long polling connections, it may cause etcd to respond slowly to http requests.](https://github.com/etcd-io/etcd/issues/14185)\n- [bug: when apisix starts for a while, its communication with etcd starts to time out](https://github.com/apache/apisix/issues/7078)\n- [the prometheus metrics API is tool slow](https://github.com/apache/apisix/issues/7353)\n- [Support configuring `MaxConcurrentStreams` for http2](https://github.com/etcd-io/etcd/pull/14169)\n\n另外一种解决办法是改用实验性的基于 gRPC 的配置同步。需要在配置文件 `conf/config.yaml` 中设置 `use_grpc: true`：\n\n```yaml\n  etcd:\n    use_grpc: true\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n```\n\n## 为什么 file-logger 记录日志会出现乱码？\n\n如果你使用的是 `file-logger` 插件，但是在日志文件中出现了乱码，那么可能是因为上游服务的响应体被进行了压缩。你可以将请求头带上不接收压缩响应参数（`gzip;q=0,deflate,sdch`）以解决这个问题，你可以使用 [proxy-rewirte](https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite/) 插件将请求头中的 `accept-encoding` 设置为不接收压缩响应：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H 'X-API-KEY: YOUR-TOKEN' -X PUT -d '\n{\n    \"methods\":[\n        \"GET\"\n    ],\n    \"uri\":\"/test/index.html\",\n    \"plugins\":{\n        \"proxy-rewrite\":{\n            \"headers\":{\n                \"set\":{\n                    \"accept-encoding\":\"gzip;q=0,deflate,sdch\"\n                }\n            }\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:80\":1\n        }\n    }\n}'\n```\n\n## APISIX 如何配置带认证的 ETCD？\n\n假设您有一个启用身份验证的 ETCD 集群。要访问该集群，需要在 `conf/config.yaml` 中为 Apache APISIX 配置正确的用户名和密码：\n\n```yaml\ndeployment:\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    user: etcd_user             # username for etcd\n    password: etcd_password     # password for etcd\n```\n\n关于 ETCD 的其他配置，比如过期时间、重试次数等等，你可以参考 `conf/config.yaml.example` 文件中的 `etcd` 部分。\n\n## SSLs 对象与 `upstream` 对象中的 `tls.client_cert` 以及 `config.yaml` 中的 `ssl_trusted_certificate` 区别是什么？\n\nAdmin API 中 `/apisix/admin/ssls` 用于管理 SSL 对象，如果 APISIX 需要接收来自外网的 HTTPS 请求，那就需要用到存放在这里的证书完成握手。SSL 对象中支持配置多个证书，不同域名的证书 APISIX 将使用 Server Name Indication (SNI) 进行区分。\n\nUpstream 对象中的 `tls.client_cert`、`tls.client_key` 与 `tls.client_cert_id` 用于存放客户端的证书，适用于需要与上游进行 [mTLS 通信](https://apisix.apache.org/zh/docs/apisix/tutorials/client-to-apisix-mtls/)的情况。\n\n`config.yaml` 中的 `ssl_trusted_certificate` 用于配置一个受信任的根证书。它仅用于在 APISIX 内部访问某些具有自签名证书的服务时，避免提示拒绝对方的 SSL 证书。注意：它不用于信任 APISIX 上游的证书，因为 APISIX 不会验证上游证书的合法性。因此，即使上游使用了无效的 TLS 证书，APISIX 仍然可以与其通信，而无需配置根证书。\n\n## 如果在使用 APISIX 过程中遇到问题，我可以在哪里寻求更多帮助？\n\n- [Apache APISIX Slack Channel](/docs/general/join/#加入-slack-频道)：加入后请选择 channel-apisix 频道，即可通过此频道进行 APISIX 相关问题的提问。\n- [邮件列表](/docs/general/join/#订阅邮件列表)：任何问题或对项目提议都可以通过社区邮件进行讨论。\n- [GitHub Issues](https://github.com/apache/apisix/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 与 [GitHub Discussions](https://github.com/apache/apisix/discussions)：也可直接在 GitHub 中进行相关 issue 创建进行问题的表述。\n"
  },
  {
    "path": "docs/zh/latest/README.md",
    "content": "---\ntitle: Apache APISIX\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<img src=\"https://svn.apache.org/repos/asf/comdev/project-logos/originals/apisix.svg\" alt=\"APISIX logo\" height=\"150px\" align=\"right\" />\n\n[![Build Status](https://github.com/apache/apisix/workflows/build/badge.svg?branch=master)](https://github.com/apache/apisix/actions)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/apache/apisix/blob/master/LICENSE)\n\n**Apache APISIX** 是一个动态、实时、高性能的 API 网关，\n提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。\n\n你可以使用 Apache APISIX 来处理传统的南北向流量，以及服务间的东西向流量，\n也可以当做 [k8s ingress controller](https://github.com/apache/apisix-ingress-controller) 来使用。\n\nApache APISIX 的技术架构如下图所示：\n\n![Apache APISIX 的技术架构](../../assets/images/apisix.png)\n\n## 社区\n\n- 邮件列表 - 发送任意内容到 dev-subscribe@apisix.apache.org 后，根据回复以订阅邮件列表。\n- QQ 群 - 781365357\n- Slack - [查看加入方式](https://apisix.apache.org/zh/docs/general/join/#join-the-slack-channel)\n- ![Twitter Follow](https://img.shields.io/twitter/follow/ApacheAPISIX?style=social) - 使用标签 `#ApacheAPISIX` 关注我们并与我们互动。\n- [哔哩哔哩](https://space.bilibili.com/551921247)\n- **新手任务列表**\n    - [Apache APISIX®](https://github.com/apache/apisix/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n    - [Apache APISIX® Ingress Controller](https://github.com/apache/apisix-ingress-controller/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n    - [Apache APISIX® dashboard](https://github.com/apache/apisix-dashboard/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n    - [Apache APISIX® Helm Chart](https://github.com/apache/apisix-helm-chart/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n    - [Docker distribution for Apache APISIX®](https://github.com/apache/apisix-docker/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n    - [Apache APISIX® Website](https://github.com/apache/apisix-website/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n    - [Apache APISIX® Java Plugin Runner](https://github.com/apache/apisix-java-plugin-runner/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)\n    - [Apache APISIX® Go Plugin Runner](https://github.com/apache/apisix-go-plugin-runner/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)\n    - [Apache APISIX® Python Plugin Runner](https://github.com/apache/apisix-python-plugin-runner/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)\n- **微信公众号**\n  <br/>![wechat official account](../../assets/images/OA.jpg)\n- **微信视频号**\n  <br/>![wechat video account](../../assets/images/MA.jpeg)\n\n## 特性\n\n你可以把 Apache APISIX 当做流量入口，来处理所有的业务数据，包括动态路由、动态上游、动态证书、\nA/B 测试、金丝雀发布（灰度发布）、蓝绿部署、限流限速、抵御恶意攻击、监控报警、服务可观测性、服务治理等。\n\n- **全平台**\n\n    - 云原生：平台无关，没有供应商锁定，无论裸机还是 Kubernetes，APISIX 都可以运行。\n    - 支持 ARM64：不用担心底层技术的锁定。\n\n- **多协议**\n\n    - [TCP/UDP 代理](stream-proxy.md)：动态 TCP/UDP 代理。\n    - [Dubbo 代理](plugins/dubbo-proxy.md)：动态代理 HTTP 请求到 Dubbo 后端。\n    - [动态 MQTT 代理](plugins/mqtt-proxy.md)：支持用 `client_id` 对 MQTT 进行负载均衡，同时支持 MQTT [3.1.\\*](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) 和 [5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html) 两个协议标准。\n    - [gRPC 代理](grpc-proxy.md)：通过 APISIX 代理 gRPC 连接，并使用 APISIX 的大部分特性管理你的 gRPC 服务。\n    - [gRPC Web 代理](plugins/grpc-web.md)：通过 APISIX 代理 gRPC Web 请求到上游 gRPC 服务。\n    - [gRPC 协议转换](plugins/grpc-transcode.md)：支持协议的转换，这样客户端可以通过 HTTP/JSON 来访问你的 gRPC API。\n    - Websocket 代理\n    - Proxy Protocol\n    - HTTP(S) 反向代理\n    - [SSL](certificate.md)：动态加载 SSL 证书。\n\n- **全动态能力**\n\n    - [热更新和热插件](terminology/plugin.md)：无需重启服务，就可以持续更新配置和插件。\n    - [代理请求重写](plugins/proxy-rewrite.md)：支持重写请求上游的`host`、`uri`、`schema`、`method`、`headers`信息。\n    - [输出内容重写](plugins/response-rewrite.md)：支持自定义修改返回内容的 `status code`、`body`、`headers`。\n    - [Serverless](plugins/serverless.md)：在 APISIX 的每一个阶段，你都可以添加并调用自己编写的函数。\n    - 动态负载均衡：动态支持有权重的 round-robin 负载平衡。\n    - 支持一致性 hash 的负载均衡：动态支持一致性 hash 的负载均衡。\n    - [健康检查](./tutorials/health-check.md)：启用上游节点的健康检查，将在负载均衡期间自动过滤不健康的节点，以确保系统稳定性。\n    - 熔断器：智能跟踪不健康上游服务。\n    - [代理镜像](plugins/proxy-mirror.md)：提供镜像客户端请求的能力。\n    - [流量拆分](plugins/traffic-split.md)：允许用户逐步控制各个上游之间的流量百分比。\n\n- **精细化路由**\n\n    - [支持全路径匹配和前缀匹配](../../en/latest/router-radixtree.md#how-to-use-libradixtree-in-apisix)\n    - [支持使用 Nginx 所有内置变量做为路由的条件](../../en/latest/router-radixtree.md#how-to-filter-route-by-nginx-builtin-variable)，所以你可以使用 `cookie`, `args` 等做为路由的条件，来实现灰度发布、A/B 测试等功能\n    - 支持[各类操作符做为路由的判断条件](https://github.com/api7/lua-resty-radixtree#operator-list)，比如 `{\"arg_age\", \">\", 24}`\n    - 支持[自定义路由匹配函数](https://github.com/api7/lua-resty-radixtree/blob/master/t/filter-fun.t#L10)\n    - IPv6：支持使用 IPv6 格式匹配路由\n    - 支持路由的[自动过期 (TTL)](admin-api.md#route)\n    - [支持路由的优先级](../../en/latest/router-radixtree.md#3-match-priority)\n    - [支持批量 Http 请求](plugins/batch-requests.md)\n    - [支持通过 GraphQL 属性过滤路由](../../en/latest/router-radixtree.md#how-to-filter-route-by-graphql-attributes)\n\n- **安全防护**\n\n    - 丰富的认证、鉴权支持：\n        * [key-auth](plugins/key-auth.md)\n        * [JWT](plugins/jwt-auth.md)\n        * [basic-auth](plugins/basic-auth.md)\n        * [wolf-rbac](plugins/wolf-rbac.md)\n        * [casbin](plugins/authz-casbin.md)\n        * [keycloak](plugins/authz-keycloak.md)\n        * [casdoor](../../en/latest/plugins/authz-casdoor.md)\n    - [IP 黑白名单](plugins/ip-restriction.md)\n    - [Referer 黑白名单](plugins/referer-restriction.md)\n    - [IdP 支持](plugins/openid-connect.md)：支持外部的身份认证平台，比如 Auth0，Okta，Authing 等。\n    - [限制速率](plugins/limit-req.md)\n    - [限制请求数](plugins/limit-count.md)\n    - [限制并发](plugins/limit-conn.md)\n    - 防御 ReDoS(正则表达式拒绝服务)：内置策略，无需配置即可抵御 ReDoS。\n    - [CORS](plugins/cors.md)：为你的 API 启用 CORS。\n    - [URI 拦截器](plugins/uri-blocker.md)：根据 URI 拦截用户请求。\n    - [请求验证器](plugins/request-validation.md)。\n    - [CSRF](plugins/csrf.md)：基于 [`Double Submit Cookie`](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Double_Submit_Cookie) 的方式保护你的 API 远离 CSRF 攻击。\n\n- **运维友好**\n\n    - OpenTracing 可观测性：支持 [Apache Skywalking](plugins/skywalking.md) 和 [Zipkin](plugins/zipkin.md)。\n    - 对接外部服务发现：除了内置的 etcd 外，还支持 [Consul](../../en/latest/discovery/consul_kv.md)、[Nacos](discovery/nacos.md)、[Eureka](discovery/eureka.md) 和 [Zookeeper（CP）](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md)。\n    - 监控和指标：[Prometheus](plugins/prometheus.md)\n    - 集群：APISIX 节点是无状态的，创建配置中心集群请参考 [etcd Clustering Guide](https://etcd.io/docs/v3.5/op-guide/clustering/)。\n    - 高可用：支持配置同一个集群内的多个 etcd 地址。\n    - [控制台](https://github.com/apache/apisix-dashboard): 操作 APISIX 集群。\n    - 版本控制：支持操作的多次回滚。\n    - CLI：使用命令行来启动、关闭和重启 APISIX。\n    - [单机模式](../../en/latest/deployment-modes.md#standalone)：支持从本地配置文件中加载路由规则，在 kubernetes(k8s) 等环境下更友好。\n    - [全局规则](terminology/global-rule.md)：允许对所有请求执行插件，比如黑白名单、限流限速等。\n    - 高性能：在单核上 QPS 可以达到 18k，同时延迟只有 0.2 毫秒。\n    - [故障注入](plugins/fault-injection.md)\n    - [REST Admin API](admin-api.md)：使用 REST Admin API 来控制 Apache APISIX，默认只允许 127.0.0.1 访问，你可以修改 `conf/config.yaml` 中的 `allow_admin` 字段，指定允许调用 Admin API 的 IP 列表。同时需要注意的是，Admin API 使用 key auth 来校验调用者身份，**在部署前需要修改 `conf/config.yaml` 中的 `admin_key` 字段，来保证安全。**\n    - 外部日志记录器：将访问日志导出到外部日志管理工具。（[HTTP Logger](plugins/http-logger.md)、[TCP Logger](plugins/tcp-logger.md)、[Kafka Logger](plugins/kafka-logger.md)、[UDP Logger](plugins/udp-logger.md)、[RocketMQ Logger](plugins/rocketmq-logger.md)、[SkyWalking Logger](plugins/skywalking-logger.md)、[Alibaba Cloud Logging(SLS)](plugins/sls-logger.md)、[Google Cloud Logging](plugins/google-cloud-logging.md)、[Splunk HEC Logging](plugins/splunk-hec-logging.md)、[File Logger](plugins/file-logger.md)、[Elasticsearch Logger](plugins/elasticsearch-logger.md)、[TencentCloud CLS](plugins/tencent-cloud-cls.md)）\n    - [Helm charts](https://github.com/apache/apisix-helm-chart)\n\n- **高度可扩展**\n    - [自定义插件](plugin-develop.md)：允许挂载常见阶段，例如`init`，`rewrite`，`access`，`balancer`，`header filter`，`body filter` 和 `log` 阶段。\n    - [插件可以用 Java/Go/Python 编写](../../zh/latest/external-plugin.md)\n    - 自定义负载均衡算法：可以在 `balancer` 阶段使用自定义负载均衡算法。\n    - 自定义路由：支持用户自己实现路由算法。\n\n- **多语言支持**\n- Apache APISIX 是一个通过 `RPC` 和 `Wasm` 支持不同语言来进行插件开发的网关。\n  ![Multi Language Support into Apache APISIX](../../../docs/assets/images/external-plugin.png)\n    - RPC 是当前采用的开发方式。开发者可以使用他们需要的语言来进行 RPC 服务的开发，该 RPC 通过本地通讯来跟 APISIX 进行数据交换。到目前为止，APISIX 已支持[Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) 和 Node.js。\n    - Wasm 或 WebAssembly 是实验性的开发方式。APISIX 能加载运行使用[Proxy Wasm SDK](https://github.com/proxy-wasm/spec#sdks)编译的 Wasm 字节码。开发者仅需要使用该 SDK 编写代码，然后编译成 Wasm 字节码，即可运行在 APISIX 中的 Wasm 虚拟机中。\n\n- **Serverless**\n    - [Lua functions](plugins/serverless.md)：能在 APISIX 每个阶段调用 lua 函数。\n    - [Azure functions](./plugins/azure-functions.md)：能无缝整合进 Azure Serverless Function 中。作为动态上游，能将特定的 URI 请求全部代理到微软 Azure 云中。\n    - [Apache OpenWhisk](./plugins/openwhisk.md)：与 Apache OpenWhisk 集成。作为动态上游，能将特定的 URI 请求代理到你自己的 OpenWhisk 集群。\n\n## 立刻开始\n\n1. 安装\n\n   请参考[APISIX 安装指南](https://apisix.apache.org/zh/docs/apisix/installation-guide/)。\n\n2. 入门指南\n\n   入门指南是学习 APISIX 基础知识的好方法。按照 [入门指南](https://apisix.apache.org/zh/docs/apisix/getting-started/)的步骤即可。\n\n   更进一步，你可以跟着文档来尝试更多的[插件](plugins)。\n\n3. Admin API\n\n   Apache APISIX 提供了 [REST Admin API](admin-api.md)，方便动态控制 Apache APISIX 集群。\n\n4. 插件二次开发\n\n   可以参考[插件开发指南](plugin-develop.md)，以及示例插件 `example-plugin` 的代码实现。\n   阅读[插件概念](terminology/plugin.md) 会帮助你学到更多关于插件的知识。\n\n更多文档请参考 [Apache APISIX 文档站](https://apisix.apache.org/zh/docs/apisix/getting-started/)。\n\n## 性能测试\n\n使用 AWS 的 8 核心服务器来压测 APISIX，QPS 可以达到 140000，同时延时只有 0.2 毫秒。\n\n[性能测试脚本](https://github.com/apache/apisix/blob/master/benchmark/run.sh) 已经开源，欢迎补充。\n\n## 贡献者变化\n\n> [访问此处](https://www.apiseven.com/contributor-graph) 使用贡献者数据服务。\n\n[![贡献者变化](https://contributor-graph-api.apiseven.com/contributors-svg?repo=apache/apisix)](https://www.apiseven.com/en/contributor-graph?repo=apache/apisix)\n\n## 视频和文章\n\n- 2020.10.16 [Apache APISIX: How to implement plugin orchestration in API Gateway](https://www.youtube.com/watch?v=iEegNXOtEhQ)\n- 2020.10.16 [Improve Apache APISIX observability with Apache Skywalking](https://www.youtube.com/watch?v=DleVJwPs4i4)\n- 2020.1.17 [API 网关 Apache APISIX 和 Kong 的选型对比](https://mp.weixin.qq.com/s/c51apneVj0O9yxiZAHF34Q)\n- 2019.12.14 [从 0 到 1：Apache APISIX 的 Apache 之路](https://zhuanlan.zhihu.com/p/99620158)\n- 2019.12.14 [基于 Apache APISIX 的下一代微服务架构](https://www.upyun.com/opentalk/445.html)\n- 2019.10.30 [Apache APISIX 微服务架构极致性能架构解析](https://www.upyun.com/opentalk/440.html)\n- 2019.9.27 [想把 APISIX 运行在 ARM64 平台上？只要三步](https://zhuanlan.zhihu.com/p/84467919)\n- 2019.8.31 [APISIX 技术选型、测试和持续集成](https://www.upyun.com/opentalk/433.html)\n- 2019.8.31 [APISIX 高性能实战 2](https://www.upyun.com/opentalk/437.html)\n- 2019.7.6 [APISIX 高性能实战](https://www.upyun.com/opentalk/429.html)\n\n## 用户实际使用案例\n\n- [新浪微博：基于 Apache APISIX，新浪微博 API 网关的定制化开发之路](https://apisix.apache.org/zh/blog/2021/07/06/the-road-to-customization-of-sina-weibo-api-gateway-based-on-apache-apisix/)\n- [欧盟数字工厂平台：API Security Gateway – Using APISIX in the eFactory Platform](https://www.efactory-project.eu/post/api-security-gateway-using-apisix-in-the-efactory-platform)\n- [贝壳找房：如何基于 Apache APISIX 搭建网关](https://mp.weixin.qq.com/s/yZl9MWPyF1-gOyCp8plflA)\n- [360：Apache APISIX 在基础运维平台项目中的实践](https://mp.weixin.qq.com/s/mF8w8hW4alIMww0MSu9Sjg)\n- [HelloTalk：基于 OpenResty 和 Apache APISIX 的全球化探索之路](https://www.upyun.com/opentalk/447.html)\n- [腾讯云：为什么选择 Apache APISIX 来实现 k8s ingress controller?](https://www.upyun.com/opentalk/448.html)\n- [思必驰：为什么我们重新写了一个 k8s ingress controller?](https://mp.weixin.qq.com/s/bmm2ibk2V7-XYneLo9XAPQ)\n\n更多用户案例，请查看 [Case Studies](https://apisix.apache.org/zh/blog/tags/case-studies/)。\n\n## APISIX 的用户有哪些？\n\n有很多公司和组织把 APISIX 用于学习、研究、生产环境和商业产品中，包括：\n\n<img src=\"https://user-images.githubusercontent.com/40708551/109484046-f7c4e280-7aa5-11eb-9d71-aab90830773a.png\" width=\"725\" height=\"1700\" />\n\n欢迎用户把自己加入到 [Powered By](../../../powered-by.md) 页面。\n\n## 全景图\n\n<p align=\"left\">\n<img src=\"https://landscape.cncf.io/images/left-logo.svg\" width=\"150\" />&nbsp;&nbsp;<img src=\"https://landscape.cncf.io/images/right-logo.svg\" width=\"200\" />\n<br /><br />\nAPISIX 被纳入 <a href=\"https://landscape.cncf.io/card-mode?category=api-gateway&grouping=category\"> 云原生软件基金会 API 网关全景图</a>\n</p>\n\n## Logo\n\n- [Apache APISIX logo(PNG)](../../../logos/apache-apisix.png)\n- [Apache APISIX logo 源文件](https://apache.org/logos/#apisix)\n\n## 贡献\n\n我们欢迎来自开源社区、个人和合作伙伴的各种贡献。\n\n- [贡献指南](../../../CONTRIBUTING.md)\n\n## 致谢\n\n灵感来自 Kong 和 Orange。\n\n## 协议\n\n[Apache 2.0 License](../../../LICENSE)\n"
  },
  {
    "path": "docs/zh/latest/admin-api.md",
    "content": "---\ntitle: Admin API\nkeywords:\n  - APISIX\n  - API 网关\n  - Admin API\n  - 路由\n  - 插件\n  - 上游\ndescription: 本文介绍了 Apache APISIX Admin API 支持的功能，你可以通过 Admin API 来获取、创建、更新以及删除资源。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述 {#description}\n\nAdmin API 是一组用于配置 Apache APISIX 路由、上游、服务、SSL 证书等功能的 RESTful API。\n\n你可以通过 Admin API 来获取、创建、更新以及删除资源。同时得益于 APISIX 的热加载能力，资源配置完成后 APISIX 将会自动更新配置，无需重启服务。如果你想要了解其工作原理，请参考 [Architecture Design](./architecture-design/apisix.md)。\n\n## 相关配置 {#basic-configuration}\n\n当 APISIX 启动时，Admin API 默认情况下将会监听 `9180` 端口，并且会占用前缀为 `/apisix/admin` 的 API。\n\n因此，为了避免你设计的 API 与 `/apisix/admin` 冲突，你可以通过修改配置文件 [`/conf/config.yaml`](https://github.com/apache/apisix/blob/master/conf/config.yaml) 中的配置修改默认监听端口。\n\nAPISIX 支持设置 Admin API 的 IP 访问白名单，防止 APISIX 被非法访问和攻击。你可以在 `./conf/config.yaml` 文件中的 `deployment.admin.allow_admin` 选项中，配置允许访问的 IP 地址。\n\n在下文出现的 `X-API-KEY` 指的是 `./conf/config.yaml` 文件中的 `deployment.admin.admin_key.key`，它是 Admin API 的访问 token。\n\n:::tip 提示\n\n建议你修改 Admin API 默认的监听端口、IP 访问白名单以及 Admin API 的 token，以保证你的 API 安全。\n\n:::\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n    admin:\n        admin_key:\n        - name: admin\n            key: edd1c9f034335f136f87ad84b625c8f1  # 使用默认的 Admin API Key 存在安全风险，部署到生产环境时请及时更新\n            role: admin\n        allow_admin:                    # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow\n            - 127.0.0.0/24\n        admin_listen:\n            ip: 0.0.0.0                 # Admin API 监听的 IP，如果不设置，默认为“0.0.0.0”。\n            port: 9180                  # Admin API 监听的 端口，必须使用与 node_listen 不同的端口。\n```\n\n### 使用环境变量 {#using-environment-variables}\n\n要通过环境变量进行配置，可以使用 `${{VAR}}` 语法。例如：\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n    - name: admin\n      key: ${{ADMIN_KEY}}\n      role: admin\n    allow_admin:\n    - 127.0.0.0/24\n    admin_listen:\n      ip: 0.0.0.0\n      port: 9180\n```\n\n然后在 `make init` 之前运行 `export ADMIN_KEY=$your_admin_key`.\n\n如果找不到配置的环境变量，将抛出错误。\n\n此外，如果要在未设置环境变量时使用默认值，请改用 `${{VAR:=default_value}}`。例如：\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n    - name: admin\n      key: ${{ADMIN_KEY:=edd1c9f034335f136f87ad84b625c8f1}}\n      role: admin\n    allow_admin:\n    - 127.0.0.0/24\n    admin_listen:\n      ip: 0.0.0.0\n      port: 9180\n```\n\n首先查找环境变量 `ADMIN_KEY`，如果该环境变量不存在，它将使用 `edd1c9f034335f136f87ad84b625c8f1` 作为默认值。\n\n您还可以在 yaml 键中指定环境变量。这在 `standalone` 模式 中特别有用，您可以在其中指定上游节点，如下所示：\n\n```yaml title=\"./conf/apisix.yaml\"\nroutes:\n  -\n    uri: \"/test\"\n    upstream:\n      nodes:\n        \"${{HOST_IP}}:${{PORT}}\": 1\n      type: roundrobin\n#END\n```\n\n### 强制删除 {#force-delete}\n\n默认情况下，Admin API 会检查资源间的引用关系，将会拒绝删除正在使用中的资源。\n\n可以通过在删除请求中添加请求参数 `force=true` 来进行强制删除，例如：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```bash\n$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '{\n    \"nodes\": {\n        \"127.0.0.1:8080\": 1\n    },\n    \"type\": \"roundrobin\"\n}'\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '{\n    \"uri\": \"/*\",\n    \"upstream_id\": 1\n}'\n{\"value\":{\"priority\":0,\"upstream_id\":1,\"uri\":\"/*\",\"create_time\":1689038794,\"id\":\"1\",\"status\":1,\"update_time\":1689038916},\"key\":\"/apisix/routes/1\"}\n\n$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H \"X-API-KEY: $admin_key\" -X DELETE\n{\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n$ curl \"http://127.0.0.1:9180/apisix/admin/upstreams/1?force=anyvalue\" -H \"X-API-KEY: $admin_key\" -X DELETE\n{\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n$ curl \"http://127.0.0.1:9180/apisix/admin/upstreams/1?force=true\" -H \"X-API-KEY: $admin_key\" -X DELETE\n{\"deleted\":\"1\",\"key\":\"/apisix/upstreams/1\"}\n```\n\n## v3 版本新功能 {#v3-new-function}\n\n在 APISIX v3 版本中，Admin API 支持了一些不向下兼容的新特性，比如支持新的响应体格式、支持分页查询、支持过滤资源等。\n\n### 支持新的响应体格式 {#support-new-response-body-format}\n\nAPISIX 在 v3 版本对响应体做了以下调整：\n\n- 移除旧版本响应体中的 `action` 字段；\n- 调整获取资源列表时的响应体结构，新的响应体结构示例如下：\n\n返回单个资源：\n\n```json\n    {\n    \"modifiedIndex\": 2685183,\n    \"value\": {\n        \"id\": \"1\",\n        ...\n    },\n    \"key\": \"/apisix/routes/1\",\n    \"createdIndex\": 2684956\n    }\n```\n\n返回多个资源：\n\n```json\n    {\n    \"list\": [\n        {\n        \"modifiedIndex\": 2685183,\n        \"value\": {\n            \"id\": \"1\",\n            ...\n        },\n        \"key\": \"/apisix/routes/1\",\n        \"createdIndex\": 2684956\n        },\n        {\n        \"modifiedIndex\": 2685163,\n        \"value\": {\n            \"id\": \"2\",\n            ...\n        },\n        \"key\": \"/apisix/routes/2\",\n        \"createdIndex\": 2685163\n        }\n    ],\n    \"total\": 2\n    }\n```\n\n### 支持分页查询 {#support-paging-query}\n\n获取资源列表时支持分页查询，目前支持分页查询的资源如下：\n\n- [Consumer](#consumer)\n- [Consumer Group](#consumer-group)\n- [Global Rules](#global-rules)\n- [Plugin Config](#plugin-config)\n- [Protos](https://apisix.apache.org/zh/docs/apisix/plugins/grpc-transcode/#%E5%90%AF%E7%94%A8%E6%8F%92%E4%BB%B6)\n- [Route](#route)\n- [Service](#service)\n- [SSL](#ssl)\n- [Stream Route](#stream-route)\n- [Upstream](#upstream)\n\n参数如下：\n\n| 名称       | 默认值 | 范围     | 描述                                                |\n| --------- | ------ | -------- | -------------------------------------------------- |\n| page      | 1      | [1, ...] | 页数，默认展示第一页。                               |\n| page_size |        | [10, 500]| 每页资源数量。如果不配置该参数，则展示所有查询到的资源。|\n\n示例如下：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes?page=1&page_size=10\" \\\n-H \"X-API-KEY: $admin_key\" -X GET\n```\n\n```json\n{\n  \"total\": 1,\n  \"list\": [\n    {\n      ...\n    }\n  ]\n}\n```\n\n### 支持过滤资源 {#support-filtering-query}\n\n在 APISIX v3 版本中，在获取资源列表时，你可以使用 `name`、`label` 和 `uri` 参数过滤资源。支持参数如下：\n\n| 名称   | 描述                                                                                                                      |\n| ----- | ------------------------------------------------------------------------------------------------------------------------ |\n| name  | 根据资源的 `name` 属性进行查询，如果资源本身没有 `name` 属性则不会出现在查询结果中。                                           |\n| label | 根据资源的 `label` 属性进行查询，如果资源本身没有 `label` 属性则不会出现在查询结果中。                                         |\n| uri   | 该参数仅在 Route 资源上支持。如果 Route 的 `uri` 等于查询的 `uri` 或 `uris` 包含查询的 `uri`，则该 Route 资源出现在查询结果中。 |\n\n:::tip 提示\n\n当使用多个过滤参数时，APISIX 将对不同过滤参数的查询结果取交集。\n\n:::\n\n以下示例将返回一个路由列表，该路由列表中的所有路由需要满足以下条件：路由的 `name` 包含字符串 `test`；`uri` 包含字符串 `foo`；对路由的 `label` 没有限制，因为 `label` 为空字符串。\n\n```shell\ncurl 'http://127.0.0.1:9180/apisix/admin/routes?name=test&uri=foo&label=' \\\n-H \"X-API-KEY: $admin_key\" -X GET\n```\n\n返回结果：\n\n```json\n{\n  \"total\": 1,\n  \"list\": [\n    {\n      ...\n    }\n  ]\n}\n```\n\n### 支持引用过滤资源 {#support-reference-filtering-query}\n\n:::note\n\n这个特性于 APISIX 3.13.0 引入。\n\nAPISIX 支持通过 `service_id` 和 `upstream_id` 查询路由和 Stream 路由。现在不支持其他资源或字段。\n\n:::\n\n在获取资源列表时，你可以使用 `filter` 参数过滤资源。\n\n它以以下方式编码：\n\n```text\nfilter=escape_uri(key1=value1&key2=value2)\n```\n\n以下是一个使用 `service_id` 进行路由列表过滤的例子。当同时设置了多个过滤条件，结果将为它们的交集。\n\n```shell\ncurl 'http://127.0.0.1:9180/apisix/admin/routes?filter=service_id%3D1' \\\n-H \"X-API-KEY: $admin_key\" -X GET\n```\n\n```json\n{\n  \"total\": 1,\n  \"list\": [\n    {\n      ...\n    }\n  ]\n}\n```\n\n## Route\n\nRoute 也称之为路由，可以通过定义一些规则来匹配客户端的请求，然后根据匹配结果加载并执行相应的插件，并把请求转发给到指定 Upstream（上游）。\n\n### 请求地址 {#route-uri}\n\n路由资源请求地址：/apisix/admin/routes/{id}?ttl=0\n\n### 请求方法 {#route-request-methods}\n\n| 名称   | 请求 URI                          | 请求 body  | 描述                                                                                                 |\n| ------ | -------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/routes             | 无        | 获取资源列表。                                                                                                                              |\n| GET    | /apisix/admin/routes/{id}        | 无        | 获取资源。                                                                                                                                         |\n| PUT    | /apisix/admin/routes/{id}        | {...}     | 根据 id 创建资源。                                                                                                                        |\n| POST   | /apisix/admin/routes             | {...}     | 创建资源，id 将会自动生成。                                                                                                                      |\n| DELETE | /apisix/admin/routes/{id}        | 无        | 删除指定资源。                                                                                                                                                |\n| PATCH  | /apisix/admin/routes/{id}        | {...}     | 标准 PATCH，修改指定 Route 的部分属性，其他不涉及的属性会原样保留；如果你需要删除某个属性，可以将该属性的值设置为 `null`；当需要修改属性的值为数组时，该属性将全量更新。 |\n| PATCH  | /apisix/admin/routes/{id}/{path} | {...}     | SubPath PATCH，通过 `{path}` 指定 Route 要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。两种 PATCH 的区别，请参考使用示例。                         |\n\n### URI 请求参数 {#route-uri-request-parameters}\n\n| 名称 | 必选项  | 类型  | 描述                                         | 示例  |\n| ---- | ------ | ---- | -------------------------------------------- | ----- |\n| ttl  | 否     | 辅助 | 路由的有效期。超过定义的时间，APISIX 将会自动删除路由，单位为秒。  | ttl=1 |\n\n### body 请求参数 {#route-request-body-parameters}\n\n| 名称             | 必选项                            | 类型     | 描述                                                                                                                                                                                                                                                                                    | 示例值                                                 |\n| ---------------- | -------------------------------- | -------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ---------------------------------------------------- |\n| uri              | 是，与 `uris` 二选一。       | 匹配规则 | 除了如 `/foo/bar`、`/foo/gloo` 这种全量匹配外，使用不同 [Router](terminology/router.md) 还允许更高级匹配，更多信息请参考 [Router](terminology/router.md)。                                                                                                                                                 | \"/hello\"                                             |\n| uris             | 是，不能与 `uri` 二选一。        | 匹配规则 | 非空数组形式，可以匹配多个 `uri`。                                                                                                                                                                                                                                                               | [\"/hello\", \"/world\"]                                 |\n| plugins          | 否                               | Plugin   | Plugin 配置，请参考 [Plugin](terminology/plugin.md)。                                                                                                                                                                                                                                            |                                                      |\n| script           | 否                               | Script   | Script 配置，请参考 [Script](terminology/script.md)。                                                                                                                                                                                                                                            |                                                      |\n| upstream         | 否                               | Upstream | Upstream 配置，请参考 [Upstream](terminology/upstream.md)。                                                                                                                                                                                                                              |                                                      |\n| upstream_id      | 否                               | Upstream | 需要使用的 Upstream id，请参考 [Upstream](terminology/upstream.md)。                                                                                                                                                                                                                     |                                                      |\n| service_id       | 否                               | Service  | 需要绑定的 Service id，请参考 [Service](terminology/service.md)。                                                                                                                                                                                                                        |                                                      |\n| plugin_config_id | 否，不能与 Script 共同使用。      | Plugin   | 需要绑定的 Plugin Config id，请参考 [Plugin Config](terminology/plugin-config.md)。                                                                                                                                                                                                           |                                                      |\n| name             | 否                               | 辅助     | 路由名称。                                                                                                                                                                                                                                                                                | route-test                                          |\n| desc             | 否                               | 辅助     | 路由描述信息。                                                                                                                                                                                                                                                                     | 用来测试的路由。                                            |\n| host             | 否，与 `hosts` 二选一。      | 匹配规则 | 当前请求域名，比如 `foo.com`；也支持泛域名，比如 `*.foo.com`。                                                                                                                                                                                                                                            | \"foo.com\"                                            |\n| hosts            | 否，与 `host` 二选一。       | 匹配规则 | 非空列表形态的 `host`，表示允许有多个不同 `host`，匹配其中任意一个即可。                                                                                                                                                                                                                                           | [\"foo.com\", \"\\*.bar.com\"]                            |\n| remote_addr      | 否，与 `remote_addrs` 二选一。| 匹配规则 | 客户端请求的 IP 地址。支持 IPv4 地址，如：`192.168.1.101` 以及 CIDR 格式的支持 `192.168.1.0/24`；支持 IPv6 地址匹配，如 `::1`，`fe80::1`，`fe80::1/64` 等。                                                                                                                                                 | \"192.168.1.0/24\"                                     |\n| remote_addrs     | 否，与 `remote_addr` 二选一。| 匹配规则 | 非空列表形态的 `remote_addr`，表示允许有多个不同 IP 地址，符合其中任意一个即可。                                                                                                                                                                                                                                     | [\"127.0.0.1\", \"192.0.0.0/8\", \"::1\"]                  |\n| methods          | 否                               | 匹配规则 | 如果为空或没有该选项，则表示没有任何 `method` 限制。你也可以配置一个或多个的组合：`GET`，`POST`，`PUT`，`DELETE`，`PATCH`，`HEAD`，`OPTIONS`，`CONNECT`，`TRACE`，`PURGE`。                                                                                                                                                                    | [\"GET\", \"POST\"]                                      |\n| priority         | 否                               | 匹配规则 | 如果不同路由包含相同的 `uri`，则根据属性 `priority` 确定哪个 `route` 被优先匹配，值越大优先级越高，默认值为 `0`。                                                                                                                                                                                                                  | priority = 10                                        |\n| vars             | 否                               | 匹配规则 | 由一个或多个`[var, operator, val]`元素组成的列表，类似 `[[var, operator, val], [var, operator, val], ...]]`。例如：`[\"arg_name\", \"==\", \"json\"]` 则表示当前请求参数 `name` 是 `json`。此处 `var` 与 NGINX 内部自身变量命名是保持一致的，所以也可以使用 `request_uri`、`host` 等。更多细节请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 | [[\"arg_name\", \"==\", \"json\"], [\"arg_age\", \">\", 18]]   |\n| filter_func      | 否                               | 匹配规则 | 用户自定义的过滤函数。可以使用它来实现特殊场景的匹配要求实现。该函数默认接受一个名为 `vars` 的输入参数，可以用它来获取 NGINX 变量。                                                                                                                                                                                                               | function(vars) return vars[\"arg_name\"] == \"json\" end |\n| labels           | 否                               | 匹配规则 | 标识附加属性的键值对。                                                                                                                                                                                                                                                                            | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"}     |\n| timeout          | 否                               | 辅助     | 为 Route 设置 Upstream 连接、发送消息和接收消息的超时时间（单位为秒）。该配置将会覆盖在 Upstream 中配置的 [timeout](#upstream) 选项。                                                                                                                                                                                               | {\"connect\": 3, \"send\": 3, \"read\": 3}              |\n| enable_websocket | 否                               | 辅助     | 当设置为 `true` 时，启用 `websocket`(boolean), 默认值为 `false`。                                                                                                                                                                                                                                                |                                                      |\n| status           | 否                               | 辅助     | 当设置为 `1` 时，启用该路由，默认值为 `1`。                                                                                                                                                                                                                                                                       | `1` 表示启用，`0` 表示禁用。                           |\n\n:::note 注意\n\n- 对于同一类参数比如 `uri`与 `uris`，`upstream` 与 `upstream_id`，`host` 与 `hosts`，`remote_addr` 与 `remote_addrs` 等，是不能同时存在，二者只能选择其一。如果同时启用，则会出现异常。\n- 在 `vars` 中，当获取 Cookie 的值时，Cookie name 是**区分大小写字母**的。例如：`var = cookie_x_foo` 与 `var  = cookie_X_Foo` 表示不同的 `cookie`。\n\n:::\n\nRoute 对象 JSON 配置示例：\n\n```shell\n{\n    \"id\": \"1\",                            # id，非必填\n    \"uris\": [\"/a\",\"/b\"],                  # 一组 URL 路径\n    \"methods\": [\"GET\",\"POST\"],            # 可以填多个方法\n    \"hosts\": [\"a.com\",\"b.com\"],           # 一组 host 域名\n    \"plugins\": {},                        # 指定 route 绑定的插件\n    \"priority\": 0,                        # apisix 支持多种匹配方式，可能会在一次匹配中同时匹配到多条路由，此时优先级高的优先匹配中\n    \"name\": \"路由 xxx\",\n    \"desc\": \"hello world\",\n    \"remote_addrs\": [\"127.0.0.1\"],        # 一组客户端请求 IP 地址\n    \"vars\": [[\"http_user\", \"==\", \"ios\"]], # 由一个或多个 [var, operator, val] 元素组成的列表\n    \"upstream_id\": \"1\",                   # upstream 对象在 etcd 中的 id，建议使用此值\n    \"upstream\": {},                       # upstream 信息对象，建议尽量不要使用\n    \"timeout\": {                          # 为 route 设置 upstream 的连接、发送消息、接收消息的超时时间。\n        \"connect\": 3,\n        \"send\": 3,\n        \"read\": 3\n    },\n    \"filter_func\": \"\"                     # 用户自定义的过滤函数，非必填\n}\n```\n\n### 使用示例 {#route-example}\n\n- 创建一个路由：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uri\": \"/index.html\",\n        \"hosts\": [\"foo.com\", \"*.bar.com\"],\n        \"remote_addrs\": [\"127.0.0.0/8\"],\n        \"methods\": [\"PUT\", \"GET\"],\n        \"enable_websocket\": true,\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 201 Created\n    Date: Sat, 31 Aug 2019 01:17:15 GMT\n    ...\n    ```\n\n- 创建一个有效期为 60 秒的路由，过期后自动删除：\n\n    ```shell\n    curl 'http://127.0.0.1:9180/apisix/admin/routes/2?ttl=60' \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uri\": \"/aa/index.html\",\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 201 Created\n    Date: Sat, 31 Aug 2019 01:17:15 GMT\n    ...\n    ```\n\n- 在路由中新增一个上游节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 1\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，上游节点将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 1\n    }\n    ```\n\n- 更新路由中上游节点的权重：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 10\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，上游节点将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- 从路由中删除一个上游节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": null\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，Upstream `nodes` 将更新为：\n\n    ```shell\n    {\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- 更新路由中的 `methods` 数组\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '{\n        \"methods\": [\"GET\", \"POST\"]\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`methods` 将不保留原来的数据，将更新为：\n\n    ```\n    [\"GET\", \"POST\"]\n    ```\n\n- 使用 `sub path` 更新路由中的上游节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1/upstream/nodes \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"127.0.0.1:1982\": 1\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`nodes` 将不保留原来的数据，整个更新为：\n\n    ```\n    {\n        \"127.0.0.1:1982\": 1\n    }\n    ```\n\n- 使用 `sub path` 更新路由中的 `methods`：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1/methods \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '[\"POST\", \"DELETE\", \"PATCH\"]'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`methods` 将不保留原来的数据，更新为：\n\n    ```\n    [\"POST\", \"DELETE\", \"PATCH\"]\n    ```\n\n- 禁用路由\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"status\": 0\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`status` 将更新为：\n\n    ```\n    {\n        \"status\": 0\n    }\n    ```\n\n- 启用路由\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"status\": 1\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`status` 将更新为：\n\n    ```\n    {\n        \"status\": 1\n    }\n    ```\n\n### 应答参数 {#route-response-parameters}\n\n目前是直接返回与 etcd 交互后的结果。\n\n## Service\n\nService 是某类 API 的抽象（也可以理解为一组 Route 的抽象）。它通常与上游服务抽象是一一对应的，`Route` 与 `Service` 之间，通常是 N:1 的关系。\n\n### 请求地址 {#service-uri}\n\n服务资源请求地址：/apisix/admin/services/{id}\n\n### 请求方法  {#service-request-methods}\n\n| 名称   | 请求 URI                          | 请求 body | 描述                                                                                                                                                               |\n| ------ | ---------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/services             | 无        | 获取资源列表。                                                                                                                                                      |\n| GET    | /apisix/admin/services/{id}        | 无        | 获取资源。                                                                                                                                                         |\n| PUT    | /apisix/admin/services/{id}        | {...}     | 创建指定 id 资源。                                                                                                                                                  |\n| POST   | /apisix/admin/services             | {...}     | 创建资源，id 由后台服务自动生成。                                                                                                                                    |\n| DELETE | /apisix/admin/services/{id}        | 无        | 删除资源。                                                                                                                                                          |\n| PATCH  | /apisix/admin/services/{id}        | {...}     | 标准 PATCH，修改已有 Service 的部分属性，其他不涉及的属性会原样保留；如果你要删除某个属性，将该属性的值设置为 null 即可删除；**注意**：当需要修改属性的值为数组时，该属性将全量更新。|\n| PATCH  | /apisix/admin/services/{id}/{path} | {...}     | SubPath PATCH，通过 {path} 指定 Service 需要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。                                                                |\n\n### body 请求参数 {#service-request-body-parameters}\n\n| 名称             | 必选项                   | 类型     | 描述                                                             | 示例                                             |\n| ---------------- | ----------------------- | -------- | ---------------------------------------------------------------- | ------------------------------------------------ |\n| plugins          | 否                      | Plugin   | Plugin 配置，请参考 [Plugin](terminology/plugin.md)。               |                                                  |\n| upstream         | 与 `upstream_id` 二选一。  | Upstream | Upstream 配置，请参考 [Upstream](terminology/upstream.md)。         |                                                  |\n| upstream_id      | 与 `upstream` 二选一。    | Upstream | 需要使用的 upstream id，请参考 [Upstream](terminology/upstream.md)。|                                                  |\n| name             | 否                     | 辅助     | 服务名称。                                                      |                                             |\n| desc             | 否                     | 辅助     | 服务描述。                                                          |                                                  |\n| labels           | 否                     | 匹配规则 | 标识附加属性的键值对。                                                 | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n| enable_websocket | 否                     | 辅助     | `websocket`(boolean) 配置，默认值为 `false`。                       |                                                  |\n| hosts            | 否                     | 匹配规则 | 非空列表形态的 `host`，表示允许有多个不同 `host`，匹配其中任意一个即可。| [\"foo.com\", \"\\*.bar.com\"]                        |\n\nService 对象 JSON 配置示例：\n\n```shell\n{\n    \"id\": \"1\",                # id\n    \"plugins\": {},            # 指定 service 绑定的插件\n    \"upstream_id\": \"1\",       # upstream 对象在 etcd 中的 id，建议使用此值\n    \"upstream\": {},           # upstream 信息对象，不建议使用\n    \"name\": \"test svc\",       # service 名称\n    \"desc\": \"hello world\",    # service 描述\n    \"enable_websocket\": true, # 启动 websocket 功能\n    \"hosts\": [\"foo.com\"]\n}\n```\n\n### 使用示例 {#service-example}\n\n- 创建一个 Service：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201  \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        },\n        \"enable_websocket\": true,\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 201 Created\n    ...\n    ```\n\n- 在 Service 中添加一个上游节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 1\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，上游节点将更新为：\n\n    ```json\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 1\n    }\n    ```\n\n- 更新一个上游节点的权重：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1981\": 10\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，上游节点将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- 删除 Service 中的一个上游节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": null\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，上游节点将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n- 替换 Service 的上游节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/201/upstream/nodes \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"127.0.0.1:1982\": 1\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，上游节点不再保留原来的数据，将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1982\": 1\n    }\n    ```\n\n### 应答参数 {#service-response-parameters}\n\n目前是直接返回与 etcd 交互后的结果。\n\n## Consumer\n\nConsumer 是某类服务的消费者，需要与用户认证体系配合才能使用。Consumer 使用 `username` 作为唯一标识，仅支持使用 HTTP `PUT` 方法创建 Consumer。\n\n### 请求地址 {#consumer-uri}\n\nConsumer 资源请求地址：/apisix/admin/consumers/{username}\n\n### 请求方法 {#consumer-request-methods}\n\n| 名称   | 请求 URI                           | 请求 body | 描述          |\n| ------ | ---------------------------------- | --------- | ------------- |\n| GET    | /apisix/admin/consumers            | 无        | 获取资源列表。|\n| GET    | /apisix/admin/consumers/{username} | 无        | 获取资源。    |\n| PUT    | /apisix/admin/consumers            | {...}     | 创建资源。    |\n| DELETE | /apisix/admin/consumers/{username} | 无        | 删除资源。    |\n\n### body 请求参数 {#consumer-body-request-methods}\n\n| 名称        | 必选项 | 类型     | 描述                                                                                                                             | 示例值                                             |\n| ----------- | ----- | ------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |\n| username    | 是   | 辅助     | Consumer 名称。                                                                                                                  |                                                  |\n| group_id    | 否   | 辅助     | Consumer Group 名称。                                                                                                            |                                                  |\n| plugins     | 否   | Plugin   | 该 Consumer 对应的插件配置，它的优先级是最高的：Consumer > Route > Plugin Config > Service。对于具体插件配置，请参考 [Plugins](#plugin)。     |                                                  |\n| desc        | 否   | 辅助     | consumer 描述。                                                                                                                  |                                                  |\n| labels      | 否   | 匹配规则  | 标识附加属性的键值对。                                                                                                             | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n\nConsumer 对象 JSON 配置示例：\n\n```shell\n{\n    \"plugins\": {},          # 指定 consumer 绑定的插件\n    \"username\": \"name\",     # 必填\n    \"desc\": \"hello world\"   # consumer 描述\n}\n```\n\n当认证插件与 Consumer 一起使用时，需要提供用户名、密码等信息；当认证插件与 Route 或 Service 绑定时，则不需要任何参数，因为此时 APISIX 是根据用户请求数据来判断用户对应的是哪个 Consumer。\n\n:::note 注意\n\n从 APISIX v2.2 版本开始，同一个 Consumer 可以绑定多个认证插件。\n\n:::\n\n### 使用示例 {#consumer-example}\n\n- 创建 Consumer，并指定认证插件 `key-auth`，并开启指定插件 `limit-count`：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers  \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"username\": \"jack\",\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-one\"\n            },\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    Date: Thu, 26 Dec 2019 08:17:49 GMT\n    ...\n\n    {\"key\":\"\\/apisix\\/consumers\\/jack\",\"value\":{\"username\":\"jack\",\"update_time\":1666260780,\"plugins\":{\"limit-count\":{\"key_type\":\"var\",\"count\":2,\"rejected_code\":503,\"show_limit_quota_header\":true,\"time_window\":60,\"key\":\"remote_addr\",\"allow_degradation\":false,\"policy\":\"local\"},\"key-auth\":{\"key\":\"auth-one\"}},\"create_time\":1666260780}}\n    ```\n\n### 应答参数  {#consumer-response-parameters}\n\n目前是直接返回与 etcd 交互后的结果。\n\n## Credential\n\nCredential 用以存放 Consumer 的认证凭证。当需要为 Consumer 配置多个凭证时，可以使用 Credential。\n\n### 请求地址 {#credential-uri}\n\nCredential 资源请求地址：/apisix/admin/consumers/{username}/credentials/{credential_id}\n\n### 请求方法 {#consumer-request-methods}\n\n| 名称   | 请求 URI                                                         | 请求 body | 描述          |\n| ------ |----------------------------------------------------------------| --------- | ------------- |\n| GET    | /apisix/admin/consumers/{username}/credentials                 | 无        | 获取资源列表。|\n| GET    | /apisix/admin/consumers/{username}/credentials/{credential_id} | 无        | 获取资源。    |\n| PUT    | /apisix/admin/consumers/{username}/credentials/{credential_id} | {...}     | 创建资源。    |\n| DELETE | /apisix/admin/consumers/{username}/credentials/{credential_id} | 无        | 删除资源。    |\n\n### body 请求参数 {#credential-body-request-methods}\n\n| 名称        | 必选项 | 类型     | 描述                    | 示例值                                             |\n| ----------- |-----| ------- |-----------------------| ------------------------------------------------ |\n| plugins     | 是   | Plugin   | 该 Credential 对应的插件配置。 |                                                  |\n| name        | 否   | 辅助     | 消费者 Credential 名     | credential_primary                               |\n| desc        | 否   | 辅助     | Credential 描述。        |                                                  |\n| labels      | 否   | 匹配规则  | 标识附加属性的键值对。           | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n\nCredential 对象 JSON 配置示例：\n\n```shell\n{\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"auth-one\"\n      }\n    },\n    \"desc\": \"hello world\"\n}\n```\n\n### 使用示例 {#credential-example}\n\n前提：已创建 Consumer `jack`。\n\n创建 Credential，并启用认证插件 `key-auth`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/auth-one  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    }\n}'\n```\n\n```\nHTTP/1.1 200 OK\nDate: Thu, 26 Dec 2019 08:17:49 GMT\n...\n\n{\"key\":\"\\/apisix\\/consumers\\/jack\\/credentials\\/auth-one\",\"value\":{\"update_time\":1666260780,\"plugins\":{\"key-auth\":{\"key\":\"auth-one\"}},\"create_time\":1666260780}}\n```\n\n## Upstream\n\nUpstream 是虚拟主机抽象，对给定的多个服务节点按照配置规则进行负载均衡。Upstream 的地址信息可以直接配置到 `Route`（或 `Service`) 上，当 Upstream 有重复时，需要用“引用”方式避免重复。\n\n### 请求地址 {#upstream-uri}\n\nUpstream 资源请求地址：/apisix/admin/upstreams/{id}\n\n### 请求方法 {#upstream-request-methods}\n\n| 名称   | 请求 URI                             | 请求 body | 描述                                                                                                                                                               |\n| ------ | ----------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/upstreams/{id}        | 无        | 获取资源。                                                                                                                                                        |\n| PUT    | /apisix/admin/upstreams/{id}        | {...}     | 创建指定 id 的资源。                                                                                                                                               |\n| POST   | /apisix/admin/upstreams             | {...}     | 创建资源，id 由后台服务自动生成。                                                                                                                                 |\n| DELETE | /apisix/admin/upstreams/{id}        | 无        | 删除资源。                                                                                                                                                        |\n| PATCH  | /apisix/admin/upstreams/{id}        | {...}     | 标准 PATCH，修改已有 Upstream 的部分属性，其他不涉及的属性会原样保留；如果需要删除某个属性，可将该属性的值设置为 `null`；**注意**：当需要修改属性的值为数组时，该属性将全量更新。|\n| PATCH  | /apisix/admin/upstreams/{id}/{path} | {...}     | SubPath PATCH，通过 `{path}` 指定 Upstream 需要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。                                                            |\n\n### body 请求参数 {#upstream-body-request-methods}\n\nAPISIX 的 Upstream 除了基本的负载均衡算法选择外，还支持对上游做主被动健康检查、重试等逻辑。详细信息如下：\n\n| 名称           | 必选项                                           | 类型           | 描述                                                                                                                                                                                                                                                                                                                                                        | 示例                                             |\n| -------------- |-----------------------------------------------| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |\n| type           | 否                                             | 枚举           | 负载均衡算法，默认值是`roundrobin`。                                                                                                                                                                                                                                                                                                                                                           |                                      |     |\n| nodes          | 是，与 `service_name` 二选一。                       | Node           | 哈希表或数组。当它是哈希表时，内部元素的 key 是上游机器地址列表，格式为`地址 +（可选的）端口`，其中地址部分可以是 IP 也可以是域名，比如 `192.168.1.100:80`、`foo.com:80`等。对于哈希表的情况，如果 key 是 IPv6 地址加端口，则必须用中括号将 IPv6 地址括起来。`value` 则是节点的权重。当它是数组时，数组中每个元素都是一个哈希表，其中包含 `host`、`weight` 以及可选的 `port`、`priority`。`nodes` 可以为空，这通常用作占位符。客户端命中这样的上游会返回 `502`。                                        | `192.168.1.100:80`, `[::1]:80`                               |\n| service_name   | 是，与 `nodes` 二选一。                              | string         | 服务发现时使用的服务名，请参考 [集成服务发现注册中心](./discovery.md)。                                                                                                                                                                                                                                                                                            | `a-bootiful-client`                              |\n| discovery_type | 是，与 `service_name` 配合使用。                      | string         | 服务发现类型，请参考 [集成服务发现注册中心](./discovery.md)。                                                                                                                                                                                                                                                                                                      | `eureka`                                         |\n| key            | 条件必需                                          | 匹配类型       | 该选项只有类型是 `chash` 才有效。根据 `key` 来查找对应的节点 `id`，相同的 `key` 在同一个对象中，则返回相同 id。目前支持的 NGINX 内置变量有 `uri, server_name, server_addr, request_uri, remote_port, remote_addr, query_string, host, hostname, arg_***`，其中 `arg_***` 是来自 URL 的请求参数，详细信息请参考 [NGINX 变量列表](http://nginx.org/en/docs/varindex.html)。 |                                                  |\n| checks         | 否                                             | health_checker | 配置健康检查的参数，详细信息请参考 [health-check](./tutorials/health-check.md)。                                                                                                                                                                                                                                                                                               |                                                  |\n| retries        | 否                                             | 整型           | 使用 NGINX 重试机制将请求传递给下一个上游，默认启用重试机制且次数为后端可用的节点数量。如果指定了具体重试次数，它将覆盖默认值。当设置为 `0` 时，表示不启用重试机制。                                                                                                                                                                                                 |                                                  |\n| retry_timeout  | 否                                             | number         | 限制是否继续重试的时间，若之前的请求和重试请求花费太多时间就不再继续重试。当设置为 `0` 时，表示不启用重试超时机制。                                                                                                                                                                                                 |                                                  |\n| timeout        | 否                                             | 超时时间对象   | 设置连接、发送消息、接收消息的超时时间，以秒为单位。| `{\"connect\": 0.5,\"send\": 0.5,\"read\": 0.5}` |\n| hash_on        | 否                                             | 辅助           | `hash_on` 支持的类型有 `vars`（NGINX 内置变量），`header`（自定义 header），`cookie`，`consumer`，默认值为 `vars`。                                                                                                                                                                                                                                           |\n| name           | 否                                             | 辅助           | 标识上游服务名称、使用场景等。                                                                                                                                                                                                                                                                                                                              |                                                  |\n| desc           | 否                                             | 辅助           | 上游服务描述、使用场景等。                                                                                                                                                                                                                                                                                                                                  |                                                  |\n| pass_host      | 否                                             | 枚举           | 请求发给上游时的 `host` 设置选型。 [`pass`，`node`，`rewrite`] 之一，默认是 `pass`。`pass`: 将客户端的 host 透传给上游； `node`: 使用 `upstream` node 中配置的 `host`； `rewrite`: 使用配置项 `upstream_host` 的值。                                                                                                                                                                        |                                                  |\n| upstream_host  | 否                                             | 辅助           | 指定上游请求的 host，只在 `pass_host` 配置为 `rewrite` 时有效。                                                                                                                                                                                                                                                                                                                  |                                                  |\n| scheme         | 否                                             | 辅助           | 跟上游通信时使用的 scheme。对于 7 层代理，可选值为 [`http`, `https`, `grpc`, `grpcs`]。对于 4 层代理，可选值为 [`tcp`, `udp`, `tls`]。默认值为 `http`，详细信息请参考下文。                                                                                                                                                                                                                                                           |\n| labels         | 否                                             | 匹配规则       | 标识附加属性的键值对。                                                                                                                                                                                                                                                                                                                                        | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n| tls.client_cert    | 否，不能和 `tls.client_cert_id` 一起使用               | https 证书           | 设置跟上游通信时的客户端证书，详细信息请参考下文。                                                                        | |\n| tls.client_key\t | 否，不能和 `tls.client_cert_id` 一起使用               | https 证书私钥           | 设置跟上游通信时的客户端私钥，详细信息请参考下文。                                                                                                                                                                                                                                                                                                              | |\n| tls.client_cert_id | 否，不能和 `tls.client_cert`、`tls.client_key` 一起使用 | SSL           | 设置引用的 SSL id，详见 [SSL](#ssl)。                                                                                                                                                                                                                                                                                                              | |\n| tls.verify                  |否，目前仅支持 Kafka 上游。                | Boolean                       | 开启服务器证书验证功能，目前仅支持 Kafka 上游。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |                                                                                                                                            |\n|keepalive_pool.size  | 否                                             | 辅助 | 动态设置 `keepalive` 指令，详细信息请参考下文。 |\n|keepalive_pool.idle_timeout  | 否                                             | 辅助 | 动态设置 `keepalive_timeout` 指令，详细信息请参考下文。 |\n|keepalive_pool.requests  | 否                                             | 辅助 | 动态设置 `keepalive_requests` 指令，详细信息请参考下文。 |\n\n`type` 详细信息如下：\n\n- `roundrobin`: 带权重的 Round Robin。\n- `chash`: 一致性哈希。\n- `ewma`: 选择延迟最小的节点，请参考 [EWMA_chart](https://en.wikipedia.org/wiki/EWMA_chart)。\n- `least_conn`: 选择 `(active_conn + 1) / weight` 最小的节点。此处的 `active connection` 概念跟 NGINX 的相同，它是当前正在被请求使用的连接。\n- 用户自定义的 balancer，需要可以通过 `require(\"apisix.balancer.your_balancer\")` 来加载。\n\n`hash_on` 详细信息如下：\n\n- 设为 `vars` 时，`key` 为必传参数，目前支持的 NGINX 内置变量有 `uri, server_name, server_addr, request_uri, remote_port, remote_addr, query_string, host, hostname, arg_***`，其中 `arg_***` 是来自 URL 的请求参数。详细信息请参考 [NGINX 变量列表](http://nginx.org/en/docs/varindex.html)。\n- 设为 `header` 时，`key` 为必传参数，其值为自定义的 Header name，即 \"http\\_`key`\"。\n- 设为 `cookie` 时，`key` 为必传参数，其值为自定义的 cookie name，即 \"cookie\\_`key`\"。请注意 cookie name 是**区分大小写字母**的。例如：`cookie_x_foo` 与 `cookie_X_Foo` 表示不同的 `cookie`。\n- 设为 `consumer` 时，`key` 不需要设置。此时哈希算法采用的 `key` 为认证通过的 `consumer_name`。\n\n以下特性需要 APISIX 运行于 [APISIX-Runtime](./FAQ.md#如何构建-APISIX-Runtime-环境？)：\n\n- `scheme` 可以设置成 `tls`，表示 `TLS over TCP`。\n- `tls.client_cert/key` 可以用来跟上游进行 mTLS 通信。他们的格式和 SSL 对象的 `cert` 和 `key` 一样。\n- `tls.client_cert_id` 可以用来指定引用的 SSL 对象。只有当 SSL 对象的 `type` 字段为 client 时才能被引用，否则请求会被 APISIX 拒绝。另外，SSL 对象中只有 `cert` 和 `key` 会被使用。\n- `keepalive_pool` 允许 Upstream 有自己单独的连接池。它下属的字段，比如 `requests`，可以用于配置上游连接保持的参数。\n\nUpstream 对象 JSON 配置示例：\n\n```shell\n{\n    \"id\": \"1\",                  # id\n    \"retries\": 1,               # 请求重试次数\n    \"timeout\": {                # 设置连接、发送消息、接收消息的超时时间，每项都为 15 秒\n        \"connect\":15,\n        \"send\":15,\n        \"read\":15\n    },\n    \"nodes\": {\"host:80\": 100},  # 上游机器地址列表，格式为`地址 + 端口`\n                                # 等价于 \"nodes\": [ {\"host\":\"host\", \"port\":80, \"weight\": 100} ],\n    \"type\":\"roundrobin\",\n    \"checks\": {},               # 配置健康检查的参数\n    \"hash_on\": \"\",\n    \"key\": \"\",\n    \"name\": \"upstream-xxx\",     # upstream 名称\n    \"desc\": \"hello world\",      # upstream 描述\n    \"scheme\": \"http\"            # 跟上游通信时使用的 scheme，默认是 `http`\n}\n```\n\n### 使用示例 {#upstream-example}\n\n#### 创建 Upstream 并对 `nodes` 的数据进行修改 {#create-upstream}\n\n1. 创建 Upstream：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100  \\\n    -H \"X-API-KEY: $admin_key\" -i -X PUT -d '\n    {\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\": 1\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 201 Created\n    ...\n    ```\n\n2. 在 Upstream 中添加一个节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"nodes\": {\n            \"127.0.0.1:1981\": 1\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`nodes` 将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 1\n    }\n    ```\n\n3. 更新 Upstream 中单个节点的权重：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"nodes\": {\n            \"127.0.0.1:1981\": 10\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`nodes` 将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n4. 删除 Upstream 中的一个节点：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"nodes\": {\n            \"127.0.0.1:1980\": null\n        }\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`nodes` 将更新为：\n\n    ```\n    {\n        \"127.0.0.1:1981\": 10\n    }\n    ```\n\n5. 更新 Upstream 的 `nodes`：\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/100/nodes \\\n    -H \"X-API-KEY: $admin_key\" -X PATCH -i -d '\n    {\n        \"127.0.0.1:1982\": 1\n    }'\n    ```\n\n    ```\n    HTTP/1.1 200 OK\n    ...\n    ```\n\n    执行成功后，`nodes` 将不再保留原来的数据：\n\n    ```\n    {\n        \"127.0.0.1:1982\": 1\n    }\n    ```\n\n#### 将客户端请求代理到上游 `https` 服务 {#proxy-https}\n\n1. 创建 Route 并配置 Upstream 的 Scheme 为 `https`：\n\n    ```shell\n    curl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/get\",\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"scheme\": \"https\",\n            \"nodes\": {\n                \"httpbin.org:443\": 1\n            }\n        }\n    }'\n    ```\n\n    执行成功后，请求与上游通信时的 Scheme 将为 `https`。\n\n2. 发送请求进行测试：\n\n    ```shell\n    curl http://127.0.0.1:9080/get\n    ```\n\n    ```shell\n    {\n    \"args\": {},\n    \"headers\": {\n        \"Accept\": \"*/*\",\n        \"Host\": \"127.0.0.1\",\n        \"User-Agent\": \"curl/7.29.0\",\n        \"X-Amzn-Trace-Id\": \"Root=1-6058324a-0e898a7f04a5e95b526bb183\",\n        \"X-Forwarded-Host\": \"127.0.0.1\"\n    },\n    \"origin\": \"127.0.0.1\",\n    \"url\": \"https://127.0.0.1/get\"\n    }\n    ```\n\n    请求成功，表示代理上游 `https` 生效了。\n\n    :::tip 提示\n\n    每个节点均可以配置优先级，只有在高优先级的节点不可用或者尝试过，才会访问一个低优先级的节点。\n\n    :::\n\n    由于上游节点的默认优先级是 `0`，你可以将一些节点的优先级设置为负数，让其作为备份节点。例如：\n\n    ```JSON\n    {\n        \"uri\": \"/hello\",\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": [\n                {\"host\": \"127.0.0.1\", \"port\": 1980, \"weight\": 2000},\n                {\"host\": \"127.0.0.1\", \"port\": 1981, \"weight\": 1, \"priority\": -1}\n            ],\n            \"checks\": {\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"healthy\": {\n                        \"interval\": 1,\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"interval\": 1,\n                        \"http_failures\": 1\n                    }\n                }\n            }\n        }\n    }\n    ```\n\n    节点 `127.0.0.2` 只有在 `127.0.0.1` 不可用或者尝试过之后才会被访问，因此它是 `127.0.0.1` 的备份。\n\n### 应答参数  {#upstream-response-parameters}\n\n目前是直接返回与 etcd 交互后的结果。\n\n## SSL\n\n你可以使用该资源创建 SSL 证书。\n\n### 请求地址 {#ssl-uri}\n\nSSL 资源请求地址：/apisix/admin/ssls/{id}\n\n### 请求方法 {#ssl-request-methods}\n\n| 名称   | 请求 URI                | 请求 body | 描述                            |\n| ------ | ----------------------- | --------- | ------------------------------- |\n| GET    | /apisix/admin/ssls      | 无       | 获取资源列表。                    |\n| GET    | /apisix/admin/ssls/{id} | 无       | 获取资源。                        |\n| PUT    | /apisix/admin/ssls/{id} | {...}    | 创建指定 id 的资源。              |\n| POST   | /apisix/admin/ssls      | {...}    | 创建资源，id 由后台服务自动生成。 |\n| DELETE | /apisix/admin/ssls/{id} | 无       | 删除资源。                        |\n\n### body 请求参数 {#ssl-body-request-methods}\n\n| 名称        | 必选项 | 类型           | 描述                                                                                                   | 示例                                             |\n| ----------- | ------ | -------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| cert        | 是     | 证书           | HTTP 证书。该字段支持使用 [APISIX Secret](./terminology/secret.md) 资源，将值保存在 Secret Manager 中。                                                                                             |                                                  |\n| key         | 是     | 私钥           | HTTPS 证书私钥。该字段支持使用 [APISIX Secret](./terminology/secret.md) 资源，将值保存在 Secret Manager 中。                                                                                         |                                                  |\n| certs       | 否   | 证书字符串数组 | 当你想给同一个域名配置多个证书时，除了第一个证书需要通过 `cert` 传递外，剩下的证书可以通过该参数传递上来。该字段支持使用 [APISIX Secret](./terminology/secret.md) 资源，将值保存在 Secret Manager 中。 |                                                  |\n| keys        | 否   | 私钥字符串数组 | `certs` 对应的证书私钥，需要与 `certs` 一一对应。该字段支持使用 [APISIX Secret](./terminology/secret.md) 资源，将值保存在 Secret Manager 中。                                                          |                                                  |\n| client.ca   | 否   | 证书 |  设置将用于客户端证书校验的 `CA` 证书。该特性需要 OpenResty 为 1.19 及以上版本。  |                                                  |\n| client.depth | 否   | 辅助 |  设置客户端证书校验的深度，默认为 1。该特性需要 OpenResty 为 1.19 及以上版本。 |                                             |\n| client.skip_mtls_uri_regex | 否   | PCRE 正则表达式数组 |  用来匹配请求的 URI，如果匹配，则该请求将绕过客户端证书的检查，也就是跳过 MTLS。 | [\"/hello[0-9]+\", \"/foobar\"]                                            |\n| snis        | 是   | 匹配规则       | 非空数组形式，可以匹配多个 SNI。                                                                         |                                                  |\n| desc        | 否   | 辅助          | 证书描述。        | certs for production env                                               |\n| labels      | 否   | 匹配规则       | 标识附加属性的键值对。                                                                                   | {\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"} |\n| type        | 否   | 辅助           | 标识证书的类型，默认值为 `server`。                                                                     | `client` 表示证书是客户端证书，APISIX 访问上游时使用；`server` 表示证书是服务端证书，APISIX 验证客户端请求时使用。     |\n| status      | 否   | 辅助           | 当设置为 `1` 时，启用此 SSL，默认值为 `1`。                                                               | `1` 表示启用，`0` 表示禁用                       |\n| ssl_protocols | 否    | tls 协议字符串数组               | 用于控制服务器与客户端之间使用的 SSL/TLS 协议版本。更多的配置示例，请参考[SSL 协议](./ssl-protocol.md)。                                  |                `[\"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\"]`                                  |\n\nSSL 对象 JSON 配置示例：\n\n```shell\n{\n    \"id\": \"1\",          # id\n    \"cert\": \"cert\",     # 证书\n    \"key\": \"key\",       # 私钥\n    \"snis\": [\"t.com\"]   # HTTPS 握手时客户端发送的 SNI\n}\n```\n\n更多的配置示例，请参考[证书](./certificate.md)。\n\n## Global Rules\n\nGlobal Rule 可以设置全局运行的插件，设置为全局规则的插件将在所有路由级别的插件之前优先运行。\n\n### 请求地址 {#global-rule-uri}\n\nGlobal Rule 资源请求地址：/apisix/admin/global_rules/{id}\n\n### 请求方法 {#global-rule-request-methods}\n\n| 名称   | 请求 URI                               | 请求 body | 描述                                                                                                                                                                                   |\n| ------ | -------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/global_rules             | 无        | 获取资源列表。                                                                                                                                                                           |\n| GET    | /apisix/admin/global_rules/{id}        | 无        | 获取资源。                                                                                                                                                                               |\n| PUT    | /apisix/admin/global_rules/{id}        | {...}     | 将创建指定 id 的资源。                                                                                                                                                                   |\n| DELETE | /apisix/admin/global_rules/{id}        | 无        | 删除资源。                                                                                                                                                                               |\n| PATCH  | /apisix/admin/global_rules/{id}        | {...}     | 标准 PATCH，修改已有 Global Rule 的部分属性，其他不涉及的属性会原样保留；如果你要删除某个属性，将该属性的值设置为 null 即可删除；**注意**：当需要修改属性的值为数组时，该属性将全量更新。       |\n| PATCH  | /apisix/admin/global_rules/{id}/{path} | {...}     | SubPath PATCH，通过 `{path}` 指定 Global Rule 要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。                                                                             |\n\n### body 请求参数  {#global-rule-body-request-parameters}\n\n| 名称        | 必选项 | 类型   | 描述                                               | 示例值       |\n| ----------- | ------ | ------ | ------------------------------------------------- | ---------- |\n| plugins     | 是     | Plugin | 插件配置。详细信息请参考 [Plugin](terminology/plugin.md)。 |            |\n\n## Consumer Group\n\n你可以使用该资源配置一组可以在 Consumer 间复用的插件。\n\n### 请求地址 {#consumer-group-uri}\n\nConsumer Group 资源请求地址：/apisix/admin/consumer_groups/{id}\n\n### 请求方法 {#consumer-group-request-methods}\n\n| 名称   | 请求 URI                                  | 请求 body | 描述                                                                                                                                                                                     |\n| ------ | ----------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/consumer_groups             | 无        | 获取资源列表。                                                                                                                                                                             |\n| GET    | /apisix/admin/consumer_groups/{id}        | 无        | 获取资源。                                                                                                                                                                                 |\n| PUT    | /apisix/admin/consumer_groups/{id}        | {...}     | 将创建指定 id 的资源。                                                                                                                                                                        |\n| DELETE | /apisix/admin/consumer_groups/{id}        | 无        | 删除资源。                                                                                                                                                                                 |\n| PATCH  | /apisix/admin/consumer_groups/{id}        | {...}     | 标准 PATCH，修改已有 Consumer Group 的部分属性，其他不涉及的属性会原样保留；如果你要删除某个属性，将该属性的值设置为 null 即可删除；**注意**：当需要修改属性的值为数组时，该属性将全量更新。 |\n| PATCH  | /apisix/admin/consumer_groups/{id}/{path} | {...}     | SubPath PATCH，通过 `{path}` 指定 Consumer Group 要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。                                                                           |\n\n### body 请求参数  {#consumer-group-body-request-parameters}\n\n| 名称      | 必选项  | 类型  | 描述                                          | 示例值 |\n|--------- |--------- |------|----------------------------------------------- |------|\n|plugins  | 是        |Plugin| 插件配置。详细信息请参考 [Plugin](terminology/plugin.md)。 |      |\n|name     | 否        | 辅助 | 消费者组名。            | premium-tier                           |\n|desc     | 否        | 辅助 | 标识描述、使用场景等。                          | Consumer 测试。|\n|labels   | 否        | 辅助 | 标识附加属性的键值对。                          |{\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"}|\n\n## Plugin Config\n\n你可以使用 Plugin Config 资源创建一组可以在路由间复用的插件。\n\n### 请求地址 {#plugin-config-uri}\n\nPlugin Config 资源请求地址：/apisix/admin/plugin_configs/{id}\n\n### 请求方法 {#plugin-config-request-methods}\n\n| 名称   | 请求 URI                                 | 请求 body | 描述                                                                                                                                                                                     |\n| ------ | ---------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| GET    | /apisix/admin/plugin_configs             | 无        | 获取资源列表。                                                                                                                                                                             |\n| GET    | /apisix/admin/plugin_configs/{id}        | 无        | 获取资源。                                                                                                                                                                                 |\n| PUT    | /apisix/admin/plugin_configs/{id}        | {...}     | 根据 id 创建资源。                                                                                                                                                                         |\n| DELETE | /apisix/admin/plugin_configs/{id}        | 无        | 删除资源。                                                                                                                                                                                 |\n| PATCH  | /apisix/admin/plugin_configs/{id}        | {...}     | 标准 PATCH，修改已有 Plugin Config 的部分属性，其他不涉及的属性会原样保留；如果你要删除某个属性，将该属性的值设置为 null 即可删除；**注意**：当需要修改属性的值为数组时，该属性将全量更新。 |\n| PATCH  | /apisix/admin/plugin_configs/{id}/{path} | {...}     | SubPath PATCH，通过 {path} 指定 Plugin Config 要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。                                                                           |\n\n### body 请求参数 {#plugin-config-body-request-parameters}\n\n| 名称      | 必选项  | 类型 | 描述        | 示例值 |\n|---------  |---------|----|-----------|----|\n|plugins    | 是      |Plugin| 更多信息请参考 [Plugin](terminology/plugin.md)。||\n|desc       | 否 | 辅助 | 标识描述、使用场景等。 |customer xxxx|\n|labels     | 否 | 辅助 | 标识附加属性的键值对。 |{\"version\":\"v2\",\"build\":\"16\",\"env\":\"production\"}|\n\n## Plugin Metadata\n\n你可以使用 Plugin Metadata 资源配置插件元数据。\n\n### 请求地址 {#plugin-metadata-uri}\n\nPlugin Config 资源请求地址：/apisix/admin/plugin_metadata/{plugin_name}\n\n### 请求方法 {#plugin-metadata-request-methods}\n\n| Method | 请求 URI                                    | 请求 body | 描述                      |\n| ------ | ------------------------------------------- | --------- | ------------------------- |\n| GET    | /apisix/admin/plugin_metadata               | 无        | 获取所有插件元数据列表。    |\n| GET    | /apisix/admin/plugin_metadata/{plugin_name} | 无        | 获取资源。                  |\n| PUT    | /apisix/admin/plugin_metadata/{plugin_name} | {...}     | 根据 `plugin name` 创建资源。 |\n| DELETE | /apisix/admin/plugin_metadata/{plugin_name} | 无        | 删除资源。                  |\n\n### body 请求参数 {#plugin-metadata-body-request-parameters}\n\n根据插件 (`{plugin_name}`) 的 `metadata_schema` 定义的数据结构的  JSON 对象。\n\n### 使用示例 {#plugin-metadata-example}\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/example-plugin  \\\n-H \"X-API-KEY: $admin_key\" -i -X PUT -d '\n{\n    \"skey\": \"val\",\n    \"ikey\": 1\n}'\n```\n\n```\nHTTP/1.1 201 Created\nDate: Thu, 26 Dec 2019 04:19:34 GMT\nContent-Type: text/plain\n```\n\n## Plugin\n\n你可以通过该资源获取插件列表。\n\n### 请求地址 {#plugin-uri}\n\nPlugin 资源请求地址：/apisix/admin/plugins/{plugin_name}\n\n### 请求参数\n\n| 名称 | 描述 | 默认 |\n| --------- | -------------------------------------- | -------- |\n| subsystem | 插件子系统。 | http |\n\n可以在子系统上过滤插件，以便在通过查询参数传递的子系统中搜索 ({plugin_name})\n\n### 请求方法 {#plugin-request-methods}\n\n| 名称        | 请求 URI                            | 请求 body | 描述          |\n| ----------- | ----------------------------------- | ---------- | ------------- |\n| GET         | /apisix/admin/plugins/list          | 无         | 获取资源列表。  |\n| GET         | /apisix/admin/plugins/{plugin_name} | 无         | 获取资源。      |\n| GET         | /apisix/admin/plugins?all=true      | 无         | 获取所有插件的所有属性。 |\n| GET         | /apisix/admin/plugins?all=true&subsystem=stream| 无 | 获取所有 Stream 插件的属性。|\n| GET         | /apisix/admin/plugins?all=true&subsystem=http| 无 | 获取所有 HTTP 插件的属性。|\n| PUT         | /apisix/admin/plugins/reload        | 无         | 根据代码中所做的更改重新加载插件。 |\n| GET         | apisix/admin/plugins/{plugin_name}?subsystem=stream         | 无         | 获取指定 Stream 插件的属性。 |\n| GET         | apisix/admin/plugins/{plugin_name}?subsystem=http         | 无         | 获取指定 HTTP 插件的属性。 |\n\n:::caution\n\n获取所有插件属性的接口 `/apisix/admin/plugins?all=true` 将很快被弃用。\n\n:::\n\n### 使用示例 {#plugin-example}\n\n获取插件  (`{plugin_name}`)  数据结构的 JSON 对象。\n\n- 获取插件列表\n\n    ```shell\n    curl \"http://127.0.0.1:9180/apisix/admin/plugins/list\" \\\n    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'\n    ```\n\n    ```shell\n    [\"zipkin\",\"request-id\",...]\n    ```\n\n- 获取指定插件的属性\n\n    ```shell\n    curl \"http://127.0.0.1:9180/apisix/admin/plugins/key-auth?subsystem=http\" \\\n    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'\n    ```\n\n    ```json\n    {\"$comment\":\"this is a mark for our injected plugin schema\",\"properties\":{\"header\":{\"default\":\"apikey\",\"type\":\"string\"},\"hide_credentials\":{\"default\":false,\"type\":\"boolean\"},\"_meta\":{\"properties\":{\"filter\":{\"type\":\"array\",\"description\":\"filter determines whether the plugin needs to be executed at runtime\"},\"disable\":{\"type\":\"boolean\"},\"error_response\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"object\"}]},\"priority\":{\"type\":\"integer\",\"description\":\"priority of plugins by customized order\"}},\"type\":\"object\"},\"query\":{\"default\":\"apikey\",\"type\":\"string\"}},\"type\":\"object\"}\n    ```\n\n:::tip\n\n你可以使用 `/apisix/admin/plugins?all=true` 接口获取所有插件的所有属性，每个插件包括 `name`，`priority`，`type`，`schema`，`consumer_schema` 和 `version`。\n\n这个 API 将很快被弃用。\n\n:::\n\n## Stream Route\n\nStream Route 是用于 TCP/UDP 动态代理的路由。详细信息请参考 [TCP/UDP 动态代理](./stream-proxy.md)。\n\n### 请求地址 {#stream-route-uri}\n\nPlugin 资源请求地址：/apisix/admin/stream_routes/{id}\n\n### 请求方法 {#stream-route-request-methods}\n\n| 名称   | 请求 URI                          | 请求 body | 描述                                               |\n| ------ | --------------------------------- | --------- | -------------------------------------------------- |\n| GET    | /apisix/admin/stream_routes       | 无        | 获取资源列表。                                     |\n| GET    | /apisix/admin/stream_routes/{id}  | 无        | 获取资源。                                        |\n| PUT    | /apisix/admin/stream_routes/{id}  | {...}     | 创建指定 id 的资源。                              |\n| POST   | /apisix/admin/stream_routes       | {...}     | 创建资源，id 由后台服务自动生成。                  |\n| DELETE | /apisix/admin/stream_routes/{id}  | 无        | 删除资源。                                        |\n\n### body 请求参数{#stream-route-body-request-parameters}\n\n| 名称             | 必选项 | 类型     | 描述                                                                           | 示例值 |\n| ---------------- | ------| -------- | ------------------------------------------------------------------------------| ------  |\n| name             | 否    | 辅助     | Stream 路由名。            | postgres-proxy                                   |\n| desc             | 否    | 辅助     | Stream 路由描述。          | proxy endpoint for postgresql                    |\n| labels           | 否    | 匹配规则  | 标识附加属性的键值对。           | {\"version\":\"17\",\"service\":\"user\",\"env\":\"production\"} |\n| upstream         | 否    | Upstream | Upstream 配置，详细信息请参考 [Upstream](terminology/upstream.md)。             |         |\n| upstream_id      | 否    | Upstream | 需要使用的 Upstream id，详细信息请 [Upstream](terminology/upstream.md)。       |         |\n| service_id       | 否    | String   | 需要使用的 [Service](terminology/service.md) id.                   |                               |\n| remote_addr      | 否    | IPv4, IPv4 CIDR, IPv6  | 过滤选项：如果客户端 IP 匹配，则转发到上游                                      | \"127.0.0.1\" 或 \"127.0.0.1/32\" 或 \"::1\" |\n| server_addr      | 否    | IPv4, IPv4 CIDR, IPv6  | 过滤选项：如果 APISIX 服务器的 IP 与 `server_addr` 匹配，则转发到上游。         | \"127.0.0.1\" 或 \"127.0.0.1/32\" 或 \"::1\" |\n| server_port      | 否    | 整数     | 过滤选项：如果 APISIX 服务器的端口 与 `server_port` 匹配，则转发到上游。        | 9090  |\n| sni              | 否    | Host     | 服务器名称。                                                                   | \"test.com\"     |\n| protocol.name    | 否    | 字符串   | xRPC 框架代理的协议的名称。                                                    | \"redis\"        |\n| protocol.conf    | 否    | 配置     | 协议特定的配置。                                                               |                    |\n\n你可以查看 [Stream Proxy](./stream-proxy.md#更多-route-匹配选项) 了解更多过滤器的信息。\n\n## Secret\n\nSecret 指的是 `Secrets Management`（密钥管理），可以使用任何支持的密钥管理器，例如 `vault`。\n\n### 请求地址 {#secret-config-uri}\n\nSecret 资源请求地址：/apisix/admin/secrets/{secretmanager}/{id}\n\n### 请求方法 {#secret-config-request-methods}\n\n| 名称 | 请求 URI                          | 请求 body | 描述                                        |\n| :--: | :----------------------------: | :---: | :---------------------------------------: |\n| GET  | /apisix/admin/secrets            | NULL  | 获取所有 secret 的列表。                  |\n| GET  | /apisix/admin/secrets/{manager}/{id} | NULL  | 根据 id 获取指定的 secret。           |\n| PUT  | /apisix/admin/secrets/{manager}            | {...} | 创建新的 secret 配置。                              |\n| DELETE | /apisix/admin/secrets/{manager}/{id} | NULL   | 删除具有指定 id 的 secret。 |\n| PATCH  | /apisix/admin/secrets/{manager}/{id}        | {...}     | 标准 PATCH，修改指定 secret 的部分属性，其他不涉及的属性会原样保留；如果你需要删除某个属性，可以将该属性的值设置为 `null`；当需要修改属性的值为数组时，该属性将全量更新。 |\n| PATCH  | /apisix/admin/secrets/{manager}/{id}/{path} | {...}     | SubPath PATCH，通过 `{path}` 指定 secret 要更新的属性，全量更新该属性的数据，其他不涉及的属性会原样保留。                         |\n\n### body 请求参数 {#secret-config-body-requset-parameters}\n\n#### 当 Secret Manager 是 Vault 时\n\n| 名称  | 必选项 | 类型        | 描述                                                                                                        | 例子                                          |\n| ----------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |\n| uri    | 是     | URI        |  Vault 服务器的 URI                                                 |                                                  |\n| prefix    | 是    | 字符串       | 密钥前缀\n| token     | 是    | 字符串       | Vault 令牌 |                                                  |\n| namespace | 否    | 字符串       | Vault 命名空间，该字段无默认值 | `admin` |\n\n配置示例：\n\n```shell\n{\n    \"uri\": \"https://localhost/vault\",\n    \"prefix\": \"/apisix/kv\",\n    \"token\": \"343effad\"\n}\n\n```\n\n使用示例：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/secrets/vault/test2 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"http://xxx/get\",\n    \"prefix\" : \"apisix\",\n    \"token\" : \"apisix\"\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n\n{\"key\":\"\\/apisix\\/secrets\\/vault\\/test2\",\"value\":{\"id\":\"vault\\/test2\",\"token\":\"apisix\",\"prefix\":\"apisix\",\"update_time\":1669625828,\"create_time\":1669625828,\"uri\":\"http:\\/\\/xxx\\/get\"}}\n```\n\n#### 当 Secret Manager 是 AWS 时\n\n| 名称              | 必选项 | 默认值                                        | 描述                    |\n| ----------------- | ------ | --------------------------------------------- | ----------------------- |\n| access_key_id     | 是     |                                               | AWS 访问密钥 ID         |\n| secret_access_key | 是     |                                               | AWS 访问密钥            |\n| session_token     | 否     |                                               | 临时访问凭证信息        |\n| region            | 否     | us-east-1                                     | AWS 区域                |\n| endpoint_url      | 否     | https://secretsmanager.{region}.amazonaws.com | AWS Secret Manager 地址 |\n\n配置示例：\n\n```json\n{\n    \"endpoint_url\": \"http://127.0.0.1:4566\",\n    \"region\": \"us-east-1\",\n    \"access_key_id\": \"access\",\n    \"secret_access_key\": \"secret\",\n    \"session_token\": \"token\"\n}\n\n```\n\n使用示例：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/secrets/aws/test3 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"endpoint_url\": \"http://127.0.0.1:4566\",\n    \"region\": \"us-east-1\",\n    \"access_key_id\": \"access\",\n    \"secret_access_key\": \"secret\",\n    \"session_token\": \"token\"\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n\n{\"value\":{\"create_time\":1726069970,\"endpoint_url\":\"http://127.0.0.1:4566\",\"region\":\"us-east-1\",\"access_key_id\":\"access\",\"secret_access_key\":\"secret\",\"id\":\"aws/test3\",\"update_time\":1726069970,\"session_token\":\"token\"},\"key\":\"/apisix/secrets/aws/test3\"}\n```\n\n#### 当 Secret Manager 是 GCP 时\n\n| 名称                     | 必选项 | 默认值                                         | 描述                                                                                                                              |\n| ------------------------ | ------ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| auth_config              | 是     |                                                | `auth_config` 和 `auth_file` 必须配置一个。                                                                                       |\n| auth_config.client_email | 是     |                                                | 谷歌服务帐号的 email 参数。                                                                                                       |\n| auth_config.private_key  | 是     |                                                | 谷歌服务帐号的私钥参数。                                                                                                          |\n| auth_config.project_id   | 是     |                                                | 谷歌服务帐号的项目 ID。                                                                                                           |\n| auth_config.token_uri    | 否     | https://oauth2.googleapis.com/token            | 请求谷歌服务帐户的令牌的 URI。                                                                                                    |\n| auth_config.entries_uri  | 否     | https://secretmanager.googleapis.com/v1        | 谷歌密钥服务访问端点 API。                                                                                                        |\n| auth_config.scope        | 否     | https://www.googleapis.com/auth/cloud-platform | 谷歌服务账号的访问范围，可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) |\n| auth_file                | 是     |                                                | `auth_config` 和 `auth_file` 必须配置一个。                                                                                       |\n| ssl_verify               | 否     | true                                           | 当设置为 `true` 时，启用 `SSL` 验证。                                                                                             |\n\n配置示例：\n\n```json\n{\n    \"auth_config\" : {\n        \"client_email\": \"email@apisix.iam.gserviceaccount.com\",\n        \"private_key\": \"private_key\",\n        \"project_id\": \"apisix-project\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"entries_uri\": \"https://secretmanager.googleapis.com/v1\",\n        \"scope\": [\"https://www.googleapis.com/auth/cloud-platform\"]\n    }\n}\n\n```\n\n使用示例：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/secrets/gcp/test4 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"auth_config\" : {\n        \"client_email\": \"email@apisix.iam.gserviceaccount.com\",\n        \"private_key\": \"private_key\",\n        \"project_id\": \"apisix-project\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"entries_uri\": \"https://secretmanager.googleapis.com/v1\",\n        \"scope\": [\"https://www.googleapis.com/auth/cloud-platform\"]\n    }\n}'\n```\n\n```shell\nHTTP/1.1 200 OK\n...\n\n{\"value\":{\"id\":\"gcp/test4\",\"ssl_verify\":true,\"auth_config\":{\"token_uri\":\"https://oauth2.googleapis.com/token\",\"scope\":[\"https://www.googleapis.com/auth/cloud-platform\"],\"entries_uri\":\"https://secretmanager.googleapis.com/v1\",\"client_email\":\"email@apisix.iam.gserviceaccount.com\",\"private_key\":\"private_key\",\"project_id\":\"apisix-project\"},\"create_time\":1726070161,\"update_time\":1726070161},\"key\":\"/apisix/secrets/gcp/test4\"}\n```\n\n### 应答参数 {#secret-config-response-parameters}\n\n当前的响应是从 etcd 返回的。\n"
  },
  {
    "path": "docs/zh/latest/apisix-variable.md",
    "content": "---\ntitle: APISIX 变量\nkeywords:\n - Apache APISIX\n - API 网关\n - APISIX variable\ndescription: 本文介绍了 Apache APISIX 支持的变量。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nAPISIX 除了支持 [NGINX 变量](http://nginx.org/en/docs/varindex.html)外，自身也提供了一些变量。\n\n## 变量列表\n\n|    变量名称         |  来源       | 描述                                                                             | 示例              |\n|---------------------|----------- |--------------------------------------------------------------------------------- | ---------------- |\n| balancer_ip         | core       | 上游服务器的 IP 地址。                                                            | 192.168.1.2      |\n| balancer_port       | core       | 上游服务器的端口。                                                                | 80               |\n| consumer_name       | core       | 消费者的名称。                                                                    |                  |\n| consumer_group_id   | core       | 消费者所在的组的 ID。                                                            |                  |\n| graphql_name        | core       | GraphQL 的 [operation name](https://graphql.org/learn/queries/#operation-name)。 | HeroComparison   |\n| graphql_operation   | core       | GraphQL 的操作类型。                                                              | mutation         |\n| graphql_root_fields | core       | GraphQL 最高级别的字段。                                                          | [\"hero\"]          |\n| mqtt_client_id      | mqtt-proxy | MQTT 协议中的客户端 ID。                                                          |                   |\n| route_id            | core       | APISIX 路由的 ID。                                                                |                   |\n| route_name          | core       | APISIX 路由的名称。                                                               |                   |\n| service_id          | core       | APISIX 服务的 ID。                                                                |                   |\n| service_name        | core       | APISIX 服务的名称。                                                               |                   |\n| redis_cmd_line      | Redis      | Redis 命令的内容。                                                                |                   |\n| resp_body           | core       | 在 logger 插件中，如果部分插件支持记录响应的 body 信息，比如配置 `include_resp_body: true`，那可以在 log format 中使用该变量。|                   |\n| rpc_time            | xRPC       | 在 RPC 请求级别所花费的时间。                                                      |                   |\n\n当然，除上述变量外，你也可以创建自定义[变量](./plugin-develop.md#register-custom-variable)。\n"
  },
  {
    "path": "docs/zh/latest/architecture-design/apisix.md",
    "content": "---\ntitle: 软件架构\nkeywords:\n  - 网关\n  - Apache APISIX\n  - APISIX 架构\ndescription: 云原生网关 Apache APISIX 的软件架构\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nApache APISIX 是一个动态、实时、高性能的云原生 API 网关。它构建于 NGINX + ngx_lua 的技术基础之上，充分利用了 LuaJIT 所提供的强大性能。 [为什么 Apache APISIX 选择 NGINX+Lua 技术栈？](https://apisix.apache.org/zh/blog/2021/08/25/why-apache-apisix-chose-nginx-and-lua/)。\n\n![软件架构](../../../assets/images/flow-software-architecture.png)\n\nAPISIX 主要分为两个部分：\n\n1. APISIX 核心：包括 Lua 插件、多语言插件运行时（Plugin Runner）、Wasm 插件运行时等；\n2. 功能丰富的各种内置插件：包括可观测性、安全、流量控制等。\n\nAPISIX 在其核心中，提供了路由匹配、负载均衡、服务发现、API 管理等重要功能，以及配置管理等基础性模块。除此之外，APISIX 插件运行时也包含其中，提供原生 Lua 插件的运行框架和多语言插件的运行框架，以及实验性的 Wasm 插件运行时等。APISIX 多语言插件运行时提供多种开发语言的支持，比如 Golang、Java、Python、JS 等。\n\nAPISIX 目前也内置了各类插件，覆盖了 API 网关的各种领域，如认证鉴权、安全、可观测性、流量管理、多协议接入等。当前 APISIX 内置的插件使用原生 Lua 实现，关于各个插件的介绍与使用方式，可以查看相关[插件文档](https://apisix.apache.org/docs/apisix/plugins/batch-requests)。\n\n## 插件加载流程\n\n![插件加载流程](../../../assets/images/flow-load-plugin.png)\n\n## 插件内部结构\n\n![插件内部结构](../../../assets/images/flow-plugin-internal.png)\n"
  },
  {
    "path": "docs/zh/latest/batch-processor.md",
    "content": "---\ntitle: 批处理器\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n批处理器可用于聚合条目（日志/任何数据）并进行批处理。\n当 `batch_max_size` 设置为 1 时，处理器将立即执行每个条目。将批处理的最大值设置为大于 1 将开始聚合条目，直到达到最大值或超时。\n\n## 配置\n\n创建批处理器的唯一必需参数是函数。当批处理达到最大值或缓冲区持续时间超过时，函数将被执行。\n\n| 名称             | 类型    | 必选项 | 默认值 | 有效值  | 描述                                                         |\n| ---------------- | ------- | ------ | ------ | ------- | ------------------------------------------------------------ |\n| name             | string  | 可选   | xxx logger | [\"http logger\", \"Some strings\",...] | 用于标识批处理器的唯一标识符，默认为调用批处理器的日志插件名字，如配置插件为 `http logger`，name 默认为 http logger。  |\n| batch_max_size   | integer | 可选   | 1000   | [1,...] | 设置每批发送日志的最大条数，当日志条数达到设置的最大值时，会自动推送全部日志到  HTTP/HTTPS 服务。 |\n| inactive_timeout | integer | 可选   | 5      | [1,...] | 刷新缓冲区的最大时间（以秒为单位），当达到最大的刷新时间时，无论缓冲区中的日志数量是否达到设置的最大条数，也会自动将全部日志推送到  HTTP/HTTPS 服务。 |\n| buffer_duration  | integer | 可选   | 60     | [1,...] | 必须先处理批次中最旧条目的最长期限（以秒为单位）。           |\n| max_retry_count  | integer | 可选   | 0      | [0,...] | 从处理管道中移除之前的最大重试次数。                         |\n| retry_delay      | integer | 可选   | 1      | [0,...] | 如果执行失败，则应延迟执行流程的秒数。                       |\n以下代码显示了如何在你的插件中使用批处理器：\n\n```lua\nlocal bp_manager_mod = require(\"apisix.utils.batch-processor-manager\")\n...\n\nlocal plugin_name = \"xxx-logger\"\nlocal batch_processor_manager = bp_manager_mod.new(plugin_name)\nlocal schema = {...}\nlocal _M = {\n    ...\n    name = plugin_name,\n    schema = batch_processor_manager:wrap_schema(schema),\n}\n\n...\n\n\nfunction _M.log(conf, ctx)\n    local entry = {...} -- data to log\n\n    if batch_processor_manager:add_entry(conf, entry) then\n        return\n    end\n    -- create a new processor if not found\n\n    -- entries is an array table of entry, which can be processed in batch\n    local func = function(entries)\n        -- serialize to json array core.json.encode(entries)\n        -- process/send data\n        return true\n        -- return false, err_msg, first_fail if failed\n        -- first_fail(optional) indicates first_fail-1 entries have been successfully processed\n        -- and during processing of entries[first_fail], the error occurred. So the batch processor\n        -- only retries for the entries having index >= first_fail as per the retry policy.\n    end\n    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func)\nend\n```\n\n批处理器的配置将通过该插件的配置设置。\n举个例子：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"http-logger\": {\n                \"uri\": \"http://mockbin.org/bin/:ID\",\n                \"batch_max_size\": 10,\n                \"max_retry_count\": 1\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n如果你的插件只使用一个全局的批处理器，\n你可以直接使用它：\n\n```lua\nlocal entry = {...} -- data to log\nif log_buffer then\n    log_buffer:push(entry)\n    return\nend\n\nlocal config_bat = {\n    name = config.name,\n    retry_delay = config.retry_delay,\n    ...\n}\n\nlocal err\n-- entries is an array table of entry, which can be processed in batch\nlocal func = function(entries)\n    ...\n    return true\n    -- return false, err_msg, first_fail if failed\nend\nlog_buffer, err = batch_processor:new(func, config_bat)\n\nif not log_buffer then\n    core.log.warn(\"error when creating the batch processor: \", err)\n    return\nend\n\nlog_buffer:push(entry)\n```\n\n注意：请确保批处理的最大值（条目数）在函数执行的范围内。\n刷新批处理的计时器基于 `inactive_timeout` 配置运行。因此，为了获得最佳使用效果，\n保持 `inactive_timeout` 小于 `buffer_duration`。\n"
  },
  {
    "path": "docs/zh/latest/benchmark.md",
    "content": "---\ntitle: 压力测试\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### 测试环境\n\n使用谷歌云的服务器进行测试，型号为 n1-highcpu-8 (8 vCPUs, 7.2 GB memory)\n\n我们最多只使用 4 核去运行 APISIX，剩下的 4 核用于系统和压力测试工具 [wrk](https://github.com/wg/wrk)。\n\n### 测试反向代理\n\n我们把 APISIX 当做反向代理来使用，不开启任何插件，响应体的大小为 1KB。\n\n#### QPS\n\n下图中 x 轴为 CPU 的使用个数，y 轴为每秒处理的请求数：\n\n![benchmark-1](../../assets/images/benchmark-1.jpg)\n\n#### 延时\n\n请注意 y 轴延时的单位是**微秒（μs）**，而不是毫秒：\n\n![latency-1](../../assets/images/latency-1.jpg)\n\n#### 火焰图\n\n火焰图的采样结果：\n\n![flamegraph-1](../../assets/images/flamegraph-1.jpg)\n\n如果你需要在本地服务器上运行基准测试，你需要同时运行另一个 NGINX 实例来监听 80 端口：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1,\n            \"127.0.0.2:80\": 1\n        }\n    }\n}'\n```\n\n在完成配置并安装 [wrk](https://github.com/wg/wrk/) 之后，可以使用以下命令进行测试：\n\n```bash\nwrk -d 60 --latency http://127.0.0.1:9080/hello\n```\n\n### 测试反向代理，开启 2 个插件\n\n我们把 APISIX 当做反向代理来使用，开启限速和 prometheus 插件，响应体的大小为 1KB。\n\n#### QPS\n\n下图中 x 轴为 CPU 的使用个数，y 轴为每秒处理的请求数：\n\n![benchmark-2](../../assets/images/benchmark-2.jpg)\n\n#### Latency\n\n请注意 y 轴延时的单位是**微秒（μs）**，而不是毫秒：\n\n![latency-2](../../assets/images/latency-2.jpg)\n\n#### 火焰图\n\n火焰图的采样结果：\n![火焰图采样结果](../../assets/images/flamegraph-2.jpg)\n\n如果你需要在本地服务器上运行基准测试，你需要同时运行另一个 NGINX 实例来监听 80 端口：\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 999999999,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        },\n        \"prometheus\":{}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1,\n            \"127.0.0.2:80\": 1\n        }\n    }\n}'\n```\n\n在完成配置并安装 [wrk](https://github.com/wg/wrk/) 之后，可以使用以下命令进行测试：\n\n```bash\nwrk -d 60 --latency http://127.0.0.1:9080/hello\n```\n\n有关如何运行基准测试的更多参考，你可以查看此[PR](https://github.com/apache/apisix/pull/6136)和此[脚本](https://gist.github.com/membphis/137db97a4bf64d3653aa42f3e016bd01)。\n\n:::tip\n\n如果您想使用大量连接运行基准测试，您可能需要更新 [**keepalive**](https://github.com/apache/apisix/blob/master/conf/config.yaml.example#L241) 配置，将配置添加到 [`config.yaml`](https://github.com/apache/apisix/blob/master/conf/config.yaml) 并重新加载 APISIX。否则超过配置数量的连接将成为短连接。你可以使用以下命令运行大量连接的基准测试：\n\n```bash\nwrk -t200 -c5000 -d30s http://127.0.0.1:9080/hello\n```\n\n如果你需要了解更多信息，请参考：[ngx_http_upstream_module](http://nginx.org/en/docs/http/ngx_http_upstream_module.html)。\n\n:::\n"
  },
  {
    "path": "docs/zh/latest/build-apisix-dev-environment-on-mac.md",
    "content": "---\nid: build-apisix-dev-environment-on-mac\ntitle: 在 Mac 上构建开发环境\ndescription: 本文介绍了如何用 Docker 的方式在 Mac 上快速构建 API 网关 Apache APISIX 的开发环境。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n如果你希望快速的在你的 Mac 平台上构建和开发 APISIX，你可以参考本教程。\n\n:::note\n\n本教程适合需要在 Mac 平台快速开始入门阶段开发的情况，如果你想要更进一步，有更好的开发体验，更好的选择是 Linux-based 虚拟机，或是直接使用这类系统作为你的开发环境。\n\n你可以在[这里](install-dependencies.md#安装)看到具体支持的系统。\n\n:::\n\n## 快速构建 Apache APISIX 开发环境\n\n### 实现思路\n\n我们通过 Docker 来构建 Apache APISIX 的测试环境，在容器启动时将 Apache APISIX 的源代码挂载到容器内，就可以做到在容器内构建以及运行测试用例。\n\n### 实现步骤\n\n首先，我们需要拉取 APISIX 源码，并构建一个可以运行测试用例以及编译运行 Apache APISIX 的镜像：\n\n```shell\ngit clone https://github.com/apache/apisix.git\ncd apisix\ndocker build -t apisix-dev-env -f example/build-dev-image.dockerfile .\n```\n\n然后，我们要启动 Etcd：\n\n```shell\ndocker run -d --name etcd-apisix --net=host pachyderm/etcd:v3.5.2\n```\n\n挂载 APISIX 目录并启动开发环境容器：\n\n```shell\ndocker run -d --name apisix-dev-env --net=host -v $(pwd):/apisix:rw apisix-dev-env:latest\n```\n\n最后，构建 Apache APISIX 运行时并配置测试环境：\n\n```shell\ndocker exec -it apisix-dev-env make deps\ndocker exec -it apisix-dev-env ln -s /usr/bin/openresty /usr/bin/nginx\n```\n\n### 启动和停止 APISIX\n\n```shell\ndocker exec -it apisix-dev-env make run\ndocker exec -it apisix-dev-env make stop\n```\n\n:::note\n\n如果你在运行 `make run` 时收到类似 `nginx: [emerg] bind() to unix:/apisix/logs/worker_events.sock failed (95: Operation not supported)` 的错误消息，请使用此解决方案。\n\n更改你的 Docker-Desktop 的 `File Sharing` 设置：\n\n![Docker-Desktop File Sharing 设置](../../assets/images/update-docker-desktop-file-sharing.png)\n\n修改为 `gRPC FUSE` 或 `osxfs` 都可以解决此问题。\n\n:::\n\n### 运行指定测试用例\n\n```shell\ndocker exec -it apisix-dev-env prove t/admin/routes.t\n```\n"
  },
  {
    "path": "docs/zh/latest/building-apisix.md",
    "content": "---\nid: building-apisix\ntitle: 源码安装 APISIX\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 贡献代码\n  - 构建 APISIX\n  - 源码安装 APISIX\ndescription: 本文介绍了如何在本地使用源码安装 API 网关 Apache APISIX 来构建开发环境。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n如果你希望为 APISIX 做出贡献或配置开发环境，你可以参考本教程。\n\n如果你想通过其他方式安装 APISIX，你可以参考[安装指南](./installation-guide.md)。\n\n:::note\n\n如果你想为特定的环境或打包 APISIX，请参考 [apisix-build-tools](https://github.com/api7/apisix-build-tools)。\n\n:::\n\n## 源码安装 APISIX\n\n首先，我们需要指定需要安装的版本`APISIX_VERSION`:\n\n```shell\nAPISIX_BRANCH='release/3.13'\n```\n\n然后，你可以运行以下命令，从 Github 克隆 APISIX 源码：\n\n```shell\ngit clone --depth 1 --branch ${APISIX_BRANCH} https://github.com/apache/apisix.git apisix-${APISIX_BRANCH}\n```\n\n你可以从[下载页面](https://apisix.apache.org/downloads/)下载源码包。但是官网的源码包缺少测试用例，可能会对你后续操作产生困扰。\n\n另外，你也可以在该页面找到 APISIX Dashboard 和 APISIX Ingress Controller 的源码包。\n\n安装之前，请安装[OpenResty](https://openresty.org/en/installation.html)。\n\n然后切换到 APISIX 源码的目录，创建依赖项并安装 APISIX，命令如下所示：\n\n```shell\ncd apisix-${APISIX_BRANCH}\nmake deps\nmake install\n```\n\n该命令将安装 APISIX 运行时依赖的 Lua 库以及 `apisix-runtime` 和 `apisix` 命令。\n\n:::note\n\n如果你在运行 `make deps` 时收到类似 `Could not find header file for LDAP/PCRE/openssl` 的错误消息，请使用此解决方案。\n\n`luarocks` 支持自定义编译时依赖项（请参考：[配置文件格式](https://github.com/luarocks/luarocks/wiki/Config-file-format)）。你可以使用第三方工具安装缺少的软件包并将其安装目录添加到 `luarocks` 变量表中。此方法适用于 macOS、Ubuntu、CentOS 和其他类似操作系统。\n\n此处仅给出 macOS 的具体解决步骤，其他操作系统的解决方案类似：\n\n1. 安装 `openldap`：\n\n   ```shell\n   brew install openldap\n   ```\n\n2. 使用以下命令命令找到本地安装目录：\n\n   ```shell\n   brew --prefix openldap\n   ```\n\n3. 将路径添加到项目配置文件中（选择两种方法中的一种即可）：\n   1. 你可以使用 `luarocks config` 命令设置 `LDAP_DIR`：\n\n      ```shell\n      luarocks config variables.LDAP_DIR /opt/homebrew/cellar/openldap/2.6.1\n      ```\n\n   2. 你还可以更改 `luarocks` 的默认配置文件。打开 `~/.luaorcks/config-5.1.lua` 文件并添加以下内容：\n\n      ```shell\n      variables = { LDAP_DIR = \"/opt/homebrew/cellar/openldap/2.6.1\", LDAP_INCDIR = \"/opt/homebrew/cellar/openldap/2.6.1/include\", }\n      ```\n\n      `/opt/homebrew/cellar/openldap/` 是 `brew` 在 macOS(Apple Silicon) 上安装 `openldap` 的默认位置。`/usr/local/opt/openldap/` 是 brew 在 macOS(Intel) 上安装 openldap 的默认位置。\n\n:::\n\n如果你不再需要 APISIX，可以执行以下命令卸载：\n\n```shell\nmake uninstall && make undeps\n```\n\n:::danger\n\n该操作将删除所有相关文件。\n\n:::\n\n## 安装 etcd\n\nAPISIX 默认使用 [etcd](https://github.com/etcd-io/etcd) 来保存和同步配置。在运行 APISIX 之前，你需要在你的机器上安装 etcd。\n\n<Tabs\n  groupId=\"os\"\n  defaultValue=\"linux\"\n  values={[\n    {label: 'Linux', value: 'linux'},\n    {label: 'macOS', value: 'mac'},\n  ]}>\n<TabItem value=\"linux\">\n\n```shell\nETCD_VERSION='3.4.18'\nwget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz\ntar -xvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz && \\\n  cd etcd-v${ETCD_VERSION}-linux-amd64 && \\\n  sudo cp -a etcd etcdctl /usr/bin/\nnohup etcd >/tmp/etcd.log 2>&1 &\n```\n\n</TabItem>\n\n<TabItem value=\"mac\">\n\n```shell\nbrew install etcd\nbrew services start etcd\n```\n\n</TabItem>\n</Tabs>\n\n## 管理 APISIX 服务\n\n运行以下命令初始化 NGINX 配置文件和 etcd。\n\n```shell\napisix init\n```\n\n:::tip\n\n你可以运行 `apisix help` 命令，查看返回结果，获取其他操作命令及其描述。\n\n:::\n\n运行以下命令测试配置文件，APISIX 将根据 `config.yaml` 生成 `nginx.conf`，并检查 `nginx.conf` 的语法是否正确。\n\n```shell\napisix test\n```\n\n最后，你可以使用以下命令运行 APISIX。\n\n```shell\napisix start\n```\n\n如果需要停止 APISIX，你可以使用 `apisix quit` 或者 `apisix stop` 命令。\n\n`apisix quit` 将正常关闭 APISIX，该指令确保在停止之前完成所有收到的请求。\n\n```shell\napisix quit\n```\n\n`apisix stop` 命令会强制关闭 APISIX 并丢弃所有请求。\n\n```shell\napisix stop\n```\n\n## 为 APISIX 构建 APISIX-Runtime\n\nAPISIX 的一些特性需要在 OpenResty 中引入额外的 NGINX 模块。\n\n如果要使用这些功能，你需要构建一个自定义的 OpenResty 发行版（APISIX-Runtime）。请参考 [apisix-build-tools](https://github.com/api7/apisix-build-tools) 配置你的构建环境并进行构建。\n\n## 运行测试用例\n\n以下步骤展示了如何运行 APISIX 的测试用例：\n\n1. 安装 `perl` 的包管理器 [cpanminus](https://metacpan.org/pod/App::cpanminus#INSTALLATION)。\n2. 通过 `cpanm` 来安装 [test-nginx](https://github.com/openresty/test-nginx) 的依赖：\n\n   ```shell\n   sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)\n   ```\n\n3. 将 `test-nginx` 源码克隆到本地：\n\n   ```shell\n   git clone https://github.com/openresty/test-nginx.git\n   ```\n\n4. 运行以下命令将当前目录添加到 Perl 的模块目录：\n\n   ```shell\n   export PERL5LIB=.:$PERL5LIB\n   ```\n\n   你可以通过运行以下命令指定 NGINX 二进制路径：\n\n   ```shell\n   TEST_NGINX_BINARY=/usr/local/bin/openresty prove -Itest-nginx/lib -r t\n   ```\n\n5. 运行测试：\n\n   ```shell\n   make test\n   ```\n\n:::note\n\n部分测试需要依赖外部服务和修改系统配置。如果想要完整地构建测试环境，请参考 [ci/linux_openresty_common_runner.sh](https://github.com/apache/apisix/blob/master/ci/linux_openresty_common_runner.sh)。\n\n:::\n\n### 故障排查\n\n以下是运行 APISIX 测试用例的常见故障排除步骤。\n\n出现 `Error unknown directive \"lua_package_path\" in /API_ASPIX/apisix/t/servroot/conf/nginx.conf` 报错，是因为默认的 NGINX 安装路径未找到，解决方法如下：\n\n- Linux 默认安装路径：\n\n  ```shell\n  export PATH=/usr/local/openresty/nginx/sbin:$PATH\n  ```\n\n### 运行指定的测试用例\n\n使用以下命令运行指定的测试用例：\n\n```shell\nprove -Itest-nginx/lib -r t/plugin/openid-connect.t\n```\n\n如果你想要了解更多信息，请参考 [testing framework](../../en/latest/internal/testing-framework.md)。\n"
  },
  {
    "path": "docs/zh/latest/certificate.md",
    "content": "---\ntitle: 证书\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n`APISIX` 支持通过 TLS 扩展 SNI 实现加载特定的 SSL 证书以实现对 https 的支持。\n\nSNI（Server Name Indication）是用来改善 SSL 和 TLS 的一项特性，它允许客户端在服务器端向其发送证书之前向服务器端发送请求的域名，服务器端根据客户端请求的域名选择合适的 SSL 证书发送给客户端。\n\n### 单一域名指定\n\n通常情况下一个 SSL 证书只包含一个静态域名，配置一个 `ssl` 参数对象，它包括 `cert`、`key`和`sni`三个属性，详细如下：\n\n* `cert`：SSL 密钥对的公钥，pem 格式\n* `key`：SSL 密钥对的私钥，pem 格式\n* `snis`：SSL 证书所指定的一个或多个域名，注意在设置这个参数之前，你需要确保这个证书对应的私钥是有效的。\n\n创建一个包含证书和密钥，单一域名 SNI 的 SSL 对象：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat t/certs/apisix.crt)\"'\",\n     \"key\": \"'\"$(cat t/certs/apisix.key)\"'\",\n     \"snis\": [\"test.com\"]\n}'\n```\n\n创建路由：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/get\",\n    \"hosts\": [\"test.com\"],\n    \"methods\": [\"GET\"],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n\n测试：\n\n```shell\ncurl --resolve 'test.com:9443:127.0.0.1' https://test.com:9443/get -k -vvv\n\n* Added test.com:9443:127.0.0.1 to DNS cache\n* About to connect() to test.com port 9443 (#0)\n*   Trying 127.0.0.1...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*   subject: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*   start date: Jun 24 22:18:05 2019 GMT\n*   expire date: May 31 22:18:05 2119 GMT\n*   issuer: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*   SSL certificate verify result: self-signed certificate (18), continuing anyway.\n> GET /get HTTP/2\n> Host: test.com:9443\n> user-agent: curl/7.81.0\n> accept: */*\n```\n\n### 泛域名\n\n一个 SSL 证书的域名也可能包含泛域名，如 `*.test.com`，它代表所有以 `test.com` 结尾的域名都可以使用该证书。比如 `*.test.com`，可以匹配 `www.test.com`、`mail.test.com`。\n\n以下是在 APISIX 中配置泛域名 SNI 的 SSL 证书的示例。\n\n创建一个包含证书和密钥，泛域名 SNI 的 SSL 对象：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat t/certs/apisix.crt)\"'\",\n     \"key\": \"'\"$(cat t/certs/apisix.key)\"'\",\n     \"snis\": [\"*.test.com\"]\n}'\n```\n\n创建路由：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/hello\",\n    \"hosts\": [\"*.test.com\"],\n    \"methods\": [\"GET\"],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n测试：\n\n```shell\ncurl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/get -k -vvv\n\n* Added www.test.com:9443:127.0.0.1 to DNS cache\n* Hostname www.test.com was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* Connected to www.test.com (127.0.0.1) port 9443 (#0)\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*  start date: Jun 24 22:18:05 2019 GMT\n*  expire date: May 31 22:18:05 2119 GMT\n*  issuer: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com\n*  SSL certificate verify result: self signed certificate (18), continuing anyway.\n> GET /get HTTP/2\n> Host: www.test.com:9443\n> user-agent: curl/7.74.0\n> accept: */*\n```\n\n### 多域名的情况\n\n如果一个 SSL 证书包含多个独立域名，比如 `www.test.com` 和 `mail.test.com`，你可以把它们都放入 `snis` 数组中，就像这样：\n\n```json\n{\n    \"snis\": [\"www.test.com\", \"mail.test.com\"]\n}\n```\n\n### 单域名多证书的情况\n\n如果你期望为一个域名配置多张证书，例如以此来同时支持使用 ECC 和 RSA\n的密钥交换算法，那么你可以将额外的证书和私钥（第一张证书和其私钥依然使用 `cert` 和 `key`）配置在 `certs` 和 `keys` 中。\n\n* `certs`：PEM 格式的 SSL 证书列表\n* `keys`：PEM 格式的 SSL 证书私钥列表\n\n`APISIX` 会将相同下标的证书和私钥配对使用，因此 `certs` 和 `keys` 列表的长度必须一致。\n\n### 设置多个 CA 证书\n\nAPISIX 目前支持在多处设置 CA 证书，比如 [保护 Admin API](./mtls.md#保护-admin-api)，[保护 ETCD](./mtls.md#保护-etcd)，以及 [部署模式](../../en/latest/deployment-modes.md) 等。\n\n在这些地方，使用 `ssl_trusted_certificate` 或 `trusted_ca_cert` 来配置 CA 证书，但是这些配置最终将转化为 OpenResty 的 [lua_ssl_trusted_certificate](https://github.com/openresty/lua-nginx-module#lua_ssl_trusted_certificate) 指令。\n\n如果你需要在不同的地方指定不同的 CA 证书，你可以将这些 CA 证书制作成一个 CA bundle 文件，在需要用到 CA 证书的地方将配置指向这个文件。这样可以避免生成的 `lua_ssl_trusted_certificate` 存在多处并且互相覆盖的问题。\n\n下面用一个完整的例子来展示如何在 APISIX 设置多个 CA 证书。\n\n假设让 client 与 APISIX Admin API，APISIX 与 ETCD 之间都使用 mTLS 协议进行通信，目前有两张 CA 证书，分别是 `foo_ca.crt` 和 `bar_ca.crt`，用这两张 CA 证书各自签发 client 与 server 证书对，`foo_ca.crt` 及其签发的证书对用于保护 Admin API，`bar_ca.crt` 及其签发的证书对用于保护 ETCD。\n\n下表详细列出这个示例所涉及到的配置及其作用：\n\n| 配置              | 类型     | 用途                                                                                                               |\n| -------------    | ------- | -----------------------------------------------------------------------------------------------------------        |\n| foo_ca.crt       | CA 证书  | 签发客户端与 APISIX Admin API 进行 mTLS 通信所需的次级证书。                                                             |\n| foo_client.crt   | 证书     | 由 `foo_ca.crt` 签发，客户端使用，访问 APISIX Admin API 时证明自身身份的证书。                                             |\n| foo_client.key   | 密钥文件  | 由 `foo_ca.crt` 签发，客户端使用，访问 APISIX Admin API 所需的密钥文件。                                                  |\n| foo_server.crt   | 证书     | 由 `foo_ca.crt` 签发，APISIX 使用，对应 `admin_api_mtls.admin_ssl_cert` 配置项。                                 |\n| foo_server.key   | 密钥文件  | 由 `foo_ca.crt` 签发，APISIX 使用，对应 `admin_api_mtls.admin_ssl_cert_key` 配置项。                             |\n| admin.apisix.dev | 域名     | 签发 `foo_server.crt` 证书时使用的 Common Name，客户端通过该域名访问 APISIX Admin API                                     |\n| bar_ca.crt       | CA 证书  | 签发 APISIX 与 ETCD 进行 mTLS 通信所需的次级证书。                                                                       |\n| bar_etcd.crt     | 证书     | 由 `bar_ca.crt` 签发，ETCD 使用，对应 ETCD 启动命令中的 `--cert-file` 选项。                                              |\n| bar_etcd.key     | 密钥文件  | 由 `bar_ca.crt` 签发，ETCD 使用，对应 ETCD 启动命令中的 `--key-file` 选项。                                               |\n| bar_apisix.crt   | 证书     | 由 `bar_ca.crt` 签发，APISIX 使用，对应 `etcd.tls.cert` 配置项。                                                         |\n| bar_apisix.key   | 密钥文件  | 由 `bar_ca.crt` 签发，APISIX 使用，对应 `etcd.tls.key` 配置项。                                                          |\n| etcd.cluster.dev | 域名     | 签发 `bar_etcd.crt` 证书时使用的 Common Name，APISIX 与 ETCD 进行 mTLS 通信时，使用该域名作为 SNI。对应 `etcd.tls.sni` 配置项。|\n| apisix.ca-bundle | CA bundle | 由 `foo_ca.crt` 与 `bar_ca.crt` 合并而成，替代 `foo_ca.crt` 与 `bar_ca.crt`。                                |\n\n1. 制作 CA bundle 文件\n\n```shell\ncat /path/to/foo_ca.crt /path/to/bar_ca.crt > apisix.ca-bundle\n```\n\n2. 启动 ETCD 集群，并开启客户端验证\n\n先编写 `goreman` 配置，命名为 `Procfile-single-enable-mtls`，内容如下：\n\n```text\n# 运行 `go get github.com/mattn/goreman` 安装 goreman，用 goreman 执行以下命令：\netcd1: etcd --name infra1 --listen-client-urls https://127.0.0.1:12379 --advertise-client-urls https://127.0.0.1:12379 --listen-peer-urls http://127.0.0.1:12380 --initial-advertise-peer-urls http://127.0.0.1:12380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle\netcd2: etcd --name infra2 --listen-client-urls https://127.0.0.1:22379 --advertise-client-urls https://127.0.0.1:22379 --listen-peer-urls http://127.0.0.1:22380 --initial-advertise-peer-urls http://127.0.0.1:22380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle\netcd3: etcd --name infra3 --listen-client-urls https://127.0.0.1:32379 --advertise-client-urls https://127.0.0.1:32379 --listen-peer-urls http://127.0.0.1:32380 --initial-advertise-peer-urls http://127.0.0.1:32380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle\n```\n\n使用 `goreman` 来启动 ETCD 集群：\n\n```shell\ngoreman -f Procfile-single-enable-mtls start > goreman.log 2>&1 &\n```\n\n3. 更新 `config.yaml`\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key\n      - name: admin\n        key: edd1c9f034335f136f87ad84b625c8f1\n        role: admin\n    admin_listen:\n      ip: 127.0.0.1\n      port: 9180\n    https_admin: true\n    admin_api_mtls:\n      admin_ssl_ca_cert: /path/to/apisix.ca-bundle\n      admin_ssl_cert: /path/to/foo_server.crt\n      admin_ssl_cert_key: /path/to/foo_server.key\n\napisix:\n  ssl:\n    ssl_trusted_certificate: /path/to/apisix.ca-bundle\n\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:12379\"\n      - \"https://127.0.0.1:22379\"\n      - \"https://127.0.0.1:32379\"\n    tls:\n      cert: /path/to/bar_apisix.crt\n      key: /path/to/bar_apisix.key\n      sni: etcd.cluster.dev\n```\n\n4. 测试 Admin API\n\n启动 APISIX，如果 APISIX 启动成功，`logs/error.log` 中没有异常输出，表示 APISIX 与 ETCD 之间进行 mTLS 通信正常。\n\n用 curl 模拟客户端，与 APISIX Admin API 进行 mTLS 通信，并创建一条路由：\n\n```shell\ncurl -vvv \\\n    --resolve 'admin.apisix.dev:9180:127.0.0.1' https://admin.apisix.dev:9180/apisix/admin/routes/1 \\\n    --cert /path/to/foo_client.crt \\\n    --key /path/to/foo_client.key \\\n    --cacert /path/to/apisix.ca-bundle \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/get\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n\n如果输出以下 SSL 握手过程，表示 curl 与 APISIX Admin API 之间 mTLS 通信成功：\n\n```shell\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n```\n\n5. 验证 APISIX 代理\n\n```shell\ncurl http://127.0.0.1:9080/get -i\n\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 298\nConnection: keep-alive\nDate: Tue, 26 Jul 2022 16:31:00 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/2.14.1\n\n……\n```\n\nAPISIX 将请求代理到了上游 `httpbin.org` 的 `/get` 路径，并返回了 `HTTP/1.1 200 OK`。整个过程使用 CA bundle 替代 CA 证书是正常可用的。\n"
  },
  {
    "path": "docs/zh/latest/config.json",
    "content": "{\n  \"version\": \"3.15.0\",\n  \"sidebar\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"快速开始\",\n      \"items\": [\n        \"getting-started/README\",\n        \"getting-started/configure-routes\",\n        \"getting-started/load-balancing\",\n        \"getting-started/key-authentication\",\n        \"getting-started/rate-limiting\"\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"installation-guide\"\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"architecture-design/apisix\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"教程\",\n      \"items\": [\n        \"tutorials/expose-api\",\n        \"tutorials/protect-api\",\n        \"tutorials/observe-your-api\",\n        \"tutorials/health-check\",\n        \"tutorials/client-to-apisix-mtls\",\n        \"tutorials/keycloak-oidc\",\n        \"tutorials/manage-api-consumers\",\n        \"tutorials/cache-api-responses\"\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"APISIX 术语\",\n      \"items\": [\n        \"terminology/api-gateway\",\n        \"terminology/consumer\",\n        \"terminology/consumer-group\",\n        \"terminology/credential\",\n        \"terminology/global-rule\",\n        \"terminology/plugin\",\n        \"terminology/plugin-config\",\n        \"terminology/plugin-metadata\",\n        \"terminology/route\",\n        \"terminology/router\",\n        \"terminology/script\",\n        \"terminology/service\",\n        \"terminology/upstream\",\n        \"terminology/secret\"\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"插件\",\n      \"items\": [\n        {\n          \"type\": \"category\",\n          \"label\": \"人工智能\",\n          \"items\": [\n            \"plugins/ai-proxy\",\n            \"plugins/ai-proxy-multi\",\n            \"plugins/ai-rate-limiting\",\n            \"plugins/ai-prompt-guard\",\n            \"plugins/ai-aws-content-moderation\",\n            \"plugins/ai-aliyun-content-moderation\",\n            \"plugins/ai-prompt-decorator\",\n            \"plugins/ai-prompt-template\",\n            \"plugins/ai-rag\",\n            \"plugins/ai-request-rewrite\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"普通插件\",\n          \"items\": [\n            \"plugins/batch-requests\",\n            \"plugins/redirect\",\n            \"plugins/echo\",\n            \"plugins/gzip\",\n            \"plugins/brotli\",\n            \"plugins/real-ip\",\n            \"plugins/server-info\",\n            \"plugins/ext-plugin-pre-req\",\n            \"plugins/ext-plugin-post-req\",\n            \"plugins/ext-plugin-post-resp\",\n            \"plugins/ocsp-stapling\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"转换请求\",\n          \"items\": [\n            \"plugins/response-rewrite\",\n            \"plugins/proxy-rewrite\",\n            \"plugins/grpc-transcode\",\n            \"plugins/grpc-web\",\n            \"plugins/fault-injection\",\n            \"plugins/mocking\",\n            \"plugins/body-transformer\",\n            \"plugins/attach-consumer-label\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"身份认证\",\n          \"items\": [\n            \"plugins/authz-keycloak\",\n            \"plugins/authz-casdoor\",\n            \"plugins/wolf-rbac\",\n            \"plugins/key-auth\",\n            \"plugins/jwt-auth\",\n            \"plugins/jwe-decrypt\",\n            \"plugins/basic-auth\",\n            \"plugins/openid-connect\",\n            \"plugins/hmac-auth\",\n            \"plugins/authz-casbin\",\n            \"plugins/ldap-auth\",\n            \"plugins/opa\",\n            \"plugins/forward-auth\",\n            \"plugins/multi-auth\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"安全防护\",\n          \"items\": [\n            \"plugins/cors\",\n            \"plugins/uri-blocker\",\n            \"plugins/ip-restriction\",\n            \"plugins/ua-restriction\",\n            \"plugins/referer-restriction\",\n            \"plugins/consumer-restriction\",\n            \"plugins/csrf\",\n            \"plugins/public-api\",\n            \"plugins/gm\",\n            \"plugins/chaitin-waf\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"流量控制\",\n          \"items\": [\n            \"plugins/limit-req\",\n            \"plugins/limit-conn\",\n            \"plugins/limit-count\",\n            \"plugins/proxy-cache\",\n            \"plugins/request-validation\",\n            \"plugins/proxy-mirror\",\n            \"plugins/api-breaker\",\n            \"plugins/traffic-split\",\n            \"plugins/request-id\",\n            \"plugins/proxy-control\",\n            \"plugins/client-control\",\n            \"plugins/workflow\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"可观测性\",\n          \"items\": [\n            {\n              \"type\": \"category\",\n              \"label\": \"数据链路\",\n              \"items\": [\n                \"plugins/zipkin\",\n                \"plugins/skywalking\",\n                \"plugins/opentelemetry\"\n              ]\n            },\n            {\n              \"type\": \"category\",\n              \"label\": \"数据指标\",\n              \"items\": [\n                \"plugins/prometheus\",\n                \"plugins/node-status\",\n                \"plugins/datadog\"\n              ]\n            },\n            {\n              \"type\": \"category\",\n              \"label\": \"日志采集\",\n              \"items\": [\n                \"plugins/http-logger\",\n                \"plugins/skywalking-logger\",\n                \"plugins/tcp-logger\",\n                \"plugins/kafka-logger\",\n                \"plugins/rocketmq-logger\",\n                \"plugins/udp-logger\",\n                \"plugins/clickhouse-logger\",\n                \"plugins/syslog\",\n                \"plugins/log-rotate\",\n                \"plugins/error-log-logger\",\n                \"plugins/sls-logger\",\n                \"plugins/google-cloud-logging\",\n                \"plugins/splunk-hec-logging\",\n                \"plugins/file-logger\",\n                \"plugins/loggly\",\n                \"plugins/elasticsearch-logger\",\n                \"plugins/tencent-cloud-cls\",\n                \"plugins/loki-logger\"\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"无服务器架构\",\n          \"items\": [\n            \"plugins/serverless\",\n            \"plugins/azure-functions\",\n            \"plugins/openwhisk\",\n            \"plugins/aws-lambda\",\n            \"plugins/openfunction\"\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"其它协议\",\n          \"items\": [\n            \"plugins/dubbo-proxy\",\n            \"plugins/mqtt-proxy\",\n            \"plugins/http-dubbo\"\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"相关 API\",\n      \"items\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"admin-api\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"control-api\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"status-api\"\n        }\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"dashboard\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开发者\",\n      \"items\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"building-apisix\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"build-apisix-dev-environment-on-mac\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"support-fips-in-apisix\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"external-plugin\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"wasm\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"CODE_STYLE\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"plugin-develop\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"debug-mode\"\n        }\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"FAQ\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"其它\",\n      \"items\": [\n        {\n          \"type\": \"category\",\n          \"label\": \"服务发现\",\n          \"items\": [\n            \"discovery\",\n            \"discovery/dns\",\n            \"discovery/nacos\",\n            \"discovery/eureka\",\n            \"discovery/control-plane-service-discovery\",\n            \"discovery/kubernetes\"\n          ]\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"router-radixtree\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"stream-proxy\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"grpc-proxy\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"customize-nginx-configuration\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"certificate\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"apisix-variable\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"batch-processor\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"benchmark\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"install-dependencies\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"mtls\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"debug-function\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"profile\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"ssl-protocol\"\n        },\n        {\n          \"type\": \"doc\",\n          \"id\": \"http3\"\n        }\n      ]\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"CHANGELOG\"\n    },\n    {\n      \"type\": \"doc\",\n      \"id\": \"upgrade-guide-from-2.15.x-to-3.0.0\"\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/zh/latest/control-api.md",
    "content": "---\ntitle: Control API\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\ncontrol API 可以被用来：\n\n* 暴露 APISIX 内部状态信息\n* 控制单个 APISIX 的数据平面的行为\n\n默认情况下，control API 是启用的，监听 `127.0.0.1:9090`。你可以通过修改 `apisix/conf/config.yaml` 中的 control 部分来更改设置，如下：\n\n```yaml\napisix:\n  ...\n  enable_control: true\n  control:\n    ip: \"127.0.0.1\"\n    port: 9090\n```\n\n插件的 control API 在默认情况下不支持参数匹配，如果想启用参数匹配功能可以在 control 部分添加 `router: 'radixtree_uri_with_parameter'`\n\n注意：control API server 不应该被配置成监听公网地址。\n\n## 通过插件添加的 control API\n\nAPISIX 中一些插件添加了自己的 control API。如果你对他们感兴趣，请参阅对应插件的文档。\n\n## 独立于插件的 control API\n\n以下是支持的 API:\n\n### GET /v1/schema\n\n引入自 2.2 版本\n\n使用以下格式返回被该 APISIX 实例使用的 json schema：\n\n```json\n{\n    \"main\": {\n        \"route\": {\n            \"properties\": {...}\n        },\n        \"upstream\": {\n            \"properties\": {...}\n        },\n        ...\n    },\n    \"plugins\": {\n        \"example-plugin\": {\n            \"consumer_schema\": {...},\n            \"metadata_schema\": {...},\n            \"schema\": {...},\n            \"type\": ...,\n            \"priority\": 0,\n            \"version\": 0.1\n        },\n        ...\n    },\n    \"stream-plugins\": {\n        \"mqtt-proxy\": {\n            ...\n        },\n        ...\n    }\n}\n```\n\n只有启用了的插件才会被包含在返回结果中 `plugins` 部分。（返回结果中的）一些插件可能会缺失如 `consumer_schema` 或者 `type` 字段，这取决于插件的定义。\n\n### GET /v1/healthcheck\n\n引入自 2.3 版本\n\n使用以下格式返回当前的 [health check](./tutorials/health-check.md) 状态\n\n```json\n[\n  {\n    \"nodes\": [\n      {\n        \"ip\": \"52.86.68.46\",\n        \"counter\": {\n          \"http_failure\": 0,\n          \"success\": 0,\n          \"timeout_failure\": 0,\n          \"tcp_failure\": 0\n        },\n        \"port\": 80,\n        \"status\": \"healthy\"\n      },\n      {\n        \"ip\": \"100.24.156.8\",\n        \"counter\": {\n          \"http_failure\": 5,\n          \"success\": 0,\n          \"timeout_failure\": 0,\n          \"tcp_failure\": 0\n        },\n        \"port\": 80,\n        \"status\": \"unhealthy\"\n      }\n    ],\n    \"name\": \"/apisix/routes/1\",\n    \"type\": \"http\"\n  }\n]\n\n```\n\n每个 entry 包含以下字段：\n\n* name: 资源 ID，健康检查的报告对象。\n* type: 健康检查类型，取值为 `[\"http\", \"https\", \"tcp\"]`。\n* nodes: 检查节点列表。\n* nodes[i].ip: IP 地址。\n* nodes[i].port: 端口。\n* nodes[i].status: 状态：`[\"healthy\", \"unhealthy\", \"mostly_healthy\", \"mostly_unhealthy\"]`。\n* nodes[i].counter.success: 成功计数器。\n* nodes[i].counter.http_failure: HTTP 访问失败计数器。\n* nodes[i].counter.tcp_failure: TCP 连接或读写的失败计数器。\n* nodes[i].counter.timeout_failure: 超时计数器。\n\n用户也可以通过 `/v1/healthcheck/$src_type/$src_id` 来获取指定 health checker 的状态。\n\n例如，`GET /v1/healthcheck/upstreams/1` 返回：\n\n```json\n{\n  \"nodes\": [\n    {\n      \"ip\": \"52.86.68.46\",\n      \"counter\": {\n        \"http_failure\": 0,\n        \"success\": 2,\n        \"timeout_failure\": 0,\n        \"tcp_failure\": 0\n      },\n      \"port\": 80,\n      \"status\": \"healthy\"\n    },\n    {\n      \"ip\": \"100.24.156.8\",\n      \"counter\": {\n        \"http_failure\": 5,\n        \"success\": 0,\n        \"timeout_failure\": 0,\n        \"tcp_failure\": 0\n      },\n      \"port\": 80,\n      \"status\": \"unhealthy\"\n    }\n  ],\n  \"type\": \"http\"\n  \"name\": \"/apisix/routes/1\"\n}\n\n```\n\n:::note\n\n只有一个上游满足以下条件时，它的健康检查状态才会出现在结果里面：\n\n* 上游配置了健康检查。\n* 上游在任何一个 worker 进程处理过客户端请求。\n\n:::\n\n如果你使用浏览器访问该 API，你将得到一个网页：\n\n![Health Check Status Page](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/health_check_status_page.png)\n\n### POST /v1/gc\n\n引入自 2.8 版本\n\n在 http 子系统中触发一次全量 GC\n\n注意，当你启用 stream proxy 时，APISIX 将为 stream 子系统运行另一个 Lua 虚拟机。它不会触发这个 Lua 虚拟机中的全量 GC。\n\n### GET /v1/plugin_metadatas\n\n引入自 3.0.0 版本\n\n打印所有插件的元数据：\n\n```json\n[\n    {\n        \"log_format\": {\n            \"upstream_response_time\": \"$upstream_response_time\"\n        },\n        \"id\": \"file-logger\"\n    },\n    {\n        \"ikey\": 1,\n        \"skey\": \"val\",\n        \"id\": \"example-plugin\"\n    }\n]\n```\n\n### GET /v1/plugin_metadata/{plugin_name}\n\n引入自 3.0.0 版本\n\n打印指定插件的元数据：\n\n```json\n{\n    \"log_format\": {\n        \"upstream_response_time\": \"$upstream_response_time\"\n    },\n    \"id\": \"file-logger\"\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/customize-nginx-configuration.md",
    "content": "---\ntitle: 自定义 Nginx 配置\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nAPISIX 使用的 Nginx 配置是通过模板文件 `apisix/cli/ngx_tpl.lua` 以及 `apisix/cli/config.lua` 和`conf/config.yaml` 中的参数生成的。\n\n在执行完 `./bin/apisix start`，你可以在 `conf/nginx.conf` 看到生成的 Nginx 配置文件。\n\n如果你需要自定义 Nginx 配置，请阅读 `conf/config.default.example` 中的 `nginx_config`。你可以在 `conf/config.yaml` 中覆盖默认值。例如，你可以在 `conf/nginx.conf` 中通过配置 `xxx_snippet` 条目注入一些代码片段：\n\n```yaml\n...\n# config.yaml 里面的内容\nnginx_config:\n    main_configuration_snippet: |\n        daemon on;\n    http_configuration_snippet: |\n        server\n        {\n            listen 45651;\n            server_name _;\n            access_log off;\n\n            location /ysec_status {\n                req_status_show;\n                allow 127.0.0.1;\n                deny all;\n            }\n        }\n\n        chunked_transfer_encoding on;\n\n    http_server_configuration_snippet: |\n        set $my \"var\";\n    http_admin_configuration_snippet: |\n        log_format admin \"$request_time $pipe\";\n    http_end_configuration_snippet: |\n        server_names_hash_bucket_size 128;\n    stream_configuration_snippet: |\n        tcp_nodelay off;\n...\n```\n\n注意`nginx_config`及其子项的格式缩进，在执行`./bin/apisix start`时，错误的缩进将导致更新`conf/nginx.conf`文件失败。\n"
  },
  {
    "path": "docs/zh/latest/dashboard.md",
    "content": "---\ntitle: Apache APISIX Dashboard\nid: dashboard\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 概述\n\n[Apache APISIX Dashboard](https://github.com/apache/apisix-dashboard) 为用户提供了一个直观的 Web 界面来操作和管理 Apache APISIX。APISIX 内置了 Dashboard UI，默认启用，让用户可以通过图形界面轻松配置路由、插件、上游服务等。\n\n## 配置 Dashboard\n\n### 启用或关闭 Dashboard\n\nApache APISIX 默认启用内嵌 Dashboard。如需修改此设置，请编辑 `conf/config.yaml` 文件：\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    # 启用内嵌 APISIX Dashboard\n    enable_admin_ui: true\n```\n\n**配置说明：**\n\n- `enable_admin_ui: true` - 启用内嵌 Dashboard（默认启用）\n- `enable_admin_ui: false` - 关闭内嵌 Dashboard\n\n修改配置后，重启 Apache APISIX 生效。\n\n### 限制 IP 访问\n\nApache APISIX 支持设置 Admin API 的 IP 访问白名单，防止 Apache APISIX 被非法访问和攻击。\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n    admin:\n        # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow\n        allow_admin:\n            - 127.0.0.0/24\n```\n\n### Admin API Key\n\nDashboard 通过 Admin API 与 Apache APISIX 交互，需要正确的 Admin API Key 进行身份验证。\n\n#### 配置\n\n在 `conf/config.yaml` 中配置 Admin API Key：\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n      -\n        name: admin\n        role: admin\n        # 使用简单的 Admin API Key 存在安全风险，部署到生产环境时请及时更新\n        key: edd1c9f034335f136f87ad84b625c8f1\n```\n\n也支持通过环境变量配置：\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n      - name: admin\n        # 从环境变量读取\n        key: ${{ADMIN_KEY}}\n        role: admin\n```\n\n使用前需设置环境变量：\n\n```bash\nexport ADMIN_KEY=your-secure-api-key\n```\n\n修改配置后需重启 Apache APISIX 生效。\n\n#### 在 Dashboard 中使用\n\n访问 Dashboard，以 `http://127.0.0.1:9180/ui` 为例。\n\n在未配置 Admin API Key 时，设置模态框将会弹出：\n\n![Apache APISIX Dashboard - Need Admin Key](../../assets/images/dashboard-need-admin-key.png)\n\n如果不小心关闭了设置模态框，也可以点击导航栏右侧的按钮 <img src=\"../../assets/images/dashboard-settings-btn-icon.png\" alt=\"Apache APISIX Dashboard - Settings btn icon\" width=\"25px\" /> 再次打开。\n\n![Apache APISIX Dashboard - Reopen Settings Modal](../../assets/images/dashboard-reopen-settings-modal.png)\n\n接下来，填入上一小节中配置的 Admin API Key，Dashboard 会自动发起请求。如配置错误，Dashboard 仍将在右上角显示 `failed to check token`：\n\n![Apache APISIX Dashboard - Admin Key is wrong](../../assets/images/dashboard-admin-key-is-wrong.png)\n\n如配置正确，Dashboard 将不再显示 `failed to check token`。此时，点击 `X` 或空白处，关闭设置模态框，即可正常使用。\n\n![Apache APISIX Dashboard - Admin Key is correct](../../assets/images/dashboard-admin-key-is-correct.png)\n\n## FAQ\n\n### 为什么 Apache APISIX Dashboard 进行了重构？\n\nApache APISIX Dashboard 经历了多个版本的演进：\n\n- **1.x 版本**：基于 Vue.js 的简单 Web UI，直接调用 Admin API\n- **2.x 版本**：采用 React + Ant Design Pro 前端架构，引入了 Golang 后端和数据库存储\n\n在 2.x 版本发展过程中，由于社区对功能的需求不断增加，项目逐渐变得复杂臃肿，同时与 APISIX 主版本的同步也面临挑战。\n\n经过充分讨论，社区决定明确 Dashboard 的定位和功能边界，回归轻量化设计，确保与 APISIX 核心的紧密集成和版本同步。\n\n未来 Apache APISIX Dashboard 将专注于：\n\n- **简化架构**：移除不必要的复杂组件，回归 Dashboard 的本质功能\n- **增强用户体验**：提供直观、高效的管理界面\n- **版本同步**：与 Apache APISIX 主版本保持同步发布\n- **生产就绪**：确保稳定性和可靠性，适合生产环境使用\n\n更多规划信息请查看：[Dashboard 路线图](https://github.com/apache/apisix-dashboard/issues/2981)\n\n### 发布周期\n\n项目不再独立发布，且已弃用 release 和 tag 的版本标记方式。\n\n在 Apache APISIX 发布时，将直接基于指定的 Git commit hash 构建 Dashboard，并将产物嵌入到 Apache APISIX 中。\n\n### 旧版本的 Apache APISIX Dashboard\n\nApache APISIX Dashboard 3.0.1 是在重构前，使用旧发布模式的最后一个版本。它仅应与 Apache APISIX 3.0 一起使用，任何更高或更低版本未进行测试。\n\n如有需要，可阅读 [旧版本的 Apache APISIX Dashboard 文档](https://apache-apisix.netlify.app/docs/dashboard/user_guide/)。\n\n如果您是 Apache APISIX 或 Apache APISIX Dashboard 的新用户，强烈建议您始终以最新版本而不是任何历史版本开始。\n\n### 贡献指南\n\n请阅读 [Apache APISIX Dashboard README](https://github.com/apache/apisix-dashboard/blob/master/README.md)。\n"
  },
  {
    "path": "docs/zh/latest/debug-function.md",
    "content": "---\ntitle: 调试功能\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## `5xx` 响应状态码\n\n500、502、503 等类似的 `5xx` 状态码，是由于服务器错误而响应的状态码，当一个请求出现 `5xx` 状态码时；它可能来源于 `APISIX` 或 `Upstream` 。如何识别这些响应状态码的来源，是一件很有意义的事，它能够快速的帮助我们确定问题的所在。(当修改 `conf/config.yaml` 的配置 `show_upstream_status_in_response_header` 为 `true` 时，会返回所有上游状态码，不仅仅是 `5xx` 状态。)\n\n## 如何识别 `5xx` 响应状态码的来源\n\n在请求的响应头中，通过 `X-APISIX-Upstream-Status` 这个响应头，我们可以有效的识别 `5xx` 状态码的来源。当 `5xx` 状态码来源于 `Upstream` 时，在响应头中可以看到 `X-APISIX-Upstream-Status` 这个响应头，并且这个响应头的值为响应的状态码。当 `5xx` 状态码来源于 `APISIX` 时，响应头中没有 `X-APISIX-Upstream-Status` 的响应头信息。也就是只有 `5xx` 状态码来源于 `Upstream` 时，才会有 `X-APISIX-Upstream-Status` 响应头。\n\n## 示例\n\n示例 1：`502` 响应状态码来源于 `Upstream` (IP 地址不可用)\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n测试：\n\n```shell\n$ curl http://127.0.0.1:9080/hello -v\n......\n< HTTP/1.1 502 Bad Gateway\n< Date: Wed, 25 Nov 2020 14:40:22 GMT\n< Content-Type: text/html; charset=utf-8\n< Content-Length: 154\n< Connection: keep-alive\n< Server: APISIX/2.0\n< X-APISIX-Upstream-Status: 502\n<\n<html>\n<head><title>502 Bad Gateway</title></head>\n<body>\n<center><h1>502 Bad Gateway</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n\n```\n\n具有 `X-APISIX-Upstream-Status: 502` 的响应头。\n\n示例 2：`502` 响应状态码来源于 `APISIX`\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"fault-injection\": {\n            \"abort\": {\n                \"http_status\": 500,\n                \"body\": \"Fault Injection!\\n\"\n            }\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n测试：\n\n```shell\n$ curl http://127.0.0.1:9080/hello -v\n......\n< HTTP/1.1 500 Internal Server Error\n< Date: Wed, 25 Nov 2020 14:50:20 GMT\n< Content-Type: text/plain; charset=utf-8\n< Transfer-Encoding: chunked\n< Connection: keep-alive\n< Server: APISIX/2.0\n<\nFault Injection!\n```\n\n没有 `X-APISIX-Upstream-Status` 的响应头。\n\n示例 3：`Upstream` 具有多节点，并且所有节点不可用\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/upstreams/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"nodes\": {\n        \"127.0.0.3:1\": 1,\n        \"127.0.0.2:1\": 1,\n        \"127.0.0.1:1\": 1\n    },\n    \"retries\": 2,\n    \"type\": \"roundrobin\"\n}'\n```\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream_id\": \"1\"\n}'\n```\n\n测试：\n\n```shell\n$ curl http://127.0.0.1:9080/hello -v\n< HTTP/1.1 502 Bad Gateway\n< Date: Wed, 25 Nov 2020 15:07:34 GMT\n< Content-Type: text/html; charset=utf-8\n< Content-Length: 154\n< Connection: keep-alive\n< Server: APISIX/2.0\n< X-APISIX-Upstream-Status: 502, 502, 502\n<\n<html>\n<head><title>502 Bad Gateway</title></head>\n<body>\n<center><h1>502 Bad Gateway</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n具有 `X-APISIX-Upstream-Status: 502, 502, 502` 的响应头。\n"
  },
  {
    "path": "docs/zh/latest/debug-mode.md",
    "content": "---\ntitle: 调试模式\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### 基本调试模式\n\n设置 `conf/debug.yaml` 即可开启基本调试模式：\n\n```\nbasic:\n  enable: true\n#END\n```\n\n注意：在 APISIX 2.10 之前，开启基本调试模式曾经是设置 `conf/config.yaml` 中的 `apisix.enable_debug` 为 `true`。\n\n比如对 `/hello` 开启了 `limit-conn` 和 `limit-count` 插件，这时候应答头中会有 `Apisix-Plugins: limit-conn, limit-count`。\n\n```shell\n$ curl http://127.0.0.1:1984/hello -i\nHTTP/1.1 200 OK\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nApisix-Plugins: limit-conn, limit-count\nX-RateLimit-Limit: 2\nX-RateLimit-Remaining: 1\nServer: openresty\n\nhello world\n```\n\n如果这个信息无法通过 HTTP 应答头传递，比如插件在 stream 子系统里面执行，\n那么这个信息会以 warn 等级日志写入到错误日志中。\n\n### 高级调试模式\n\n设置 `conf/debug.yaml` 中的选项，开启高级调试模式。由于 APISIX 服务启动后是每秒定期检查该文件，\n当可以正常读取到 `#END` 结尾时，才认为文件处于写完关闭状态。\n\n根据文件最后修改时间判断文件内容是否有变化，如有变化则重新加载，如没变化则跳过本次检查。\n所以高级调试模式的开启、关闭都是热更新方式完成。\n\n| 名称                             | 必选项 | 说明                                                          | 默认值 |\n| ------------------------------- | ------ | ------------------------------------------------------------- | ------ |\n| hook_conf.enable                | 是     | 是否开启 hook 追踪调试。开启后将打印指定模块方法的请求参数或返回值。 | false  |\n| hook_conf.name                  | 是     | 开启 hook 追踪调试的模块列表名称。                               |        |\n| hook_conf.log_level             | 是     | 打印请求参数和返回值的日志级别。                                  | warn   |\n| hook_conf.is_print_input_args   | 是     | 是否打印输入参数。                                              | true   |\n| hook_conf.is_print_return_value | 是     | 是否打印返回值。                                                | true   |\n\n请看下面示例：\n\n```yaml\nhook_conf:\n  enable: false # 是否开启 hook 追踪调试\n  name: hook_phase # 开启 hook 追踪调试的模块列表名称\n  log_level: warn # 日志级别\n  is_print_input_args: true # 是否打印输入参数\n  is_print_return_value: true # 是否打印返回值\n\nhook_phase: # 模块函数列表，名字：hook_phase\n  apisix: # 引用的模块名称\n    - http_access_phase # 函数名：数组\n    - http_header_filter_phase\n    - http_body_filter_phase\n    - http_log_phase\n#END\n```\n\n### 动态高级调试模式\n\n动态高级调试模式是基于高级调试模式，可以由单个请求动态开启高级调试模式。设置 `conf/debug.yaml` 中的选项。\n\n示例：\n\n```yaml\nhttp_filter:\n  enable: true # 是否动态开启高级调试模式\n  enable_header_name: X-APISIX-Dynamic-Debug # 追踪携带此 header 的请求\n......\n#END\n```\n\n动态开启高级调试模式，示例：\n\n```shell\ncurl 127.0.0.1:9090/hello --header 'X-APISIX-Dynamic-Debug: foo'\n```\n\n注意：动态高级调试模式无法调试 `apisix.http_access_phase`，模块（因为请求进入 `apisix.http_access_phase` 模块后，才会判断是否动态开启高级调试模式）。\n"
  },
  {
    "path": "docs/zh/latest/discovery/control-plane-service-discovery.md",
    "content": "---\ntitle: 控制面服务发现\nkeywords:\n  - API 网关\n  - APISIX\n  - ZooKeeper\n  - Nacos\n  - APISIX-Seed\ndescription: 本文档介绍了如何在 API 网关 Apache APISIX 控制面通过 Nacos 和 Zookeeper 实现服务发现。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n本文档介绍了如何在 APISIX 控制面通过 Nacos 和 Zookeeper 实现服务发现。\n\n## APISIX-Seed 架构\n\nApache APISIX 在早期已经支持了数据面服务发现，现在 APISIX 也通过 [APISIX-Seed](https://github.com/api7/apisix-seed) 项目实现了控制面服务发现，下图为 APISIX-Seed 架构图。\n\n![control-plane-service-discovery](../../../assets/images/control-plane-service-discovery.png)\n\n图中的数字代表的具体信息如下：\n\n1. 通过 Admin API 向 APISIX 注册上游并指定服务发现类型。APISIX-Seed 将监听 etcd 中的 APISIX 资源变化，过滤服务发现类型并获取服务名称（如 ZooKeeper）；\n2. APISIX-Seed 将在服务注册中心（如 ZooKeeper）订阅指定的服务名称，以监控和更新对应的服务信息；\n3. 客户端向服务注册中心注册服务后，APISIX-Seed 会获取新的服务信息，并将更新后的服务节点写入 etcd；\n4. 当 APISIX-Seed 在 etcd 中更新相应的服务节点信息时，APISIX 会将最新的服务节点信息同步到内存中。\n\n:::note\n\n引入 APISIX-Seed 后，如果注册中心的服务变化频繁，etcd 中的数据也会频繁变化。因此，需要在启动 etcd 时设置 `--auto-compaction` 选项，用来定期压缩历史记录，避免耗尽 etcd 存储空间。详细信息请参考 [revisions](https://etcd.io/docs/v3.5/learning/api/#revisions)。\n\n:::\n\n## 为什么需要 APISIX-Seed？\n\n- 网络拓扑变得更简单\n\n  APISIX 不需要与每个注册中心保持网络连接，只需要关注 etcd 中的配置信息即可。这将大大简化网络拓扑。\n\n- 上游服务总数据量变小\n\n  由于 `registry` 的特性，APISIX 可能会在 Worker 中存储全量的 `registry` 服务数据，例如 Consul_KV。通过引入 APISIX-Seed，APISIX 的每个进程将不需要额外缓存上游服务相关信息。\n\n- 更容易管理\n\n  服务发现配置需要为每个 APISIX 实例配置一次。通过引入 APISIX-Seed，APISIX 将对服务注册中心的配置变化无感知。\n\n## 支持的服务发现类型\n\n目前已经支持了 ZooKeeper 和 Nacos，后续还将支持更多的服务注册中心，更多信息请参考：[APISIX Seed](https://github.com/api7/apisix-seed#apisix-seed-for-apache-apisix)。\n\n- 如果你想启用控制面 ZooKeeper 服务发现，请参考：[ZooKeeper 部署教程](https://github.com/api7/apisix-seed/blob/main/docs/zh/latest/zookeeper.md)。\n\n- 如果你想启用控制面 Nacos 服务发现，请参考：[Nacos 部署教程](https://github.com/api7/apisix-seed/blob/main/docs/zh/latest/nacos.md)。\n"
  },
  {
    "path": "docs/zh/latest/discovery/dns.md",
    "content": "---\ntitle: DNS\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 基于 DNS 的服务发现\n\n某些服务发现系统如 Consul，支持通过 DNS 提供系统信息。我们可以使用这种方法直接实现服务发现，七层与四层均支持。\n\n首先我们需要配置 DNS 服务器的地址：\n\n```yaml\n# 添加到 config.yaml\ndiscovery:\n   dns:\n     servers:\n       - \"127.0.0.1:8600\"          # 使用 DNS 服务器的真实地址\n```\n\n与在 Upstream 的 `nodes` 对象中配置域名不同的是，DNS 服务发现将返回所有的记录。例如按照以下的 upstream 配置：\n\n```json\n{\n    \"id\": 1,\n    \"discovery_type\": \"dns\",\n    \"service_name\": \"test.consul.service\",\n    \"type\": \"roundrobin\"\n}\n```\n\n之后 `test.consul.service` 将被解析为 `1.1.1.1` 和 `1.1.1.2`，这个结果等同于：\n\n```json\n{\n    \"id\": 1,\n    \"type\": \"roundrobin\",\n    \"nodes\": [\n        {\"host\": \"1.1.1.1\", \"weight\": 1},\n        {\"host\": \"1.1.1.2\", \"weight\": 1}\n    ]\n}\n```\n\n注意所有来自 `test.consul.service` 的 IP 都有相同的权重。\n\n解析的记录将根据它们的 TTL 来进行缓存。对于记录不在缓存中的服务，我们将默认按照 `SRV -> A -> AAAA -> CNAME` 的顺序进行查询，刷新缓存记录时，我们将从上次成功的类型开始尝试。也可以通过修改配置文件来自定义 DNS 的解析顺序。\n\n```yaml\n# 添加到 config.yaml\ndiscovery:\n   dns:\n     servers:\n       - \"127.0.0.1:8600\"          # 使用 DNS 服务器的真实地址\n     order:                        # DNS 解析的顺序\n       - last                      # \"last\" 表示从上次成功的类型开始\n       - SRV\n       - A\n       - AAAA\n       - CNAME\n\n```\n\n如果你想指定 upstream 服务器的端口，可以把以下内容添加到 `service_name`：\n\n```json\n{\n    \"id\": 1,\n    \"discovery_type\": \"dns\",\n    \"service_name\": \"test.consul.service:1980\",\n    \"type\": \"roundrobin\"\n}\n```\n\n另一种方法是通过 SRV 记录，见如下。\n\n### SRV 记录\n\n通过使用 SRV 记录你可以指定一个服务的端口和权重。\n\n假设你有一条这样的 SRV 记录：\n\n```\n; under the section of blah.service\nA       300 IN      A     1.1.1.1\nB       300 IN      A     1.1.1.2\nB       300 IN      A     1.1.1.3\n\n; name  TTL         type    priority    weight  port\nsrv     86400 IN    SRV     10          60      1980 A\nsrv     86400 IN    SRV     20          20      1981 B\n```\n\nUpstream 配置是这样的：\n\n```json\n{\n    \"id\": 1,\n    \"discovery_type\": \"dns\",\n    \"service_name\": \"srv.blah.service\",\n    \"type\": \"roundrobin\"\n}\n```\n\n效果等同于：\n\n```json\n{\n    \"id\": 1,\n    \"type\": \"roundrobin\",\n    \"nodes\": [\n        {\"host\": \"1.1.1.1\", \"port\": 1980, \"weight\": 60, \"priority\": -10},\n        {\"host\": \"1.1.1.2\", \"port\": 1981, \"weight\": 10, \"priority\": -20},\n        {\"host\": \"1.1.1.3\", \"port\": 1981, \"weight\": 10, \"priority\": -20}\n    ]\n}\n```\n\n注意 B 域名的两条记录均分权重。\n对于 SRV 记录，低优先级的节点被先选中，所以最后一项的优先级是负数。\n\n关于 0 权重的 SRV 记录，在 [RFC 2782](https://www.ietf.org/rfc/rfc2782.txt) 中是这么描述的：\n\n> 当没有任何候选服务器时，域管理员应使用权重为 0 的，使 RR 更为易读（噪音更少）。当存在权重大于 0 的记录时，权重为 0 的记录被选中的可能性很小。\n\n我们把权重为 0 的记录当作权重为 1，因此节点“被选中的可能性很小”，这也是处理此类记录的常用方法。\n\n对于端口为 0 的 SRV 记录，我们会使用上游协议的默认端口。\n你也可以在“service_name”字段中直接指定端口，比如“srv.blah.service:8848”。\n"
  },
  {
    "path": "docs/zh/latest/discovery/eureka.md",
    "content": "---\ntitle: eureka\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nApache APISIX 支持使用 [Eureka](https://github.com/Netflix/eureka#eureka) 做服务发现。\n详情请阅读 [支持的服务注册发现](../discovery.md#当前支持的注册中心) 。\n"
  },
  {
    "path": "docs/zh/latest/discovery/kubernetes.md",
    "content": "---\ntitle: Kubernetes\nkeywords:\n  - Kubernetes\n  - Apache APISIX\n  - 服务发现\n  - 集群\n  - API 网关\ndescription: 本文将介绍如何在 Apache APISIX 中基于 Kubernetes 进行服务发现以及相关问题汇总。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 基于 Kubernetes 的服务发现\n\nKubernetes 服务发现以 [_List-Watch_](https://kubernetes.io/docs/reference/using-api/api-concepts) 方式监听 [_Kubernetes_](https://kubernetes.io) 集群 [_Endpoints_](https://kubernetes.io/docs/concepts/services-networking/service) 资源的实时变化，并将其值存储到 ngx.shared.DICT 中。\n\n同时遵循 [_APISIX Discovery 规范_](../discovery.md) 提供了节点查询接口。\n\n## Kubernetes 服务发现的使用\n\n目前 Kubernetes 服务发现支持单集群和多集群模式，分别适用于待发现的服务分布在单个或多个 Kubernetes 的场景。\n\n### 单集群模式 Kubernetes 服务发现的配置格式\n\n单集群模式 Kubernetes 服务发现的完整配置如下：\n\n```yaml\ndiscovery:\n  kubernetes:\n    service:\n      # apiserver schema, options [http, https]\n      schema: https #default https\n\n      # apiserver host, options [ipv4, ipv6, domain, environment variable]\n      host: ${KUBERNETES_SERVICE_HOST} #default ${KUBERNETES_SERVICE_HOST}\n\n      # apiserver port, options [port number, environment variable]\n      port: ${KUBERNETES_SERVICE_PORT}  #default ${KUBERNETES_SERVICE_PORT}\n\n    client:\n      # serviceaccount token or token_file\n      token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n\n      #token: |-\n       # eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEif\n       # 6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEifeyJhbGciOiJSUzI1NiIsImtpZCI\n\n    default_weight: 50 # weight assigned to each discovered endpoint. default 50, minimum 0\n\n    # kubernetes discovery support namespace_selector\n    # you can use one of [equal, not_equal, match, not_match] filter namespace\n    namespace_selector:\n      # only save endpoints with namespace equal default\n      equal: default\n\n      # only save endpoints with namespace not equal default\n      #not_equal: default\n\n      # only save endpoints with namespace match one of [default, ^my-[a-z]+$]\n      #match:\n       #- default\n       #- ^my-[a-z]+$\n\n      # only save endpoints with namespace not match one of [default, ^my-[a-z]+$]\n      #not_match:\n       #- default\n       #- ^my-[a-z]+$\n\n    # kubernetes discovery support label_selector\n    # for the expression of label_selector, please refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\n    label_selector: |-\n      first=\"a\",second=\"b\"\n\n    # reserved lua shared memory size, 1m memory can store about 1000 pieces of endpoint\n    shared_size: 1m #default 1m\n\n    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints\n    watch_endpoint_slices: false #default false\n```\n\n如果 Kubernetes 服务发现运行在 Pod 内，你可以使用如下最简配置：\n\n```yaml\ndiscovery:\n  kubernetes: { }\n```\n\n如果 Kubernetes 服务发现运行在 Pod 外，你需要新建或选取指定的 [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/), 获取其 Token 值，然后使用如下配置：\n\n```yaml\ndiscovery:\n  kubernetes:\n    service:\n      schema: https\n      host: # enter apiserver host value here\n      port: # enter apiServer port value here\n    client:\n      token: # enter serviceaccount token value here\n      #token_file: # enter token file path here\n```\n\n### 单集群模式 Kubernetes 服务发现的查询接口\n\n单集群模式 Kubernetes 服务发现遵循 [_APISIX Discovery 规范_](../discovery.md) 提供节点查询接口。\n\n**函数：**\nnodes(service_name)\n\n**说明：**\nservice_name 必须满足格式：[namespace]/[name]:[portName]\n\n+ namespace: Endpoints 所在的命名空间\n\n+ name: Endpoints 的资源名\n\n+ portName: Endpoints 定义包含的 `ports.name` 值，如果 Endpoints 没有定义 `ports.name`，请依次使用 `targetPort`, `port` 代替。设置了 `ports.name` 的情况下，不能使用后两者。\n\n**返回值：**\n以如下 Endpoints 为例：\n\n  ```yaml\n  apiVersion: v1\n  kind: Endpoints\n  metadata:\n    name: plat-dev\n    namespace: default\n  subsets:\n    - addresses:\n        - ip: \"10.5.10.109\"\n        - ip: \"10.5.10.110\"\n      ports:\n        - port: 3306\n          name: port\n  ```\n\nnodes(\"default/plat-dev:port\") 调用会得到如下的返回值：\n\n  ```\n   {\n       {\n           host=\"10.5.10.109\",\n           port= 3306,\n           weight= 50,\n       },\n       {\n           host=\"10.5.10.110\",\n           port= 3306,\n           weight= 50,\n       },\n   }\n  ```\n\n### 多集群模式 Kubernetes 服务发现的配置格式\n\n多集群模式 Kubernetes 服务发现的完整配置如下：\n\n```yaml\ndiscovery:\n  kubernetes:\n  - id: release  # a custom name refer to the cluster, pattern ^[a-z0-9]{1,8}\n    service:\n      # apiserver schema, options [http, https]\n      schema: https #default https\n\n      # apiserver host, options [ipv4, ipv6, domain, environment variable]\n      host: \"1.cluster.com\"\n\n      # apiserver port, options [port number, environment variable]\n      port: \"6443\"\n\n    client:\n      # serviceaccount token or token_file\n      token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n\n      #token: |-\n       # eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEif\n       # 6Ikx5ME1DNWdnbmhQNkZCNlZYMXBsT3pYU3BBS2swYzBPSkN3ZnBESGpkUEEifeyJhbGciOiJSUzI1NiIsImtpZCI\n\n    default_weight: 50 # weight assigned to each discovered endpoint. default 50, minimum 0\n\n    # kubernetes discovery support namespace_selector\n    # you can use one of [equal, not_equal, match, not_match] filter namespace\n    namespace_selector:\n      # only save endpoints with namespace equal default\n      equal: default\n\n      # only save endpoints with namespace not equal default\n      #not_equal: default\n\n      # only save endpoints with namespace match one of [default, ^my-[a-z]+$]\n      #match:\n       #- default\n       #- ^my-[a-z]+$\n\n      # only save endpoints with namespace not match one of [default, ^my-[a-z]+$]\n      #not_match:\n       #- default\n       #- ^my-[a-z]+$\n\n    # kubernetes discovery support label_selector\n    # for the expression of label_selector, please refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels\n    label_selector: |-\n      first=\"a\",second=\"b\"\n\n    # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint\n    shared_size: 1m #default 1m\n\n    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints\n    watch_endpoint_slices: false #default false\n```\n\n多集群模式 Kubernetes 服务发现没有为 `service` 和 `client` 域填充默认值，你需要根据集群配置情况自行填充。\n\n### 多集群模式 Kubernetes 服务发现的查询接口\n\n多集群模式 Kubernetes 服务发现遵循 [_APISIX Discovery 规范_](../discovery.md) 提供节点查询接口。\n\n**函数：**\nnodes(service_name)\n\n**说明：**\nservice_name 必须满足格式：[id]/[namespace]/[name]:[portName]\n\n+ id: Kubernetes 服务发现配置中定义的集群 id 值\n\n+ namespace: Endpoints 所在的命名空间\n\n+ name: Endpoints 的资源名\n\n+ portName: Endpoints 定义包含的 `ports.name` 值，如果 Endpoints 没有定义 `ports.name`，请依次使用 `targetPort`, `port` 代替。设置了 `ports.name` 的情况下，不能使用后两者。\n\n**返回值：**\n以如下 Endpoints 为例：\n\n  ```yaml\n  apiVersion: v1\n  kind: Endpoints\n  metadata:\n    name: plat-dev\n    namespace: default\n  subsets:\n    - addresses:\n        - ip: \"10.5.10.109\"\n        - ip: \"10.5.10.110\"\n      ports:\n        - port: 3306\n          name: port\n  ```\n\nnodes(\"release/default/plat-dev:port\") 调用会得到如下的返回值：\n\n  ```\n   {\n       {\n           host=\"10.5.10.109\",\n           port= 3306,\n           weight= 50,\n       },\n       {\n           host=\"10.5.10.110\",\n           port= 3306,\n           weight= 50,\n       },\n   }\n  ```\n\n## Q&A\n\n**Q: 为什么只支持配置 token 来访问 Kubernetes APIServer?**\n\nA: 一般情况下，我们有三种方式可以完成与 Kubernetes APIServer 的认证：\n\n- mTLS\n- Token\n- Basic authentication\n\n因为 lua-resty-http 目前不支持 mTLS, Basic authentication 不被推荐使用，所以当前只实现了 Token 认证方式。\n\n**Q: APISIX 继承了 NGINX 的多进程模型，是否意味着每个 APISIX 工作进程都会监听 Kubernetes Endpoints？**\n\nA: Kubernetes 服务发现只使用特权进程监听 Kubernetes Endpoints，然后将其值存储到 `ngx.shared.DICT` 中，工作进程通过查询 `ngx.shared.DICT` 来获取结果。\n\n**Q: [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) 需要的权限有哪些？**\n\nA: [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) 需要集群级 [ get,list,watch ] endpoints 和 endpointslices 资源的的权限，其声明式定义如下：\n\n```yaml\nkind: ServiceAccount\napiVersion: v1\nmetadata:\n name: apisix-test\n namespace: default\n---\n\nkind: ClusterRole\napiVersion: rbac.authorization.k8s.io/v1\nmetadata:\n  name: apisix-test\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ endpoints]\n    verbs: [ get,list,watch ]\n  - apiGroups: [ \"discovery.k8s.io\" ]\n    resources: [ endpointslices ]\n    verbs: [ get,list,watch ]\n---\n\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: apisix-test\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: ClusterRole\n name: apisix-test\nsubjects:\n - kind: ServiceAccount\n   name: apisix-test\n   namespace: default\n```\n\n**Q: 怎样获取指定 [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) 的 Token 值？**\n\nA: 假定你指定的 [_ServiceAccount_](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) 资源名为“kubernetes-discovery“, 命名空间为“apisix”, 请按如下步骤获取其 Token 值。\n\n 1. 获取 _Secret_ 资源名。执行以下命令，输出的第一列内容就是目标 _Secret_ 资源名：\n\n ```shell\n kubectl -n apisix get secrets | grep kubernetes-discovery\n ```\n\n 2. 获取 Token 值。假定你获取到的 _Secret_ 资源名为 \"kubernetes-discovery-token-c64cv\", 执行以下命令，输出内容就是目标 Token 值：\n\n ```shell\n kubectl -n apisix get secret kubernetes-discovery-token-c64cv -o jsonpath={.data.token} | base64 -d\n ```\n\n## 调试 API\n\n它还提供了用于调试的控制 api。\n\n### 内存 Dump API\n\n```shell\nGET /v1/discovery/kubernetes/dump\n```\n\n例子\n\n```shell\n# curl http://127.0.0.1:9090/v1/discovery/kubernetes/dump | jq\n{\n  \"endpoints\": [\n    {\n      \"endpoints\": [\n        {\n          \"value\": \"{\\\"https\\\":[{\\\"host\\\":\\\"172.18.164.170\\\",\\\"port\\\":6443,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.171\\\",\\\"port\\\":6443,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.172\\\",\\\"port\\\":6443,\\\"weight\\\":50}]}\",\n          \"name\": \"default/kubernetes\"\n        },\n        {\n          \"value\": \"{\\\"metrics\\\":[{\\\"host\\\":\\\"172.18.164.170\\\",\\\"port\\\":2379,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.171\\\",\\\"port\\\":2379,\\\"weight\\\":50},{\\\"host\\\":\\\"172.18.164.172\\\",\\\"port\\\":2379,\\\"weight\\\":50}]}\",\n          \"name\": \"kube-system/etcd\"\n        },\n        {\n          \"value\": \"{\\\"http-85\\\":[{\\\"host\\\":\\\"172.64.89.2\\\",\\\"port\\\":85,\\\"weight\\\":50}]}\",\n          \"name\": \"test-ws/testing\"\n        }\n      ],\n      \"id\": \"first\"\n    }\n  ],\n  \"config\": [\n    {\n      \"default_weight\": 50,\n      \"id\": \"first\",\n      \"client\": {\n        \"token\": \"xxx\"\n      },\n      \"service\": {\n        \"host\": \"172.18.164.170\",\n        \"port\": \"6443\",\n        \"schema\": \"https\"\n      },\n      \"shared_size\": \"1m\"\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/discovery/nacos.md",
    "content": "---\ntitle: nacos\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 基于 [Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html) 的服务发现\n\n当前模块的性能有待改进：\n\n1. 并行发送请求。\n\n### Nacos 配置\n\n在文件 `conf/config.yaml` 中添加以下配置到：\n\n```yaml\ndiscovery:\n  nacos:\n    host:\n      - \"http://${username}:${password}@${host1}:${port1}\"\n    prefix: \"/nacos/v1/\"\n    fetch_interval: 30    # default 30 sec\n    weight: 100           # default 100\n    timeout:\n      connect: 2000       # default 2000 ms\n      send: 2000          # default 2000 ms\n      read: 5000          # default 5000 ms\n```\n\n也可以这样简洁配置（未配置项使用默认值）：\n\n```yaml\ndiscovery:\n  nacos:\n    host:\n      - \"http://192.168.33.1:8848\"\n```\n\n### Upstream 设置\n\n#### 七层\n\n例如，转发 URI 匹配 \"/nacos/*\" 的请求到一个上游服务，\n该服务在 Nacos 中的服务名是 APISIX-NACOS，查询地址是 http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS，创建路由时指定服务发现类型为 nacos。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacos/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\"\n    }\n}'\n```\n\n响应如下：\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/1\",\n    \"value\": {\n      \"id\": \"1\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\"\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacos\\/*\"\n    }\n  }\n}\n```\n\n#### 四层\n\nnacos 服务发现也支持在四层中使用，配置方式与七层的类似。\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n        \"scheme\": \"tcp\",\n        \"discovery_type\": \"nacos\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n### 参数\n\n| 名字         | 类型   | 可选项 | 默认值 | 有效值 | 说明                                                  |\n| ------------ | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ |\n| namespace_id | string | 可选    | public     |       | 服务所在的命名空间 |\n| group_name   | string | 可选    | DEFAULT_GROUP       |       | 服务所在的组 |\n\n#### 指定命名空间\n\n例如，转发 URI 匹配 \"/nacosWithNamespaceId/*\" 的请求到一个上游服务，\n该服务在 Nacos 中的服务名是 APISIX-NACOS，命名空间是 test_ns，查询地址是 http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS&namespaceId=test_ns，创建路由时指定服务发现类型为 nacos。\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacosWithNamespaceId/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\"\n        }\n    }\n}'\n```\n\n响应如下：\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/2\",\n    \"value\": {\n      \"id\": \"2\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\"\n        }\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacosWithNamespaceId\\/*\"\n    }\n  }\n}\n```\n\n#### 指定组\n\n例如，转发 URI 匹配 \"/nacosWithGroupName/*\" 的请求到一个上游服务，\n该服务在 Nacos 中的服务名是 APISIX-NACOS，组名是 test_group，查询地址是 http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS&groupName=test_group，创建路由时指定服务发现类型为 nacos。\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/3 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacosWithGroupName/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"group_name\": \"test_group\"\n        }\n    }\n}'\n```\n\n响应如下：\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/3\",\n    \"value\": {\n      \"id\": \"3\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"group_name\": \"test_group\"\n        }\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacosWithGroupName\\/*\"\n    }\n  }\n}\n```\n\n#### 同时指定命名空间和组\n\n例如，转发 URI 匹配 \"/nacosWithNamespaceIdAndGroupName/*\" 的请求到一个上游服务，\n该服务在 Nacos 中的服务名是 APISIX-NACOS，命名空间是 test_ns，组名是 test_group，查询地址是 http://192.168.33.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS&namespaceId=test_ns&groupName=test_group，创建路由时指定服务发现类型为 nacos。\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/4 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/nacosWithNamespaceIdAndGroupName/*\",\n    \"upstream\": {\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\",\n          \"group_name\": \"test_group\"\n        }\n    }\n}'\n```\n\n响应如下：\n\n```json\n{\n  \"node\": {\n    \"key\": \"\\/apisix\\/routes\\/4\",\n    \"value\": {\n      \"id\": \"4\",\n      \"create_time\": 1615796097,\n      \"status\": 1,\n      \"update_time\": 1615799165,\n      \"upstream\": {\n        \"hash_on\": \"vars\",\n        \"pass_host\": \"pass\",\n        \"scheme\": \"http\",\n        \"service_name\": \"APISIX-NACOS\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"nacos\",\n        \"discovery_args\": {\n          \"namespace_id\": \"test_ns\",\n          \"group_name\": \"test_group\"\n        }\n      },\n      \"priority\": 0,\n      \"uri\": \"\\/nacosWithNamespaceIdAndGroupName\\/*\"\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/discovery.md",
    "content": "---\ntitle: 集成服务发现注册中心\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 摘要\n\n当业务量发生变化时，需要对上游服务进行扩缩容，或者因服务器硬件故障需要更换服务器。如果网关是通过配置来维护上游服务信息，在微服务架构模式下，其带来的维护成本可想而知。再者因不能及时更新这些信息，也会对业务带来一定的影响，还有人为误操作带来的影响也不可忽视，所以网关非常必要通过服务注册中心动态获取最新的服务实例信息。架构图如下所示：\n\n![discovery through service registry](../../assets/images/discovery-cn.png)\n\n1. 服务启动时将自身的一些信息，比如服务名、IP、端口等信息上报到注册中心；各个服务与注册中心使用一定机制（例如心跳）通信，如果注册中心与服务长时间无法通信，就会注销该实例；当服务下线时，会删除注册中心的实例信息；\n2. 网关会准实时地从注册中心获取服务实例信息；\n3. 当用户通过网关请求服务时，网关从注册中心获取的实例列表中选择一个进行代理；\n\n常见的注册中心：Eureka, Etcd, Consul, Nacos, Zookeeper 等\n\n## 如何扩展注册中心？\n\n### 基本步骤\n\nAPISIX 要扩展注册中心其实是件非常容易的事情，其基本步骤如下：\n\n1. 在 `apisix/discovery/` 目录中添加注册中心客户端的实现；\n2. 实现用于初始化的 `_M.init_worker()` 函数以及用于获取服务实例节点列表的 `_M.nodes(service_name)` 函数；\n3. 将注册中心数据转换为 APISIX 格式的数据；\n\n### 以 Eureka 举例\n\n#### 实现 eureka 客户端\n\n首先，在 `apisix/discovery` 下创建 `eureka` 目录；\n\n其次，在 `apisix/discovery/eureka` 目录中添加 [`init.lua`](https://github.com/apache/apisix/blob/master/apisix/discovery/init.lua);\n\n然后在 `init.lua` 实现用于初始化的 `init_worker` 函数以及用于获取服务实例节点列表的 `nodes` 函数即可：\n\n```lua\nlocal _M = {\n    version = 0.1,\n}\n\n\nfunction _M.nodes(service_name)\n    ... ...\nend\n\n\nfunction _M.init_worker()\n    ... ...\nend\n\n\nreturn _M\n```\n\n最后，在 `apisix/discovery/eureka` 下的 `schema.lua` 里面提供 YAML 配置的 schema。\n\n#### Eureka 与 APISIX 之间数据转换逻辑\n\nAPISIX 是通过 `upstream.nodes` 来配置上游服务的，所以使用注册中心后，通过注册中心获取服务的所有 node 后，赋值给 `upstream.nodes` 来达到相同的效果。那么 APISIX 是怎么将 Eureka 的数据转成 node 的呢？假如从 Eureka 获取如下数据：\n\n```json\n{\n  \"applications\": {\n      \"application\": [\n          {\n              \"name\": \"USER-SERVICE\",                 # 服务名称\n              \"instance\": [\n                  {\n                      \"instanceId\": \"192.168.1.100:8761\",\n                      \"hostName\": \"192.168.1.100\",\n                      \"app\": \"USER-SERVICE\",          # 服务名称\n                      \"ipAddr\": \"192.168.1.100\",      # 实例 IP 地址\n                      \"status\": \"UP\",                 # 状态\n                      \"overriddenStatus\": \"UNKNOWN\",  # 覆盖状态\n                      \"port\": {\n                          \"$\": 8761,                  # 端口\n                          \"@enabled\": \"true\"          # 开始端口\n                      },\n                      \"securePort\": {\n                          \"$\": 443,\n                          \"@enabled\": \"false\"\n                      },\n                      \"metadata\": {\n                          \"management.port\": \"8761\",\n                          \"weight\": 100               # 权重，需要通过 spring boot 应用的 eureka.instance.metadata-map.weight 进行配置\n                      },\n                      \"homePageUrl\": \"http://192.168.1.100:8761/\",\n                      \"statusPageUrl\": \"http://192.168.1.100:8761/actuator/info\",\n                      \"healthCheckUrl\": \"http://192.168.1.100:8761/actuator/health\",\n                      ... ...\n                  }\n              ]\n          }\n      ]\n  }\n}\n```\n\n解析 instance 数据步骤：\n\n1. 首先要选择状态为“UP”的实例：overriddenStatus 值不为 \"UNKNOWN\" 以 overriddenStatus 为准，否则以 status 的值为准；\n2. IP 地址：以 ipAddr 的值为 IP; 并且必须是 IPv4 或 IPv6 格式的；\n3. 端口：端口取值规则是，如果 port[\"@enabled\"] 等于 \"true\" 那么使用 port[\"\\$\"] 的值；如果 securePort[\"@enabled\"] 等于 \"true\" 那么使用 securePort[\"$\"] 的值；\n4. 权重：权重取值顺序是，先判断 `metadata.weight` 是否有值，如果没有，则取配置中的 `eureka.weight` 的值，如果还没有，则取默认值`100`；\n\n这个例子转成 APISIX nodes 的结果如下：\n\n```json\n[\n  {\n    \"host\": \"192.168.1.100\",\n    \"port\": 8761,\n    \"weight\": 100,\n    \"metadata\": {\n      \"management.port\": \"8761\"\n    }\n  }\n]\n```\n\n## 注册中心配置\n\n### 初始化服务发现\n\n首先要在 `conf/config.yaml` 文件中增加如下配置，添加不同的服务发现客户端，以便在使用过程中动态选择：\n\n```yaml\ndiscovery:\n  eureka: ...\n```\n\n此名称要与 `apisix/discovery/` 目录中实现对应注册中心的文件名保持一致。\n\n现已支持注册中心有：Eureka。\n\n### Eureka 的配置\n\n在 `conf/config.yaml` 增加如下格式的配置：\n\n```yaml\ndiscovery:\n  eureka:\n    host: # it's possible to define multiple eureka hosts addresses of the same eureka cluster.\n      - \"http://${username}:${password}@${eureka_host1}:${eureka_port1}\"\n      - \"http://${username}:${password}@${eureka_host2}:${eureka_port2}\"\n    prefix: \"/eureka/\"\n    fetch_interval: 30 # 从 eureka 中拉取数据的时间间隔，默认 30 秒\n    weight: 100 # default weight for node\n    timeout:\n      connect: 2000 # 连接 eureka 的超时时间，默认 2000ms\n      send: 2000 # 向 eureka 发送数据的超时时间，默认 2000ms\n      read: 5000 # 从 eureka 读数据的超时时间，默认 5000ms\n```\n\n通过 `discovery.eureka.host` 配置 eureka 的服务器地址。\n\n如果 eureka 的地址是 `http://127.0.0.1:8761/` ，并且不需要用户名和密码验证的话，配置如下：\n\n```yaml\ndiscovery:\n  eureka:\n    host:\n      - \"http://127.0.0.1:8761\"\n    prefix: \"/eureka/\"\n```\n\n## upstream 配置\n\n### 七层\n\nAPISIX 是通过 `upstream.discovery_type` 选择使用的服务发现，`upstream.service_name` 与注册中心的服务名进行关联。下面是将 URL 为 \"/user/\\*\" 的请求路由到注册中心名为 \"USER-SERVICE\" 的服务上例子：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/user/*\",\n    \"upstream\": {\n        \"service_name\": \"USER-SERVICE\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"eureka\"\n    }\n}'\n\nHTTP/1.1 201 Created\nDate: Sat, 31 Aug 2019 01:17:15 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\n{\"node\":{\"value\":{\"uri\":\"\\/user\\/*\",\"upstream\": {\"service_name\": \"USER-SERVICE\", \"type\": \"roundrobin\", \"discovery_type\": \"eureka\"}},\"createdIndex\":61925,\"key\":\"\\/apisix\\/routes\\/1\",\"modifiedIndex\":61925}}\n```\n\n因为上游的接口 URL 可能会有冲突，通常会在网关通过前缀来进行区分：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/a/*\",\n    \"plugins\": {\n        \"proxy-rewrite\" : {\n            \"regex_uri\": [\"^/a/(.*)\", \"/${1}\"]\n        }\n    },\n    \"upstream\": {\n        \"service_name\": \"A-SERVICE\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"eureka\"\n    }\n}'\n\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/b/*\",\n    \"plugins\": {\n        \"proxy-rewrite\" : {\n            \"regex_uri\": [\"^/b/(.*)\", \"/${1}\"]\n        }\n    },\n    \"upstream\": {\n        \"service_name\": \"B-SERVICE\",\n        \"type\": \"roundrobin\",\n        \"discovery_type\": \"eureka\"\n    }\n}'\n```\n\n假如 A-SERVICE 和 B-SERVICE 都提供了一个 `/test` 的接口，通过上面的配置，可以通过 `/a/test` 访问 A-SERVICE 的 `/test` 接口，通过 `/b/test` 访问 B-SERVICE 的 `/test` 接口。\n\n**注意**：配置 `upstream.service_name` 后 `upstream.nodes` 将不再生效，而是使用从注册中心的数据来替换，即使注册中心的数据是空的。\n\n### 四层\n\neureka 服务发现也支持在四层中使用，配置方式与七层的类似。\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n        \"scheme\": \"tcp\",\n        \"discovery_type\": \"eureka\",\n        \"service_name\": \"APISIX-EUREKA\",\n        \"type\": \"roundrobin\"\n    }\n}'\nHTTP/1.1 200 OK\nDate: Fri, 30 Dec 2022 03:52:19 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.0.0\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nAccess-Control-Expose-Headers: *\nAccess-Control-Max-Age: 3600\nX-API-VERSION: v3\n\n{\"key\":\"\\/apisix\\/stream_routes\\/1\",\"value\":{\"remote_addr\":\"127.0.0.1\",\"upstream\":{\"hash_on\":\"vars\",\"type\":\"roundrobin\",\"discovery_type\":\"eureka\",\"scheme\":\"tcp\",\"pass_host\":\"pass\",\"service_name\":\"APISIX-EUREKA\"},\"id\":\"1\",\"create_time\":1672106762,\"update_time\":1672372339}}\n```\n"
  },
  {
    "path": "docs/zh/latest/external-plugin.md",
    "content": "---\ntitle: 外部插件\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 什么是 External Plugin 和 Plugin Runner\n\nAPISIX 支持使用 Lua 语言编写插件，这种类型的插件在 APISIX 内部执行。\n有时候你想使用其他语言来开发插件，因此，APISIX 支持以 `Sidecar` 的方式加载和运行你写的插件。\n这里的 `Sidecar` 就是 Plugin Runner，你写的插件叫做 External Plugin。\n\n## 它是如何工作的\n\n![external-plugin](../../assets/images/external-plugin.png)\n\n当你在 APISIX 中配置了一个 Plugin Runner，APISIX 将以子进程的方式运行该 Plugin Runner。\n\n该子进程与 APISIX 进程从属相同用户。当重启或者重新加载 APISIX 时，该 Plugin Runner 也将被重启。\n\n一旦你为指定路由配置了 `ext-plugin-*` 插件，\n匹配该路由的请求将触发从 APISIX 到  Plugin Runner 的 RPC 调用。\n\n Plugin Runner 将处理该 RPC 调用，在其侧创建一个请求，运行 External Plugin 并将结果返回给 APISIX。\n\n External Plugin 及其执行顺序在这里 `ext-plugin-*` 配置。与其他插件一样，External Plugin 可以动态启用和重新配置。\n\n## 它是如何实现的\n\n如果你对 Plugin Runner 内部实现感兴趣，请参考这份文档：\n[The Implementation of Plugin Runner](../../en/latest/internal/plugin-runner.md)\n\n## 支持的 Plugin Runner\n\n- Java: https://github.com/apache/apisix-java-plugin-runner\n- Go: https://github.com/apache/apisix-go-plugin-runner\n- Python: https://github.com/apache/apisix-python-plugin-runner\n- JavaScript: https://github.com/zenozeng/apisix-javascript-plugin-runner\n\n## 在 APISIX 中配置 Plugin Runner\n\n在生产环境运行 Plugin Runner，添加以下配置到 `config.yaml`：\n\n```yaml\next-plugin:\n  cmd: [\"blah\"] # replace it to the real runner executable according to the runner you choice\n```\n\nAPISIX 将以子进程的方式管理该 Plugin Runner。\n\n注意：在 Mac 上，APISIX `v2.6` 无法管理该 Plugin Runner。\n\n在开发过程中，我们希望单独运行 Plugin Runner，这样就可以重新启动它，而无需先重新启动 APISIX。\n\n通过指定环境变量 `APISIX_LISTEN_ADDRESS`, 我们可以使 Plugin Runner 监听一个固定的地址。\n例如：\n\n```bash\nAPISIX_LISTEN_ADDRESS=unix:/tmp/x.sock\n```\n\n此时，Plugin Runner 将监听 `/tmp/x.sock`\n\n同时，你需要配置 APISIX 发送 RPC 请求到该固定的地址：\n\n```yaml\next-plugin:\n  # cmd: [\"blah\"] # don't configure the executable!\n  path_for_test: \"/tmp/x.sock\" # without 'unix:' prefix\n```\n\n在生产环境，不应该使用 `path_for_test`，此时监听的地址将动态生成。\n\n## 常见问题\n\n### 由 APISIX 管理时，Plugin Runner 无法访问我的环境变量\n\n自 `v2.7`，APISIX 可以将环境变量传递给 Plugin Runner。\n\n然而，默认情况下，Nginx 将隐藏所有环境变量。所以你需要首先在 `conf/config.yaml` 中声明环境变量：\n\n```yaml\nnginx_config:\n  envs:\n    - MY_ENV_VAR\n```\n\n### APISIX 使用 SIGKILL 终止 Plugin Runner，而不是使用 SIGTERM！\n\n自 `v2.7`，当跑在 OpenResty 1.19+ 时，APISIX 将使用 SIGTERM 来停止 Plugin Runner。\n\n但是，APISIX 需要等待 Plugin Runner 退出，这样我们才能确保资源得以被释放。\n\n因此，我们先发送 SIGTERM。然后在 1 秒后，如果 Plugin Runner 仍然在运行，我们将发送 SIGKILL。\n"
  },
  {
    "path": "docs/zh/latest/getting-started/README.md",
    "content": "---\ntitle: 入门指南\ndescription: 本教程使用脚本在本地环境快速安装 Apache APISIX，并且通过管理 API 来验证是否安装成功。\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/\" />\n</head>\n\n> 本教程由 [API7.ai](https://api7.ai/) 编写。\n\nApache APISIX 是 Apache 软件基金会下的[顶级项目](https://projects.apache.org/project.html?apisix)，由 API7.ai 开发并捐赠。它是一个具有动态、实时、高性能等特点的云原生 API 网关。\n\n你可以使用 APISIX 网关作为所有业务的流量入口，它提供了动态路由、动态上游、动态证书、A/B 测试、灰度发布（金丝雀发布）、蓝绿部署、限速、防攻击、收集指标、监控报警、可观测、服务治理等功能。\n\n本教程使用脚本在本地环境快速安装 Apache APISIX，并且通过管理 API 来验证是否安装成功。\n\n## 前置条件\n\n快速启动脚本需要以下条件：\n\n* 已安装 [Docker](https://docs.docker.com/get-docker/)，用于部署  **etcd** 和 **APISIX**。\n* 已安装 [curl](https://curl.se/)，用于验证 APISIX 是否安装成功。\n\n## 安装 APISIX\n\n:::caution\n\n为了提供更好的体验，管理 API 默认无需授权，请在生产环境中打开授权开关。\n\n:::\nAPISIX 可以借助 quickstart 脚本快速安装并启动：\n\n```shell\ncurl -sL https://run.api7.ai/apisix/quickstart | sh\n```\n\n该命令启动 _apisix-quickstart_ 和 _etcd_ 两个容器，APISIX 使用 etcd 保存和同步配置。APISIX 和 etcd 容器使用 Docker 的 [**host**](https://docs.docker.com/network/host/) 网络模式，因此可以从本地直接访问。\n\n如果一切顺利，将输出如下信息：\n\n```text\n✔ APISIX is ready!\n```\n\n## 验证\n\n你可以通过 curl 来访问正在运行的 APISIX 实例。比如，你可以发送一个简单的 HTTP 请求来验证 APISIX 运行状态是否正常：\n\n```shell\ncurl \"http://127.0.0.1:9080\" --head | grep Server\n```\n\n如果一切顺利，将输出如下信息：\n\n```text\nServer: APISIX/Version\n```\n\n这里的 `Version` 是指你已经安装的 APISIX 版本，比如 `APISIX/3.3.0`。\n\n现在，你已经成功安装并运行了 APISIX！\n\nAPISIX 提供内置的 Dashboard UI，可访问 `http://127.0.0.1:9180/ui` 使用。更多指南请阅读 [Apache APISIX Dashboard](../dashboard.md)。\n\n## 下一步\n\n如果你已经成功地安装了 APISIX 并且正常运行，那么你可以继续进行下面的教程。\n\n* [配置路由](configure-routes.md)\n* [负载均衡](load-balancing.md)\n* [限速](rate-limiting.md)\n* [密钥验证](key-authentication.md)\n"
  },
  {
    "path": "docs/zh/latest/getting-started/configure-routes.md",
    "content": "---\ntitle: 配置路由\nslug: /getting-started/configure-routes\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/configure-routes\" />\n</head>\n\n> 本教程由 [API7.ai](https://api7.ai/) 编写。\n\nApache APISIX 使用 _routes_ 来提供灵活的网关管理功能，在一个请求中，_routes_ 包含了访问路径和上游目标等信息。\n\n本教程将引导你创建一个 route 并验证它，你可以参考以下步骤：\n\n1. 创建一个指向 [httpbin.org](http://httpbin.org)的 _upstream_。\n2. 使用 _cURL_ 发送一个请求，了解 APISIX 的代理和转发请求机制。\n\n## Route 是什么\n\nRoute（也称之为路由）是访问上游目标的路径，在 [Apache APISIX](https://api7.ai/apisix) 中，Route 首先通过预定的规则来匹配客户端请求，然后加载和执行相应的插件，最后将请求转发至特定的 Upstream。\n\n在 APISIX 中，一个最简单的 Route 仅由匹配路径和 Upstream 地址两个信息组成。\n\n## Upstream 是什么\n\nUpstream（也称之为上游）是一组具备相同功能的节点集合，它是对虚拟主机的抽象。Upstream 可以通过预先配置的规则对多个服务节点进行负载均衡。\n\n## 前置条件\n\n1. 参考[入门指南](./README.md)完成 APISIX 的安装。\n\n## 创建路由\n\n你可以创建一个路由，将客户端的请求转发至 [httpbin.org](http://httpbin.org)（这个网站能测试 HTTP 请求和响应的各种信息）。\n\n通过下面的命令，你将创建一个路由，把请求`http://127.0.0.1:9080/ip` 转发至 [httpbin.org/ip](http://httpbin.org/ip)：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"getting-started-ip\",\n  \"uri\": \"/ip\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n如果配置成功，将会返回 `HTTP/1.1 201 Created`。\n\n## 验证\n\n```shell\ncurl \"http://127.0.0.1:9080/ip\"\n```\n\n你将会得到类似下面的返回：\n\n```text\n{\n  \"origin\": \"183.94.122.205\"\n}\n```\n\n## 下一步\n\n本教程创建的路由仅对应一个上游目标。在下个教程中，你将会学习如何配置多个上游目标的负载均衡。\n"
  },
  {
    "path": "docs/zh/latest/getting-started/key-authentication.md",
    "content": "---\ntitle: 密钥验证\nslug: /getting-started/key-authentication\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/key-authentication\" />\n</head>\n\n> 本教程由 [API7.ai](https://api7.ai/) 编写。\n\nAPI 网关主要作用是连接 API 消费者和提供者。出于安全考虑，在访问内部资源之前，应先对消费者进行身份验证和授权。\n\n![身份验证](https://static.apiseven.com/uploads/2023/02/08/8mRaK3v1_consumer.png)\n\nAPISIX 拥有灵活的插件扩展系统，目前有很多可用于用户身份验证和授权的插件。例如：\n\n- [Key Authentication](https://apisix.apache.org/zh/docs/apisix/plugins/key-auth/)\n- [Basic Authentication](https://apisix.apache.org/zh/docs/apisix/plugins/basic-auth/)\n- [JSON Web Token (JWT) Authentication](https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth/)\n- [Keycloak](https://apisix.apache.org/zh/docs/apisix/plugins/authz-keycloak/)\n- [Casdoor](https://apisix.apache.org/zh/docs/apisix/plugins/authz-casdoor/)\n- [Wolf RBAC](https://apisix.apache.org/zh/docs/apisix/plugins/wolf-rbac/)\n- [OpenID Connect](https://apisix.apache.org/zh/docs/apisix/plugins/openid-connect/)\n- [Central Authentication Service (CAS)](https://apisix.apache.org/zh/docs/apisix/plugins/cas-auth/)\n- [HMAC](https://apisix.apache.org/zh/docs/apisix/plugins/hmac-auth/)\n- [Casbin](https://apisix.apache.org/zh/docs/apisix/plugins/authz-casbin/)\n- [LDAP](https://apisix.apache.org/zh/docs/apisix/plugins/ldap-auth/)\n- [Open Policy Agent (OPA)](https://apisix.apache.org/zh/docs/apisix/plugins/opa/)\n- [Forward Authentication](https://apisix.apache.org/zh/docs/apisix/plugins/forward-auth/)\n- [Multiple Authentications](https://apisix.apache.org/docs/apisix/plugins/multi-auth/)\n\n本教程中，你将创建一个带有 _密钥验证_ 插件的 _消费者_，并学习如何启用和停用身份验证插件。\n\n## Consumer 是什么\n\nConsumer（也称之为消费者）是指使用 API 的应用或开发人员。\n\n在 APISIX 中，消费者需要一个全局唯一的 _名称_，并从上面的列表中选择一个身份验证 _插件_。\n\n## Key Authentication 是什么\n\nKey Authentication（也称之为密钥验证）是一个相对比较简单但是应用广泛的身份验证方法，它的设计思路如下：\n\n1. 管理员为路由添加一个身份验证密钥（API 密钥）。\n2. API 消费者在发送请求时，在查询字符串或者请求头中添加密钥。\n\n## 启用 Key Authentication\n\n### 前置条件\n\n1. 参考[快入门指南](./README.md)完成 APISIX 的安装。\n2. 完成[配置路由](./configure-routes.md#route-是什么)。\n\n### 创建消费者\n\n创建一个名为 `tom` 的消费者，并启用 `key-auth` 插件，密钥设置为 `secret-key`。所有携带密钥 `secret-key` 的请求都会被识别为消费者 `tom`。\n\n:::caution\n\n生产环境请使用复杂的密钥。\n\n:::\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT -d '\n{\n  \"username\": \"tom\",\n  \"plugins\": {\n    \"key-auth\": {\n      \"key\": \"secret-key\"\n    }\n  }\n}'\n```\n\n如果消费者创建成功，你将得到返回 `HTTP/1.1 201 Created`。\n\n### 启用 Authentication\n\n在教程[配置路由](./configure-routes.md)中，我们已经创建了路由 `getting-started-ip`，我们通过 `PATCH` 方法为该路由增加 `key-auth` 插件：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n  \"plugins\": {\n    \"key-auth\": {}\n  }\n}'\n```\n\n如果增加插件成功，你将得到返回 `HTTP/1.1 201 Created`。\n\n### 验证\n\n我们可以在以下场景中进行验证：\n\n#### 1. 发送不带任何密钥的请求\n\n发送一个不带请求头 `apikey` 的请求。\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\"\n```\n\n如果你已经启用了密钥身份验证，你将会得到返回 `HTTP/1.1 401 Unauthorized`，即未授权。\n\n```text\nHTTP/1.1 401 Unauthorized\nDate: Wed, 08 Feb 2023 09:38:36 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.1.0\n```\n\n#### 2. 发送携带错误密钥的请求\n\n发送一个携带错误密钥（比如 `wrong-key`）的请求。\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\" -H 'apikey: wrong-key'\n```\n\n如果密钥错误，你也将得到返回 `HTTP/1.1 401 Unauthorized`，即未授权。\n\n```text\nHTTP/1.1 401 Unauthorized\nDate: Wed, 08 Feb 2023 09:38:27 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.1.0\n```\n\n#### 3. 发送携带正确密钥的请求\n\n发送一个携带正确密钥（`secret-key`）的请求。\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\" -H 'apikey: secret-key'\n```\n\n你将会得到返回 `HTTP/1.1 200 OK`。\n\n```text\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 44\nConnection: keep-alive\nDate: Thu, 09 Feb 2023 03:27:57 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/3.1.0\n```\n\n### 禁用 Authentication\n\n将参数设置 `_meta.disable` 为 `true`，即可禁用密钥验证插件。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n  \"plugins\": {\n    \"key-auth\": {\n      \"_meta\": {\n        \"disable\": true\n      }\n    }\n  }\n}'\n```\n\n你可以发送一个不带任何密钥的请求来验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/ip\"\n```\n\n因为你已经禁用了密钥验证插件，所以你将会得到返回 `HTTP/1.1 200 OK`。\n\n## 下一步\n\n你已经学习了如何为路由配置密钥验证。在下个教程中，你将学习如何配置限速。\n"
  },
  {
    "path": "docs/zh/latest/getting-started/load-balancing.md",
    "content": "---\ntitle: 负载均衡\nslug: /getting-started/load-balancing\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/load-balancing\" />\n</head>\n\n> 本教程由 [API7.ai](https://api7.ai/) 编写。\n\n负载均衡管理客户端和服务端之间的流量。它决定由哪个服务来处理特定的请求，从而提高性能、可扩展性和可靠性。在设计需要处理大量流量的系统时，负载均衡是一个关键的考虑因素。\n\nApache APISIX 支持加权负载均衡算法，传入的流量按照预定顺序轮流分配给一组服务器的其中一个。\n\n在本教程中，你将创建一个具有两个上游服务的路由，并且启用负载均衡来测试在两个服务之间的切换情况。\n\n## 前置条件\n\n1. 参考[入门指南](./README.md)完成 APISIX 的安装。\n2. 了解 APISIX 中[路由及上游](./configure-routes.md#route-是什么)的概念。\n\n## 启用负载均衡\n\n创建一个具有两个上游服务的路由，访问 `/headers` 将被转发到 [httpbin.org](https://httpbin.org/headers) 和 [mock.api7.ai](https://mock.api7.ai/headers) 这两个上游服务，并且会返回请求头。\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"getting-started-headers\",\n  \"uri\": \"/headers\",\n  \"upstream\" : {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:443\": 1,\n      \"mock.api7.ai:443\": 1\n    },\n    \"pass_host\": \"node\",\n    \"scheme\": \"https\"\n  }\n}'\n```\n\n如果路由创建成功，你将会收到返回 `HTTP/1.1 201 Created`。\n\n:::info\n\n1. 将 `pass_host` 字段设置为 `node`，将传递请求头给上游。\n2. 将 `scheme` 字段设置为 `https`，向上游发送请求时将启用 TLS。\n\n:::\n\n## 验证\n\n这两个服务返回不同的数据。\n\n`httpbin.org` 返回：\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/7.58.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-63e34b15-19f666602f22591b525e1e80\",\n    \"X-Forwarded-Host\": \"localhost\"\n  }\n}\n```\n\n`mock.api7.ai` 返回：\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"mock.api7.ai\",\n    \"user-agent\": \"curl/7.58.0\",\n    \"content-type\": \"application/json\",\n    \"x-application-owner\": \"API7.ai\"\n  }\n}\n```\n\n我们生成 100 个请求来测试负载均衡的效果：\n\n```shell\nhc=$(seq 100 | xargs -I {} curl \"http://127.0.0.1:9080/headers\" -sL | grep \"httpbin\" | wc -l); echo httpbin.org: $hc, mock.api7.ai: $((100 - $hc))\n```\n\n结果显示，请求几乎平均分配给这两个上游服务：\n\n```text\nhttpbin.org: 51, mock.api7.ai: 49\n```\n\n## 下一步\n\n你已经学习了如何配置负载均衡。在下个教程中，你将学习如何配置身份验证。\n"
  },
  {
    "path": "docs/zh/latest/getting-started/rate-limiting.md",
    "content": "---\ntitle: 限速\nslug: /getting-started/rate-limiting\n---\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/apisix/getting-started/rate-limiting\" />\n</head>\n\n> 本教程由 [API7.ai](https://api7.ai/) 编写。\n\nAPISIX 是一个统一的控制中心，它管理 API 和微服务的进出流量。除了客户端发来的合理的请求，还可能存在网络爬虫产生的不必要的流量，此外，网络攻击（比如 DDos）也可能产生非法请求。\n\nAPISIX 提供限速功能，通过限制在规定时间内发送到上游服务的请求数量来保护 APIs 和微服务。请求的计数在内存中完成，具有低延迟和高性能的特点。\n\n<br />\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.apiseven.com/uploads/2023/02/20/l9G9Kq41_rate-limiting.png\" alt=\"Routes Diagram\" />\n</div>\n<br />\n\n在本教程中，你将启用 `limit-count` 插件来限制传入流量的速率。\n\n## 前置条件\n\n1. 参考[入门指南](./README.md)完成 APISIX 的安装。\n2. 完成[配置路由](./configure-routes.md#route-是什么)。\n\n## 启用 Rate Limiting\n\n在教程[配置路由](./configure-routes.md)中，我们已经创建了路由 `getting-started-ip`，我们通过 `PATCH` 方法为该路由增加 `limit-count` 插件：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n  \"plugins\": {\n    \"limit-count\": {\n        \"count\": 2,\n        \"time_window\": 10,\n        \"rejected_code\": 503\n     }\n  }\n}'\n```\n\n如果增加插件成功，你将得到返回 `HTTP/1.1 201 Created`。上述配置将传入流量的速率限制为每 10 秒最多 2 个请求。\n\n### 验证\n\n我们同时生成 100 个请求来测试限速插件的效果。\n\n```shell\ncount=$(seq 100 | xargs -I {} curl \"http://127.0.0.1:9080/ip\" -I -sL | grep \"503\" | wc -l); echo \\\"200\\\": $((100 - $count)), \\\"503\\\": $count\n```\n\n请求结果同预期一致：在这 100 个请求中，有 2 个请求发送成功（状态码为 `200`），其他请求均被拒绝（状态码为 `503`）。\n\n```text\n\"200\": 2, \"503\": 98\n```\n\n## 禁用 Rate Limiting\n\n将参数设置 `_meta.disable` 为 `true`，即可禁用限速插件。\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes/getting-started-ip\" -X PATCH -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"_meta\": {\n                \"disable\": true\n            }\n        }\n    }\n}'\n```\n\n### 验证\n\n我们再次同时生成 100 个请求来测试限速插件是否已被禁用：\n\n```shell\ncount=$(seq 100 | xargs -i curl \"http://127.0.0.1:9080/ip\" -I -sL | grep \"503\" | wc -l); echo \\\"200\\\": $((100 - $count)), \\\"503\\\": $count\n```\n\n结果显示所有的请求均成功：\n\n```text\n\"200\": 100, \"503\": 0\n```\n\n## 更多\n\n你可以使用 APISIX 的变量来配置限速插件的规则，比如 `$host` 和 `$uri`。此外，APISIX 也支持使用 Redis 集群进行限速配置，即通过 Redis 来进行计数。\n\n## 下一步\n\n恭喜你！你已经学习了如何配置限速插件，这也意味着你已经完成了所有的入门教程。\n\n你可以继续学习其他文档来定制 APISIX，以满足你的生产环境需要。\n"
  },
  {
    "path": "docs/zh/latest/grpc-proxy.md",
    "content": "---\ntitle: gRPC 代理\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n通过 APISIX 代理 gRPC 连接，并使用 APISIX 的大部分特性管理你的 gRPC 服务。\n\n## 参数\n\n* `scheme`: Route 对应的 Upstream 的 `scheme` 必须设置为 `grpc` 或者 `grpcs`\n* `uri`: 格式为 /service/method 如：/helloworld.Greeter/SayHello\n\n## 示例\n\n### 创建代理 gRPC 的 Route\n\n在指定 Route 中，代理 gRPC 服务接口：\n\n* 注意：这个 Route 对应的 Upstream 的 `scheme` 必须设置为 `grpc` 或者 `grpcs`。\n* 注意：APISIX 使用 TLS 加密的 HTTP/2 暴露 gRPC 服务，所以需要先 [配置 SSL 证书](certificate.md)；\n* 注意：APISIX 也支持通过纯文本的 HTTP/2 暴露 gRPC 服务，这不需要依赖 SSL，通常用于内网环境代理 gRPC 服务\n* 下面例子所代理的 gRPC 服务可供参考：[grpc_server_example](https://github.com/api7/grpc_server_example)。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/helloworld.Greeter/SayHello\",\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\n### 测试 TLS 加密的 HTTP/2\n\n访问上面配置的 Route：\n\n```shell\ngrpcurl -insecure -import-path /pathtoprotos  -proto helloworld.proto  \\\n    -d '{\"name\":\"apisix\"}' 127.0.0.1:9443 helloworld.Greeter.SayHello\n{\n  \"message\": \"Hello apisix\"\n}\n```\n\n> grpcurl 是一个 CLI 工具，类似于 curl，充当 gRPC 客户端并让您与 gRPC 服务器进行交互。安装方式请查看官方[文档](https://github.com/fullstorydev/grpcurl#installation)\n\n这表示已成功代理。\n\n### 测试纯文本的 HTTP/2\n\n默认情况下，APISIX 只在 `9443` 端口支持 TLS 加密的 HTTP/2。你也可以支持纯本文的 HTTP/2，只需要修改 `conf/config.yaml` 文件中的 `node_listen` 配置即可。\n\n```yaml\napisix:\n    node_listen:\n        - port: 9080\n        - port: 9081\n    enable_http2: true\n```\n\n访问上面配置的 Route：\n\n```shell\ngrpcurl -plaintext -import-path /pathtoprotos  -proto helloworld.proto  \\\n    -d '{\"name\":\"apisix\"}' 127.0.0.1:9081 helloworld.Greeter.SayHello\n{\n  \"message\": \"Hello apisix\"\n}\n```\n\n这表示已成功代理。\n\n### gRPCS\n\n如果你的 gRPC 服务使用了自己的 TLS 加密，即所谓的 `gPRCS` (gRPC + TLS)，那么需要修改 scheme 为 `grpcs`。继续上面的例子，50052 端口上跑的是 gPRCS 的服务，这时候应该这么配置：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/helloworld.Greeter/SayHello\",\n    \"upstream\": {\n        \"scheme\": \"grpcs\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50052\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/http3.md",
    "content": "---\ntitle: HTTP3 协议\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n[HTTP/3](https://en.wikipedia.org/wiki/HTTP/3) 是 Hypertext Transfer Protocol(HTTP) 的第三个主要版本。与依赖 TCP 的前辈不同，HTTP/3 基于 [QUIC (Quick UDP Internet Connections) protocol](https://en.wikipedia.org/wiki/QUIC)。它带来了多项好处，减少了延迟并提高了性能：\n\n* 实现不同网络连接之间的无缝过渡，例如从 Wi-Fi 切换到移动数据。\n* 消除队头阻塞，以便丢失的数据包不会阻塞所有流。\n* 在 TLS 握手的同时协商 TLS 版本，从而实现更快的连接。\n* 默认提供加密，确保通过 HTTP/3 连接传输的所有数据都受到保护和保密。\n* 在与客户端已建立连接的服务器通信时提供零往返时间 (0-RTT)。\n\nAPISIX 目前支持下游客户端和 APISIX 之间的 HTTP/3 连接。尚不支持与上游服务的 HTTP/3 连接。欢迎社区贡献。\n\n:::caution\n\n此功能尚未经过大规模测试，因此不建议用于生产使用。\n\n:::\n\n本文档将向您展示如何配置 APISIX 以在客户端和 APISIX 之间启用 HTTP/3 连接，并记录一些已知问题。\n\n## 使用示例\n\n### 启用 HTTP/3\n\n将以下配置添加到 APISIX 的配置文件。该配置将在端口 `9443`（或其他端口）上启用 HTTP/3：\n\n```yaml title=\"config.yaml\"\napisix:\n  ssl:\n    listen:\n      - port: 9443\n        enable_http3: true\n    ssl_protocols: TLSv1.3\n```\n\n:::info\n\n如果您使用 Docker 部署 APISIX，请确保在 HTTP3 端口中允许 UDP，例如 `-p 9443:9443/udp`。\n\n:::\n\n然后重新加载 APISIX 以使配置更改生效：\n\n```shell\napisix reload\n```\n\n### 生成证书和密钥\n\nHTTP/3 需要 TLS。您可以利用购买的证书或自行生成证书。\n\n如自行生成，首先生成证书颁发机构 (CA) 密钥和证书：\n\n```shell\nopenssl genrsa -out ca.key 2048 && \\\n  openssl req -new -sha256 -key ca.key -out ca.csr -subj \"/CN=ROOTCA\" && \\\n  openssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.crt\n```\n\n接下来，生成具有 APISIX 通用名称的密钥和证书，并使用 CA 证书进行签名：\n\n```shell\nopenssl genrsa -out server.key 2048 && \\\n  openssl req -new -sha256 -key server.key -out server.csr -subj \"/CN=test.com\" && \\\n  openssl x509 -req -days 36500 -sha256 -extensions v3_req \\\n  -CA ca.crt -CAkey ca.key -CAserial ca.srl -CAcreateserial \\\n  -in server.csr -out server.crt\n```\n\n### 配置 HTTPS\n\n可选择性地将存储在 `server.crt` 和 `server.key` 中的内容加载到环境变量中：\n\n```shell\nserver_cert=$(cat server.crt)\nserver_key=$(cat server.key)\n```\n\n创建一个保存服务器证书及其密钥的 SSL 对象：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/ssls\" -X PUT -d '\n{\n  \"id\": \"quickstart-tls-client-ssl\",\n  \"sni\": \"test.com\",\n  \"cert\": \"'\"${server_cert}\"'\",\n  \"key\": \"'\"${server_key}\"'\"\n}'\n```\n\n### 创建路由\n\n创建一个路由至 `httpbin.org`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\":\"httpbin-route\",\n  \"uri\":\"/get\",\n  \"upstream\": {\n    \"type\":\"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n### 验证 HTTP/3 连接\n\n验证前需要安装支持 HTTP/3 的 curl，如 [static-curl](https://github.com/stunnel/static-curl) 或其他支持 HTTP/3 的 curl。\n\n发送一个请求到路由：\n\n```shell\ncurl -kv --http3-only \\\n  -H \"Host: test.com\" \\\n  --resolve \"test.com:9443:127.0.0.1\" \"https://test.com:9443/get\"\n```\n\n应收到 `HTTP/3 200` 相应如下：\n\n```text\n* Added test.com:9443:127.0.0.1 to DNS cache\n* Hostname test.com was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* QUIC cipher selection: TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256\n* Skipped certificate verification\n* Connected to test.com (127.0.0.1) port 9443\n* using HTTP/3\n* [HTTP/3] [0] OPENED stream for https://test.com:9443/get\n* [HTTP/3] [0] [:method: GET]\n* [HTTP/3] [0] [:scheme: https]\n* [HTTP/3] [0] [:authority: test.com]\n* [HTTP/3] [0] [:path: /get]\n* [HTTP/3] [0] [user-agent: curl/8.7.1]\n* [HTTP/3] [0] [accept: */*]\n> GET /get HTTP/3\n> Host: test.com\n> User-Agent: curl/8.7.1\n> Accept: */*\n>\n* Request completely sent off\n< HTTP/3 200\n...\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"0\",\n    \"Host\": \"test.com\",\n    \"User-Agent\": \"curl/8.7.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6656013a-27da6b6a34d98e3e79baaf5b\",\n    \"X-Forwarded-Host\": \"test.com\"\n  },\n  \"origin\": \"172.19.0.1, 123.40.79.456\",\n  \"url\": \"http://test.com/get\"\n}\n* Connection #0 to host test.com left intact\n```\n\n## 已知问题\n\n- 对于 APISIX-3.9, Tongsuo 相关测试用例会失败，因为 Tongsuo 不支持 QUIC TLS。\n- APISIX-3.9 基于 NGINX-1.25.3，存在 HTTP/3 漏洞（CVE-2024-24989、CVE-2024-24990）。\n"
  },
  {
    "path": "docs/zh/latest/install-dependencies.md",
    "content": "---\ntitle: 安装依赖\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 注意\n\n- Apache APISIX 从 v2.0 开始不再支持 `v2` 版本的 etcd，并且 etcd 最低支持版本为 v3.4.0，因此请使用 etcd 3.4.0+。更重要的是，因为 etcd v3 使用 gRPC 作为消息传递协议，而 Apache APISIX 使用 HTTP(S) 与 etcd 集群通信，因此请确保启用 [etcd gRPC gateway](https://etcd.io/docs/v3.4.0/dev-guide/api_grpc_gateway/) 功能。\n\n- 目前 Apache APISIX 默认使用 HTTP 协议与 etcd 集群通信，这并不安全，如果希望保障数据的安全性和完整性。请为您的 etcd 集群配置证书及对应私钥，并在您的 Apache APISIX etcd endpoints 配置列表中明确使用 `https` 协议前缀。请查阅 `conf/config.yaml.example` 中 `etcd` 一节相关的配置来了解更多细节。\n\n- 如果是 OpenResty 1.19，APISIX 会使用 OpenResty 内置的 LuaJIT 来运行 `bin/apisix`；否则会使用 Lua 5.1。如果运行过程中遇到 `luajit: lj_asm_x86.h:2819: asm_loop_fixup: Assertion '((intptr_t)target & 15) == 0' failed`，这是低版本 OpenResty 内置的 LuaJIT 在特定编译条件下的问题。\n\n- 在某些平台上，通过包管理器安装 LuaRocks 会导致 Lua 被升级为 Lua 5.3，所以我们建议通过源代码的方式安装 LuaRocks。如果你通过官方仓库安装 OpenResty 和 OpenResty 的 OpenSSL 开发库（rpm 版本：openresty-openssl111-devel，deb 版本：openresty-openssl111-dev），那么 [我们提供了自动安装的脚本](https://github.com/apache/apisix/tree/master/utils/linux-install-luarocks.sh)。如果你是自己编译的 OpenResty，可以参考上述脚本并修改里面的路径。如果编译时没有指定 OpenSSL 库的路径，那么无需配置 LuaRocks 内跟 OpenSSL 相关的变量，因为默认都是用的系统自带的 OpenSSL。如果编译时指定了 OpenSSL 库，那么需要保证 LuaRocks 的 OpenSSL 配置跟 OpenResty 的相一致。\n\n- OpenResty 是 APISIX 的一个依赖项，如果是第一次部署 APISIX 并且不需要使用 OpenResty 部署其他服务，可以在 OpenResty 安装完成后停止并禁用 OpenResty，这不会影响 APISIX 的正常工作，请根据自己的业务谨慎操作。例如 Ubuntu：`systemctl stop openresty && systemctl disable openresty`。\n\n## 安装\n\n在支持的操作系统上运行以下指令即可安装 Apache APISIX dependencies。\n\n支持的操作系统版本：Debian 11/12, Ubuntu 20.04/22.04/24.04 等。\n\n注意，对于 Arch Linux 来说，我们使用 AUR 源中的 `openresty`，所以需要 AUR Helper 才能正常安装。目前支持 `yay` 和 `pacaur`。\n\n```\ncurl https://raw.githubusercontent.com/apache/apisix/master/utils/install-dependencies.sh -sL | bash -\n```\n\n如果你已经克隆了 Apache APISIX 仓库，在根目录运行以下指令安装 Apache APISIX dependencies。\n\n```\nbash utils/install-dependencies.sh\n```\n"
  },
  {
    "path": "docs/zh/latest/installation-guide.md",
    "content": "---\ntitle: APISIX 安装指南\nkeywords:\n  - APISIX\n  - APISIX 安装教程\n  - 部署 APISIX\ndescription: 本文档主要介绍了 APISIX 多种安装方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n本文将介绍如何在你的环境中安装并运行 APISIX。\n\n关于如何快速运行 Apache APISIX，请参考[入门指南](./getting-started/README.md)。\n\n## 安装 APISIX\n\n你可以选择以下任意一种方式安装 APISIX：\n\n<Tabs\n  groupId=\"install-method\"\n  defaultValue=\"docker\"\n  values={[\n    {label: 'Docker', value: 'docker'},\n    {label: 'Helm', value: 'helm'},\n    {label: 'RPM', value: 'rpm'},\n    {label: 'DEB', value: 'deb'},\n    {label: 'Source Code', value: 'source code'},\n  ]}>\n<TabItem value=\"docker\">\n\n使用此方法安装 APISIX，你需要安装 [Docker](https://www.docker.com/) 和 [Docker Compose](https://docs.docker.com/compose/)。\n\n首先下载 [apisix-docker](https://github.com/apache/apisix-docker) 仓库。\n\n```shell\ngit clone https://github.com/apache/apisix-docker.git\ncd apisix-docker/example\n```\n\n然后，使用 `docker-compose` 启用 APISIX。\n\n<Tabs\n  groupId=\"cpu-arch\"\n  defaultValue=\"x86\"\n  values={[\n    {label: 'x86', value: 'x86'},\n    {label: 'ARM/M1', value: 'arm'},\n  ]}>\n<TabItem value=\"x86\">\n\n```shell\ndocker-compose -p docker-apisix up -d\n```\n\n</TabItem>\n\n<TabItem value=\"arm\">\n\n```shell\ndocker-compose -p docker-apisix -f docker-compose-arm64.yml up -d\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n\n<TabItem value=\"helm\">\n\n通过 Helm 安装 APISIX，请执行以下命令：\n\n```shell\nhelm repo add apisix https://charts.apiseven.com\nhelm repo update\nhelm install apisix apisix/apisix --create-namespace  --namespace apisix\n```\n\n你可以从 [apisix-helm-chart](https://github.com/apache/apisix-helm-chart) 仓库找到其他组件。\n\n</TabItem>\n\n<TabItem value=\"rpm\">\n\n该安装方法适用于 CentOS 7 和 CentOS 8。如果你选择该方法安装 APISIX，需要先安装 etcd。具体安装方法请参考 [安装 etcd](#安装-etcd)。\n\n### 通过 RPM 仓库安装\n\n```shell\nsudo yum-config-manager --add-repo https://repos.apiseven.com/packages/redhat/apache-apisix.repo\n```\n\n完成上述操作后使用以下命令安装 APISIX：\n\n```shell\nsudo yum install apisix\n```\n\n:::tip\n\n你也可以安装指定版本的 APISIX（本示例为 APISIX v3.8.0 版本）：\n\n```shell\nsudo yum install apisix-3.8.0\n```\n\n:::\n\n### 通过 RPM 包离线安装：\n\n将 APISIX 离线 RPM 包下载到 `apisix` 文件夹：\n\n```shell\nsudo mkdir -p apisix\nsudo yum install -y https://repos.apiseven.com/packages/redhat/8/x86_64/apisix-3.13.0-0.ubi8.6.x86_64.rpm\nsudo yum clean all && yum makecache\nsudo yum install -y --downloadonly --downloaddir=./apisix apisix\n```\n\n然后将 `apisix` 文件夹复制到目标主机并运行以下命令：\n\n```shell\nsudo yum install ./apisix/*.rpm\n```\n\n### 管理 APISIX 服务\n\nAPISIX 安装完成后，你可以运行以下命令初始化 NGINX 配置文件和 etcd：\n\n```shell\napisix init\n```\n\n使用以下命令启动 APISIX：\n\n```shell\napisix start\n```\n\n:::tip\n\n你可以运行 `apisix help` 命令，通过查看返回结果，获取其他操作的命令及描述。\n\n:::\n\n</TabItem>\n\n<TabItem value=\"deb\">\n\n### 通过 DEB 仓库安装\n\n目前 APISIX 支持的 DEB 仓库仅支持 Debian 11（Bullseye），并且支持 amd64 和 arm64 架构。\n\n```shell\n# amd64\nwget -O - http://repos.apiseven.com/pubkey.gpg | sudo apt-key add -\necho \"deb http://repos.apiseven.com/packages/debian bullseye main\" | sudo tee /etc/apt/sources.list.d/apisix.list\n\n# arm64\nwget -O - http://repos.apiseven.com/pubkey.gpg | sudo apt-key add -\necho \"deb http://repos.apiseven.com/packages/arm64/debian bullseye main\" | sudo tee /etc/apt/sources.list.d/apisix.list\n```\n\n完成上述操作后使用以下命令安装 APISIX：\n\n```shell\nsudo apt update\nsudo apt install -y apisix=3.8.0-0\n```\n\n### 管理 APISIX 服务\n\nAPISIX 安装完成后，你可以运行以下命令初始化 NGINX 配置文件和 etcd：\n\n```shell\nsudo apisix init\n```\n\n使用以下命令启动 APISIX：\n\n```shell\nsudo apisix start\n```\n\n:::tip\n\n你可以运行 `apisix help` 命令，通过查看返回结果，获取其他操作的命令及描述。\n\n:::\n\n</TabItem>\n\n<TabItem value=\"source code\">\n\n如果你想要使用源码构建 APISIX，请参考 [源码安装 APISIX](./building-apisix.md)。\n\n</TabItem>\n</Tabs>\n\n## 安装 etcd\n\nAPISIX 使用 [etcd](https://github.com/etcd-io/etcd) 作为配置中心进行保存和同步配置。在安装 APISIX 之前，需要在你的主机上安装 etcd。\n\n如果你在安装 APISIX 时选择了 Docker 或 Helm 安装，那么 etcd 将会自动安装；如果你选择其他方法或者需要手动安装 APISIX，请参考以下步骤安装 etcd：\n\n<Tabs\n  groupId=\"os\"\n  defaultValue=\"linux\"\n  values={[\n    {label: 'Linux', value: 'linux'},\n    {label: 'macOS', value: 'mac'},\n  ]}>\n<TabItem value=\"linux\">\n\n```shell\nETCD_VERSION='3.5.4'\nwget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz\ntar -xvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz && \\\n  cd etcd-v${ETCD_VERSION}-linux-amd64 && \\\n  sudo cp -a etcd etcdctl /usr/bin/\nnohup etcd >/tmp/etcd.log 2>&1 &\n```\n\n</TabItem>\n\n<TabItem value=\"mac\">\n\n```shell\nbrew install etcd\nbrew services start etcd\n```\n\n</TabItem>\n</Tabs>\n\n## 后续操作\n\n### 配置 APISIX\n\n通过修改本地 `./conf/config.yaml` 文件，或者在启动 APISIX 时使用 `-c` 或 `--config` 添加文件路径参数 `apisix start -c <path string>`，完成对 APISIX 服务本身的基本配置。默认配置不应修改，可以在 `apisix/cli/config.lua` 中找到。\n\n比如将 APISIX 默认监听端口修改为 8000，其他配置保持默认，在 `./conf/config.yaml` 中只需这样配置：\n\n```yaml title=\"./conf/config.yaml\"\napisix:\n  node_listen: 8000 # APISIX listening port\n```\n\n比如指定 APISIX 默认监听端口为 8000，并且设置 etcd 地址为 `http://foo:2379`，其他配置保持默认。在 `./conf/config.yaml` 中只需这样配置：\n\n```yaml title=\"./conf/config.yaml\"\napisix:\n  node_listen: 8000 # APISIX listening port\n\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://foo:2379\"\n```\n\n:::warning\n\n请不要手动修改 APISIX 安装目录下的 `./conf/nginx.conf` 文件。当 APISIX 启动时，会根据 `config.yaml` 的配置自动生成新的 `nginx.conf` 并自动启动服务。\n\n:::\n\n### 更新 Admin API key\n\n建议修改 Admin API 的 key，保护 APISIX 的安全。\n\n请参考如下信息更新配置文件：\n\n```yaml title=\"./conf/config.yaml\"\ndeployment:\n  admin:\n    admin_key:\n      - name: \"admin\"\n        key: newsupersecurekey  # 请修改 key 的值\n        role: admin\n```\n\n更新完成后，你可以使用新的 key 访问 Admin API：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes?api_key=newsupersecurekey -i\n```\n\n### 为 APISIX 添加 systemd 配置文件\n\n如果你是通过 RPM 包安装 APISIX，配置文件已经自动安装，你可以直接使用以下命令：\n\n```shell\nsystemctl start apisix\nsystemctl stop apisix\n```\n\n如果你是通过其他方法安装的 APISIX，可以参考[配置文件模板](https://github.com/api7/apisix-build-tools/blob/master/usr/lib/systemd/system/apisix.service)进行修改，并将其添加在 `/usr/lib/systemd/system/apisix.service` 路径下。\n\n如需了解 APISIX 后续使用，请参考[入门指南](./getting-started/README.md)获取更多信息。\n"
  },
  {
    "path": "docs/zh/latest/mtls.md",
    "content": "---\ntitle: TLS 双向认证\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 保护 Admin API\n\n### 为什么使用\n\n双向认证提供了一种更好的方法来阻止未经授权的对 APISIX Admin API 的访问。\n\n客户端需要向服务器提供证书，服务器将检查该客户端证书是否由受信的 CA 签名，并决定是否响应其请求。\n\n### 如何配置\n\n1. 生成自签证书对，包括 CA、server、client 证书对。\n\n2. 修改 `conf/config.yaml` 中的配置项：\n\n```yaml title=\"conf/config.yaml\"\n  admin_listen:\n    ip: 127.0.0.1\n    port: 9180\n  https_admin: true\n\n  admin_api_mtls:\n    admin_ssl_ca_cert: \"/data/certs/mtls_ca.crt\"              # Path of your self-signed ca cert.\n    admin_ssl_cert: \"/data/certs/mtls_server.crt\"             # Path of your self-signed server side cert.\n    admin_ssl_cert_key: \"/data/certs/mtls_server.key\"         # Path of your self-signed server side key.\n```\n\n3. 执行命令，使配置生效：\n\n```shell\napisix init\napisix reload\n```\n\n### 客户端如何调用\n\n需要将证书文件的路径与域名按实际情况替换。\n\n* 注意：提供的 CA 证书需要与服务端的相同。*\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl --cacert /data/certs/mtls_ca.crt --key /data/certs/mtls_client.key --cert /data/certs/mtls_client.crt  https://admin.apisix.dev:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\"\n```\n\n## 保护 ETCD\n\n### 如何配置\n\n你需要构建 [APISIX-runtime](./FAQ.md#如何构建-APISIX-runtime-环境？)，并且需要在配置文件中设定 `etcd.tls` 来使 ETCD 的双向认证功能正常工作。\n\n```yaml title=\"conf/config.yaml\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    tls:\n      cert: /data/certs/etcd_client.pem       # path of certificate used by the etcd client\n      key: /data/certs/etcd_client.key        # path of key used by the etcd client\n```\n\n如果 APISIX 不信任 etcd server 使用的 CA 证书，我们需要设置 CA 证书。\n\n```yaml title=\"conf/config.yaml\"\napisix:\n  ssl:\n    ssl_trusted_certificate: /path/to/certs/ca-certificates.crt       # path of CA certificate used by the etcd server\n```\n\n## 保护路由\n\n### 为什么使用\n\n双向认证是一种密码学安全的验证客户端身份的手段。当你需要加密并保护流量的双向安全时很有用。\n\n* 注意：双向认证只发生在 HTTPS 中。如果你的路由也可以通过 HTTP 访问，你应该在 HTTP 中添加额外的保护，或者禁止通过 HTTP 访问。*\n\n### 如何配置\n\n我们提供了一个[演示教程](./tutorials/client-to-apisix-mtls.md)，详细地讲解了如何配置客户端和 APISIX 之间的 mTLS。\n\n在配置 `ssl` 资源时，同时需要配置 `client.ca` 和 `client.depth` 参数，分别代表为客户端证书签名的 CA 列表，和证书链的最大深度。可参考：[SSL API 文档](./admin-api.md#ssl)。\n\n下面是一个可用于生成带双向认证配置的 SSL 资源的 shell 脚本示例（如果需要，可修改 API 地址、API Key 和 SSL 资源的 ID。）：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n    \"cert\": \"'\"$(cat t/certs/mtls_server.crt)\"'\",\n    \"key\": \"'\"$(cat t/certs/mtls_server.key)\"'\",\n    \"snis\": [\n        \"admin.apisix.dev\"\n    ],\n    \"client\": {\n        \"ca\": \"'\"$(cat t/certs/mtls_ca.crt)\"'\",\n        \"depth\": 10\n    }\n}'\n```\n\n测试：\n\n```bash\ncurl -vvv --resolve 'admin.apisix.dev:9443:127.0.0.1' https://admin.apisix.dev:9443/hello --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key --cacert t/certs/mtls_ca.crt\n\n* Added admin.apisix.dev:9443:127.0.0.1 to DNS cache\n* Hostname admin.apisix.dev was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* Connected to admin.apisix.dev (127.0.0.1) port 9443 (#0)\n* ALPN: offers h2\n* ALPN: offers http/1.1\n*  CAfile: t/certs/mtls_ca.crt\n*  CApath: none\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, Client hello (1):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Server hello (2):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Unknown (8):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Request CERT (13):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Certificate (11):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, CERT verify (15):\n* [CONN-0-0][CF-SSL] (304) (IN), TLS handshake, Finished (20):\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, Certificate (11):\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, CERT verify (15):\n* [CONN-0-0][CF-SSL] (304) (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384\n* ALPN: server accepted h2\n* Server certificate:\n*  subject: C=cn; ST=GuangDong; L=ZhuHai; CN=admin.apisix.dev; OU=ops\n*  start date: Dec  1 10:17:24 2022 GMT\n*  expire date: Aug 18 10:17:24 2042 GMT\n*  subjectAltName: host \"admin.apisix.dev\" matched cert's \"admin.apisix.dev\"\n*  issuer: C=cn; ST=GuangDong; L=ZhuHai; CN=ca.apisix.dev; OU=ops\n*  SSL certificate verify ok.\n* Using HTTP2, server supports multiplexing\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* h2h3 [:method: GET]\n* h2h3 [:path: /hello]\n* h2h3 [:scheme: https]\n* h2h3 [:authority: admin.apisix.dev:9443]\n* h2h3 [user-agent: curl/7.87.0]\n* h2h3 [accept: */*]\n* Using Stream ID: 1 (easy handle 0x13000bc00)\n> GET /hello HTTP/2\n> Host: admin.apisix.dev:9443\n> user-agent: curl/7.87.0\n> accept: */*\n```\n\n注意，测试时使用的域名需要符合证书的参数。\n\n## APISIX 与上游间的双向认证\n\n### 为什么使用\n\n有时候上游的服务启用了双向认证。在这种情况下，APISIX 作为上游服务的客户端，需要提供客户端证书来正常与其进行通信。\n\n### 如何配置\n\n在配置 upstream 资源时，可以使用参数 `tls.client_cert` 和 `tls.client_key` 来配置 APISIX 用于与上游进行通讯时使用的证书。可参考 [Upstream API 文档](./admin-api.md#upstream)。\n\n该功能需要 APISIX 运行在 [APISIX-Runtime](./FAQ.md#如何构建-apisix-runtime-环境) 上。\n\n下面是一个与配置 SSL 时相似的 shell 脚本，可为一个已存在的 upstream 资源配置双向认证。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/upstreams/1 \\\n-H \"X-API-KEY: $admin_key\" -X PATCH -d '\n{\n    \"tls\": {\n        \"client_cert\": \"'\"$(cat t/certs/mtls_client.crt)\"'\",\n        \"client_key\": \"'\"$(cat t/certs/mtls_client.key)\"'\"\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugin-develop.md",
    "content": "---\ntitle: 插件开发\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n此文档是关于 lua 语言的插件开发，其他语言请看：[external plugin](./external-plugin.md)。\n\n## 插件放置路径\n\n通过在 `conf/config.yaml` 中配置 `extra_lua_path` 来加载你自定义的 lua 插件代码 (或者配置 `extra_lua_cpath` 指定编译的 .so 或 .dll 文件)。\n\n比如，你可以创建一个目录 `/path/to/example` 作为 `extra_lua_path` 配置的值：\n\n```yaml\napisix:\n    ...\n    extra_lua_path: \"/path/to/example/?.lua\"\n```\n\n`example` 目录的结构应该像下面这样：\n\n```\n├── example\n│   └── apisix\n│       ├── plugins\n│       │   └── 3rd-party.lua\n│       └── stream\n│           └── plugins\n│               └── 3rd-party.lua\n```\n\n:::note\n\n该目录 (`/path/to/example`) 下必须包含 `/apisix/plugins` 的子目录。\n\n:::\n\n:::important\n\n你应该给自己的代码文件起一个与内置插件代码文件 (在 `apisix/plugins` 目录下) 不同的名字。但是如果有需要，你可以使用相同名称的代码文件覆盖内置的代码文件。\n\n:::\n\n## 启用插件\n\n要启用您的自定义插件，请将插件列表添加到 `conf/config.yaml` 并附加您的插件名称。例如：\n\n```yaml\nplugins: # 请参阅 `conf/config.yaml.example` 示例\n  - ... # 添加现有插件\n  - your-plugin # 添加您的自定义插件名称 (名称是在代码中定义的插件名称)\n```\n\n:::warning\n\n特别注意的是，在 plugins 字段配置没有定义的情况下，大多数 APISIX 插件都是默认启用的 (默认启用的插件请参考[apisix/cli/config.lua](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua))。\n\n一旦在 `conf/config.yaml` 中定义了 plugins 配置，新的 plugins 列表将会替代默认的配置，而不是合并，因此在新增配置`plugins`字段时请确保包含正在使用的内置插件。为了在定义 plugins 配置的同时与默认行为保持一致，可以在 plugins 中包含 `apisix/cli/config.lua` 定义的所有默认启用的插件。\n\n:::\n\n## 编写插件\n\n[example-plugin](https://github.com/apache/apisix/blob/master/apisix/plugins/example-plugin.lua) 插件 (本地位置： **apisix/plugins/example-plugin.lua**) 提供了一个示例。\n\n### 命名和优先级\n\n在代码里指定插件名称（名称是插件的唯一标识，不可重名）和加载优先级。\n\n```lua\nlocal plugin_name = \"example-plugin\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 0,\n    name = plugin_name,\n    schema = schema,\n    metadata_schema = metadata_schema,\n}\n```\n\n注：新插件的优先级（priority 属性）不能与现有插件的优先级相同，您可以使用 [control API](./control-api.md#get-v1schema) 的 `/v1/schema` 方法查看所有插件的优先级。另外，同一个阶段里面，优先级 ( priority ) 值大的插件，会优先执行，比如 `example-plugin` 的优先级是 0，`ip-restriction` 的优先级是 3000，所以在每个阶段，会先执行 `ip-restriction` 插件，再去执行 `example-plugin` 插件。这里的“阶段”的定义，参见后续的 [确定执行阶段](#确定执行阶段) 这一节。对于你的插件，建议采用 1 到 99 之间的优先级。\n\n注：先后顺序与执行顺序无关。\n\n### 配置描述与校验\n\n定义插件的配置项，以及对应的 [JSON Schema](https://json-schema.org) 描述，并完成对 JSON 的校验，这样方便对配置的数据规格进行验证，以确保数据的完整性以及程序的健壮性。同样，我们以 example-plugin 插件为例，看看他的配置数据：\n\n```json\n{\n  \"example-plugin\": {\n    \"i\": 1,\n    \"s\": \"s\",\n    \"t\": [1]\n  }\n}\n```\n\n我们看下他的 Schema 描述：\n\n```lua\nlocal schema = {\n    type = \"object\",\n    properties = {\n        i = {type = \"number\", minimum = 0},\n        s = {type = \"string\"},\n        t = {type = \"array\", minItems = 1},\n        ip = {type = \"string\"},\n        port = {type = \"integer\"},\n    },\n    required = {\"i\"},\n}\n```\n\n这个 schema 定义了一个非负数 `i`，字符串 `s`，非空数组 `t`，和 `ip` 跟 `port`。只有 `i` 是必需的。\n\n同时，需要实现 **check_schema(conf, schema_type)** 方法，完成配置参数的合法性校验。\n\n```lua\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n```\n\n:::note\n\n项目已经提供了 **core.schema.check** 公共方法，直接使用即可完成配置参数校验。\n\n:::\n\n通过函数输入参数 **schema_type** 可以对不同类型的 schema 进行对应的校验。例如很多插件需要使用一些[元数据](./terminology/plugin-metadata.md)，可以定义插件的 `metadata_schema`。\n\n```lua title=\"example-plugin.lua\"\n-- schema definition for metadata\nlocal metadata_schema = {\n    type = \"object\",\n    properties = {\n        ikey = {type = \"number\", minimum = 0},\n        skey = {type = \"string\"},\n    },\n    required = {\"ikey\", \"skey\"},\n}\n\nfunction _M.check_schema(conf, schema_type)\n    --- check schema for metadata\n    if schema_type == core.schema.TYPE_METADATA then\n        return core.schema.check(metadata_schema, conf)\n    end\n    return core.schema.check(schema, conf)\nend\n```\n\n再比如 [key-auth](https://github.com/apache/apisix/blob/master/apisix/plugins/key-auth.lua) 插件为了跟 [Consumer](./admin-api.md#consumer) 资源一起使用，认证插件需要提供一个 `consumer_schema` 来检验 `Consumer` 资源的 `plugins` 属性里面的配置。\n\n```lua title=\"key-auth.lua\"\n\nlocal consumer_schema = {\n    type = \"object\",\n    properties = {\n        key = {type = \"string\"},\n    },\n    required = {\"key\"},\n}\n\nfunction _M.check_schema(conf, schema_type)\n    if schema_type == core.schema.TYPE_CONSUMER then\n        return core.schema.check(consumer_schema, conf)\n    else\n        return core.schema.check(schema, conf)\n    end\nend\n```\n\n### 确定执行阶段\n\n根据业务功能，确定你的插件需要在哪个[阶段](./terminology/plugin.md#插件执行生命周期)执行。\n\n以 `key-auth` 为例， `key-auth`是一个认证插件，所以需要在 rewrite 阶段执行。在 APISIX，只有认证逻辑可以在 rewrite 阶段里面完成，其他需要在代理到上游之前执行的逻辑都是在 access 阶段完成的。\n\n**注意：我们不能在 rewrite 和 access 阶段调用 `ngx.exit`、`ngx.redirect` 或者 `core.respond.exit`。如果确实需要退出，只需要 return 状态码和正文，插件引擎将使用返回的状态码和正文进行退出。[例子](https://github.com/apache/apisix/blob/35269581e21473e1a27b11cceca6f773cad0192a/apisix/plugins/limit-count.lua#L177)**\n\n#### APISIX 的自定义阶段\n\n除了 OpenResty 的阶段，我们还提供额外的阶段来满足特定的目的：\n\n- `delayed_body_filter`\n\n```lua\nfunction _M.delayed_body_filter(conf, ctx)\n    -- delayed_body_filter 在 body_filter 之后被调用。\n    -- 它被 tracing 类型插件用来在 body_filter 之后立即结束 span。\nend\n```\n\n### 编写执行逻辑\n\n在对应的阶段方法里编写功能的逻辑代码，在阶段方法中具有 `conf` 和 `ctx` 两个参数，以 `limit-conn` 插件配置为例。\n\n#### conf 参数\n\n`conf` 参数是插件的相关配置信息，您可以通过 `core.log.warn(core.json.encode(conf))` 将其输出到 `error.log` 中进行查看，如下所示：\n\n```lua\nfunction _M.access(conf, ctx)\n    core.log.warn(core.json.encode(conf))\n    ......\nend\n```\n\nconf:\n\n```json\n{\n  \"rejected_code\": 503,\n  \"burst\": 0,\n  \"default_conn_delay\": 0.1,\n  \"conn\": 1,\n  \"key\": \"remote_addr\"\n}\n```\n\n#### ctx 参数\n\n`ctx` 参数缓存了请求相关的数据信息，您可以通过 `core.log.warn(core.json.encode(ctx, true))` 将其输出到 `error.log` 中进行查看，如下所示：\n\n```lua\nfunction _M.access(conf, ctx)\n    core.log.warn(core.json.encode(ctx, true))\n    ......\nend\n```\n\n### 其它注意事项\n\n特别需要注意的是，如果你的插件有新建自己的代码目录，那么就需要修改 Makefile 文件，新增创建文件夹的操作，比如：\n\n```\n$(INSTALL) -d $(INST_LUADIR)/apisix/plugins/skywalking\n$(INSTALL) apisix/plugins/skywalking/*.lua $(INST_LUADIR)/apisix/plugins/skywalking/\n```\n\n`_M` 中还有其他字段会影响到插件的行为。\n\n```lua\nlocal _M = {\n    ...\n    type = 'auth',\n    run_policy = 'prefer_route',\n}\n```\n\n`run_policy` 字段可以用来控制插件执行。当这个字段设置成 `prefer_route` 时，且该插件同时配置在全局和路由级别，那么只有路由级别的配置生效。\n\n如果你的插件需要跟 `consumer` 一起使用，需要把 `type` 设置成 `auth`。\n\n## 加载插件和替换插件\n\n现在使用 `require \"apisix.plugins.3rd-party\"` 会加载你自己的插件，比如 `require \"apisix.plugins.jwt-auth\"`会加载 `jwt-auth` 插件。\n\n可能你会想覆盖一个文件中的函数，你可以在 `conf/config.yaml` 文件中配置 `lua_module_hook` 来使你的 hook 生效。\n\n你的配置可以像下面这样：\n\n```yaml\napisix:\n    ...\n    extra_lua_path: \"/path/to/example/?.lua\"\n    lua_module_hook: \"my_hook\"\n```\n\n当 APISIX 启动的时候，`example/my_hook.lua` 就会被加载，这时你可以使用这个钩子在 APISIX 中来全局替换掉一个方法。\n这个例子：[my_hook.lua](https://github.com/apache/apisix/blob/master/example/my_hook.lua) 可以在项目的 `example` 路径下被找到。\n\n## 检查外部依赖\n\n如果你的插件，涉及到一些外部的依赖和三方库，请首先检查一下依赖项的内容。如果插件需要用到共享内存，需要在 [自定义 Nginx 配置](./customize-nginx-configuration.md)中声明，例如：\n\n```yaml\n# put this in config.yaml:\nnginx_config:\n  http_configuration_snippet: |\n    # for openid-connect plugin\n    lua_shared_dict discovery             1m; # cache for discovery metadata documents\n    lua_shared_dict jwks                  1m; # cache for JWKs\n    lua_shared_dict introspection        10m; # cache for JWT verification results\n```\n\n插件本身提供了 init 方法。方便插件加载后做初始化动作。如果你需要清理初始化动作创建出来的内容，你可以在对应的 destroy 方法里完成这一操作。\n\n注：如果部分插件的功能实现，需要在 Nginx 初始化启动，则可能需要在 `apisix/init.lua` 文件的初始化方法 http_init 中添加逻辑，并且可能需要在 `apisix/cli/ngx_tpl.lua` 文件中，对 Nginx 配置文件生成的部分，添加一些你需要的处理。但是这样容易对全局产生影响，根据现有的插件机制，**我们不建议这样做，除非你已经对代码完全掌握**。\n\n## 加密存储字段\n\n有些插件需要将参数加密存储，比如 `basic-auth` 插件的 `password` 参数。这个插件需要在 `schema` 中指定哪些参数需要被加密存储。\n\n```lua\nencrypt_fields = {\"password\"}\n```\n\n如果是嵌套的参数，比如 `error-log-logger` 插件的 `clickhouse.password` 参数，需要用 `.` 来分隔：\n\n```lua\nencrypt_fields = {\"clickhouse.password\"}\n```\n\n目前还不支持：\n\n1. 两层以上的嵌套\n2. 数组中的字段\n\n通过在 `schema` 中指定 `encrypt_fields = {\"password\"}`，可以将参数加密存储。APISIX 将提供以下功能：\n\n- 新增和更新资源时，对于 `encrypt_fields` 中声明的参数，APISIX 会自动加密存储在 etcd 中\n- 获取资源时，以及在运行插件时，对于 `encrypt_fields` 中声明的参数，APISIX 会自动解密\n\n默认情况下，APISIX 启用数据加密并使用[两个默认的密钥](https://github.com/apache/apisix/blob/85563f016c35834763376894e45908b2fb582d87/apisix/cli/config.lua#L75)，你可以在 `config.yaml` 中修改：\n\n```yaml\napisix:\n    data_encryption:\n        enable: true\n        keyring:\n            - ...\n```\n\n`keyring` 是一个数组，可以指定多个 key，APISIX 会按照 keyring 中 key 的顺序，依次尝试用 key 来解密数据（只对在 `encrypt_fields` 声明的参数）。如果解密失败，会尝试下一个 key，直到解密成功。\n\n如果 `keyring` 中的 key 都无法解密数据，则使用原始数据。\n\n## 注册公共接口\n\n插件可以注册暴露给公网的接口。以 batch-requests 插件为例，这个插件注册了 `POST /apisix/batch-requests` 接口，让客户端可以将多个 API 请求组合在一个请求/响应中：\n\n```lua\nfunction batch_requests()\n    -- ...\nend\n\nfunction _M.api()\n    -- ...\n    return {\n        {\n            methods = {\"POST\"},\n            uri = \"/apisix/batch-requests\",\n            handler = batch_requests,\n        }\n    }\nend\n```\n\n注意，注册的接口将不会默认暴露，需要使用[public-api 插件](./plugins/public-api.md)来暴露它。\n\n## 注册控制接口\n\n如果你只想暴露 API 到 localhost 或内网，你可以通过 [Control API](./control-api.md) 来暴露它。\n\n以 example-plugin 插件为例：\n\n```lua\nlocal function hello()\n    local args = ngx.req.get_uri_args()\n    if args[\"json\"] then\n        return 200, {msg = \"world\"}\n    else\n        return 200, \"world\\n\"\n    end\nend\n\n\nfunction _M.control_api()\n    return {\n        {\n            methods = {\"GET\"},\n            uris = {\"/v1/plugin/example-plugin/hello\"},\n            handler = hello,\n        }\n    }\nend\n```\n\n如果你没有改过默认的 control API 配置，这个插件暴露的 `GET /v1/plugin/example-plugin/hello` API 只有通过 `127.0.0.1` 才能访问它。通过以下命令进行测试：\n\n```shell\ncurl -i -X GET \"http://127.0.0.1:9090/v1/plugin/example-plugin/hello\"\n```\n\n[查看更多有关 control API 介绍](./control-api.md)\n\n## 注册自定义变量\n\n我们可以在 APISIX 的许多地方使用变量。例如，在 http-logger 中自定义日志格式，用它作为 `limit-*` 插件的键。在某些情况下，内置的变量是不够的。因此，APISIX 允许开发者在全局范围内注册他们的变量，并将它们作为普通的内置变量使用。\n\n例如，让我们注册一个叫做 `a6_labels_zone` 的变量来获取路由中 `zone` 标签的值。\n\n```\nlocal core = require \"apisix.core\"\n\ncore.ctx.register_var(\"a6_labels_zone\", function(ctx)\n    local route = ctx.matched_route and ctx.matched_route.value\n    if route and route.labels then\n        return route.labels.zone\n    end\n    return nil\nend)\n```\n\n此后，任何对 `$a6_labels_zone` 的获取操作都会调用注册的获取器来获取数值。\n\n注意，自定义变量不能用于依赖 Nginx 指令的功能，如 `access_log_format`。\n\n## 编写测试用例\n\n针对功能，完善各种维度的测试用例，对插件做个全方位的测试吧！插件的测试用例，都在 __t/plugin__ 目录下，可以前去了解。\n项目测试框架采用的 [**test-nginx**](https://github.com/openresty/test-nginx) 。\n一个测试用例 __.t__ 文件，通常用 \\__DATA\\__ 分割成 序言部分 和 数据部分。这里我们简单介绍下数据部分，\n也就是真正测试用例的部分，仍然以 key-auth 插件为例：\n\n```perl\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.key-auth\")\n            local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\n[error]\n```\n\n一个测试用例主要有三部分内容：\n\n- 程序代码：Nginx location 的配置内容\n- 输入：http 的 request 信息\n- 输出检查：status，header，body，error_log 检查\n\n这里请求 __/t__，经过配置文件 __location__，调用 __content_by_lua_block__ 指令完成 lua 的脚本，最终返回。\n用例的断言是 response_body 返回 \"done\"，__no_error_log__ 表示会对 Nginx 的 error.log 检查，\n必须没有 ERROR 级别的记录。\n\n### 附上 test-nginx 执行流程\n\n根据我们在 Makefile 里配置的 PATH，和每一个 __.t__ 文件最前面的一些配置项，框架会组装成一个完整的 nginx.conf 文件，\n__t/servroot__ 会被当成 Nginx 的工作目录，启动 Nginx 实例。根据测试用例提供的信息，发起 http 请求并检查 http 的返回项，\n包括 http status，http response header，http response body 等。\n\n## 相关资源\n\n- 核心概念 - [插件](https://apisix.apache.org/docs/apisix/terminology/plugin/)\n- [Apache APISIX 扩展指南](https://apisix.apache.org/zh/blog/2021/10/26/extension-guide/)\n- [Create a Custom Plugin in Lua](https://docs.api7.ai/apisix/how-to-guide/custom-plugins/create-plugin-in-lua)\n- [example-plugin 代码](https://github.com/apache/apisix/blob/master/apisix/plugins/example-plugin.lua)\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-aliyun-content-moderation.md",
    "content": "---\ntitle: ai-aliyun-content-moderation\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-aliyun-content-moderation\ndescription: 本文档包含有关 Apache APISIX ai-aliyun-content-moderation 插件的信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ai-aliyun-content-moderation` 插件集成了阿里云的内容审核服务，用于在与大语言模型 (LLM) 交互时检查请求和响应内容是否包含不当材料。它支持实时流式检查和最终数据包审核两种模式。\n\n此插件必须在使用 `ai-proxy` 或 `ai-proxy-multi` 插件的路由中使用。\n\n## 插件属性\n\n| **字段**                     | **必选项** | **类型**  | **描述**                                                                                                                                                                    |\n| ---------------------------- | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| endpoint                     | 是         | String    | 阿里云服务端点 URL                                                                                                                                                          |\n| region_id                    | 是         | String    | 阿里云区域标识符                                                                                                                                                            |\n| access_key_id                | 是         | String    | 阿里云访问密钥 ID                                                                                                                                                           |\n| access_key_secret            | 是         | String    | 阿里云访问密钥密码                                                                                                                                                          |\n| check_request                | 否         | Boolean   | 启用请求内容审核。默认值：`true`                                                                                                                                            |\n| check_response               | 否         | Boolean   | 启用响应内容审核。默认值：`false`                                                                                                                                           |\n| stream_check_mode            | 否         | String    | 流式审核模式。默认值：`\"final_packet\"`。有效值：`[\"realtime\", \"final_packet\"]`                                                                                             |\n| stream_check_cache_size      | 否         | Integer   | 实时模式下每次审核批次的最大字符数。默认值：`128`。必须 `>= 1`                                                                                                              |\n| stream_check_interval        | 否         | Number    | 实时模式下批次检查之间的间隔秒数。默认值：`3`。必须 `>= 0.1`                                                                                                                |\n| request_check_service        | 否         | String    | 用于请求审核的阿里云服务。默认值：`\"llm_query_moderation\"`                                                                                                                  |\n| request_check_length_limit   | 否         | Number    | 每个请求审核块的最大字符数。默认值：`2000`                                                                                                                                  |\n| response_check_service       | 否         | String    | 用于响应审核的阿里云服务。默认值：`\"llm_response_moderation\"`                                                                                                               |\n| response_check_length_limit  | 否         | Number    | 每个响应审核块的最大字符数。默认值：`5000`                                                                                                                                  |\n| risk_level_bar               | 否         | String    | 内容拒绝的阈值。默认值：`\"high\"`。有效值：`[\"none\", \"low\", \"medium\", \"high\", \"max\"]`                                                                                       |\n| deny_code                    | 否         | Number    | 被拒绝内容的 HTTP 状态码。默认值：`200`                                                                                                                                     |\n| deny_message                 | 否         | String    | 被拒绝内容的自定义消息。默认值：`-`                                                                                                                                         |\n| timeout                      | 否         | Integer   | 请求超时时间（毫秒）。默认值：`10000`。必须 `>= 1`                                                                                                                          |\n| ssl_verify                   | 否         | Boolean   | 启用 SSL 证书验证。默认值：`true`                                                                                                                                           |\n\n## 使用示例\n\n首先初始化这些 shell 变量：\n\n```shell\nADMIN_API_KEY=edd1c9f034335f136f87ad84b625c8f1\nALIYUN_ACCESS_KEY_ID=your-aliyun-access-key-id\nALIYUN_ACCESS_KEY_SECRET=your-aliyun-access-key-secret\nALIYUN_REGION=cn-hangzhou\nALIYUN_ENDPOINT=https://green.cn-hangzhou.aliyuncs.com\nOPENAI_KEY=your-openai-api-key\n```\n\n创建一个带有 `ai-aliyun-content-moderation` 和 `ai-proxy` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_KEY\"'\"\n          }\n        },\n        \"override\": {\n          \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n        }\n      },\n      \"ai-aliyun-content-moderation\": {\n        \"endpoint\": \"'\"$ALIYUN_ENDPOINT\"'\",\n        \"region_id\": \"'\"$ALIYUN_REGION\"'\",\n        \"access_key_id\": \"'\"$ALIYUN_ACCESS_KEY_ID\"'\",\n        \"access_key_secret\": \"'\"$ALIYUN_ACCESS_KEY_SECRET\"'\",\n        \"risk_level_bar\": \"high\",\n        \"check_request\": true,\n        \"check_response\": true,\n        \"deny_code\": 400,\n        \"deny_message\": \"您的请求违反了内容政策\"\n      }\n    }\n  }'\n```\n\n这里使用 `ai-proxy` 插件是因为它简化了对 LLM 的访问。不过，您也可以在上游配置中配置 LLM。\n\n现在发送一个请求：\n\n```shell\ncurl http://127.0.0.1:9080/v1/chat/completions -i \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"gpt-3.5-turbo\",\n    \"messages\": [\n      {\"role\": \"user\", \"content\": \"I want to kill you\"}\n    ],\n    \"stream\": false\n  }'\n```\n\n然后请求将被阻止，并返回如下错误：\n\n```text\nHTTP/1.1 400 Bad Request\nContent-Type: application/json\n\n{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion\",\"model\":\"gpt-3.5-turbo\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"您的请求违反了内容政策\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":0,\"completion_tokens\":0,\"total_tokens\":0}}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-aws-content-moderation.md",
    "content": "---\ntitle: ai-aws-content-moderation\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-aws-content-moderation\n  - AWS\n  - 内容审核\ndescription: 本文档包含有关 Apache APISIX ai-aws-content-moderation 插件的信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ai-aws-content-moderation` 插件处理请求体以检查毒性内容，如果超过配置的阈值则拒绝请求。\n\n**_此插件只能在代理请求到 LLM 的路由中使用。_**\n\n**_目前，该插件仅支持与 [AWS Comprehend](https://aws.amazon.com/comprehend/) 的集成进行内容审核。欢迎提交 PR 以引入对其他服务提供商的支持。_**\n\n## 插件属性\n\n| **字段**                     | **必选项** | **类型** | **描述**                                                                                                                                                                                                                                                |\n| ---------------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| comprehend.access_key_id     | 是         | String   | AWS 访问密钥 ID                                                                                                                                                                                                                                         |\n| comprehend.secret_access_key | 是         | String   | AWS 秘密访问密钥                                                                                                                                                                                                                                       |\n| comprehend.region            | 是         | String   | AWS 区域                                                                                                                                                                                                                                                |\n| comprehend.endpoint          | 否         | String   | AWS Comprehend 服务端点。必须匹配模式 `^https?://`                                                                                                                                                                                                      |\n| comprehend.ssl_verify        | 否         | String   | 启用 SSL 证书验证                                                                                                                                                                                                                                       |\n| moderation_categories        | 否         | Object   | 审核类别及其分数的键值对。在每个对中，键应该是 `PROFANITY`、`HATE_SPEECH`、`INSULT`、`HARASSMENT_OR_ABUSE`、`SEXUAL` 或 `VIOLENCE_OR_THREAT` 之一；值应该在 0 和 1 之间（包含）                                                                      |\n| moderation_threshold         | 否         | Number   | 内容有害、冒犯或不当的程度。较高的值表示允许更多毒性内容。范围：0 - 1。默认值：0.5                                                                                                                                                                      |\n\n## 使用示例\n\n首先初始化这些 shell 变量：\n\n```shell\nADMIN_API_KEY=edd1c9f034335f136f87ad84b625c8f1\nACCESS_KEY_ID=aws-comprehend-access-key-id-here\nSECRET_ACCESS_KEY=aws-comprehend-secret-access-key-here\nOPENAI_KEY=open-ai-key-here\n```\n\n创建一个带有 `ai-aws-content-moderation` 和 `ai-proxy` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"ai-aws-content-moderation\": {\n        \"comprehend\": {\n          \"access_key_id\": \"'\"$ACCESS_KEY_ID\"'\",\n          \"secret_access_key\": \"'\"$SECRET_ACCESS_KEY\"'\",\n          \"region\": \"us-east-1\"\n        },\n        \"moderation_categories\": {\n          \"PROFANITY\": 0.5\n        }\n      },\n      \"ai-proxy\": {\n        \"auth\": {\n          \"header\": {\n            \"api-key\": \"'\"$OPENAI_KEY\"'\"\n          }\n        },\n        \"model\": {\n          \"provider\": \"openai\",\n          \"name\": \"gpt-4\",\n          \"options\": {\n            \"max_tokens\": 512,\n            \"temperature\": 1.0\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n这里使用 `ai-proxy` 插件是因为它简化了对 LLM 的访问。不过，您也可以在上游配置中配置 LLM。\n\n现在发送一个请求：\n\n```shell\ncurl http://127.0.0.1:9080/post -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"<very profane message here>\"\n    }\n  ]\n}'\n```\n\n然后请求将被阻止，并返回如下错误：\n\n```text\nHTTP/1.1 400 Bad Request\nDate: Thu, 03 Oct 2024 11:53:15 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.10.0\n\nrequest body exceeds PROFANITY threshold\n```\n\n发送一个在请求体中包含合规内容的请求：\n\n```shell\ncurl http://127.0.0.1:9080/post -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are a mathematician\"\n    },\n    { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n  ]\n}'\n```\n\n此请求将正常代理到配置的 LLM。\n\n```text\nHTTP/1.1 200 OK\nDate: Thu, 03 Oct 2024 11:53:00 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.10.0\n\n{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"content\":\"1+1 equals 2.\",\"role\":\"assistant\"}}],\"created\":1727956380,\"id\":\"chatcmpl-AEEg8Pe5BAW5Sw3C1gdwXnuyulIkY\",\"model\":\"gpt-4o-2024-05-13\",\"object\":\"chat.completion\",\"system_fingerprint\":\"fp_67802d9a6d\",\"usage\":{\"completion_tokens\":7,\"prompt_tokens\":23,\"total_tokens\":30}}\n```\n\n您还可以配置其他审核类别的过滤器，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"ai-aws-content-moderation\": {\n        \"comprehend\": {\n          \"access_key_id\": \"'\"$ACCESS_KEY_ID\"'\",\n          \"secret_access_key\": \"'\"$SECRET_ACCESS_KEY\"'\",\n          \"region\": \"us-east-1\"\n        },\n        \"moderation_categories\": {\n          \"PROFANITY\": 0.5,\n          \"HARASSMENT_OR_ABUSE\": 0.7,\n          \"SEXUAL\": 0.2\n        }\n      },\n      \"ai-proxy\": {\n        \"auth\": {\n          \"header\": {\n            \"api-key\": \"'\"$OPENAI_KEY\"'\"\n          }\n        },\n        \"model\": {\n          \"provider\": \"openai\",\n          \"name\": \"gpt-4\",\n          \"options\": {\n            \"max_tokens\": 512,\n            \"temperature\": 1.0\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n如果没有配置任何 `moderation_categories`，请求体将基于整体毒性进行审核。\n默认的 `moderation_threshold` 是 0.5，可以这样配置。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n  \"uri\": \"/post\",\n  \"plugins\": {\n    \"ai-aws-content-moderation\": {\n      \"provider\": {\n        \"comprehend\": {\n          \"access_key_id\": \"'\"$ACCESS_KEY_ID\"'\",\n          \"secret_access_key\": \"'\"$SECRET_ACCESS_KEY\"'\",\n          \"region\": \"us-east-1\"\n        }\n      },\n      \"moderation_threshold\": 0.7,\n      \"llm_provider\": \"openai\"\n    },\n    \"ai-proxy\": {\n      \"auth\": {\n        \"header\": {\n          \"api-key\": \"'\"$OPENAI_KEY\"'\"\n        }\n      },\n      \"model\": {\n        \"provider\": \"openai\",\n        \"name\": \"gpt-4\",\n        \"options\": {\n          \"max_tokens\": 512,\n          \"temperature\": 1.0\n        }\n      }\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-prompt-decorator.md",
    "content": "---\ntitle: ai-prompt-decorator\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-prompt-decorator\ndescription: 本文档包含有关 Apache APISIX ai-prompt-decorator 插件的信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ai-prompt-decorator` 插件通过在请求中追加或前置提示，简化了对 LLM 提供商（如 OpenAI 和 Anthropic）及其模型的访问。\n\n## 插件属性\n\n| **字段**          | **必选项**      | **类型** | **描述**                                     |\n| ----------------- | --------------- | -------- | -------------------------------------------- |\n| `prepend`         | 条件必选\\*      | Array    | 要前置的提示对象数组                         |\n| `prepend.role`    | 是              | String   | 消息的角色（`system`、`user`、`assistant`） |\n| `prepend.content` | 是              | String   | 消息的内容。最小长度：1                      |\n| `append`          | 条件必选\\*      | Array    | 要追加的提示对象数组                         |\n| `append.role`     | 是              | String   | 消息的角色（`system`、`user`、`assistant`） |\n| `append.content`  | 是              | String   | 消息的内容。最小长度：1                      |\n\n\\* **条件必选**：必须提供 `prepend` 或 `append` 中的至少一个。\n\n## 使用示例\n\n创建一个带有 `ai-prompt-decorator` 插件的路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-prompt-decorator\": {\n        \"prepend\":[\n          {\n            \"role\": \"system\",\n            \"content\": \"我明天有考试，所以请简要地从概念上解释\"\n          }\n        ],\n        \"append\":[\n          {\n            \"role\": \"system\",\n            \"content\": \"用一个类比来结束回答。\"\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"api.openai.com:443\": 1\n      },\n      \"pass_host\": \"node\",\n      \"scheme\": \"https\"\n    }\n  }'\n```\n\n现在发送一个请求：\n\n```shell\ncurl http://127.0.0.1:9080/v1/chat/completions -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"model\": \"gpt-4\",\n  \"messages\": [{ \"role\": \"user\", \"content\": \"什么是 TLS 握手？\" }]\n}' -H \"Authorization: Bearer <your token here>\"\n```\n\n然后请求体将被修改为类似这样：\n\n```json\n{\n  \"model\": \"gpt-4\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"我明天有考试，所以请简要地从概念上解释\"\n    },\n    { \"role\": \"user\", \"content\": \"什么是 TLS 握手？\" },\n    {\n      \"role\": \"system\",\n      \"content\": \"用一个类比来结束回答。\"\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-prompt-guard.md",
    "content": "---\ntitle: ai-prompt-guard\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-prompt-guard\ndescription: 本文档包含有关 Apache APISIX ai-prompt-guard 插件的信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ai-prompt-guard` 插件通过检查和验证传入的提示消息来保护您的 AI 端点。它根据用户定义的允许和拒绝模式检查请求内容，确保只有经过批准的输入才会被处理。根据其配置，插件可以检查最新消息或整个对话历史，并且可以设置为检查所有角色的提示或仅检查最终用户的提示。\n\n当同时配置了**允许**和**拒绝**模式时，插件首先确保至少匹配一个允许的模式。如果没有匹配，请求将被拒绝并返回 _\"Request doesn't match allow patterns\"_ 错误。如果找到允许的模式，它会检查是否存在任何拒绝模式的匹配——如果检测到任何匹配，则拒绝请求并返回 _\"Request contains prohibited content\"_ 错误。\n\n## 插件属性\n\n| **字段**                       | **必选项** | **类型**  | **描述**                                                                                                                                                      |\n| ------------------------------ | ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| match_all_roles                | 否         | boolean   | 如果设置为 `true`，插件将检查所有角色的提示消息。否则，它只在角色为 `\"user\"` 时进行验证。默认值为 `false`。 |\n| match_all_conversation_history | 否         | boolean   | 启用时，对话历史中的所有消息将被连接并检查。如果为 `false`，则只检查最后一条消息的内容。默认值为 `false`。 |\n| allow_patterns                 | 否         | array     | 正则表达式模式列表。提供时，提示必须匹配**至少一个**模式才被认为是有效的。                                                      |\n| deny_patterns                  | 否         | array     | 正则表达式模式列表。如果任何这些模式匹配提示内容，请求将被拒绝。                                                                  |\n\n## 使用示例\n\n创建一个带有 `ai-prompt-guard` 插件的路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-prompt-guard\": {\n        \"match_all_roles\": true,\n          \"allow_patterns\": [\n            \"goodword\"\n          ],\n        \"deny_patterns\": [\n          \"badword\"\n        ]\n     }\n  },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"api.openai.com:443\": 1\n      },\n      \"pass_host\": \"node\",\n      \"scheme\": \"https\"\n    }\n  }'\n```\n\n现在发送一个请求：\n\n```shell\ncurl http://127.0.0.1:9080/v1/chat/completions -i -XPOST  -H 'Content-Type: application/json' -d '{\n  \"model\": \"gpt-4\",\n  \"messages\": [{ \"role\": \"user\", \"content\": \"badword request\" }]\n}' -H \"Authorization: Bearer <your token here>\"\n```\n\n请求将失败并返回 400 错误和以下响应。\n\n```bash\n{\"message\":\"Request doesn't match allow patterns\"}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-prompt-template.md",
    "content": "---\ntitle: ai-prompt-template\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-prompt-template\ndescription: ai-prompt-template 插件支持预先配置提示词模板，这些模板仅接受用户在指定的模板变量中输入，采用填空的方式。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-prompt-template\" />\n</head>\n\n## 描述\n\n`ai-prompt-template` 插件简化了对 OpenAI、Anthropic 等大语言模型提供商及其模型的访问。它预先配置提示词模板，这些模板仅接受用户在指定的模板变量中输入，采用“填空”的方式。\n\n## 插件属性\n\n| **字段** | **是否必填** | **类型** | **描述** |\n| :--- | :--- | :--- | :--- |\n| `templates` | 是 | Array | 模板对象数组。 |\n| `templates.name` | 是 | String | 模板的名称。在请求路由时，请求中应包含与所配置模板相对应的模板名称。 |\n| `templates.template` | 是 | Object | 模板规范。 |\n| `templates.template.model` | 是 | String | AI 模型的名称，例如 `gpt-4` 或 `gpt-3.5`。更多可用模型请参阅 LLM 提供商的 API 文档。 |\n| `templates.template.messages` | 是 | Array | 模板消息规范。 |\n| `templates.template.messages.role` | 是 | String | 消息的角色，例如 `system`、`user` 或 `assistant`。 |\n| `templates.template.messages.content` | 是 | String | 消息（提示词）的内容。 |\n\n## 使用示例\n\n以下示例将使用 OpenAI 作为上游服务提供商。开始之前，请先创建一个 OpenAI 账户和一个 API 密钥。你可以选择将密钥保存到环境变量中，如下所示：\n\n```shell\nexport OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>   # 替换为你的 API 密钥\n```\n\n如果你正在使用其他 LLM 提供商，请参阅该提供商的文档以获取 API 密钥。\n\n### 为自定义复杂度的开放式问题配置模板\n\n以下示例演示了如何使用 `ai-prompt-template` 插件配置一个模板，该模板可用于回答开放式问题并接受用户指定的回答复杂度。\n\n创建一个指向聊天补全端点的路由，并配置预定义的提示词模板：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\": {\n          \"model\": \"gpt-4\"\n        }\n      },\n      \"ai-prompt-template\": {\n        \"templates\": [\n          {\n            \"name\": \"QnA with complexity\",\n            \"template\": {\n              \"model\": \"gpt-4\",\n              \"messages\": [\n                {\n                  \"role\": \"system\",\n                  \"content\": \"Answer in {{complexity}}.\"\n                },\n                {\n                  \"role\": \"user\",\n                  \"content\": \"Explain {{prompt}}.\"\n                }\n              ]\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向该路由发送一个 POST 请求，在请求体中包含示例问题和期望的回答复杂度。\n\n发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/v1/chat/completions\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template_name\": \"QnA with complexity\",\n    \"complexity\": \"brief\",\n    \"prompt\": \"quick sort\"\n  }'\n```\n\n你应该会收到类似于以下的响应：\n\n```json\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": {\n        \"content\": \"Quick sort is a highly efficient sorting algorithm that uses a divide-and-conquer approach to arrange elements in a list or array in order. Here’s a brief explanation:\\n\\n1. **Choose a Pivot**: Select an element from the list as a 'pivot'. Common methods include choosing the first element, the last element, the middle element, or a random element.\\n\\n2. **Partitioning**: Rearrange the elements in the list such that all elements less than the pivot are moved before it, and all elements greater than the pivot are moved after it. The pivot is now in its final position.\\n\\n3. **Recursively Apply**: Recursively apply the same process to the sub-lists of elements to the left and right of the pivot.\\n\\nThe base case of the recursion is lists of size zero or one, which are already sorted.\\n\\nQuick sort has an average-case time complexity of O(n log n), making it suitable for large datasets. However, its worst-case time complexity is O(n^2), which occurs when the smallest or largest element is always chosen as the pivot. This can be mitigated by using good pivot selection strategies or randomization.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  \"created\": 1723194057,\n  \"id\": \"chatcmpl-9uFmTYN4tfwaXZjyOQwcp0t5law4x\",\n  \"model\": \"gpt-4o-2024-05-13\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": {\n    \"completion_tokens\": 234,\n    \"prompt_tokens\": 18,\n    \"total_tokens\": 252\n  }\n}\n```\n\n### 配置多个模板\n\n以下示例演示了如何在同一条路由上配置多个模板。请求该路由时，用户将能够通过指定模板名称向不同模板传递自定义输入。\n\n该示例延续自[上一个示例](#为自定义复杂度的开放式问题配置模板)。使用另一个模板更新插件配置：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/v1/chat/completions\",\n    \"plugins\": {\n      \"ai-prompt-template\": {\n        \"templates\": [\n          {\n            \"name\": \"QnA with complexity\",\n            \"template\": {\n              \"model\": \"gpt-4\",\n              \"messages\": [\n                {\n                  \"role\": \"system\",\n                  \"content\": \"Answer in {{complexity}}.\"\n                },\n                {\n                  \"role\": \"user\",\n                  \"content\": \"Explain {{prompt}}.\"\n                }\n              ]\n            }\n          },\n          {\n            \"name\": \"echo\",\n            \"template\": {\n              \"model\": \"gpt-4\",\n              \"messages\": [\n                {\n                  \"role\": \"system\",\n                  \"content\": \"You are an echo bot. You must repeat exactly what the user says without any changes or additional text.\"\n                },\n                {\n                  \"role\": \"user\",\n                  \"content\": \"Echo {{prompt}}.\"\n                }\n              ]\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n现在，你应该能够通过同一条路由使用这两个模板。\n\n向路由发送一个 POST 请求，使用第一个模板：\n\n```shell\ncurl \"http://127.0.0.1:9080/v1/chat/completions\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template_name\": \"QnA with complexity\",\n    \"complexity\": \"brief\",\n    \"prompt\": \"quick sort\"\n  }'\n```\n\n你应该会收到类似于以下的响应：\n\n```json\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": {\n        \"content\": \"Quick sort is a highly efficient sorting algorithm that uses a divide-and-conquer approach to arrange elements in a list or array in order. Here’s a brief explanation:\\n\\n1. **Choose a Pivot**: Select an element from the list as a 'pivot'. Common methods include choosing the first element, the last element, the middle element, or a random element.\\n\\n2. **Partitioning**: Rearrange the elements in the list such that all elements less than the pivot are moved before it, and all elements greater than the pivot are moved after it. The pivot is now in its final position.\\n\\n3. **Recursively Apply**: Recursively apply the same process to the sub-lists of elements to the left and right of the pivot.\\n\\nThe base case of the recursion is lists of size zero or one, which are already sorted.\\n\\nQuick sort has an average-case time complexity of O(n log n), making it suitable for large datasets. However, its worst-case time complexity is O(n^2), which occurs when the smallest or largest element is always chosen as the pivot. This can be mitigated by using good pivot selection strategies or randomization.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  ...\n}\n```\n\n向路由发送一个 POST 请求，使用第二个模板：\n\n```shell\ncurl \"http://127.0.0.1:9080/v1/chat/completions\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template_name\": \"echo\",\n    \"prompt\": \"hello APISIX\"\n  }'\n```\n\n你应该会收到类似于以下的响应：\n\n```json\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": {\n        \"content\": \"hello APISIX\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  ...\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-proxy-multi.md",
    "content": "---\ntitle: ai-proxy-multi\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-proxy-multi\n  - AI\n  - LLM\ndescription: ai-proxy-multi 插件通过负载均衡、重试、故障转移和健康检查扩展了 ai-proxy 的功能，简化了与 OpenAI、DeepSeek、Azure、AIMLAPI、Anthropic、OpenRouter、Gemini、Vertex AI 和其他 OpenAI 兼容 API 的集成。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-proxy-multi\" />\n</head>\n\n## 描述\n\n`ai-proxy-multi` 插件通过将插件配置转换为 OpenAI、DeepSeek、Azure、AIMLAPI、Anthropic、OpenRouter、Gemini、Vertex AI 和其他 OpenAI 兼容 API 的指定请求格式，简化了对 LLM 和嵌入模型的访问。它通过负载均衡、重试、故障转移和健康检查扩展了 [`ai-proxy`](./ai-proxy.md) 的功能。\n\n此外，该插件还支持在访问日志中记录 LLM 请求信息，如令牌使用量、模型、首次响应时间等。\n\n## 请求格式\n\n| 名称               | 类型   | 必选项 | 描述                                         |\n| ------------------ | ------ | -------- | --------------------------------------------------- |\n| `messages`         | Array  | 是      | 消息对象数组。                        |\n| `messages.role`    | String | 是      | 消息的角色（`system`、`user`、`assistant`）。|\n| `messages.content` | String | 是      | 消息的内容。                             |\n\n## 属性\n\n| 名称                               | 类型            | 必选项 | 默认值                           | 有效值 | 描述 |\n|------------------------------------|----------------|----------|-----------------------------------|--------------|-------------|\n| fallback_strategy                  | string 或 array         | 否    |  | string: \"instance_health_and_rate_limiting\", \"http_429\", \"http_5xx\"<br />array: [\"rate_limiting\", \"http_429\", \"http_5xx\"] | 故障转移策略。设置后，插件将在转发请求时检查指定实例的令牌是否已耗尽。如果是，则无论实例优先级如何，都将请求转发到下一个实例。未设置时，当高优先级实例的令牌耗尽时，插件不会将请求转发到低优先级实例。 |\n| balancer                           | object         | 否    |                                   |              | 负载均衡配置。 |\n| balancer.algorithm                 | string         | 否    | roundrobin                     | [roundrobin, chash] | 负载均衡算法。设置为 `roundrobin` 时，使用加权轮询算法。设置为 `chash` 时，使用一致性哈希算法。 |\n| balancer.hash_on                   | string         | 否    |                                   | [vars, headers, cookie, consumer, vars_combinations] | 当 `type` 为 `chash` 时使用。支持基于 [NGINX 变量](https://nginx.org/en/docs/varindex.html)、标头、cookie、消费者或 [NGINX 变量](https://nginx.org/en/docs/varindex.html)组合进行哈希。 |\n| balancer.key                       | string         | 否    |                                   |              | 当 `type` 为 `chash` 时使用。当 `hash_on` 设置为 `header` 或 `cookie` 时，需要 `key`。当 `hash_on` 设置为 `consumer` 时，不需要 `key`，因为消费者名称将自动用作键。 |\n| instances                          | array[object]  | 是     |                                   |              | LLM 实例配置。 |\n| instances.name                     | string         | 是     |                                   |              | LLM 服务实例的名称。 |\n| instances.provider                 | string         | 是     |                                   | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, openai-compatible] | LLM 服务提供商。设置为 `openai` 时，插件将代理请求到 `api.openai.com`。设置为 `deepseek` 时，插件将代理请求到 `api.deepseek.com`。设置为 `aimlapi` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `api.aimlapi.com`。设置为 `anthropic` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `api.anthropic.com`。设置为 `openrouter` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `openrouter.ai`。设置为 `gemini` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `generativelanguage.googleapis.com`。设置为 `vertex-ai` 时，插件默认将请求代理到 `aiplatform.googleapis.com`，且需要配置 `provider_conf` 或 `override`。设置为 `openai-compatible` 时，插件将代理请求到在 `override` 中配置的自定义端点。 |\n| instances.provider_conf            | object         | 否     |                                   |              | 特定提供商的配置。当 `provider` 设置为 `vertex-ai` 且未配置 `override` 时必填。 |\n| instances.provider_conf.project_id | string         | 是     |                                   |              | Google Cloud 项目 ID。 |\n| instances.provider_conf.region     | string         | 是     |                                   |              | Google Cloud 区域。 |\n| instances.priority                  | integer        | 否    | 0                               |              | LLM 实例在负载均衡中的优先级。`priority` 优先于 `weight`。 |\n| instances.weight                    | string         | 是     | 0                               | 大于或等于 0 | LLM 实例在负载均衡中的权重。 |\n| instances.auth                      | object         | 是     |                                   |              | 身份验证配置。 |\n| instances.auth.header               | object         | 否    |                                   |              | 身份验证标头。应配置 `header` 和 `query` 中的至少一个。 |\n| instances.auth.query                | object         | 否    |                                   |              | 身份验证查询参数。应配置 `header` 和 `query` 中的至少一个。 |\n| instances.auth.gcp                  | object         | 否    |                                   |              | Google Cloud Platform (GCP) 身份验证配置。 |\n| instances.auth.gcp.service_account_json | string     | 否    |                                   |              | GCP 服务账号 JSON 文件的内容。 |\n| instances.auth.gcp.max_ttl          | integer        | 否    |                                   | minimum = 1  | GCP 服务帐户 JSON 文件的内容。也可以通过设置“GCP_SERVICE_ACCOUNT”环境变量来配置 |\n| instances.auth.gcp.expire_early_secs| integer        | 否    | 60                                | minimum = 0  | 在访问令牌实际过期时间之前使其过期的秒数，以避免边缘情况。 |\n| instances.options                   | object         | 否    |                                   |              | 模型配置。除了 `model` 之外，您还可以配置其他参数，它们将在请求体中转发到上游 LLM 服务。例如，如果您使用 OpenAI、DeepSeek 或 AIMLAPI，可以配置其他参数，如 `max_tokens`、`temperature`、`top_p` 和 `stream`。有关更多可用选项，请参阅您的 LLM 提供商的 API 文档。 |\n| instances.options.model             | string         | 否    |                                   |              | LLM 模型的名称，如 `gpt-4` 或 `gpt-3.5`。有关更多可用模型，请参阅您的 LLM 提供商的 API 文档。 |\n| logging                             | object         | 否    |                                   |              | 日志配置。 |\n| logging.summaries                   | boolean        | 否    | false                           |              | 如果为 true，记录请求 LLM 模型、持续时间、请求和响应令牌。 |\n| logging.payloads                    | boolean        | 否    | false                           |              | 如果为 true，记录请求和响应负载。 |\n| logging.override                    | object         | 否    |                                   |              | 覆盖设置。 |\n| logging.override.endpoint           | string         | 否    |                                   |              | 用于替换默认端点的 LLM 提供商端点。如果未配置，插件使用默认的 OpenAI 端点 `https://api.openai.com/v1/chat/completions`。 |\n| checks                              | object         | 否    |                                   |              | 健康检查配置。请注意，目前 OpenAI、DeepSeek 和 AIMLAPI 不提供官方健康检查端点。您可以在 `openai-compatible` 提供商下配置的其他 LLM 服务可能有可用的健康检查端点。 |\n| checks.active                       | object         | 是     |                                   |              | 主动健康检查配置。 |\n| checks.active.type                  | string         | 否    | http                            | [http, https, tcp] | 健康检查连接类型。 |\n| checks.active.timeout               | number         | 否    | 1                               |              | 健康检查超时时间（秒）。 |\n| checks.active.concurrency           | integer        | 否    | 10                              |              | 同时检查的上游节点数量。 |\n| checks.active.host                  | string         | 否    |                                   |              | HTTP 主机。 |\n| checks.active.port                  | integer        | 否    |                                   | 1 到 65535（包含） | HTTP 端口。 |\n| checks.active.http_path             | string         | 否    | /                               |              | HTTP 探测请求的路径。 |\n| checks.active.https_verify_certificate | boolean   | 否    | true                            |              | 如果为 true，验证节点的 TLS 证书。 |\n| timeout                             | integer        | 否    | 30000                           | 大于或等于 1 | 请求 LLM 服务时的请求超时时间（毫秒）。 |\n| keepalive                           | boolean        | 否    | true                            |              | 如果为 true，在请求 LLM 服务时保持连接活跃。 |\n| keepalive_timeout                   | integer        | 否    | 60000                           | 大于或等于 1000 | 请求 LLM 服务时的请求超时时间（毫秒）。 |\n| keepalive_pool                      | integer        | 否    | 30                              |              | 连接 LLM 服务时的保活池大小。 |\n| ssl_verify                          | boolean        | 否    | true                            |              | 如果为 true，验证 LLM 服务的证书。 |\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `ai-proxy-multi`。\n\n:::note\n\n您可以使用以下命令从 `config.yaml` 获取 `admin_key` 并保存到环境变量中：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 实例间负载均衡\n\n以下示例演示了如何配置两个模型进行负载均衡，将 80% 的流量转发到一个实例，20% 转发到另一个实例。\n\n为了演示和更容易区分，您将配置一个 OpenAI 实例和一个 DeepSeek 实例作为上游 LLM 服务。\n\n创建路由并更新您的 LLM 提供商、模型、API 密钥和端点（如果适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 8,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 2,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向路由发送 10 个 POST 请求，在请求体中包含系统提示和示例用户问题，以查看转发到 OpenAI 和 DeepSeek 的请求数量：\n\n```shell\nopenai_count=0\ndeepseek_count=0\n\nfor i in {1..10}; do\n  model=$(curl -s \"http://127.0.0.1:9080/anything\" -X POST \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\n      \"messages\": [\n        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n      ]\n    }' | jq -r '.model')\n\n  if [[ \"$model\" == *\"gpt-4\"* ]]; then\n    ((openai_count++))\n  elif [[ \"$model\" == \"deepseek-chat\" ]]; then\n    ((deepseek_count++))\n  fi\ndone\n\necho \"OpenAI responses: $openai_count\"\necho \"DeepSeek responses: $deepseek_count\"\n```\n\n您应该看到类似以下的响应：\n\n```text\nOpenAI responses: 8\nDeepSeek responses: 2\n```\n\n### 配置实例优先级和速率限制\n\n以下示例演示了如何配置两个具有不同优先级的模型，并在优先级较高的实例上应用速率限制。在 `fallback_strategy` 设置为 `[\"rate_limiting\"]` 的情况下，一旦高优先级实例的速率限制配额完全消耗，插件应继续将请求转发到低优先级实例。\n\n创建路由并更新您的 LLM 提供商、模型、API 密钥和端点（如果适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"fallback_strategy\": [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"priority\": 1,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"priority\": 0,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n由于 `total_tokens` 值超过了配置的 `10` 配额，预计在 60 秒窗口内的下一个请求将转发到另一个实例。\n\n在同一个 60 秒窗口内，向路由发送另一个 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newton law\" }\n    ]\n  }'\n```\n\n您应该看到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```#\n## 按消费者进行负载均衡和速率限制\n\n以下示例演示了如何配置两个模型进行负载均衡，并按消费者应用速率限制。\n\n创建消费者 `johndoe` 并在 `openai-instance` 实例上设置 60 秒窗口内 10 个令牌的速率限制配额：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n为 `johndoe` 配置 `key-auth` 凭据：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建另一个消费者 `janedoe` 并在 `deepseek-instance` 实例上设置 60 秒窗口内 10 个令牌的速率限制配额：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"janedoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n为 `janedoe` 配置 `key-auth` 凭据：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/janedoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\n创建路由并更新您的 LLM 提供商、模型、API 密钥和端点（如果适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"key-auth\": {},\n      \"ai-proxy-multi\": {\n        \"fallback_strategy\": [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，不带任何消费者密钥：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到 `HTTP/1.1 401 Unauthorized` 响应。\n\n使用 `johndoe` 的密钥向路由发送 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n由于 `total_tokens` 值超过了 `johndoe` 的 `openai` 实例配置配额，预计在 60 秒窗口内来自 `johndoe` 的下一个请求将转发到 `deepseek` 实例。\n\n在同一个 60 秒窗口内，使用 `johndoe` 的密钥向路由发送另一个 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\n您应该看到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\n使用 `janedoe` 的密钥向路由发送 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"The sum of 1 and 1 is 2. This is a basic arithmetic operation where you combine two units to get a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 14,\n    \"completion_tokens\": 31,\n    \"total_tokens\": 45,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 14\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\n由于 `total_tokens` 值超过了 `janedoe` 的 `deepseek` 实例配置配额，预计在 60 秒窗口内来自 `janedoe` 的下一个请求将转发到 `openai` 实例。\n\n在同一个 60 秒窗口内，使用 `janedoe` 的密钥向路由发送另一个 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\n您应该看到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure, here are Newton's three laws of motion:\\n\\n1) Newton's First Law, also known as the Law of Inertia, states that an object at rest will stay at rest, and an object in motion will stay in motion, unless acted on by an external force. In simple words, this law suggests that an object will keep doing whatever it is doing until something causes it to do otherwise. \\n\\n2) Newton's Second Law states that the force acting on an object is equal to the mass of that object times its acceleration (F=ma). This means that force is directly proportional to mass and acceleration. The heavier the object and the faster it accelerates, the greater the force.\\n\\n3) Newton's Third Law, also known as the law of action and reaction, states that for every action, there is an equal and opposite reaction. Essentially, any force exerted onto a body will create a force of equal magnitude but in the opposite direction on the object that exerted the first force.\\n\\nRemember, these laws become less accurate when considering speeds near the speed of light (where Einstein's theory of relativity becomes more appropriate) or objects very small or very large. However, for everyday situations, they provide a good model of how things move.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n这显示了 `ai-proxy-multi` 根据消费者在 `ai-rate-limiting` 中的速率限制规则对流量进行负载均衡。\n\n### 限制完成令牌的最大数量\n\n以下示例演示了如何在生成聊天完成时限制使用的 `completion_tokens` 数量。\n\n为了演示和更容易区分，您将配置一个 OpenAI 实例和一个 DeepSeek 实例作为上游 LLM 服务。\n\n创建路由并更新您的 LLM 提供商、模型、API 密钥和端点（如果适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\",\n              \"max_tokens\": 50\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\",\n              \"max_tokens\": 100\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons law\" }\n    ]\n  }'\n```\n\n如果请求被代理到 OpenAI，您应该看到类似以下的响应，其中内容根据 50 个 `max_tokens` 阈值被截断：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Newton's Laws of Motion are three physical laws that form the bedrock for classical mechanics. They describe the relationship between a body and the forces acting upon it, and the body'\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 20,\n    \"completion_tokens\": 50,\n    \"total_tokens\": 70,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n如果请求被代理到 DeepSeek，您应该看到类似以下的响应，其中内容根据 100 个 `max_tokens` 阈值被截断：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Newton's Laws of Motion are three fundamental principles that form the foundation of classical mechanics. They describe the relationship between a body and the forces acting upon it, and the body's motion in response to those forces. Here's a brief explanation of each law:\\n\\n1. **Newton's First Law (Law of Inertia):**\\n   - **Statement:** An object will remain at rest or in uniform motion in a straight line unless acted upon by an external force.\\n   - **Explanation:** This law\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 10,\n    \"completion_tokens\": 100,\n    \"total_tokens\": 110,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 10\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\n### 代理到嵌入模型\n\n以下示例演示了如何配置 `ai-proxy-multi` 插件以代理请求并在嵌入模型之间进行负载均衡。\n\n创建路由并更新您的 LLM 提供商、嵌入模型、API 密钥和端点：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"text-embedding-3-small\"\n            },\n            \"override\": {\n              \"endpoint\": \"https://api.openai.com/v1/embeddings\"\n            }\n          },\n          {\n            \"name\": \"az-openai-instance\",\n            \"provider\": \"openai-compatible\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$AZ_OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"text-embedding-3-small\"\n            },\n            \"override\": {\n              \"endpoint\": \"https://ai-plugin-developer.openai.azure.com/openai/deployments/text-embedding-3-small/embeddings?api-version=2023-05-15\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，包含输入字符串：\n\n```shell\ncurl \"http://127.0.0.1:9080/embeddings\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": \"hello world\"\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  \"object\": \"list\",\n  \"data\": [\n    {\n      \"object\": \"embedding\",\n      \"index\": 0,\n      \"embedding\": [\n        -0.0067144386,\n        -0.039197803,\n        0.034177095,\n        0.028763203,\n        -0.024785956,\n        -0.04201061,\n        ...\n      ],\n    }\n  ],\n  \"model\": \"text-embedding-3-small\",\n  \"usage\": {\n    \"prompt_tokens\": 2,\n    \"total_tokens\": 2\n  }\n}\n```\n\n### 启用主动健康检查\n\n以下示例演示了如何配置 `ai-proxy-multi` 插件以代理请求并在模型之间进行负载均衡，并启用主动健康检查以提高服务可用性。您可以在一个或多个实例上启用健康检查。\n\n创建路由并更新 LLM 提供商、嵌入模型、API 密钥和健康检查相关配置：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-multi-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"llm-instance-1\",\n            \"provider\": \"openai-compatible\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$YOUR_LLM_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"'\"$YOUR_LLM_MODEL\"'\"\n            }\n          },\n          {\n            \"name\": \"llm-instance-2\",\n            \"provider\": \"openai-compatible\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$YOUR_LLM_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"'\"$YOUR_LLM_MODEL\"'\"\n            },\n            \"checks\": {\n              \"active\": {\n                \"type\": \"https\",\n                \"host\": \"yourhost.com\",\n                \"http_path\": \"/your/probe/path\",\n                \"healthy\": {\n                  \"interval\": 2,\n                  \"successes\": 1\n                },\n                \"unhealthy\": {\n                  \"interval\": 1,\n                  \"http_failures\": 3\n                }\n              }\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n为了验证，行为应与[主动健康检查](../tutorials/health-check.md)中的验证一致。\n\n### 在访问日志中包含 LLM 信息\n\n以下示例演示了如何在网关的访问日志中记录 LLM 请求相关信息，以改进分析和审计。以下变量可用：\n\n* `request_llm_model`：请求中指定的 LLM 模型名称。\n* `apisix_upstream_response_time`：APISIX 向上游服务发送请求并接收完整响应所花费的时间\n* `request_type`：请求类型，值可能是 `traditional_http`、`ai_chat` 或 `ai_stream`。\n* `llm_time_to_first_token`：从发送请求到从 LLM 服务接收第一个令牌的持续时间（毫秒）。\n* `llm_model`：LLM 模型。\n* `llm_prompt_tokens`：提示中的令牌数量。\n* `llm_completion_tokens`：提示中的聊天完成令牌数量。\n\n在配置文件中更新访问日志格式以包含其他 LLM 相关变量：\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host \\\"$request_line\\\" $status $body_bytes_sent $request_time \\\"$http_referer\\\" \\\"$http_user_agent\\\" $upstream_addr $upstream_status $apisix_upstream_response_time \\\"$upstream_scheme://$upstream_host$upstream_uri\\\" \\\"$apisix_request_id\\\" \\\"$request_type\\\" \\\"$llm_time_to_first_token\\\" \\\"$llm_model\\\" \\\"$request_llm_model\\\"  \\\"$llm_prompt_tokens\\\" \\\"$llm_completion_tokens\\\"\"\n```\n\n重新加载 APISIX 以使配置更改生效。\n\n接下来，使用 `ai-proxy-multi` 插件创建路由并发送请求。例如，如果请求转发到 OpenAI 并且您收到以下响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null,\n        \"annotations\": []\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    ...\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n在网关的访问日志中，您应该看到类似以下的日志条目：\n\n```text\n192.168.215.1 - - [21/Mar/2025:04:28:03 +0000] api.openai.com \"POST /anything HTTP/1.1\" 200 804 2.858 \"-\" \"curl/8.6.0\" - - - 5765 \"http://api.openai.com\" \"5c5e0b95f8d303cb81e4dc456a4b12d9\" \"ai_chat\" \"2858\" \"gpt-4\" \"gpt-4\" \"23\" \"8\"\n```\n\n访问日志条目显示请求类型为 `ai_chat`，Apisix 上游响应时间为 `5765` 毫秒，首次令牌时间为 `2858` 毫秒，请求的 LLM 模型为 `gpt-4`。LLM 模型为 `gpt-4`，提示令牌使用量为 `23`，完成令牌使用量为 `8`。\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-proxy.md",
    "content": "---\ntitle: ai-proxy\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-proxy\n  - AI\n  - LLM\ndescription: ai-proxy 插件通过将插件配置转换为所需的请求格式，简化了对 LLM 和嵌入模型提供商的访问，支持 OpenAI、DeepSeek、Azure、AIMLAPI、Anthropic、OpenRouter、Gemini、Vertex AI 和其他 OpenAI 兼容的 API。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-proxy\" />\n</head>\n\n## 描述\n\n`ai-proxy` 插件通过将插件配置转换为指定的请求格式，简化了对 LLM 和嵌入模型的访问。它支持与 OpenAI、DeepSeek、Azure、AIMLAPI、Anthropic、OpenRouter、Gemini、Vertex AI 和其他 OpenAI 兼容的 API 集成。\n\n此外，该插件还支持在访问日志中记录 LLM 请求信息，如令牌使用量、模型、首次响应时间等。\n\n## 请求格式\n\n| 名称               | 类型   | 必选项 | 描述                                         |\n| ------------------ | ------ | -------- | --------------------------------------------------- |\n| `messages`         | Array  | 是      | 消息对象数组。                        |\n| `messages.role`    | String | 是      | 消息的角色（`system`、`user`、`assistant`）。|\n| `messages.content` | String | 是      | 消息的内容。                             |\n\n## 属性\n\n| 名称               | 类型    | 必选项 | 默认值 | 有效值                              | 描述 |\n|--------------------|--------|----------|---------|------------------------------------------|-------------|\n| provider          | string  | 是     |         | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, openai-compatible] | LLM 服务提供商。当设置为 `openai` 时，插件将代理请求到 `https://api.openai.com/chat/completions`。当设置为 `deepseek` 时，插件将代理请求到 `https://api.deepseek.com/chat/completions`。当设置为 `aimlapi` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `https://api.aimlapi.com/v1/chat/completions`。当设置为 `anthropic` 时，插件将代理请求到 `https://api.anthropic.com/v1/chat/completions`。当设置为 `openrouter` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `https://openrouter.ai/api/v1/chat/completions`。当设置为 `gemini` 时，插件使用 OpenAI 兼容驱动程序，默认将请求代理到 `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`。当设置为 `vertex-ai` 时，插件默认将请求代理到 `https://aiplatform.googleapis.com`，且需要配置 `provider_conf` 或 `override`。当设置为 `openai-compatible` 时，插件将代理请求到在 `override` 中配置的自定义端点。 |\n| provider_conf     | object  | 否     |         |                                          | 特定提供商的配置。当 `provider` 设置为 `vertex-ai` 且未配置 `override` 时必填。 |\n| provider_conf.project_id | string | 是 |       |                                          | Google Cloud 项目 ID。 |\n| provider_conf.region | string | 是   |         |                                          | Google Cloud 区域。 |\n| auth             | object  | 是     |         |                                          | 身份验证配置。 |\n| auth.header      | object  | 否    |         |                                          | 身份验证标头。必须配置 `header` 或 `query` 中的至少一个。 |\n| auth.query       | object  | 否    |         |                                          | 身份验证查询参数。必须配置 `header` 或 `query` 中的至少一个。 |\n| auth.gcp         | object  | 否    |         |                                          | Google Cloud Platform (GCP) 身份验证配置。 |\n| auth.gcp.service_account_json | string | 否 |  |                                          | GCP 服务账号 JSON 文件的内容。 |\n| auth.gcp.max_ttl | integer | 否    |         | minimum = 1                              | GCP 服务帐户 JSON 文件的内容。也可以通过设置“GCP_SERVICE_ACCOUNT”环境变量来配置。 |\n| auth.gcp.expire_early_secs | integer | 否 | 60 | minimum = 0                              | 在访问令牌实际过期时间之前使其过期的秒数，以避免边缘情况。 |\n| options         | object  | 否    |         |                                          | 模型配置。除了 `model` 之外，您还可以配置其他参数，它们将在请求体中转发到上游 LLM 服务。例如，如果您使用 OpenAI，可以配置其他参数，如 `temperature`、`top_p` 和 `stream`。有关更多可用选项，请参阅您的 LLM 提供商的 API 文档。  |\n| options.model   | string  | 否    |         |                                          | LLM 模型的名称，如 `gpt-4` 或 `gpt-3.5`。请参阅 LLM 提供商的 API 文档以了解可用模型。 |\n| override        | object  | 否    |         |                                          | 覆盖设置。 |\n| override.endpoint | string | 否    |         |                                          | 自定义 LLM 提供商端点，当 `provider` 为 `openai-compatible` 时必需。 |\n| logging        | object  | 否    |         |                                          | 日志配置。 |\n| logging.summaries | boolean | 否 | false |                                          | 如果为 true，记录请求 LLM 模型、持续时间、请求和响应令牌。 |\n| logging.payloads  | boolean | 否 | false |                                          | 如果为 true，记录请求和响应负载。 |\n| timeout        | integer | 否    | 30000    | ≥ 1                                      | 请求 LLM 服务时的请求超时时间（毫秒）。 |\n| keepalive      | boolean | 否    | true   |                                          | 如果为 true，在请求 LLM 服务时保持连接活跃。 |\n| keepalive_timeout | integer | 否 | 60000  | ≥ 1000                                   | 连接到 LLM 服务时的保活超时时间（毫秒）。 |\n| keepalive_pool | integer | 否    | 30       |                                          | LLM 服务连接的保活池大小。 |\n| ssl_verify     | boolean | 否    | true   |                                          | 如果为 true，验证 LLM 服务的证书。 |\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `ai-proxy`。\n\n:::note\n\n您可以使用以下命令从 `config.yaml` 获取 `admin_key` 并保存到环境变量中：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 代理到 OpenAI\n\n以下示例演示了如何在 `ai-proxy` 插件中配置 API 密钥、模型和其他参数，并在路由上配置插件以将用户提示代理到 OpenAI。\n\n获取 OpenAI [API 密钥](https://openai.com/blog/openai-api)并保存到环境变量：\n\n```shell\nexport OPENAI_API_KEY=<your-api-key>\n```\n\n创建路由并配置 `ai-proxy` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\":{\n          \"model\": \"gpt-4\"\n        }\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Host: api.openai.com\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n### 代理到 DeepSeek\n\n以下示例演示了如何配置 `ai-proxy` 插件以将请求代理到 DeepSeek。\n\n获取 DeepSeek API 密钥并保存到环境变量：\n\n```shell\nexport DEEPSEEK_API_KEY=<your-api-key>\n```\n\n创建路由并配置 `ai-proxy` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"deepseek\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n          }\n        },\n        \"options\": {\n          \"model\": \"deepseek-chat\"\n        }\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含示例问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      {\n        \"role\": \"system\",\n        \"content\": \"You are an AI assistant that helps people find information.\"\n      },\n      {\n        \"role\": \"user\",\n        \"content\": \"Write me a 50-word introduction for Apache APISIX.\"\n      }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Apache APISIX is a dynamic, real-time, high-performance API gateway and cloud-native platform. It provides rich traffic management features like load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability, and more. Designed for microservices and serverless architectures, APISIX ensures scalability, security, and seamless integration with modern DevOps workflows.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n### 代理到 Azure OpenAI\n\n以下示例演示了如何配置 `ai-proxy` 插件以将请求代理到其他 LLM 服务，如 Azure OpenAI。\n\n获取 Azure OpenAI API 密钥并保存到环境变量：\n\n```shell\nexport AZ_OPENAI_API_KEY=<your-api-key>\n```\n\n创建路由并配置 `ai-proxy` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai-compatible\",\n        \"auth\": {\n          \"header\": {\n            \"api-key\": \"'\"$AZ_OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\":{\n          \"model\": \"gpt-4\"\n        },\n        \"override\": {\n          \"endpoint\": \"https://api7-auzre-openai.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-15-preview\"\n        }\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含示例问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      {\n        \"role\": \"system\",\n        \"content\": \"You are an AI assistant that helps people find information.\"\n      },\n      {\n        \"role\": \"user\",\n        \"content\": \"Write me a 50-word introduction for Apache APISIX.\"\n      }\n    ],\n    \"max_tokens\": 800,\n    \"temperature\": 0.7,\n    \"frequency_penalty\": 0,\n    \"presence_penalty\": 0,\n    \"top_p\": 0.95,\n    \"stop\": null\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  \"choices\": [\n    {\n      ...,\n      \"message\": {\n        \"content\": \"Apache APISIX is a modern, cloud-native API gateway built to handle high-performance and low-latency use cases. It offers a wide range of features, including load balancing, rate limiting, authentication, and dynamic routing, making it an ideal choice for microservices and cloud-native architectures.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  ...\n}\n```\n\n### 代理到嵌入模型\n\n以下示例演示了如何配置 `ai-proxy` 插件以将请求代理到嵌入模型。此示例将使用 OpenAI 嵌入模型端点。\n\n获取 OpenAI [API 密钥](https://openai.com/blog/openai-api)并保存到环境变量：\n\n```shell\nexport OPENAI_API_KEY=<your-api-key>\n```\n\n创建路由并配置 `ai-proxy` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-proxy-route\",\n    \"uri\": \"/embeddings\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\":{\n          \"model\": \"text-embedding-3-small\",\n          \"encoding_format\": \"float\"\n        },\n        \"override\": {\n          \"endpoint\": \"https://api.openai.com/v1/embeddings\"\n        }\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，包含输入字符串：\n\n```shell\ncurl \"http://127.0.0.1:9080/embeddings\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": \"hello world\"\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  \"object\": \"list\",\n  \"data\": [\n    {\n      \"object\": \"embedding\",\n      \"index\": 0,\n      \"embedding\": [\n        -0.0067144386,\n        -0.039197803,\n        0.034177095,\n        0.028763203,\n        -0.024785956,\n        -0.04201061,\n        ...\n      ],\n    }\n  ],\n  \"model\": \"text-embedding-3-small\",\n  \"usage\": {\n    \"prompt_tokens\": 2,\n    \"total_tokens\": 2\n  }\n}\n```\n\n### 在访问日志中包含 LLM 信息\n\n以下示例演示了如何在网关的访问日志中记录 LLM 请求相关信息，以改进分析和审计。以下变量可用：\n\n* `request_llm_model`：请求中指定的 LLM 模型名称。\n* `apisix_upstream_response_time`：APISIX 向上游服务发送请求并接收完整响应所花费的时间\n* `request_type`：请求类型，值可能是 `traditional_http`、`ai_chat` 或 `ai_stream`。\n* `llm_time_to_first_token`：从发送请求到从 LLM 服务接收第一个令牌的持续时间（毫秒）。\n* `llm_model`：LLM 模型。\n* `llm_prompt_tokens`：提示中的令牌数量。\n* `llm_completion_tokens`：提示中的聊天完成令牌数量。\n\n在配置文件中更新访问日志格式以包含其他 LLM 相关变量：\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host \\\"$request_line\\\" $status $body_bytes_sent $request_time \\\"$http_referer\\\" \\\"$http_user_agent\\\" $upstream_addr $upstream_status $apisix_upstream_response_time \\\"$upstream_scheme://$upstream_host$upstream_uri\\\" \\\"$apisix_request_id\\\" \\\"$request_type\\\" \\\"$llm_time_to_first_token\\\" \\\"$llm_model\\\" \\\"$request_llm_model\\\"  \\\"$llm_prompt_tokens\\\" \\\"$llm_completion_tokens\\\"\"\n```\n\n重新加载 APISIX 以使配置更改生效。\n\n现在，如果您创建路由并按照[代理到 OpenAI 示例](#代理到-openai)发送请求，您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null,\n        \"annotations\": []\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    ...\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n在网关的访问日志中，您应该看到类似以下的日志条目：\n\n```text\n192.168.215.1 - - [21/Mar/2025:04:28:03 +0000] api.openai.com \"POST /anything HTTP/1.1\" 200 804 2.858 \"-\" \"curl/8.6.0\" - - - 5765 \"http://api.openai.com\" \"5c5e0b95f8d303cb81e4dc456a4b12d9\" \"ai_chat\" \"2858\" \"gpt-4\" \"gpt-4\" \"23\" \"8\"\n```\n\n访问日志条目显示请求类型为 `ai_chat`，Apisix 上游响应时间为 `5765` 毫秒，首次令牌时间为 `2858` 毫秒，请求的 LLM 模型为 `gpt-4`。LLM 模型为 `gpt-4`，提示令牌使用量为 `23`，完成令牌使用量为 `8`。\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-rag.md",
    "content": "---\ntitle: ai-rag\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-rag\n  - AI\n  - LLM\ndescription: ai-rag 插件通过检索增强生成（RAG）增强 LLM 输出，高效检索相关文档以提高响应的准确性和上下文相关性。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-rag\" />\n</head>\n\n## 描述\n\n`ai-rag` 插件为 LLM 提供检索增强生成（Retrieval-Augmented Generation，RAG）功能。它促进从外部数据源高效检索相关文档或信息，这些信息用于增强 LLM 响应，从而提高生成输出的准确性和上下文相关性。\n\n该插件支持使用 [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) 和 [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) 服务来生成嵌入和执行向量搜索。\n\n**_目前仅支持 [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) 和 [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) 服务来生成嵌入和执行向量搜索。欢迎提交 PR 以引入对其他服务提供商的支持。_**\n\n## 属性\n\n| 名称                                      |   必选项   |   类型   |   描述                                                                                                                             |\n| ----------------------------------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| embeddings_provider                             | 是          | object   | 嵌入模型提供商的配置。                                                                                           |\n| embeddings_provider.azure_openai                | 是          | object   | [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) 作为嵌入模型提供商的配置。 |\n| embeddings_provider.azure_openai.endpoint       | 是          | string   | Azure OpenAI 嵌入模型端点。                                                                                  |\n| embeddings_provider.azure_openai.api_key        | 是          | string   | Azure OpenAI API 密钥。                                                                                                                    |\n| vector_search_provider                          | 是          | object   | 向量搜索提供商的配置。                                                                                              |\n| vector_search_provider.azure_ai_search          | 是          | object   | Azure AI Search 的配置。                                                                                                         |\n| vector_search_provider.azure_ai_search.endpoint | 是          | string   | Azure AI Search 端点。                                                                                                                  |\n| vector_search_provider.azure_ai_search.api_key  | 是          | string   | Azure AI Search API 密钥。                                                                                                                  |\n\n## 请求体格式\n\n请求体中必须包含以下字段。\n\n|   字段              |   类型   |    描述                                                                                                                   |\n| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |\n| ai_rag               | object   | 请求体 RAG 规范。                                                                              |\n| ai_rag.embeddings    | object   | 生成嵌入所需的请求参数。内容将取决于配置的提供商的 API 规范。   |\n| ai_rag.vector_search | object   | 执行向量搜索所需的请求参数。内容将取决于配置的提供商的 API 规范。 |\n\n- `ai_rag.embeddings` 的参数\n\n  - Azure OpenAI\n\n  |   名称          |   必选项   |   类型   |   描述                                                                                                              |\n  | --------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------- |\n  | input           | 是          | string   | 用于计算嵌入的输入文本，编码为字符串。                                                                |\n  | user            | 否           | string   | 代表您的最终用户的唯一标识符，可以帮助监控和检测滥用。                          |\n  | encoding_format | 否           | string   | 返回嵌入的格式。可以是 `float` 或 `base64`。默认为 `float`。                            |\n  | dimensions      | 否           | integer  | 结果输出嵌入应具有的维数。仅在 text-embedding-3 及更高版本的模型中支持。 |\n\n有关其他参数，请参阅 [Azure OpenAI 嵌入文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings)。\n\n- `ai_rag.vector_search` 的参数\n\n  - Azure AI Search\n\n  |   字段   |   必选项   |   类型   |   描述                |\n  | --------- | ------------ | -------- | ---------------------------- |\n  | fields    | 是          | String   | 向量搜索的字段。 |\n\n  有关其他参数，请参阅 [Azure AI Search 文档](https://learn.microsoft.com/en-us/rest/api/searchservice/documents/search-post)。\n\n示例请求体：\n\n```json\n{\n  \"ai_rag\": {\n    \"vector_search\": { \"fields\": \"contentVector\" },\n    \"embeddings\": {\n      \"input\": \"which service is good for devops\",\n      \"dimensions\": 1024\n    }\n  }\n}\n```\n\n## 示例\n\n要跟随示例，请创建一个 [Azure 账户](https://portal.azure.com)并完成以下步骤：\n\n* 在 [Azure AI Foundry](https://oai.azure.com/portal) 中，部署一个生成式聊天模型，如 `gpt-4o`，以及一个嵌入模型，如 `text-embedding-3-large`。获取 API 密钥和模型端点。\n* 按照 [Azure 的示例](https://github.com/Azure/azure-search-vector-samples/blob/main/demo-python/code/basic-vector-workflow/azure-search-vector-python-sample.ipynb)使用 Python 在 [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) 中准备向量搜索。该示例将创建一个名为 `vectest` 的搜索索引，具有所需的架构，并上传包含 108 个各种 Azure 服务描述的[示例数据](https://github.com/Azure/azure-search-vector-samples/blob/main/data/text-sample.json)，以便基于 `title` 和 `content` 生成嵌入 `titleVector` 和 `contentVector`。在 Python 中执行向量搜索之前完成所有设置。\n* 在 [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) 中，[获取 Azure 向量搜索 API 密钥和搜索服务端点](https://learn.microsoft.com/en-us/azure/search/search-get-started-vector?tabs=api-key#retrieve-resource-information)。\n\n将 API 密钥和端点保存到环境变量：\n\n```shell\n# 替换为您的值\n\nAZ_OPENAI_DOMAIN=https://ai-plugin-developer.openai.azure.com\nAZ_OPENAI_API_KEY=9m7VYroxITMDEqKKEnpOknn1rV7QNQT7DrIBApcwMLYJQQJ99ALACYeBjFXJ3w3AAABACOGXGcd\nAZ_CHAT_ENDPOINT=${AZ_OPENAI_DOMAIN}/openai/deployments/gpt-4o/chat/completions?api-version=2024-02-15-preview\nAZ_EMBEDDING_MODEL=text-embedding-3-large\nAZ_EMBEDDINGS_ENDPOINT=${AZ_OPENAI_DOMAIN}/openai/deployments/${AZ_EMBEDDING_MODEL}/embeddings?api-version=2023-05-15\n\nAZ_AI_SEARCH_SVC_DOMAIN=https://ai-plugin-developer.search.windows.net\nAZ_AI_SEARCH_KEY=IFZBp3fKVdq7loEVe9LdwMvVdZrad9A4lPH90AzSeC06SlR\nAZ_AI_SEARCH_INDEX=vectest\nAZ_AI_SEARCH_ENDPOINT=${AZ_AI_SEARCH_SVC_DOMAIN}/indexes/${AZ_AI_SEARCH_INDEX}/docs/search?api-version=2024-07-01\n```\n\n:::note\n\n您可以使用以下命令从 `config.yaml` 获取 `admin_key` 并保存到环境变量中：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 与 Azure 集成以获得 RAG 增强响应\n\n以下示例演示了如何使用 [`ai-proxy`](ai-proxy.md) 插件将请求代理到 Azure OpenAI LLM，并使用 `ai-rag` 插件生成嵌入和执行向量搜索以增强 LLM 响应。\n\n创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n  \"id\": \"ai-rag-route\",\n  \"uri\": \"/rag\",\n  \"plugins\": {\n    \"ai-rag\": {\n      \"embeddings_provider\": {\n        \"azure_openai\": {\n          \"endpoint\": \"'\"$AZ_EMBEDDINGS_ENDPOINT\"'\",\n          \"api_key\": \"'\"$AZ_OPENAI_API_KEY\"'\"\n        }\n      },\n      \"vector_search_provider\": {\n        \"azure_ai_search\": {\n          \"endpoint\": \"'\"$AZ_AI_SEARCH_ENDPOINT\"'\",\n          \"api_key\": \"'\"$AZ_AI_SEARCH_KEY\"'\"\n        }\n      }\n    },\n    \"ai-proxy\": {\n      \"provider\": \"openai\",\n      \"auth\": {\n        \"header\": {\n          \"api-key\": \"'\"$AZ_OPENAI_API_KEY\"'\"\n        }\n      },\n      \"model\": \"gpt-4o\",\n      \"override\": {\n        \"endpoint\": \"'\"$AZ_CHAT_ENDPOINT\"'\"\n      }\n    }\n  }\n}'\n```\n\n向路由发送 POST 请求，在请求体中包含向量字段名称、嵌入模型维度和输入提示：\n\n```shell\ncurl \"http://127.0.0.1:9080/rag\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"ai_rag\":{\n      \"vector_search\":{\n        \"fields\":\"contentVector\"\n      },\n      \"embeddings\":{\n        \"input\":\"Which Azure services are good for DevOps?\",\n        \"dimensions\":1024\n      }\n    }\n  }'\n```\n\n您应该收到类似以下的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"choices\": [\n    {\n      \"content_filter_results\": {\n        ...\n      },\n      \"finish_reason\": \"length\",\n      \"index\": 0,\n      \"logprobs\": null,\n      \"message\": {\n        \"content\": \"Here is a list of Azure services categorized along with a brief description of each based on the provided JSON data:\\n\\n### Developer Tools\\n- **Azure DevOps**: A suite of services that help you plan, build, and deploy applications, including Azure Boards, Azure Repos, Azure Pipelines, Azure Test Plans, and Azure Artifacts.\\n- **Azure DevTest Labs**: A fully managed service to create, manage, and share development and test environments in Azure, supporting custom templates, cost management, and integration with Azure DevOps.\\n\\n### Containers\\n- **Azure Kubernetes Service (AKS)**: A managed container orchestration service based on Kubernetes, simplifying deployment and management of containerized applications with features like automatic upgrades and scaling.\\n- **Azure Container Instances**: A serverless container runtime to run and scale containerized applications without managing the underlying infrastructure.\\n- **Azure Container Registry**: A fully managed Docker registry service to store and manage container images and artifacts.\\n\\n### Web\\n- **Azure App Service**: A fully managed platform for building, deploying, and scaling web apps, mobile app backends, and RESTful APIs with support for multiple programming languages.\\n- **Azure SignalR Service**: A fully managed real-time messaging service to build and scale real-time web applications.\\n- **Azure Static Web Apps**: A serverless hosting service for modern web applications using static front-end technologies and serverless APIs.\\n\\n### Compute\\n- **Azure Virtual Machines**: Infrastructure-as-a-Service (IaaS) offering for deploying and managing virtual machines in the cloud.\\n- **Azure Functions**: A serverless compute service to run event-driven code without managing infrastructure.\\n- **Azure Batch**: A job scheduling service to run large-scale parallel and high-performance computing (HPC) applications.\\n- **Azure Service Fabric**: A platform to build, deploy, and manage scalable and reliable microservices and container-based applications.\\n- **Azure Quantum**: A quantum computing service to build and run quantum applications.\\n- **Azure Stack Edge**: A managed edge computing appliance to run Azure services and AI workloads on-premises or at the edge.\\n\\n### Security\\n- **Azure Bastion**: A fully managed service providing secure and scalable remote access to virtual machines.\\n- **Azure Security Center**: A unified security management service to protect workloads across Azure and on-premises infrastructure.\\n- **Azure DDoS Protection**: A cloud-based service to protect applications and resources from distributed denial-of-service (DDoS) attacks.\\n\\n### Databases\\n\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  \"created\": 1740625850,\n  \"id\": \"chatcmpl-B54gQdumpfioMPIybFnirr6rq9ZZS\",\n  \"model\": \"gpt-4o-2024-05-13\",\n  \"object\": \"chat.completion\",\n  \"prompt_filter_results\": [\n    {\n      \"prompt_index\": 0,\n      \"content_filter_results\": {\n        ...\n      }\n    }\n  ],\n  \"system_fingerprint\": \"fp_65792305e4\",\n  \"usage\": {\n    ...\n  }\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-rate-limiting.md",
    "content": "---\ntitle: ai-rate-limiting\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ai-rate-limiting\n  - AI\n  - LLM\ndescription: ai-rate-limiting 插件对发送到 LLM 服务的请求实施基于令牌的速率限制，防止过度使用，优化 API 消费，并确保高效的资源分配。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ai-rate-limiting\" />\n</head>\n\n## 描述\n\n`ai-rate-limiting` 插件对发送到 LLM 服务的请求实施基于令牌的速率限制。它通过控制在指定时间范围内消耗的令牌数量来帮助管理 API 使用，确保公平的资源分配并防止服务过载。它通常与 [`ai-proxy`](ai-proxy.md) 或 [`ai-proxy-multi`](ai-proxy-multi.md) 插件一起使用。\n\n## 属性\n\n| 名称                         | 类型            | 必选项 | 默认值  | 有效值                                             | 描述 |\n|------------------------------|----------------|----------|----------|---------------------------------------------------------|-------------|\n| limit                        | integer        | 否    |          | >0                             | 在给定时间间隔内允许的最大令牌数。`limit` 和 `instances.limit` 中至少应配置一个。如果未配置 `rules`,则为必填项。 |\n| time_window                  | integer        | 否    |          | >0                             | 与速率限制 `limit` 对应的时间间隔（秒）。`time_window` 和 `instances.time_window` 中至少应配置一个。如果未配置 `rules`,则为必填项。 |\n| rules                        | array[object]  | 否    |          |                                                         | 速率限制规则列表。每个规则是一个包含 `count`、`time_window` 和 `key` 的对象。如果配置了此项，则优先于 `limit` 和 `time_window`。 |\n| rules.count                  | integer 或 string | 是  |          | >0 或变量表达式                              | 在给定时间间隔内允许的最大令牌数。可以是静态整数或变量表达式，如 `$http_custom_limit`。 |\n| rules.time_window            | integer 或 string | 是  |          | >0 或变量表达式                              | 与速率限制 `count` 对应的时间间隔（秒）。可以是静态整数或变量表达式。 |\n| rules.key                    | string         | 是     |          |                                                         | 用于计数请求的键。如果配置的键不存在，则不会执行该规则。`key` 被解释为变量组合，例如：`$http_custom_a $http_custom_b`。 |\n| rules.header_prefix          | string         | 否    |          |                                                         | 速率限制头部的前缀。如果配置了此项，响应将包含 `X-AI-{header_prefix}-RateLimit-Limit`、`X-AI-{header_prefix}-RateLimit-Remaining` 和 `X-AI-{header_prefix}-RateLimit-Reset` 头部。如果未配置，将使用规则索引 (从 1 开始) 作为前缀。|\n| show_limit_quota_header      | boolean        | 否    | true     |                                                         | 如果为 true，则在响应中包含 `X-AI-RateLimit-Limit-*`、`X-AI-RateLimit-Remaining-*` 和 `X-AI-RateLimit-Reset-*` 头部，其中 `*` 是实例名称。 |\n| limit_strategy               | string         | 否    | total_tokens | [total_tokens, prompt_tokens, completion_tokens] | 应用速率限制的令牌类型。`total_tokens` 是 `prompt_tokens` 和 `completion_tokens` 的总和。 |\n| instances                    | array[object]  | 否    |          |                                                         | LLM 实例速率限制配置。 |\n| instances.name               | string         | 是     |          |                                                         | LLM 服务实例的名称。 |\n| instances.limit              | integer        | 是     |          | >0                             | 实例在给定时间间隔内允许的最大令牌数。 |\n| instances.time_window        | integer        | 是     |          | >0                             | 实例速率限制 `limit` 对应的时间间隔（秒）。 |\n| rejected_code                | integer        | 否    | 503      |  [200, 599]                           | 当超出配额的请求被拒绝时返回的 HTTP 状态码。 |\n| rejected_msg                 | string         | 否    |          |                                           | 当超出配额的请求被拒绝时返回的响应体。 |\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `ai-rate-limiting`。\n\n:::note\n\n您可以使用以下命令从 `config.yaml` 获取 `admin_key` 并保存到环境变量中：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 与 `ai-proxy` 一起应用速率限制\n\n以下示例演示了如何使用 `ai-proxy` 代理 LLM 流量，并使用 `ai-rate-limiting` 在实例上配置基于令牌的速率限制。\n\n创建一个路由并更新您的 LLM 提供商、模型、API 密钥和端点（如适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy\": {\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n          }\n        },\n        \"options\": {\n          \"model\": \"gpt-35-turbo-instruct\",\n          \"max_tokens\": 512,\n          \"temperature\": 1.0\n        }\n      },\n      \"ai-rate-limiting\": {\n        \"limit\": 300,\n        \"time_window\": 30,\n        \"limit_strategy\": \"prompt_tokens\"\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1 + 1 equals 2. This is a fundamental arithmetic operation where adding one unit to another results in a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n如果在 30 秒窗口内消耗了 300 个提示令牌的速率限制配额，所有额外的请求将被拒绝。\n\n### 对多个实例中的一个进行速率限制\n\n以下示例演示了如何使用 `ai-proxy-multi` 配置两个模型进行负载均衡，将 80% 的流量转发到一个实例，20% 转发到另一个实例。此外，使用 `ai-rate-limiting` 对接收 80% 流量的实例配置基于令牌的速率限制，这样当配置的配额完全消耗时，额外的流量将被转发到另一个实例。\n\n创建一个路由，对 `deepseek-instance-1` 实例应用 30 秒窗口内 100 个总令牌的速率限制配额，并更新您的 LLM 提供商、模型、API 密钥和端点（如适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance-1\",\n            \"provider\": \"deepseek\",\n            \"weight\": 8,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance-2\",\n            \"provider\": \"deepseek\",\n            \"weight\": 2,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance-1\",\n            \"limit_strategy\": \"total_tokens\",\n            \"limit\": 100,\n            \"time_window\": 30\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1 + 1 equals 2. This is a fundamental arithmetic operation where adding one unit to another results in a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n如果 `deepseek-instance-1` 实例在 30 秒窗口内消耗了 100 个令牌的速率限制配额，额外的请求将全部转发到 `deepseek-instance-2`，该实例没有速率限制。\n\n### 对所有实例应用相同配额\n\n以下示例演示了如何对 `ai-rate-limiting` 中的所有 LLM 上游实例应用相同的速率限制配额。\n\n为了演示和更容易区分，您将配置一个 OpenAI 实例和一个 DeepSeek 实例作为上游 LLM 服务。\n\n创建一个路由，对所有实例在 60 秒窗口内应用 100 个总令牌的速率限制配额，并更新您的 LLM 提供商、模型、API 密钥和端点（如适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"limit\": 100,\n        \"time_window\": 60,\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws\" }\n    ]\n  }'\n```\n\n您应该收到来自任一 LLM 实例的响应，类似以下内容：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure! Sir Isaac Newton formulated three laws of motion that describe the motion of objects. These laws are widely used in physics and engineering for studying and understanding how things move. Here they are:\\n\\n1. Newton's First Law - Law of Inertia: An object at rest tends to stay at rest and an object in motion tends to stay in motion with the same speed and in the same direction unless acted upon by an unbalanced force. This is also known as the principle of inertia.\\n\\n2. Newton's Second Law of Motion - Force and Acceleration: The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. This is usually formulated as F=ma where F is the force applied, m is the mass of the object and a is the acceleration produced.\\n\\n3. Newton's Third Law - Action and Reaction: For every action, there is an equal and opposite reaction. This means that any force exerted on a body will create a force of equal magnitude but in the opposite direction on the object that exerted the first force.\\n\\nIn simple terms: \\n1. If you slide a book on a table and let go, it will stop because of the friction (or force) between it and the table.\\n2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 256,\n    \"total_tokens\": 279,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n由于 `total_tokens` 值超过了配置的 `100` 配额，预期在 60 秒窗口内的下一个请求将被转发到另一个实例。\n\n在同一个 60 秒窗口内，向路由发送另一个 POST 请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws\" }\n    ]\n  }'\n```\n\n您应该收到来自另一个 LLM 实例的响应，类似以下内容：\n\n```json\n{\n  ...\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics. Here's an explanation of each law:\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia)**\\n- **Statement**: An object will remain at rest or in uniform motion in a straight line unless acted upon by an external force.\\n- **What it means**: This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion. If no net force acts on an object, its velocity (speed and direction) will not change.\\n- **Example**: A book lying on a table will stay at rest unless you push it. Similarly, a hockey puck sliding on ice will keep moving at a constant speed unless friction or another force slows it down.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration)**\\n- **Statement**: The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"length\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 13,\n    \"completion_tokens\": 256,\n    \"total_tokens\": 269,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 13\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\n由于 `total_tokens` 值超过了配置的 `100` 配额，预期在 60 秒窗口内的下一个请求将被拒绝。\n\n在同一个 60 秒窗口内，向路由发送第三个 POST 请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws\" }\n    ]\n  }'\n```\n\n您应该收到 `HTTP 429 Too Many Requests` 响应并观察到以下头部：\n\n```text\nX-AI-RateLimit-Limit-openai-instance: 100\nX-AI-RateLimit-Remaining-openai-instance: 0\nX-AI-RateLimit-Reset-openai-instance: 0\nX-AI-RateLimit-Limit-deepseek-instance: 100\nX-AI-RateLimit-Remaining-deepseek-instance: 0\nX-AI-RateLimit-Reset-deepseek-instance: 0\n```\n\n### 配置实例优先级和速率限制\n\n以下示例演示了如何配置两个具有不同优先级的模型，并对具有较高优先级的实例应用速率限制。在 `fallback_strategy` 设置为 `[\"rate_limiting\"]` 的情况下，一旦高优先级实例的速率限制配额完全消耗，插件应继续将请求转发到低优先级实例。\n\n创建一个路由，对 `openai-instance` 实例设置速率限制和更高的优先级，并将 `fallback_strategy` 设置为 `[\"rate_limiting\"]`。更新您的 LLM 提供商、模型、API 密钥和端点（如适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"ai-proxy-multi\": {\n        \"fallback_strategy\": [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"priority\": 1,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"priority\": 0,\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      },\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求，在请求体中包含系统提示和示例用户问题：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n由于 `total_tokens` 值超过了配置的 `10` 配额，预期在 60 秒窗口内的下一个请求将被转发到另一个实例。\n\n在同一个 60 秒窗口内，向路由发送另一个 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newton law\" }\n    ]\n  }'\n```\n\n您应该看到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\n### 按消费者进行负载均衡和速率限制\n\n以下示例演示了如何配置两个模型进行负载均衡，并按消费者应用速率限制。\n\n创建消费者 `johndoe` 并对 `openai-instance` 实例设置 60 秒窗口内 10 个令牌的速率限制配额：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n为 `johndoe` 配置 `key-auth` 凭据：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建另一个消费者 `janedoe` 并对 `deepseek-instance` 实例设置 60 秒窗口内 10 个令牌的速率限制配额：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"janedoe\",\n    \"plugins\": {\n      \"ai-rate-limiting\": {\n        \"instances\": [\n          {\n            \"name\": \"deepseek-instance\",\n            \"limit\": 10,\n            \"time_window\": 60\n          }\n        ],\n        \"rejected_code\": 429,\n        \"limit_strategy\": \"total_tokens\"\n      }\n    }\n  }'\n```\n\n为 `janedoe` 配置 `key-auth` 凭据：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/janedoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\n创建一个路由并更新您的 LLM 提供商、模型、API 密钥和端点（如适用）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ai-rate-limiting-route\",\n    \"uri\": \"/anything\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"key-auth\": {},\n      \"ai-proxy-multi\": {\n        \"fallback_strategy\": [\"rate_limiting\"],\n        \"instances\": [\n          {\n            \"name\": \"openai-instance\",\n            \"provider\": \"openai\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$OPENAI_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"gpt-4\"\n            }\n          },\n          {\n            \"name\": \"deepseek-instance\",\n            \"provider\": \"deepseek\",\n            \"weight\": 0,\n            \"auth\": {\n              \"header\": {\n                \"Authorization\": \"Bearer '\"$DEEPSEEK_API_KEY\"'\"\n              }\n            },\n            \"options\": {\n              \"model\": \"deepseek-chat\"\n            }\n          }\n        ]\n      }\n    }\n  }'\n```\n\n向路由发送不带任何消费者密钥的 POST 请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到 `HTTP/1.1 401 Unauthorized` 响应。\n\n使用 `johndoe` 的密钥向路由发送 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"1+1 equals 2.\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 23,\n    \"completion_tokens\": 8,\n    \"total_tokens\": 31,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0,\n      \"audio_tokens\": 0\n    },\n    \"completion_tokens_details\": {\n      \"reasoning_tokens\": 0,\n      \"audio_tokens\": 0,\n      \"accepted_prediction_tokens\": 0,\n      \"rejected_prediction_tokens\": 0\n    }\n  },\n  \"service_tier\": \"default\",\n  \"system_fingerprint\": null\n}\n```\n\n由于 `total_tokens` 值超过了 `johndoe` 的 `openai` 实例配置配额，预期在 60 秒窗口内来自 `johndoe` 的下一个请求将被转发到 `deepseek` 实例。\n\n在同一个 60 秒窗口内，使用 `johndoe` 的密钥向路由发送另一个 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: john-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\n您应该看到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Certainly! Newton's laws of motion are three fundamental principles that describe the relationship between the motion of an object and the forces acting on it. They were formulated by Sir Isaac Newton in the late 17th century and are foundational to classical mechanics.\\n\\n---\\n\\n### **1. Newton's First Law (Law of Inertia):**\\n- **Statement:** An object at rest will remain at rest, and an object in motion will continue moving at a constant velocity (in a straight line at a constant speed), unless acted upon by an external force.\\n- **Key Idea:** This law introduces the concept of **inertia**, which is the tendency of an object to resist changes in its state of motion.\\n- **Example:** If you slide a book across a table, it eventually stops because of the force of friction acting on it. Without friction, the book would keep moving indefinitely.\\n\\n---\\n\\n### **2. Newton's Second Law (Law of Acceleration):**\\n- **Statement:** The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass. Mathematically, this is expressed as:\\n  \\\\[\\n  F = ma\\n  \\\\]\\n  where:\\n  - \\\\( F \\\\) = net force applied (in Newtons),\\n  -\"\n      },\n      ...\n    }\n  ],\n  ...\n}\n```\n\n使用 `janedoe` 的密钥向路由发送 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n    ]\n  }'\n```\n\n您应该收到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"deepseek-chat\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"The sum of 1 and 1 is 2. This is a basic arithmetic operation where you combine two units to get a total of two units.\"\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 14,\n    \"completion_tokens\": 31,\n    \"total_tokens\": 45,\n    \"prompt_tokens_details\": {\n      \"cached_tokens\": 0\n    },\n    \"prompt_cache_hit_tokens\": 0,\n    \"prompt_cache_miss_tokens\": 14\n  },\n  \"system_fingerprint\": \"fp_3a5770e1b4_prod0225\"\n}\n```\n\n由于 `total_tokens` 值超过了 `janedoe` 的 `deepseek` 实例配置配额，预期在 60 秒窗口内来自 `janedoe` 的下一个请求将被转发到 `openai` 实例。\n\n在同一个 60 秒窗口内，使用 `janedoe` 的密钥向路由发送另一个 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H 'apikey: jane-key' \\\n  -d '{\n    \"messages\": [\n      { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n      { \"role\": \"user\", \"content\": \"Explain Newtons laws to me\" }\n    ]\n  }'\n```\n\n您应该看到类似以下的响应：\n\n```json\n{\n  ...,\n  \"model\": \"gpt-4-0613\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"Sure, here are Newton's three laws of motion:\\n\\n1) Newton's First Law, also known as the Law of Inertia, states that an object at rest will stay at rest, and an object in motion will stay in motion, unless acted on by an external force. In simple words, this law suggests th\",\n        \"refusal\": null\n      },\n      \"logprobs\": null,\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  ...\n}\n```\n\n这显示了 `ai-proxy-multi` 根据消费者在 `ai-rate-limiting` 中的速率限制规则对流量进行负载均衡。\n"
  },
  {
    "path": "docs/zh/latest/plugins/ai-request-rewrite.md",
    "content": "---\ntitle: ai-request-rewrite\nkeywords:\n  - Apache APISIX\n  - AI 网关\n  - Plugin\n  - ai-request-rewrite\ndescription: ai-request-rewrite 插件在客户端请求转发到上游服务之前拦截请求。它将预定义的提示与原始请求体一起发送到指定的 LLM 服务。LLM 处理输入并返回修改后的请求体，然后用于上游请求。这允许基于 AI 生成的内容动态转换 API 请求。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ai-request-rewrite` 插件在客户端请求转发到上游服务之前拦截请求。它将预定义的提示与原始请求体一起发送到指定的 LLM 服务。LLM 处理输入并返回修改后的请求体，然后用于上游请求。这允许基于 AI 生成的内容动态转换 API 请求。\n\n## 插件属性\n\n| **字段**                 | **必选项** | **类型** | **描述**                                                                      |\n| ------------------------- | ------------ | -------- | ------------------------------------------------------------------------------------ |\n| prompt                    | 是          | String   | 发送到 LLM 服务的提示。                                                      |\n| provider                  | 是          | String   | LLM 服务的名称。可用选项：openai、deekseek、azure-openai、aimlapi、anthropic、openrouter、gemini、vertex-ai 和 openai-compatible。当选择 `aimlapi` 时，插件使用 OpenAI 兼容驱动程序，默认端点为 `https://api.aimlapi.com/v1/chat/completions`。   |\n| provider_conf             | 否           | Object   | 特定提供商的配置。当 `provider` 设置为 `vertex-ai` 且未配置 `override` 时必填。 |\n| provider_conf.project_id  | 是           | String   | Google Cloud 项目 ID。 |\n| provider_conf.region      | 是           | String   | Google Cloud 区域。 |\n| auth                      | 是          | Object   | 身份验证配置                                                         |\n| auth.header               | 否           | Object   | 身份验证头部。键必须匹配模式 `^[a-zA-Z0-9._-]+$`。                  |\n| auth.query                | 否           | Object   | 身份验证查询参数。键必须匹配模式 `^[a-zA-Z0-9._-]+$`。         |\n| auth.gcp                  | 否           | Object   | Google Cloud Platform (GCP) 身份验证配置。 |\n| auth.gcp.service_account_json | 否       | String   | GCP 服务账号 JSON 文件的内容。也可以通过设置“GCP_SERVICE_ACCOUNT”环境变量来配置。 |\n| auth.gcp.max_ttl          | 否           | Integer  | 缓存 GCP 访问令牌的最大 TTL（秒）。最小值：1。 |\n| auth.gcp.expire_early_secs| 否           | Integer  | 在访问令牌实际过期时间之前使其过期的秒数，以避免边缘情况。最小值：0。默认值：60。 |\n| options                   | 否           | Object   | 模型的键/值设置                                                     |\n| options.model             | 否           | String   | 要执行的模型。示例：openai 的 \"gpt-3.5-turbo\"，deekseek 的 \"deepseek-chat\"，或 openai-compatible 或 aimlapi 服务的 \"qwen-turbo\" |\n| override.endpoint         | 否           | String   | 使用 OpenAI 兼容服务时覆盖默认端点（例如，自托管模型或第三方 LLM 服务）。当提供商为 'openai-compatible' 时，endpoint 字段是必需的。 |\n| timeout                   | 否           | Integer  | 对 LLM 服务请求的总超时时间（毫秒），包括连接、发送和读取超时。范围：1 - 60000。默认值：30000|\n| keepalive                 | 否           | Boolean  | 为对 LLM 服务的请求启用 keepalive。默认值：true                                  |\n| keepalive_timeout         | 否           | Integer  | 对 LLM 服务请求的 keepalive 超时时间（毫秒）。最小值：1000。默认值：60000 |\n| keepalive_pool            | 否           | Integer  | 对 LLM 服务请求的 keepalive 池大小。最小值：1。默认值：30                     |\n| ssl_verify                | 否           | Boolean  | 对 LLM 服务请求的 SSL 验证。默认值：true                                  |\n\n## 工作原理\n\n![image](https://github.com/user-attachments/assets/c7288e4f-00fc-46ca-b69e-d3d74d7085ca)\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `ai-request-rewrite`。\n\n:::note\n\n您可以使用以下命令从 config.yaml 获取 admin_key 并保存到环境变量中：\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n\n:::\n\n### 编辑敏感信息\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ai-request-rewrite\": {\n        \"prompt\": \"Given a JSON request body, identify and mask any sensitive information such as credit card numbers, social security numbers, and personal identification numbers (e.g., passport or driver'\\''s license numbers). Replace detected sensitive values with a masked format (e.g., \\\"*** **** **** 1234\\\") for credit card numbers. Ensure the JSON structure remains unchanged.\",\n        \"provider\": \"openai\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer <some-token>\"\n          }\n        },\n        \"options\": {\n          \"model\": \"gpt-4\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n现在发送一个请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"John Doe\",\n    \"email\": \"john.doe@example.com\",\n    \"credit_card\": \"4111 1111 1111 1111\",\n    \"ssn\": \"123-45-6789\",\n    \"address\": \"123 Main St\"\n  }'\n```\n\n发送到 LLM 服务的请求体如下：\n\n```json\n{\n  \"messages\": [\n     {\n       \"role\": \"system\",\n       \"content\": \"Given a JSON request body, identify and mask any sensitive information such as credit card numbers, social security numbers, and personal identification numbers (e.g., passport or driver's license numbers). Replace detected sensitive values with a masked format (e.g., '*** **** **** 1234') for credit card numbers). Ensure the JSON structure remains unchanged.\"\n     },\n     {\n       \"role\": \"user\",\n       \"content\": \"{\\n\\\"name\\\":\\\"John Doe\\\",\\n\\\"email\\\":\\\"john.doe@example.com\\\",\\n\\\"credit_card\\\":\\\"4111 1111 1111 1111\\\",\\n\\\"ssn\\\":\\\"123-45-6789\\\",\\n\\\"address\\\":\\\"123 Main St\\\"\\n}\"\n     }\n   ]\n}\n\n```\n\nLLM 处理输入并返回修改后的请求体，将检测到的敏感值替换为掩码格式，然后用于上游请求：\n\n```json\n{\n  \"name\": \"John Doe\",\n  \"email\": \"john.doe@example.com\",\n  \"credit_card\": \"**** **** **** 1111\",\n  \"ssn\": \"***-**-6789\",\n  \"address\": \"123 Main St\"\n}\n```\n\n### 向 OpenAI 兼容的 LLM 发送请求\n\n创建一个带有 `ai-request-rewrite` 插件的路由，将 `provider` 设置为 `openai-compatible`，并将模型的端点设置为 `override.endpoint`，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ai-request-rewrite\": {\n        \"prompt\": \"Given a JSON request body, identify and mask any sensitive information such as credit card numbers, social security numbers, and personal identification numbers (e.g., passport or driver'\\''s license numbers). Replace detected sensitive values with a masked format (e.g., '*** **** **** 1234') for credit card numbers). Ensure the JSON structure remains unchanged.\",\n        \"provider\": \"openai-compatible\",\n        \"auth\": {\n          \"header\": {\n            \"Authorization\": \"Bearer <some-token>\"\n          }\n        },\n        \"options\": {\n          \"model\": \"qwen-plus\",\n          \"max_tokens\": 1024,\n          \"temperature\": 1\n        },\n        \"override\": {\n          \"endpoint\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/api-breaker.md",
    "content": "---\ntitle: api-breaker\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - API Breaker\ndescription: 本文介绍了 Apache APISIX api-breaker 插件的相关操作，你可以使用此插件的 API 熔断机制来保护上游业务服务。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`api-breaker` 插件实现了 API 熔断功能，从而帮助我们保护上游业务服务。\n\n:::note 注意\n\n关于熔断超时逻辑，由代码逻辑自动按**触发不健康状态**的次数递增运算：\n\n当上游服务返回 `unhealthy.http_statuses` 配置中的状态码（默认为 `500`），并达到 `unhealthy.failures` 预设次数时（默认为 3 次），则认为上游服务处于不健康状态。\n\n第一次触发不健康状态时，熔断 2 秒。超过熔断时间后，将重新开始转发请求到上游服务，如果继续返回 `unhealthy.http_statuses` 状态码，记数再次达到 `unhealthy.failures` 预设次数时，熔断 4 秒。依次类推（2，4，8，16，……），直到达到预设的 `max_breaker_sec`值。\n\n当上游服务处于不健康状态时，如果转发请求到上游服务并返回 `healthy.http_statuses` 配置中的状态码（默认为 `200`），并达到 `healthy.successes` 次时，则认为上游服务恢复至健康状态。\n\n:::\n\n## 属性\n\n| 名称                    | 类型           | 必选项 | 默认值     | 有效值          | 描述                             |\n| ----------------------- | -------------- | ------ | ---------- | --------------- | -------------------------------- |\n| break_response_code     | integer        | 是   |           | [200, ..., 599] | 当上游服务处于不健康状态时返回的 HTTP 错误码。                 |\n| break_response_body     | string         | 否   |           |                 | 当上游服务处于不健康状态时返回的 HTTP 响应体信息。                   |\n| break_response_headers  | array[object]  | 否   |           | [{\"key\":\"header_name\",\"value\":\"can contain Nginx $var\"}] | 当上游服务处于不健康状态时返回的 HTTP 响应头信息。该字段仅在配置了 `break_response_body` 属性时生效，并能够以 `$var` 的格式包含 APISIX 变量，比如 `{\"key\":\"X-Client-Addr\",\"value\":\"$remote_addr:$remote_port\"}`。 |\n| max_breaker_sec         | integer        | 否   | 300        | >=3             | 上游服务熔断的最大持续时间，以秒为单位。                 |\n| unhealthy.http_statuses | array[integer] | 否   | [500]      | [500, ..., 599] | 上游服务处于不健康状态时的 HTTP 状态码。               |\n| unhealthy.failures      | integer        | 否   | 3          | >=1             | 上游服务在一定时间内触发不健康状态的异常请求次数。 |\n| healthy.http_statuses   | array[integer] | 否   | [200]      | [200, ..., 499] | 上游服务处于健康状态时的 HTTP 状态码。                 |\n| healthy.successes       | integer        | 否   | 3          | >=1             | 上游服务触发健康状态的连续正常请求次数。   |\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用 `api-breaker` 插件，该路由配置表示在一定时间内返回 `500` 或 `503` 状态码达到 3 次后触发熔断，返回 `200` 状态码 1 次后恢复健康：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"api-breaker\": {\n            \"break_response_code\": 502,\n            \"unhealthy\": {\n                \"http_statuses\": [500, 503],\n                \"failures\": 3\n            },\n            \"healthy\": {\n                \"http_statuses\": [200],\n                \"successes\": 1\n            }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n按上述配置启用插件后，使用 `curl` 命令请求该路由：\n\n```shell\ncurl -i -X POST \"http://127.0.0.1:9080/hello\"\n```\n\n如果上游服务在一定时间内返回 `500` 状态码达到 3 次，客户端将会收到 `502 Bad Gateway` 的应答：\n\n```shell\nHTTP/1.1 502 Bad Gateway\n...\n<html>\n<head><title>502 Bad Gateway</title></head>\n<body>\n<center><h1>502 Bad Gateway</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n## 删除插件\n\n当你需要禁用该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/attach-consumer-label.md",
    "content": "---\ntitle: attach-consumer-label\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - API Consumer\ndescription: 本文介绍了 Apache APISIX attach-consumer-label 插件的相关操作，你可以使用此插件向上游服务传递自定义的 Consumer labels。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`attach-consumer-label` 插件在 X-Consumer-Username 和 X-Credential-Indentifier 之外，还将自定义的消费者相关标签附加到经过身份验证的请求，以便上游服务区分消费者并实现额外的逻辑。\n\n## 属性\n\n| 名称      | 类型   | 必选项  | 默认值    | 有效值    | 描述                                                                                                                                                 |\n|----------|--------|--------|----------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------|\n| headers  | object | 是     |          |        | 要附加到请求标头的 Consumer 标签的键值对，其中键是请求标头名称，例如 \"X-Consumer-Role\"，值是对客户标签键的引用，例如 \"$role\"。请注意，该值应始终以美元符号 (`$`) 开头。如果 Consumer 上没有配置引用的值，则相应的标头将不会附加到请求中。 |\n\n## 启用插件\n\n下面的示例演示了如何在通过身份验证的请求转发到上游服务之前，将自定义标签附加到请求标头。如果请求被拒绝，就不会在请求标头上附加任何消费者标签。如果某个标签值未在消费者上配置，但在“attach-consumer-label”插件中被引用，相应的标头也不会被附加。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n创建一个有自定义标签的 Consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"labels\": {\n      \"department\": \"devops\",\n      \"company\": \"api7\"\n    }\n  }'\n```\n\n为 Consumer `john` 配置 `key-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建路由并启用 `key-auth` 和 `attach-consumer-label` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"attach-consumer-label-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"attach-consumer-label\": {\n        \"headers\": {\n          \"X-Consumer-Department\": \"$department\",\n          \"X-Consumer-Company\": \"$company\",\n          \"X-Consumer-Role\": \"$role\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n:::tip\n\n引用标签的值必须以 `$` 符号开头。\n\n:::\n\n使用正确的 apikey 请求该路由，验证插件：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key'\n```\n\n可以看到类似的 `HTTP/1.1 200 OK` 响应：\n\n```text\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Apikey\": \"john-key\",\n    \"Host\": \"127.0.0.1\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-key-auth\",\n    \"X-Consumer-Company\": \"api7\",\n    \"X-Consumer-Department\": \"devops\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66e5107c-5bb3e24f2de5baf733aec1cc\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n## 删除插件\n\n当你需要禁用该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/attach-consumer-label-route\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/get\",\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/authz-casbin.md",
    "content": "---\ntitle: authz-casbin\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Authz Casbin\n  - authz-casbin\ndescription: 本文介绍了关于 Apache APISIX `authz-casbin` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`authz-casbin` 插件是一个基于 [Lua Casbin](https://github.com/casbin/lua-casbin/) 的访问控制插件，该插件支持各种 [access control models](https://casbin.org/docs/en/supported-models) 的强大授权场景。\n\n## 属性\n\n| 名称         | 类型    | 必选项 | 描述                               |\n| ----------- | ------ | ------- | ---------------------------------- |\n| model_path  | string | 是      | Casbin 鉴权模型配置文件路径。        |\n| policy_path | string | 是      | Casbin 鉴权策略配置文件路径。        |\n| model       | string | 是      | Casbin 鉴权模型的文本定义。          |\n| policy      | string | 是      | Casbin 鉴权策略的文本定义。          |\n| username    | string | 是      | 描述请求中有可以通过访问控制的用户名。 |\n\n:::note\n\n你必须在插件配置中指定 `model_path`、`policy_path` 和 `username` 或者指定 `model`、`policy` 和 `username` 才能使插件生效。\n\n如果你想要使所有的 Route 共享 Casbin 配置，你可以先在插件元数据中指定 `model` 和 `policy`，在插件配置中仅指定 `username`，这样所有 Route 都可以使用 Casbin 插件配置。\n\n::::\n\n## 元数据\n\n| 名称        | 类型    | 必选项  | 描述                           |\n| ----------- | ------ | ------- | ------------------------------|\n| model       | string | 是      | Casbin 鉴权模型的文本定义。     |\n| policy      | string | 是      | Casbin 鉴权策略的文本定义。     |\n\n## 启用插件\n\n你可以使用 model/policy 文件路径或使用插件 configuration/metadata 中的 model/policy 文本配置在 Route 上启用插件。\n\n### 通过 model/policy 文件路径启用插件\n\n以下示例展示了通过 model/policy 配置文件来设置 Casbin 身份验证：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"authz-casbin\": {\n            \"model_path\": \"/path/to/model.conf\",\n            \"policy_path\": \"/path/to/policy.csv\",\n            \"username\": \"user\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n```\n\n### 通过 model/policy 文本配置启用插件\n\n以下示例展示了通过你的 model/policy 文本来设置 Casbin 身份验证：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"authz-casbin\": {\n            \"model\": \"[request_definition]\n            r = sub, obj, act\n\n            [policy_definition]\n            p = sub, obj, act\n\n            [role_definition]\n            g = _, _\n\n            [policy_effect]\n            e = some(where (p.eft == allow))\n\n            [matchers]\n            m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n\n            \"policy\": \"p, *, /, GET\n            p, admin, *, *\n            g, alice, admin\",\n\n            \"username\": \"user\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n```\n\n### 通过 plugin metadata 配置模型/策略\n\n首先，我们需要使用 Admin API 发送一个 `PUT` 请求，将 `model` 和 `policy` 的配置添加到插件的元数据中。\n\n所有通过这种方式创建的 Route 都会带有一个带插件元数据配置的 Casbin enforcer。你也可以使用这种方式更新 model/policy，该插件将会自动同步最新的配置信息。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/authz-casbin \\\n-H \"X-API-KEY: $admin_key\" -i -X PUT -d '\n{\n\"model\": \"[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n\n\"policy\": \"p, *, /, GET\np, admin, *, *\ng, alice, admin\"\n}'\n```\n\n更新插件元数据后，可以将插件添加到指定 Route 中：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"authz-casbin\": {\n            \"username\": \"user\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n```\n\n:::note\n\n插件路由的配置比插件元数据的配置有更高的优先级。因此，如果插件路由的配置中存在 model/policy 配置，插件将优先使用插件路由的配置而不是插件元数据中的配置。\n\n:::\n\n## 测试插件\n\n首先定义测试鉴权模型：\n\n```conf\n[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\n```\n\n然后添加测试鉴权策略：\n\n```conf\np, *, /, GET\np, admin, *, *\ng, alice, admin\n```\n\n如果想要了解更多关于 `policy` 和 `model` 的配置，请参考 [examples](https://github.com/casbin/lua-casbin/tree/master/examples)。\n\n上述配置将允许所有人使用 `GET` 请求访问主页（`/`），而只有具有管理员权限的用户才可以访问其他页面并使用其他请求方法。\n\n简单举例来说，假设我们向主页发出 `GET` 请求，通常都可以返回正常结果。\n\n```shell\ncurl -i http://127.0.0.1:9080/ -X GET\n```\n\n但如果是一个未经授权的普通用户（例如：`bob`）访问除 `/` 以外的其他页面，将得到一个 403 错误：\n\n```shell\ncurl -i http://127.0.0.1:9080/res -H 'user: bob' -X GET\n```\n\n```\nHTTP/1.1 403 Forbidden\n```\n\n而拥有管理权限的用户（如 `alice`）则可以访问其它页面。\n\n```shell\ncurl -i http://127.0.0.1:9080/res -H 'user: alice' -X GET\n```\n\n## 删除插件\n\n当你需要禁用 `authz-casbin` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/authz-casdoor.md",
    "content": "---\ntitle: authz-casdoor\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Authz Casdoor\n  - authz-casdoor\ndescription: 本篇文档介绍了 Apache APISIX auth-casdoor 插件的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n使用 `authz-casdoor` 插件可添加 [Casdoor](https://casdoor.org/) 集中认证方式。\n\n## 属性\n\n| 名称          | 类型   | 必选项 | 描述                                  |\n|---------------|--------|----------|----------------------------------------------|\n| endpoint_addr | string | 是     | Casdoor 的 URL。                           |\n| client_id     | string | 是     | Casdoor 的客户端 id。                      |\n| client_secret | string | 是     | Casdoor 的客户端密钥。                 |\n| callback_url  | string | 是     | 用于接收 code 与 state 的回调地址。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"client_secret\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n:::info IMPORTANT\n\n指定 `endpoint_addr` 和 `callback_url` 属性时不要以“/”来结尾。\n\n`callback_url` 必须是路由的 URI。具体细节可查看下方示例内容，了解相关配置。\n\n:::\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用 `auth-casdoor` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"uri\": \"/anything/*\",\n  \"plugins\": {\n    \"authz-casdoor\": {\n        \"endpoint_addr\":\"http://localhost:8000\",\n        \"callback_url\":\"http://localhost:9080/anything/callback\",\n        \"client_id\":\"7ceb9b7fda4a9061ec1c\",\n        \"client_secret\":\"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n## 测试插件\n\n一旦启用了该插件，访问该路由的新用户首先会经过 `authz-casdoor` 插件的处理，然后被重定向到 Casdoor 登录页面。\n\n成功登录后，Casdoor 会将该用户重定向到 `callback_url`，并指定 GET 参数的 `code` 和 `state`。该插件还会向 Casdoor 请求一个访问 Token，并确认用户是否已登录。在成功认证后，该流程只出现一次并且后续请求不会被打断。\n\n上述操作完成后，用户就会被重定向到目标 URL。\n\n## 删除插件\n\n当需要禁用 `authz-casdoor` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/anything/*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/authz-keycloak.md",
    "content": "---\ntitle: authz-keycloak\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Authz Keycloak\n  - authz-keycloak\ndescription: 本文介绍了关于 Apache APISIX `authz-keycloak` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`authz-keycloak` 插件可用于通过 [Keycloak Identity Server](https://www.keycloak.org/) 添加身份验证。\n\n:::tip\n\n虽然该插件是为了与 Keycloak 一起使用而开发的，但是它也可以与任何符合 OAuth/OIDC 或 UMA 协议的身份认证软件一起使用。\n\n:::\n\n如果你想了解 Keycloak 的更多信息，请参考 [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/)。\n\n## 属性\n\n| 名称                                         | 类型          | 必选项 | 默认值                                         | 有效值                                                       | 描述                                                                                                                                                                                                                                           |\n|----------------------------------------------|---------------|-------|-----------------------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| discovery                                    | string        | 否    |                                               | https://host.domain/realms/foo/.well-known/uma2-configuration | Keycloak 授权服务的 [discovery document](https://www.keycloak.org/docs/latest/authorization_services/index.html) 的 URL。                                                                                                |\n| token_endpoint                               | string        | 否    |                                               | https://host.domain/realms/foo/protocol/openid-connect/token  | 接受 OAuth2 兼容 token 的接口，需要支持 `urn:ietf:params:oauth:grant-type:uma-ticket` 授权类型。                                                                                       |\n| resource_registration_endpoint               | string        | 否    |                                               | https://host.domain/realms/foo/authz/protection/resource_set  | 符合 UMA 的资源注册端点。如果提供，则覆盖发现中的值。                                                                                                                 |\n| client_id                                    | string        | 是    |                                               |                                                                    | 客户端正在寻求访问的资源服务器的标识符。                                                                                                                                          |\n| client_secret                                | string        | 否    |                                               |                                                                    | 客户端密码（如果需要）。                                                                                                                                                                                                                       |\n| grant_type                                   | string        | 否    | \"urn:ietf:params:oauth:grant-type:uma-ticket\" | [\"urn:ietf:params:oauth:grant-type:uma-ticket\"]                    |                                                                                                                                                                                                                                                       |\n| policy_enforcement_mode                      | string        | 否    | \"ENFORCING\"                                   | [\"ENFORCING\", \"PERMISSIVE\"]                                        |                                                                                                                                                                                                                                                       |\n| permissions                                  | array[string] | 否    |                                               |                                                                    | 描述客户端应用所需访问的资源和权限范围的字符串。格式必须为：`RESOURCE_ID#SCOPE_ID`。                                                                                                                                        |\n| lazy_load_paths                              | boolean       | 否    | false                                         | [true, false]                                                      | 当设置为 true 时，使用资源注册端点而不是静态权限将请求 URI 动态解析为资源。                                                                                                      |\n| http_method_as_scope                         | boolean       | 否    | false                                         | [true, false]                                                      | 设置为 true 时，将 HTTP 请求类型映射到同名范围并添加到所有请求的权限。                                                                                                                                         |\n| timeout                                      | integer       | 否    | 3000                                          | [1000, ...]                                                        | 与 Identity Server 的 HTTP 连接超时（毫秒）。                                                                                                                                                                                       |\n| access_token_expires_in                      | integer       | 否    | 300                                           | [1, ...]                                                           | 访问令牌的有效期。token.                                                                                                                                                                                                               |\n| access_token_expires_leeway                  | integer       | 否    | 0                                             | [0, ...]                                                           | access_token 更新的到期余地。设置后，令牌将在到期前几秒更新 access_token_expires_leeway。这避免了 access_token 在到达 OAuth 资源服务器时刚刚过期的情况。 |\n| refresh_token_expires_in                     | integer       | 否    | 3600                                          | [1, ...]                                                           | 刷新令牌的失效时间。                                                                                                                                                                                                          |\n| refresh_token_expires_leeway                 | integer       | 否    | 0                                             | [0, ...]                                                           | refresh_token 更新的到期余地。设置后，令牌将在到期前几秒刷新 refresh_token_expires_leeway。这样可以避免在到达 OAuth 资源服务器时 refresh_token 刚刚过期的错误。 |\n| ssl_verify                                   | boolean       | 否    | true                                          | [true, false]                                                      | 设置为 `true` 时，验证 TLS 证书是否与主机名匹配。                                                                                                                                                                                        |\n| cache_ttl_seconds                            | integer       | 否    | 86400 (equivalent to 24h)                     | positive integer >= 1                                              | 插件缓存插件用于向 Keycloak 进行身份验证的发现文档和令牌的最长时间（以秒为单位）。                                                                                                                                                                |\n| keepalive                                    | boolean       | 否    | true                                          | [true, false]                                                      | 当设置为 `true` 时，启用 HTTP keep-alive 保证在使用后仍然保持连接打开。如果您期望对 Keycloak 有很多请求，请设置为 `true`。                                                                                                                                |\n| keepalive_timeout                            | integer       | 否    | 60000                                         | positive integer >= 1000                                           | 已建立的 HTTP 连接将关闭之前的空闲时间。                                                                                                                                         |\n| keepalive_pool                               | integer       | 否    | 5                                             | positive integer >= 1                                              | 连接池中的最大连接数。                                                                                                                                                                                                    |\n| access_denied_redirect_uri                   | string        | 否    |                                               | [1, 2048]                                                          | 需要将用户重定向到的 URI，而不是返回类似 `\"error_description\":\"not_authorized\"` 这样的错误消息。                                                                                                                                        |\n| password_grant_token_generation_incoming_uri | string        | 否    |                                               | /api/token                                                         | 将此设置为使用密码授予类型生成令牌。该插件会将传入的请求 URI 与此值进行比较。                                                                                                                |\n\n注意：schema 中还定义了 `encrypt_fields = {\"client_secret\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n除上述释义外，还有以下需要注意的点：\n\n- Discovery and endpoints\n    - 使用 `discovery` 属性后，`authz-keycloak` 插件就可以从其 URL 中发现 Keycloak API 的端点。该 URL 指向 Keyloak 针对相应领域授权服务的发现文档。\n    - 如果发现文档可用，则插件将根据该文档确定令牌端点 URL。如果 URL 存在，则 `token_endpoint` 和 `resource_registration_endpoint` 的值将被其覆盖。\n- Client ID and secret\n    - 该插件需配置 `client_id` 属性来标识自身。\n    - 如果 `lazy_load_paths` 属性被设置为 `true`，那么该插件还需要从 Keycloak 中获得一个自身访问令牌。在这种情况下，如果客户端对 Keycloak 的访问是加密的，就需要配置 `client_secret` 属性。\n- Policy enforcement mode\n    - `policy_enforcement_mode` 属性指定了在处理发送到服务器的授权请求时，该插件如何执行策略。\n        - `ENFORCING` mode：即使没有与给定资源关联的策略，请求也会默认被拒绝。`policy_enforcement_mode` 默认设置为 `ENFORCING`。\n        - `PERMISSIVE` mode：如果资源没有绑定任何访问策略，也被允许请求。\n- Permissions\n    - 在处理传入的请求时，插件可以根据请求的参数确定静态或动态检查 Keycloak 的权限。\n    - 如果 `lazy_load_paths` 参数设置为 `false`，则权限来自 `permissions` 属性。`permissions` 中的每个条目都需要按照令牌端点预设的 `permission` 属性进行格式化。详细信息请参考 [Obtaining Permissions](https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions).\n\n    :::note\n\n    有效权限可以是单个资源，也可以是与一个或多个范围配对的资源。\n\n    :::\n\n    如果 `lazy_load_paths` 属性设置为 `true`，则请求 URI 将解析为使用资源注册端点在 Keycloak 中配置的一个或多个资源。已经解析的资源被用作于检查的权限。\n\n    :::note\n\n    需要该插件从令牌端点为自己获取单独的访问令牌。因此，请确保在 Keycloak 的客户端设置中设置了 `Service Accounts Enabled` 选项。\n\n    还需要确保颁发的访问令牌包含具有 `uma_protection` 角色的 `resource_access` 声明，以保证插件能够通过 Protection API 查询资源。\n\n    :::\n\n- 自动将 HTTP method 映射到作用域\n\n    `http_method_as_scope` 通常与 `lazy_load_paths` 一起使用，但也可以与静态权限列表一起使用。\n\n    - 如果 `http_method_as_scope` 属性设置为 `true`，插件会将请求的 HTTP 方法映射到同名范围。然后将范围添加到每个要检查的权限。\n\n    - 如果 `lazy_load_paths` 属性设置为 `false`，则插件会将映射范围添加到 `permissions` 属性中配置的任意一个静态权限——即使它们已经包含一个或多个范围。\n\n- 使用 `password` 授权生成令牌\n\n    - 如果要使用 `password` 授权生成令牌，你可以设置 `password_grant_token_generation_incoming_uri` 属性的值。\n\n    - 如果传入的 URI 与配置的属性匹配并且请求方法是 POST，则使用 `token_endpoint` 生成一个令牌。\n\n    同时，你还需要添加 `application/x-www-form-urlencoded` 作为 `Content-Type` 标头，`username` 和 `password` 作为参数。\n\n    如下示例是当 `password_grant_token_generation_incoming_uri` 设置为 `/api/token` 时的命令：\n\n    ```shell\n    curl --location --request POST 'http://127.0.0.1:9080/api/token' \\\n    --header 'Accept: application/json, text/plain, */*' \\\n    --header 'Content-Type: application/x-www-form-urlencoded' \\\n    --data-urlencode 'username=<User_Name>' \\\n    --data-urlencode 'password=<Password>'\n    ```\n\n## 如何启用\n\n以下示例为你展示了如何在指定 Route 中启用 `authz-keycloak` 插件，其中 `${realm}` 是 Keycloak 中的 `realm` 名称：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/get\",\n    \"plugins\": {\n        \"authz-keycloak\": {\n            \"token_endpoint\": \"http://127.0.0.1:8090/realms/${realm}/protocol/openid-connect/token\",\n            \"permissions\": [\"resource name#scope name\"],\n            \"client_id\": \"Client ID\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以通过以下方法测试插件。\n\n首先需要从 Keycloak 获取 JWT 令牌：\n\n```shell\ncurl \"http://<YOUR_KEYCLOAK_HOST>/realms/<YOUR_REALM>/protocol/openid-connect/token\" \\\n  -d \"client_id=<YOUR_CLIENT_ID>\" \\\n  -d \"client_secret=<YOUR_CLIENT_SECRET>\" \\\n  -d \"username=<YOUR_USERNAME>\" \\\n  -d \"password=<YOUR_PASSWORD>\" \\\n  -d \"grant_type=password\"\n```\n\n你应该收到类似以下的响应：\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoT3ludlBPY2d6Y3VWWnYtTU42bXZKMUczb0dOX2d6MFo3WFl6S2FSa1NBIn0.eyJleHAiOjE3MDMyOTAyNjAsImlhdCI6MTcwMzI4OTk2MCwianRpIjoiMjJhOGFmMzItNDM5Mi00Yzg3LThkM2UtZDkyNDVmZmNiYTNmIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjAyZWZlY2VlLTBmYTgtNDg1OS1iYmIwLTgyMGZmZDdjMWRmYSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtcXVpY2tzdGFydC1yZWFsbSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InF1aWNrc3RhcnQtdXNlciJ9.WNZQiLRleqCxw-JS-MHkqXnX_BPA9i6fyVHqF8l-L-2QxcqTAwbIp7AYKX-z90CG6EdRXOizAEkQytB32eVWXaRkLeTYCI7wIrT8XSVTJle4F88ohuBOjDfRR61yFh5k8FXXdAyRzcR7tIeE2YUFkRqw1gCT_VEsUuXPqm2wTKOmZ8fRBf4T-rP4-ZJwPkHAWc_nG21TmLOBCSulzYqoC6Lc-OvX5AHde9cfRuXx-r2HhSYs4cXtvX-ijA715MY634CQdedheoGca5yzPsJWrAlBbCruN2rdb4u5bDxKU62pJoJpmAsR7d5qYpYVA6AsANDxHLk2-W5F7I_IxqR0YQ\",\"expires_in\":300,\"refresh_expires_in\":1800,\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjN2IwYmY4NC1kYjk0LTQ5YzctYWIyZC01NmU3ZDc1MmRkNDkifQ.eyJleHAiOjE3MDMyOTE3NjAsImlhdCI6MTcwMzI4OTk2MCwianRpIjoiYzcyZjAzMzctYmZhNS00MWEzLTlhYjEtZmJlNGY0NmZjMDgxIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwic3ViIjoiMDJlZmVjZWUtMGZhOC00ODU5LWJiYjAtODIwZmZkN2MxZGZhIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYiLCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiI1YzIzZjVkZC1hN2ZhLTRlMmItOWQxNC02MmI1YzYyNmU1NDYifQ.7AH7ppbVOlkYc9CoJ7kLSlDUkmFuNga28Amugn2t724\",\"token_type\":\"Bearer\",\"not-before-policy\":0,\"session_state\":\"5c23f5dd-a7fa-4e2b-9d14-62b5c626e546\",\"scope\":\"email profile\"}\n```\n\n之后就可以使用获得的访问令牌发起请求：\n\n```shell\ncurl http://127.0.0.1:9080/get -H 'Authorization: Bearer ${ACCESS_TOKEN}'\n```\n\n## 删除插件\n\n当你需要禁用 `authz-keycloak` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/get\",\n    \"plugins\": {\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n\n## 插件 Roadmap\n\n- 目前，`authz-keycloak` 插件通过要求定义资源名称和所需的范围，来强制执行路由策略。但 Keycloak 官方适配的其他语言客户端（Java、JavaScript）仍然可以通过动态查询 Keycloak 路径以及延迟加载身份资源的路径来提供路径匹配。在 Apache APISIX 之后发布的插件中即将支持此功能。\n\n- 支持从 Keycloak JSON 文件中读取权限范畴和其他配置项。\n"
  },
  {
    "path": "docs/zh/latest/plugins/aws-lambda.md",
    "content": "---\ntitle: aws-lambda\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - AWS Lambda\n  - aws-lambda\ndescription: 本文介绍了关于 Apache APISIX aws-lambda 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`aws-lambda` 插件用于将 [AWS Lambda](https://aws.amazon.com/lambda/) 和 [Amazon API Gateway](https://aws.amazon.com/api-gateway/) 作为动态上游集成至 APISIX，从而实现将访问指定 URI 的请求代理到 AWS 云。\n\n启用 `aws-lambda` 插件后，该插件会终止对已配置 URI 的请求，并代表客户端向 AWS Lambda Gateway URI 发起一个新的请求。这个新请求中携带了之前配置的授权详细信息，包括请求头、请求体和参数（以上参数都是从原始请求中传递的），然后 `aws-lambda` 插件会将带有响应头、状态码和响应体的响应信息返回给使用 APISIX 发起请求的客户端。\n\n该插件支持通过 AWS API key 和 AWS IAM secrets 进行授权。当使用 AWS IAM secrets 时，该插件支持 [AWS Signature Version 4 signing](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html)。\n\n## 属性\n\n| 名称                 | 类型     | 必选项   | 默认值  | 有效值       | 描述                                                 |\n| ------------------ - | ------- | -------- | ------- | ------------ | ------------------------------------------------------------ |\n| function_uri         | string  | 是       |         |              | 触发 lambda serverless 函数的 AWS API Gateway 端点。        |\n| authorization        | object  | 否       |         |              | 访问云函数的授权凭证。                                       |\n| authorization.apikey | string  | 否       |         |              | 生成的 API 密钥，用于授权对 AWS Gateway 端点的请求。         |\n| authorization.iam    | object  | 否       |         |              | 用于通过 AWS v4 请求签名执行的基于 AWS IAM 角色的授权。请参考 [IAM 授权方案](#iam-授权方案)。 |\n| authorization.iam.accesskey  | string | 是       |               | 从 AWS IAM 控制台生成的访问密钥 ID。                     |\n| authorization.iam.secretkey | string | 是       |               | 从 AWS IAM 控制台生成的访问密钥。                          |\n| authorization.iam.aws_region | string | 否       | \"us-east-1\"   | 发出请求的 AWS 区域。有关更多 AWS 区域代码的信息请参考 [AWS 区域代码表](https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#region-names-codes)。 |\n| authorization.iam.service    | string | 否       | \"execute-api\" | 接收该请求的服务。若使用 Amazon API gateway APIs, 应设置为 `execute-api`。若使用 Lambda function, 应设置为 `lambda`。 |\n| timeout              | integer | 否       | 3000    | [100,...]    | 代理请求超时（以毫秒为单位）。                                 |\n| ssl_verify           | boolean | 否       | true    | true/false   | 当设置为 `true` 时执行 SSL 验证。                          |\n| keepalive            | boolean | 否       | true    | true/false   | 当设置为 `true` 时，保持连接的活动状态以便重复使用。         |\n| keepalive_pool       | integer | 否       | 5       | [1,...]      | 在关闭该连接之前，可以在该连接上发送的最大请求数。           |\n| keepalive_timeout    | integer | 否       | 60000   | [1000,...]   | 当连接空闲时，保持该连接处于活动状态的时间，以毫秒为单位。           |\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"aws-lambda\": {\n            \"function_uri\": \"https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com/default/test-apisix\",\n            \"authorization\": {\n                \"apikey\": \"<Generated API Key from aws console>\"\n            },\n            \"ssl_verify\":false\n        }\n    },\n    \"uri\": \"/aws\"\n}'\n```\n\n通过上述示例配置插件后，任何对 `/aws` URI 的请求（`HTTP/1.1`、`HTTPS`、`HTTP2`）都将调用已配置的 AWS 函数的 URI，并且会将响应信息返回给客户端。\n\n下述命令的含义是：AWS Lambda 从请求中获取 `name` 参数，并返回一条 `\"Hello $name\"` 消息：\n\n```shell\ncurl -i -XGET localhost:9080/aws\\?name=APISIX\n```\n\n正常返回结果：\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: application/json\n...\n\"Hello, APISIX!\"\n```\n\n以下示例是客户端通过 HTTP/2 协议与 APISIX 进行通信。\n\n在进行测试之前，由于该 `enable_http2: true` 默认是禁用状态，你可以通过在 `./conf/config.yaml` 中添加 `apisix.node_listen` 下的 `- port: 9081` 和 `enable_http2: true` 字段启用。示例如下\n\n```yaml\napisix:\n  node_listen:                      # 支持监听多个端口\n    - 9080\n    - port: 9081\n      enable_http2: true            # 该字段如果不设置，默认值为 `false`\n```\n\n使用 `curl` 命令测试：\n\n```shell\ncurl -i -XGET --http2 --http2-prior-knowledge localhost:9081/aws\\?name=APISIX\n```\n\n正常返回结果：\n\n```shell\nHTTP/2 200\ncontent-type: application/json\n...\n\"Hello, APISIX!\"\n```\n\n与上面的示例类似，AWS Lambda 函数也可以通过 AWS API Gateway 触发，但需要使用 AWS IAM 权限进行授权。`aws-lambda` 插件的配置文件中包含了 `\"authorization\"` 字段，用户可以在 HTTP 调用中通过 AWS v4 请求签名。\n\n以下示例展示了如何通过配置文件实现授权：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"aws-lambda\": {\n            \"function_uri\": \"https://ajycz5e0v9.execute-api.us-east-1.amazonaws.com/default/test-apisix\",\n            \"authorization\": {\n                \"iam\": {\n                    \"accesskey\": \"<access key>\",\n                    \"secretkey\": \"<access key secret>\"\n                }\n            },\n            \"ssl_verify\": false\n        }\n    },\n    \"uri\": \"/aws\"\n}'\n```\n\n:::note 注意\n\n使用该方法时已经假设你有一个启用了程序化访问的 IAM 用户，并具有访问端点的必要权限（AmazonAPIGatewayInvokeFullAccess）。\n\n:::\n\n### 配置路径转发\n\n`aws-lambda` 插件在代理请求到 AWS 上游时也支持 URL 路径转发。基本请求路径的扩展被附加到插件配置中指定的 `function_uri` 字段上。\n\n:::info 重要\n\n因为 APISIX 路由是严格匹配的，所以为了使 `aws-lambda` 插件正常工作，在路由上配置的 `uri` 字段必须以 `*` 结尾，`*` 意味着这个 URI 的任何子路径都会被匹配到同一个路由。\n\n:::\n\n以下示例展示了如何通过配置文件实现路径转发：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"aws-lambda\": {\n            \"function_uri\": \"https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com\",\n            \"authorization\": {\n                \"apikey\": \"<Generate API key>\"\n            },\n            \"ssl_verify\":false\n        }\n    },\n    \"uri\": \"/aws/*\"\n}'\n```\n\n通过上述示例配置插件后，任何访问 `aws/default/test-apisix` 的请求都会调用 AWS Lambda 函数，并转发附加的参数。\n\n使用 `curl` 命令测试：\n\n```shell\ncurl -i -XGET http://127.0.0.1:9080/aws/default/test-apisix\\?name\\=APISIX\n```\n\n正常返回结果：\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: application/json\n...\n\"Hello, APISIX!\"\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/aws\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/azure-functions.md",
    "content": "---\ntitle: azure-functions\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Azure Functions\n  - azure-functions\ndescription: 本文介绍了关于 API 网关 Apache APISIX azure-functions 插件的基本信息及使用方法。\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`azure-functions` 插件用于将 [Azure Serverless Function](https://azure.microsoft.com/en-in/services/functions/) 作为动态上游集成至 APISIX，从而实现将访问指定 URI 的请求代理到 Microsoft Azure 云服务。\n\n启用 `azure-functions` 插件后，该插件会终止对已配置 URI 的请求，并代表客户端向 Azure Functions 发起一个新的请求。该新请求中携带了之前配置的授权详细信息，包括请求头、请求体和参数（以上参数都是从原始请求中传递的）。之后便会通过 `azure-functions` 插件，将带有响应头、状态码和响应体的信息返回给使用 APISIX 发起请求的客户端。\n\n## 属性\n\n| 名称                   | 类型    | 必选项 | 默认值 | 有效值     | 描述                                                         |\n| ---------------------- | ------- | ------ | ------ | ---------- | ------------------------------------------------------------ |\n| function_uri           | string  | 是     |        |            | 触发 Serverless Functions 的 Azure Functions 端点。例如 `http://test-apisix.azurewebsites.net/api/HttpTrigger`。 |\n| authorization          | object  | 否     |        |            | 访问 Azure Functions 的授权凭证。                            |\n| authorization.apikey   | string  | 否     |        |            | 授权凭证内的字段。生成 API 密钥来授权对端点的请求。          |\n| authorization.clientid | string  | 否     |        |            | 授权凭证内的字段。生成客户端 ID（Azure Active Directory）来授权对端点的请求。 |\n| timeout                | integer | 否     | 3000   | [100,...]  | 代理请求超时（以毫秒为单位）。                               |\n| ssl_verify             | boolean | 否     | true   | true/false | 当设置为 `true` 时执行 SSL 验证。                            |\n| keepalive              | boolean | 否     | true   | true/false | 当设置为 `true` 时，保持连接的活动状态以便重复使用。         |\n| keepalive_pool         | integer | 否     | 5      | [1,...]    | 连接断开之前，可接收的最大请求数。                           |\n| keepalive_timeout      | integer | 否     | 60000  | [1000,...] | 当连接空闲时，保持该连接处于活动状态的时间（以毫秒为单位）。 |\n\n## 元数据\n\n| 名称            | 类型   | 必选项 | 默认值 | 描述                                                         |\n| --------------- | ------ | ------ | ------ | ------------------------------------------------------------ |\n| master_apikey   | string | 否     | \"\"     | 可用于访问 Azure Functions URI 的 API 密钥。                 |\n| master_clientid | string | 否     | \"\"     | 可用于授权 Azure Functions URI 的客户端 ID（Active Directory）。 |\n\n`azure-functions` 插件的元数据提供了授权回退的功能。它定义了 `master_apikey` 和 `master_clientid` 字段，用户可以为关键任务的应用部署声明 API 密钥或客户端 ID。因此，如果在 `azure-functions` 插件属性中没有找到相关授权凭证，此时元数据中的授权凭证就会发挥作用。\n\n:::note 注意\n\n授权方式优先级排序如下：\n\n1. 首先，`azure-functions` 插件在 APISIX 代理的请求头中寻找 `x-functions-key` 或 `x-functions-clientid` 键。\n2. 如果没有找到，`azure-functions` 插件会检查插件属性中的授权凭证。如果授权凭证存在，`azure-functions` 插件会将相应的授权标头添加到发送到 Azure Functions 的请求中。\n3. 如果未配置 `azure-functions` 插件的授权凭证属性，APISIX 将获取插件元数据配置并使用 API 密钥。\n\n:::\n\n如果你想添加一个新的 API 密钥，请向 `/apisix/admin/plugin_metadata` 端点发出请求，并附上所需的元数据。示例如下：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/azure-functions \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"master_apikey\" : \"<Your Azure master access key>\"\n}'\n```\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件，请确保你的 Azure Functions 已提前部署好，并正常提供服务。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"azure-functions\": {\n            \"function_uri\": \"http://test-apisix.azurewebsites.net/api/HttpTrigger\",\n            \"authorization\": {\n                \"apikey\": \"${Generated API key to access the Azure-Function}\"\n            }\n        }\n    },\n    \"uri\": \"/azure\"\n}'\n```\n\n通过上述示例配置插件后，任何对 `/azure` URI 的请求（`HTTP/1.1`、`HTTPS`、`HTTP2`）都将调用已配置的 Azure Functions 的 URI，并且会将响应信息返回给客户端。\n\n下述命令的含义是：Azure Functions 从请求中获取 `name` 参数，并返回一条 `\"Hello $name\"` 消息：\n\n```shell\ncurl -i -XGET http://localhost:9080/azure\\?name=APISIX\n```\n\n正常返回结果：\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=utf-8\n...\nHello, APISIX\n```\n\n以下示例是客户端通过 HTTP/2 协议与 APISIX 进行通信。\n\n在进行测试之前，由于该 `enable_http2: true` 默认是禁用状态，你可以通过在 `./conf/config.yaml` 中添加 `apisix.node_listen` 下的 `- port: 9081` 和 `enable_http2: true` 字段启用。示例如下：\n\n```yaml\napisix:\n  node_listen:                      # 支持监听多个端口\n    - 9080\n    - port: 9081\n      enable_http2: true            # 该字段如果不设置，默认值为 `false`\n```\n\n使用 `curl` 命令测试：\n\n```shell\ncurl -i -XGET --http2 --http2-prior-knowledge http://localhost:9081/azure\\?name=APISIX\n```\n\n正常返回结果：\n\n```shell\nHTTP/2 200\ncontent-type: text/plain; charset=utf-8\n...\nHello, APISIX\n```\n\n### 配置路径转发\n\n`azure-functions` 插件在代理请求到 Azure Functions 上游时也支持 URL 路径转发。基本请求路径的扩展被附加到插件配置中指定的 `function_uri` 字段上。\n\n:::info 重要\n\n因为 APISIX 路由是严格匹配的，所以为了使 `azure-functions` 插件正常工作，在路由上配置的 `uri` 字段必须以 `*` 结尾，`*` 意味着这个 URI 的任何子路径都会被匹配到同一个路由。\n\n:::\n\n以下示例展示了如何通过配置文件实现路径转发：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"azure-functions\": {\n            \"function_uri\": \"http://app-bisakh.azurewebsites.net/api\",\n            \"authorization\": {\n                \"apikey\": \"${Generated API key to access the Azure-Function}\"\n            }\n        }\n    },\n    \"uri\": \"/azure/*\"\n}'\n```\n\n通过上述示例配置插件后，任何访问 `azure/HttpTrigger1` 的请求都会调用 Azure Functions 并转发附加的参数。\n\n使用 `curl` 命令测试：\n\n```shell\ncurl -i -XGET http://127.0.0.1:9080/azure/HttpTrigger1\\?name\\=APISIX\\\n```\n\n正常返回结果：\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: text/plain; charset=utf-8\n...\nHello, APISIX\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/azure\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/basic-auth.md",
    "content": "---\ntitle: basic-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Basic Auth\n  - basic-auth\ndescription: basic-auth 插件为消费者添加了基本访问身份验证，以便消费者在访问上游资源之前进行身份验证。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/basic-auth\" />\n</head>\n\n## 描述\n\n`basic-auth` 插件为 [消费者](../terminology/consumer.md) 添加了 [基本访问身份验证](https://en.wikipedia.org/wiki/Basic_access_authentication)，以便消费者在访问上游资源之前进行身份验证。\n\n当消费者成功通过身份验证后，APISIX 会在将请求代理到上游服务之前向请求添加其他标头，例如 `X-Consumer-Username`、`X-Credential-Indentifier` 和其他消费者自定义标头（如果已配置）。上游服务将能够区分消费者并根据需要实现其他逻辑。如果这些值中的任何一个不可用，则不会添加相应的标头。\n\n## 属性\n\nConsumer/Credentials 端：\n\n| 名称     | 类型   | 必选项 | 描述                                                                                           |\n| -------- | ------ | -----| ----------------------------------------------------------------------------------------------- |\n| username | string | 是   | Consumer 的用户名并且该用户名是唯一，如果多个 Consumer 使用了相同的 `username`，将会出现请求匹配异常。|\n| password | string | 是   | 用户的密码。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源，将值保存在 Secret Manager 中。        |\n\n注意：schema 中还定义了 `encrypt_fields = {\"password\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\nRoute 端：\n\n| 名称             | 类型     | 必选项 | 默认值  | 描述                                                            |\n| ---------------- | ------- | ------ | ------ | --------------------------------------------------------------- |\n| hide_credentials | boolean | 否     | false  | 该参数设置为 `true` 时，则不会将 Authorization 请求头传递给 Upstream。|\n| anonymous_consumer | boolean | 否    | false | 匿名消费者名称。如果已配置，则允许匿名用户绕过身份验证。 |\n| realm | string | 否 | basic |在身份验证失败时，应包含在 `WWW-Authenticate` 标头中的域。|\n\n## 示例\n\n以下示例演示了如何在不同场景中使用 `basic-auth` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n### 在路由上实现基本身份验证\n\n以下示例演示如何在路由上实现基本身份验证。\n\n创建消费者 `johndoe`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\"\n  }'\n```\n\n为消费者创建 `basic-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `basic-auth` 的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"basic-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"basic-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n#### 使用有效密钥进行验证\n\n使用有效密钥发送请求至：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\n您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Apikey\": \"john-key\",\n    \"Authorization\": \"Basic am9obmRvZTpqb2huLWtleQ==\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66e5107c-5bb3e24f2de5baf733aec1cc\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-basic-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n#### 使用无效密钥进行验证\n\n使用无效密钥发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:invalid-key\n```\n\n您应该看到以下 `HTTP/1.1 401 Unauthorized` 响应：\n\n```text\n{\"message\":\"Invalid user authorization\"}\n```\n\n#### 无需密钥即可验证\n\n无需密钥即可发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该看到以下 `HTTP/1.1 401 Unauthorized` 响应：\n\n```text\n{\"message\":\"Missing authorization in request\"}\n```\n\n### 隐藏上游的身份验证信息\n\n以下示例演示了如何通过配置 `hide_credentials` 来防止密钥被发送到上游服务。APISIX 默认情况下会将身份验证密钥转发到上游服务，这在某些情况下可能会导致安全风险，您应该考虑更新 `hide_credentials`。\n\n创建消费者 `johndoe` ：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\"\n  }'\n```\n\n为消费者创建 `basic-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\n#### 不隐藏凭据\n\n使用 `basic-auth` 创建路由，并将 `hide_credentials` 配置为 `false`，这是默认配置：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"id\": \"basic-auth-route\",\n  \"uri\": \"/anything\",\n  \"plugins\": {\n    \"basic-auth\": {\n      \"hide_credentials\": false\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n发送带有有效密钥的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\n您应该看到以下 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Basic am9obmRvZTpqb2huLWtleQ==\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66cc2195-22bd5f401b13480e63c498c6\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-basic-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 43.228.226.23\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\n请注意，凭证以 base64 编码格式对上游服务可见。\n\n:::tip\n\n您还可以使用 `Authorization` 标头在请求中传递 base64 编码的凭据，如下所示：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"Authorization: Basic am9obmRvZTpqb2huLWtleQ==\"\n```\n\n:::\n\n#### 隐藏凭据\n\n将插件的 `hide_credentials` 更新为 `true`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/basic-auth-route\" -X PATCH \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"plugins\": {\n    \"basic-auth\": {\n      \"hide_credentials\": true\n    }\n  }\n}'\n```\n\n发送带有有效密钥的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\n您应该看到以下 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66cc21a7-4f6ac87946e25f325167d53a\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Indentifier\": \"cred-john-basic-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 43.228.226.23\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\n请注意，上游服务不再可见这些凭据。\n\n### 将消费者自定义 ID 添加到标头\n\n以下示例演示了如何在 `Consumer-Custom-Id` 标头中将消费者自定义 ID 附加到经过身份验证的请求，该 ID 可用于根据需要实现其他逻辑。\n\n创建带有自定义 ID 标签的消费者 `johndoe`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\n为消费者创建 `basic-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `basic-auth` 的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"basic-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"basic-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n使用有效密钥向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -u johndoe:john-key\n```\n\n您应该看到一个带有 `X-Consumer-Custom-Id` 的 `HTTP/1.1 200 OK` 响应，类似于以下内容：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Basic am9obmRvZTpqb2huLWtleQ==\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66ea8d64-33df89052ae198a706e18c2a\",\n    \"X-Consumer-Username\": \"johndoe\",\n    \"X-Credential-Identifier\": \"cred-john-basic-auth\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\n### 匿名消费者的速率限制\n\n以下示例演示了如何为普通消费者和匿名消费者配置不同的速率限制策略，其中匿名消费者不需要进行身份验证，并且配额较少。\n\n创建普通消费者 `johndoe` 并配置 `limit-count` 插件以允许 30 秒内的配额为 3：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"johndoe\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n为消费者 `johndoe` 创建 `basic-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-basic-auth\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"username\": \"johndoe\",\n        \"password\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建匿名用户 `anonymous`，并配置 `limit-count` 插件，以允许 30 秒内配额为 1：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n创建一个路由并配置 `basic-auth` 插件来接受匿名消费者 `anonymous` 绕过身份验证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"basic-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"basic-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n为了验证，请使用 `john` 的密钥发送五个连续的请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -u johndoe:john-key -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 5 个请求中，3 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200:    3, 429:    2\n```\n\n发送五个匿名请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，表明只有一个请求成功：\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/batch-requests.md",
    "content": "---\ntitle: batch-requests\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Batch Requests\ndescription: 本文介绍了关于 Apache APISIX `batch-request` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n在启用 `batch-requests` 插件后，用户可以通过将多个请求组装成一个请求的形式，把请求发送给网关，网关会从请求体中解析出对应的请求，再分别封装成独立的请求，以 [HTTP pipeline](https://en.wikipedia.org/wiki/HTTP_pipelining) 的方式代替用户向网关自身再发起多个 HTTP 请求，经历路由匹配，转发到对应上游等多个阶段，合并结果后再返回客户端。\n\n![batch-request](https://static.apiseven.com/uploads/2023/06/27/ATzEuOn4_batch-request.png)\n\n在客户端需要访问多个 API 的情况下，这将显著提高性能。\n\n:::note\n\n用户原始请求中的请求头（除了以 `Content-` 开始的请求头，例如：`Content-Type`）将被赋给 HTTP pipeline 中的每个请求，因此对于网关来说，这些以 HTTP pipeline 方式发送给自身的请求与用户直接发起的外部请求没有什么不同，只能访问已经配置好的路由，并将经历完整的鉴权过程，因此不存在安全问题。\n\n如果原始请求的请求头与插件中配置的请求头冲突，则以插件中配置的请求头优先（配置文件中指定的 real_ip_header 除外）。\n\n:::\n\n## 属性\n\n无。\n\n## 接口\n\n该插件会增加 `/apisix/batch-requests` 接口。\n\n:::note\n\n你需要通过 [public-api](../../../zh/latest/plugins/public-api.md) 插件来暴露它。\n\n:::\n\n## 启用插件\n\n该插件默认是禁用状态，你可以在配置文件（`./conf/config.yaml`）添加如下配置启用 `batch-requests` 插件：\n\n```yaml title=\"conf/config.yaml\"\nplugins:\n  - ...\n  - batch-requests\n```\n\n## 配置插件\n\n默认情况下，可以发送到 `/apisix/batch-requests` 的最大请求体不能大于 1 MiB。你可以通过 `apisix/admin/plugin_metadata/batch-requests` 更改插件的此配置：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/batch-requests \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"max_body_size\": 4194304\n}'\n```\n\n## 元数据\n\n| 名称          | 类型     | 必选项 | 默认值   | 有效值 | 描述                          |\n| ------------- | ------- | -------| ------- | ------ | ---------------------------- |\n| max_body_size | integer | 是     | 1048576 |[1, ...]| 请求体的最大大小，单位：bytes。 |\n\n## 请求和响应格式\n\n该插件会为 `apisix` 创建一个 `/apisix/batch-requests` 的接口，用来处理批量请求。\n\n### 请求参数\n\n| 参数名   | 类型                                 | 必选项 | 默认值 |  描述                             |\n| -------- |------------------------------------| ------ | ------ |  -------------------------------- |\n| query    | object                             | 否     |        | 给所有请求都携带的 `query string`。 |\n| headers  | object                             | 否     |        | 给所有请求都携带的 `header`。       |\n| timeout  | number                             | 否     | 30000  | 聚合请求的超时时间，单位为 `ms`。    |\n| pipeline | array[[HttpRequest](#httprequest)] | 是     |        | HTTP 请求的详细信息。               |\n\n#### HttpRequest\n\n| 参数名      | 类型    | 必选项    | 默认值  | 有效值                                                                            | 描述                                                                  |\n| ---------- | ------- | -------- | ------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| version    | string  | 否       | 1.1     | [1.0, 1.1]                                                                       | 请求所使用的 HTTP 协议版本。                                              |\n| method     | string  | 否       | GET     | [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\", \"CONNECT\", \"TRACE\"] | 请求使用的 HTTP 方法。                                                   |\n| query      | object  | 否       |         |                                                                                  | 独立请求所携带的 `query string`, 如果 `Key` 和全局的有冲突，以此设置为主。 |\n| headers    | object  | 否       |         |                                                                                  | 独立请求所携带的 `header`, 如果 `Key` 和全局的有冲突，以此设置为主。       |\n| path       | string  | 是       |         |                                                                                  | HTTP 请求路径。                                                        |\n| body       | string  | 否       |         |                                                                                  | HTTP 请求体。                                                          |\n| ssl_verify | boolean | 否       | false   |                                                                                  | 验证 SSL 证书与主机名是否匹配。                                          |\n\n### 响应参数\n\n返回值是一个 [HttpResponse](#httpresponse) 的`数组`。\n\n#### HttpResponse\n\n| 参数名   | 类型    | 描述                 |\n| ------- | ------- | ------------------- |\n| status  | integer | HTTP 请求的状态码。   |\n| reason  | string  | HTTP 请求的返回信息。 |\n| body    | string  | HTTP 请求的响应体。   |\n| headers | object  | HTTP 请求的响应头。   |\n\n## 修改自定义 URI\n\n你可以通过 [public-api](../../../en/latest/plugins/public-api.md) 插件设置自定义 URI。\n\n只需要在创建路由时设置所需的 URI 并更改 `public-api` 插件的配置：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/br \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/batch-requests\",\n    \"plugins\": {\n        \"public-api\": {\n            \"uri\": \"/apisix/batch-requests\"\n        }\n    }\n}'\n```\n\n## 测试插件\n\n首先，你需要为 `batch-requests` 插件的 API 创建一个路由，它将使用 [public-api](../../../en/latest/plugins/public-api.md) 插件。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/batch-requests\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\n之后，你就可以将要访问的请求信息传到网关的批量请求接口（`/apisix/batch-requests`）了，网关会以 [http pipeline](https://en.wikipedia.org/wiki/HTTP_pipelining) 的方式自动帮你完成请求。\n\n```shell\ncurl --location --request POST 'http://127.0.0.1:9080/apisix/batch-requests' \\\n--header 'Content-Type: application/json' \\\n--data '{\n    \"headers\": {\n        \"Content-Type\": \"application/json\",\n        \"admin-jwt\":\"xxxx\"\n    },\n    \"timeout\": 500,\n    \"pipeline\": [\n        {\n            \"method\": \"POST\",\n            \"path\": \"/community.GiftSrv/GetGifts\",\n            \"body\": \"test\"\n        },\n        {\n            \"method\": \"POST\",\n            \"path\": \"/community.GiftSrv/GetGifts\",\n            \"body\": \"test2\"\n        }\n    ]\n}'\n```\n\n正常返回结果如下：\n\n```json\n[\n  {\n    \"status\": 200,\n    \"reason\": \"OK\",\n    \"body\": \"{\\\"ret\\\":500,\\\"msg\\\":\\\"error\\\",\\\"game_info\\\":null,\\\"gift\\\":[],\\\"to_gets\\\":0,\\\"get_all_msg\\\":\\\"\\\"}\",\n    \"headers\": {\n      \"Connection\": \"keep-alive\",\n      \"Date\": \"Sat, 11 Apr 2020 17:53:20 GMT\",\n      \"Content-Type\": \"application/json\",\n      \"Content-Length\": \"81\",\n      \"Server\": \"APISIX web server\"\n    }\n  },\n  {\n    \"status\": 200,\n    \"reason\": \"OK\",\n    \"body\": \"{\\\"ret\\\":500,\\\"msg\\\":\\\"error\\\",\\\"game_info\\\":null,\\\"gift\\\":[],\\\"to_gets\\\":0,\\\"get_all_msg\\\":\\\"\\\"}\",\n    \"headers\": {\n      \"Connection\": \"keep-alive\",\n      \"Date\": \"Sat, 11 Apr 2020 17:53:20 GMT\",\n      \"Content-Type\": \"application/json\",\n      \"Content-Length\": \"81\",\n      \"Server\": \"APISIX web server\"\n    }\n  }\n]\n```\n\n## 删除插件\n\n如果你想禁用插件，可以将 `batch-requests` 从配置文件中的插件列表删除，重新加载 APISIX 后即可生效。\n\n```yaml title=\"conf/config.yaml\"\nplugins:    # plugin list\n  - ...\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/body-transformer.md",
    "content": "---\ntitle: body-transformer\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - BODY TRANSFORMER\n  - body-transformer\ndescription: body-transformer 插件执行基于模板的转换，将请求和/或响应主体从一种格式转换为另一种格式，例如从 JSON 到 JSON、从 JSON 到 HTML 或从 XML 到 YAML。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/body-transformer\" />\n</head>\n\n## 描述\n\n`body-transformer` 插件执行基于模板的转换，将请求和/或响应主体从一种格式转换为另一种格式，例如从 JSON 到 JSON、从 JSON 到 HTML 或从 XML 到 YAML。\n\n## 属性\n\n| 名称           | 类型                   | 必选项   | 默认值           | 有效值 | 描述                                                                                                                                         |\n|--------------|----------------------|-------|---------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------|\n| `request` | object | 否 | | | 请求体转换配置。 |\n| `request.input_format` | string | 否 | | [`xml`,`json`,`encoded`,`args`,`plain`,`multipart`] | 请求体原始媒体类型。若未指定，则该值将由 `Content-Type` 标头确定以应用相应的解码器。`xml` 选项对应于 `text/xml` 媒体类型。`json` 选项对应于 `application/json` 媒体类型。`encoded` 选项对应于 `application/x-www-form-urlencoded` 媒体类型。`args` 选项对应于 GET 请求。`plain` 选项对应于 `text/plain` 媒体类型。`multipart` 选项对应于 `multipart/related` 媒体类型。如果媒体类型不是这两种类型，则该值将保留未设置状态并直接应用转换模板。 |\n| `request.template` | string | True | | | 请求体转换模板。模板使用 [lua-resty-template](https://github.com/bungle/lua-resty-template) 语法。有关更多详细信息，请参阅 [模板语法](https://github.com/bungle/lua-resty-template#template-syntax)。您还可以使用辅助函数 `_escape_json()` 和 `_escape_xml()` 转义双引号等特殊字符，使用 `_body` 访问请求正文，使用 `_ctx` 访问上下文变量。|\n| `request.template_is_base64` | boolean | 否 | false | | 如果模板是 base64 编码的，则设置为 true。|\n| `response` | object | 否 | | | 响应体转换配置。|\n| `response.input_format` | string | 否 | | [`xml`,`json`] | 响应体原始媒体类型。如果未指定，则该值将由 `Content-Type` 标头确定以应用相应的解码器。如果媒体类型既不是 `xml` 也不是 `json`，则该值将保留未设置状态，并直接应用转换模板。|\n| `response.template` | string | True | | | 响应主体转换模板。|\n| `response.template_is_base64` | boolean | 否 | false | | 如果模板是 base64 编码的，则设置为 true。|\n\n## 示例\n\n以下示例演示了如何针对不同场景配置 `body-transformer`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n转换模板使用 [lua-resty-template](https://github.com/bungle/lua-resty-template) 语法。请参阅 [模板语法](https://github.com/bungle/lua-resty-template#template-syntax) 了解更多信息。\n\n您还可以使用辅助函数 `_escape_json()` 和 `_escape_xml()` 转义特殊字符（例如双引号）、`_body` 访问请求正文以及 `_ctx` 访问上下文变量。\n\n在所有情况下，您都应确保转换模板是有效的 JSON 字符串。\n\n### JSON 和 XML SOAP 之间的转换\n\n以下示例演示了在使用 SOAP 上游服务时如何将请求主体从 JSON 转换为 XML，将响应主体从 XML 转换为 JSON。\n\n启动示例 SOAP 服务：\n\n```shell\ncd /tmp\ngit clone https://github.com/spring-guides/gs-soap-service.git\ncd gs-soap-service/complete\n./mvnw spring-boot:run\n```\n\n创建请求和响应转换模板：\n\n```shell\nreq_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1' | awk '{$1=$1};1' | tr -d '\\r\\n'\n<?xml version=\"1.0\"?>\n<soap-env:Envelope xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <soap-env:Body>\n  <ns0:getCountryRequest xmlns:ns0=\"http://spring.io/guides/gs-producing-web-service\">\n   <ns0:name>{{_escape_xml(name)}}</ns0:name>\n  </ns0:getCountryRequest>\n </soap-env:Body>\n</soap-env:Envelope>\nEOF\n)\n\nrsp_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1' | awk '{$1=$1};1' | tr -d '\\r\\n'\n{% if Envelope.Body.Fault == nil then %}\n{\n  \"status\":\"{{_ctx.var.status}}\",\n  \"currency\":\"{{Envelope.Body.getCountryResponse.country.currency}}\",\n  \"population\":{{Envelope.Body.getCountryResponse.country.population}},\n  \"capital\":\"{{Envelope.Body.getCountryResponse.country.capital}}\",\n  \"name\":\"{{Envelope.Body.getCountryResponse.country.name}}\"\n}\n{% else %}\n{\n  \"message\":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},\n  \"code\":\"{{Envelope.Body.Fault.faultcode}}\"\n  {% if Envelope.Body.Fault.faultactor ~= nil then %}\n  , \"actor\":\"{{Envelope.Body.Fault.faultactor}}\"\n  {% end %}\n}\n{% end %}\nEOF\n)\n```\n\n上面使用了 `awk` 和 `tr` 来操作模板，使模板成为有效的 JSON 字符串。\n\n使用之前创建的模板创建带有 `body-transformer` 的路由。在插件中，将请求输入格式设置为 JSON，将响应输入格式设置为 XML，并将 `Content-Type` 标头设置为 `text/xml`，以便上游服务正确响应：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"methods\": [\"POST\"],\n    \"uri\": \"/ws\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"'\"$req_template\"'\",\n          \"input_format\": \"json\"\n        },\n        \"response\": {\n          \"template\": \"'\"$rsp_template\"'\",\n          \"input_format\": \"xml\"\n        }\n      },\n      \"proxy-rewrite\": {\n        \"headers\": {\n          \"set\": {\n            \"Content-Type\": \"text/xml\"\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"localhost:8080\": 1\n      }\n    }\n  }'\n```\n\n:::tip\n\n如果将复杂的文本文件调整为有效的转换模板很麻烦，则可以使用 base64 实用程序对文件进行编码，例如以下内容：\n\n```json\n\"body-transformer\": {\n  \"request\": {\n    \"template\": \"'\"$(base64 -w0 /path/to/request_template_file)\"'\"\n  },\n  \"response\": {\n    \"template\": \"'\"$(base64 -w0 /path/to/response_template_file)\"'\"\n  }\n}\n```\n\n:::\n\n发送具有有效 JSON 主体的请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/ws\" -X POST -d '{\"name\": \"Spain\"}'\n```\n\n请求中发送的 JSON 主体将在转发到上游 SOAP 服务之前转换为 XML，响应主体将从 XML 转换回 JSON。\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"status\": \"200\",\n  \"currency\": \"EUR\",\n  \"population\": 46704314,\n  \"capital\": \"Madrid\",\n  \"name\": \"Spain\"\n}\n```\n\n### 修改请求体\n\n以下示例演示了如何动态修改请求体。\n\n使用 `body-transformer` 创建一个路由，其中​​模板将单词 `world` 附加到 `name`，并将 `10` 添加到 `age`，以将它们分别设置为 `foo` 和 `bar` 的值：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"{\\\"foo\\\":\\\"{{name .. \\\" world\\\"}}\\\",\\\"bar\\\":{{age+10}}}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路线发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"hello\",\"age\":20}' \\\n  -i\n```\n\n您应该看到以下响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"foo\\\":\\\"hello world\\\",\\\"bar\\\":30}\",\n  ...\n  \"json\": {\n    \"bar\": 30,\n    \"foo\": \"hello world\"\n  },\n  \"method\": \"POST\",\n  ...\n}\n```\n\n### 使用变量生成请求主体\n\n以下示例演示如何使用 `ctx` 上下文变量动态生成请求主体。\n\n使用 `body-transformer` 创建路由，其中​​模板使用 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html) `arg_name` 访问请求参数：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"{\\\"foo\\\":\\\"{{_ctx.var.arg_name .. \\\" world\\\"}}\\\"}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n使用 `name` 参数向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?name=hello\"\n```\n\n您应该看到如下响应：\n\n```json\n{\n  \"args\": {\n    \"name\": \"hello\"\n  },\n  ...,\n  \"json\": {\n    \"foo\": \"hello world\"\n  },\n...\n}\n```\n\n### 将正文从 YAML 转换为 JSON\n\n以下示例演示如何将请求正文从 YAML 转换为 JSON。\n\n创建请求转换模板：\n\n```shell\nreq_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1'\n{%\n    local yaml = require(\"tinyyaml\")\n    local body = yaml.parse(_body)\n%}\n{\"foobar\":\"{{body.foobar.foo .. \" \" .. body.foobar.bar}}\"}\nEOF\n)\n```\n\n使用以下模板创建一个带有 `body-transformer` 的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"template\": \"'\"$req_template\"'\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n使用 YAML 主体向路由发送请求：\n\n```shell\nbody='\nfoobar:\n  foo: hello\n  bar: world'\n\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -d \"$body\" \\\n  -H \"Content-Type: text/yaml\" \\\n  -i\n```\n\n您应该会看到类似以下内容的响应，这验证了 YAML 主体已适当地转换为 JSON：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"foobar\\\":\\\"hello world\\\"}\",\n  ...\n  \"json\": {\n    \"foobar\": \"hello world\"\n  },\n...\n}\n```\n\n### 将表单 URL 编码主体转换为 JSON\n\n以下示例演示如何将 `form-urlencoded` 主体转换为 JSON。\n\n使用 `body-transformer` 创建路由，将 `input_format` 设置为 `encoded`，并配置一个模板，将字符串 `world` 附加到 `name` 输入，将 `10` 添加到 `age` 输入：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"encoded\",\n          \"template\": \"{\\\"foo\\\":\\\"{{name .. \\\" world\\\"}}\\\",\\\"bar\\\":{{age+10}}}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送一个带有编码主体的 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d 'name=hello&age=20'\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"{\\\"foo\\\":\\\"hello world\\\",\\\"bar\\\":30}\": \"\"\n  },\n  \"headers\": {\n    ...\n  },\n  ...\n}\n```\n\n### 将 GET 请求查询参数转换为正文\n\n以下示例演示如何将 GET 请求查询参数转换为请求正文。请注意，这不会转换 HTTP 方法。要转换方法，请参阅 [`proxy-rewrite`](./proxy-rewrite.md)。\n\n使用 `body-transformer` 创建路由，将 `input_format` 设置为 `args`，并配置一个向请求添加消息的模板：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"args\",\n          \"template\": \"{\\\"message\\\": \\\"hello {{name}}\\\"}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路线发送 GET 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything?name=john\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"message\\\": \\\"hello john\\\"}\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    ...\n  },\n  \"json\": {\n    \"message\": \"hello john\"\n  },\n  \"method\": \"GET\",\n  ...\n}\n```\n\n### 转换纯文本媒体类型\n\n以下示例演示如何转换具有 `plain` 媒体类型的请求。\n\n使用 `body-transformer` 创建路由，将 `input_format` 设置为 `plain`，并配置模板以从正文字符串中删除 `not` 和后续空格：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"plain\",\n          \"template\": \"{\\\"message\\\": \\\"{* string.gsub(_body, \\\"not \\\", \\\"\\\") *}\\\"}\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\" -X POST \\\n  -d 'not actually json' \\\n  -i\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"{\\\"message\\\": \\\"actually json\\\"}\": \"\"\n  },\n  \"headers\": {\n    ...\n  },\n  ...\n}\n```\n\n### 转换多部分媒体类型\n\n以下示例演示如何转换具有 `multipart` 媒体类型的请求。\n\n创建一个请求转换模板，该模板根据请求正文中提供的 `age` 向正文添加 `status`：\n\n```shell\nreq_template=$(cat <<EOF | awk '{gsub(/\"/,\"\\\\\\\"\");};1'\n{%\n  local core = require 'apisix.core'\n  local cjson = require 'cjson'\n\n  if tonumber(context.age) > 18 then\n      context._multipart:set_simple(\"status\", \"adult\")\n  else\n      context._multipart:set_simple(\"status\", \"minor\")\n  end\n\n  local body = context._multipart:tostring()\n%}{* body *}\nEOF\n)\n```\n\n创建一个带有 `body-transformer` 的路由，将 `input_format` 设置为 `multipart`，并使用之前创建的请求模板进行转换：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"body-transformer-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"body-transformer\": {\n        \"request\": {\n          \"input_format\": \"multipart\",\n          \"template\": \"'\"$req_template\"'\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送多部分 POST 请求：\n\n```shell\ncurl -X POST \\\n  -F \"name=john\" \\\n  -F \"age=10\" \\\n  \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"age\": \"10\",\n    \"name\": \"john\",\n    \"status\": \"minor\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"361\",\n    \"Content-Type\": \"multipart/form-data; boundary=------------------------qtPjk4c8ZjmGOXNKzhqnOP\",\n    ...\n  },\n  ...\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/brotli.md",
    "content": "---\ntitle: brotli\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - brotli\ndescription: 这个文档包含有关 Apache APISIX brotli 插件的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`brotli` 插件可以动态的设置 Nginx 中的 [brotli](https://github.com/google/ngx_brotli) 的行为。\n\n## 前提条件\n\n该插件依赖 brotli 共享库。\n\n如下是构建和安装 brotli 共享库的示例脚本：\n\n``` shell\nwget https://github.com/google/brotli/archive/refs/tags/v1.1.0.zip\nunzip v1.1.0.zip\ncd brotli-1.1.0 && mkdir build && cd build\ncmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local/brotli ..\nsudo cmake --build . --config Release --target install\nsudo sh -c \"echo /usr/local/brotli/lib >> /etc/ld.so.conf.d/brotli.conf\"\nsudo ldconfig\n```\n\n## 属性\n\n| 名称           | 类型                   | 必选项   | 默认值           | 有效值 | 描述                                                                                                                                         |\n|--------------|----------------------|-------|---------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------|\n| types        | array[string] or \"*\" | False | [\"text/html\"] |              | 动态设置 `brotli_types` 指令。特殊值 `\"*\"` 用于匹配任意的 MIME 类型。                                                                                          |\n| min_length   | integer              | False | 20            | >= 1         | 动态设置 `brotli_min_length` 指令。                                                                                                               |\n| comp_level   | integer              | False | 6             | [0, 11]      | 动态设置 `brotli_comp_level` 指令。                                                                                                               |\n| mode         | integer              | False | 0             | [0, 2]       | 动态设置 `brotli decompress mode`，更多信息参考 [RFC 7932](https://tools.ietf.org/html/rfc7932)。                                                      |\n| lgwin        | integer              | False | 19            | [0, 10-24]   | 动态设置 `brotli sliding window size`，`lgwin` 是滑动窗口大小的以 2 为底的对数，将其设置为 0 会让压缩器自行决定最佳值，更多信息请参考 [RFC 7932](https://tools.ietf.org/html/rfc7932)。  |\n| lgblock      | integer              | False | 0             | [0, 16-24]   | 动态设置 `brotli input block size`，`lgblock` 是最大输入块大小的以 2 为底的对数，将其设置为 0 会让压缩器自行决定最佳值，更多信息请参考 [RFC 7932](https://tools.ietf.org/html/rfc7932)。\t |\n| http_version | number               | False | 1.1           | 1.1, 1.0     | 与 `gzip_http_version` 指令类似，用于识别 http 的协议版本。                                                                                                |\n| vary         | boolean              | False | false         |              | 与 `gzip_vary` 指令类似，用于启用或禁用 `Vary: Accept-Encoding` 响应头。                                                                                    |\n\n## 启用插件\n\n如下示例中，在指定的路由上启用 `brotli` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/\",\n    \"plugins\": {\n        \"brotli\": {\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n\n## 使用示例\n\n通过上述命令启用插件后，可以通过以下方法测试插件：\n\n```shell\ncurl http://127.0.0.1:9080/ -i -H \"Accept-Encoding: br\"\n```\n\n```\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Tue, 05 Dec 2023 03:06:49 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/3.6.0\nContent-Encoding: br\n\nWarning: Binary output can mess up your terminal. Use \"--output -\" to tell\nWarning: curl to output it to your terminal anyway, or consider \"--output\nWarning: <FILE>\" to save to a file.\n```\n\n## 删除插件\n\n当您需要禁用 `brotli` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/chaitin-waf.md",
    "content": "---\ntitle: chaitin-waf\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - WAF\ndescription: chaitin-waf 插件与长亭雷池 WAF 集成，以检测和阻止网络威胁，加强 API 安全性并保护用户数据。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/chaitin-waf\" />\n</head>\n\n## 描述\n\n`chaitin-waf` 插件与长亭雷池 WAF 集成服务集成，提供对基于 Web 的威胁的高级检测和预防，增强应用程序安全性并保护敏感的用户数据。\n\n## 响应头\n\n该插件可以添加以下响应头，具体取决于 `append_waf_resp_header` 和 `append_waf_debug_header` 的配置：\n\n| 响应头 | 描述 |\n|--------|-------------|\n| `X-APISIX-CHAITIN-WAF` | 表示 APISIX 是否将请求转发到 WAF 服务器。<br />• `yes`: 请求已转发到 WAF 服务器。<br />• `no`: 请求未转发到 WAF 服务器。<br />• `unhealthy`: 请求匹配到配置的规则，但没有可用的 WAF 服务。<br />• `err`: 插件执行过程中发生错误，同时会包含 `X-APISIX-CHAITIN-WAF-ERROR` 响应头，提供错误详情。<br />• `waf-err`: 与 WAF 服务器交互时发生错误，同时会包含 `X-APISIX-CHAITIN-WAF-ERROR` 响应头，提供错误详情。<br />• `timeout`: 向 WAF 服务器的请求超时。 |\n| `X-APISIX-CHAITIN-WAF-TIME` | 向 WAF 服务器请求的往返时间（RTT，单位为毫秒），包括网络延迟和 WAF 服务器处理时间。 |\n| `X-APISIX-CHAITIN-WAF-STATUS` | WAF 服务器返回给 APISIX 的状态码。 |\n| `X-APISIX-CHAITIN-WAF-ACTION` | WAF 服务器返回给 APISIX 的动作。<br />• `pass`: 请求被 WAF 服务允许。<br />• `reject`: 请求被 WAF 服务拦截。 |\n| `X-APISIX-CHAITIN-WAF-ERROR` | 调试用响应头。包含 WAF 错误信息。 |\n| `X-APISIX-CHAITIN-WAF-SERVER` | 调试用响应头。表示选用了哪个 WAF 服务器。 |\n\n## 属性\n\n| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |\n|--------------------------|---------------------------|----------|---------|--------------|-------------|\n| mode                     | string        | 否       | block   | `off`, `monitor`, `block`| 用于决定插件在匹配请求时的行为模式。在 `off` 模式下，跳过 WAF 检查。在 `monitor` 模式下，记录潜在威胁请求但不拦截。在 `block` 模式下，根据 WAF 服务的判断拦截存在威胁的请求。 |\n| match                    | array[object] | 否       |         |                          | 匹配规则数组。插件使用这些规则来决定是否对请求执行 WAF 检查。如果列表为空，则处理所有请求。 |\n| match.vars               | array[array]  | 否       |         |                          | 一个或多个匹配条件数组，使用 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) 表达式来有条件地执行插件。 |\n| append_waf_resp_header   | boolean       | 否       | true    |                          | 若为 true，则在响应中添加 `X-APISIX-CHAITIN-WAF`、`X-APISIX-CHAITIN-WAF-TIME`、`X-APISIX-CHAITIN-WAF-ACTION` 和 `X-APISIX-CHAITIN-WAF-STATUS` 响应头。 |\n| append_waf_debug_header  | boolean       | 否       | false   |                          | 若为 true，则在响应中添加调试用响应头 `X-APISIX-CHAITIN-WAF-ERROR` 和 `X-APISIX-CHAITIN-WAF-SERVER`。仅当 `append_waf_resp_header` 为 true 时生效。 |\n| config                   | object        | 否       |         |                          | 长亭 WAF 服务配置。这些配置在指定时会覆盖对应的元数据默认值。 |\n| config.connect_timeout   | integer       | 否       | 1000    |                          | 与 WAF 服务的连接超时时间，单位为毫秒。 |\n| config.send_timeout      | integer       | 否       | 1000    |                          | 向 WAF 服务发送数据的超时时间，单位为毫秒。 |\n| config.read_timeout      | integer       | 否       | 1000    |                          | 从 WAF 服务读取数据的超时时间，单位为毫秒。 |\n| config.req_body_size     | integer       | 否       | 1024    |                          | 允许的最大请求体大小，单位为 KB。 |\n| config.keepalive_size    | integer       | 否       | 256     |                          | 可同时维持的与 WAF 检测服务的空闲连接数上限。 |\n| config.keepalive_timeout | integer       | 否       | 60000   |                          | 与 WAF 服务的空闲连接超时时间，单位为毫秒。 |\n| config.real_client_ip    | boolean       | 否       | true    |                          | 若为 true，则从 `X-Forwarded-For` 请求头中获取客户端 IP。若为 false，则插件使用连接中的客户端 IP。 |\n\n## 插件元数据\n\n| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |\n|--------------------------|---------------------------|----------|---------|--------------|-------------|\n| nodes                    | array[object] | 是       |         |              | 长亭雷池 WAF 服务的地址数组。 |\n| nodes.host               | string        | 是       |         |              | 长亭雷池 WAF 服务的地址，支持 IPv4、IPv6、Unix Socket 等。 |\n| nodes.port               | integer       | 否       | 80      |              | 长亭雷池 WAF 服务的端口。 |\n| mode                     | string        | 否       |         | block        | 用于决定插件在匹配请求时的行为模式。在 `off` 模式下，跳过 WAF 检查。在 `monitor` 模式下，记录潜在威胁请求但不拦截。在 `block` 模式下，根据 WAF 服务的判断拦截存在威胁的请求。 |\n| config                   | object        | 否       |         |              | 长亭雷池 WAF 服务配置。 |\n| config.connect_timeout   | integer       | 否       | 1000    |              | 与 WAF 服务的连接超时时间，单位为毫秒。 |\n| config.send_timeout      | integer       | 否       | 1000    |              | 向 WAF 服务发送数据的超时时间，单位为毫秒。 |\n| config.read_timeout      | integer       | 否       | 1000    |              | 从 WAF 服务读取数据的超时时间，单位为毫秒。 |\n| config.req_body_size     | integer       | 否       | 1024    |              | 允许的最大请求体大小，单位为 KB。 |\n| config.keepalive_size    | integer       | 否       | 256     |              | 可同时维持的与 WAF 检测服务的空闲连接数上限。 |\n| config.keepalive_timeout | integer       | 否       | 60000   |              | 与 WAF 服务的空闲连接超时时间，单位为毫秒。 |\n| config.real_client_ip    | boolean       | 否       | true    |              | 若为 true，则从 `X-Forwarded-For` 请求头中获取客户端 IP；若为 false，则插件使用连接中的客户端 IP。 |\n\n## 示例\n\n以下示例演示了如何针对不同场景配置 chaitin-waf 插件。\n\n继续操作之前，请确保您已安装 [长亭雷池 WAF](https://docs.waf.chaitin.com/en/GetStarted/Deploy)。\n\n:::note\n只有发送自 `apisix.trusted_addresses` 配置（支持 IP 和 CIDR）地址的 `X-Forwarded-*` 头才会被信任，并传递给插件或上游。如果未配置 `apisix.trusted_addresses` 或 ip 不在配置地址范围内的，`X-Forwarded-*` 头将全部被可信值覆盖。\n:::\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 拦截路由上的恶意请求\n\n以下示例演示了如何与长亭雷池 WAF 集成，以保护路由上的流量，并立即拒绝恶意请求。\n\n使用插件元数据配置长亭雷池 WAF 连接详细信息（相应地更新地址）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/chaitin-waf\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"nodes\": [\n      {\n        \"host\": \"172.22.222.5\",\n        \"port\": 8000\n      }\n    ]\n  }'\n```\n\n创建路由并在路由上启用 `chaitin-waf` 以阻止被识别为恶意的请求：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"chaitin-waf-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"chaitin-waf\": {\n        \"mode\": \"block\",\n        \"append_waf_resp_header\": true,\n        \"append_waf_debug_header\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送标准请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n向路由发送一个包含 SQL 注入的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -d 'a=1 and 1=1'\n```\n\n您应该会看到类似以下内容的 `HTTP/1.1 403 Forbidden` 响应：\n\n```text\n...\nX-APISIX-CHAITIN-WAF-STATUS: 403\nX-APISIX-CHAITIN-WAF-ACTION: reject\nX-APISIX-CHAITIN-WAF-SERVER: 172.22.222.5\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-TIME: 3\n...\n\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"276be6457d8447a4bf1f792501dfba6c\"}\n```\n\n### 监控恶意请求\n\n本示例演示如何与长亭雷池 WAF 集成，以监控所有使用 `chaitin-waf` 的路由（但不拒绝请求），并仅拒绝特定路由上的潜在恶意请求。\n\n使用插件元数据配置长亭雷池 WAF 连接详细信息（相应地更新地址）并配置模式：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/chaitin-waf\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"nodes\": [\n      {\n        \"host\": \"172.22.222.5\",\n        \"port\": 8000\n      }\n    ],\n    \"mode\": \"monitor\"\n  }'\n```\n\n创建路由并启用 `chaitin-waf`，无需在路由上进行任何配置：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"chaitin-waf-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"chaitin-waf\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送标准请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n向路由发送一个包含 SQL 注入的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -d 'a=1 and 1=1'\n```\n\n您还应该收到 `HTTP/1.1 200 OK` 响应，因为请求在 `monitor` 模式下没有被阻止，但请在日志条目中观察以下内容：\n\n```text\n2025/09/09 11:44:08 [warn] 115#115: *31683 [lua] chaitin-waf.lua:385: do_access(): chaitin-waf monitor mode: request would have been rejected, event_id: 49bed20603e242f9be5ba6f1744bba4b, client: 172.20.0.1, server: _, request: \"POST /anything HTTP/1.1\", host: \"127.0.0.1:9080\"\n```\n\n如果你在路由上明确配置了 `mode`，它将优先于插件元数据中的配置。例如，如果你创建如下路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"chaitin-waf-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"chaitin-waf\": {\n        \"mode\": \"block\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送一个标准请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到一个 `HTTP/1.1 200 OK` 响应。\n\n向路由发送一个包含 SQL 注入的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -d 'a=1 and 1=1'\n```\n\n您应该会看到一个类似以下内容的 `HTTP/1.1 403 Forbidden` 响应：\n\n```text\n...\nX-APISIX-CHAITIN-WAF-STATUS: 403\nX-APISIX-CHAITIN-WAF-ACTION: reject\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-TIME: 3\n...\n\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"c3eb25eaa7ae4c0d82eb8ceebf3600d0\"}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/clickhouse-logger.md",
    "content": "---\ntitle: clickhouse-logger\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - ClickHouse\ndescription: 本文介绍了 API 网关 Apache APISIX 如何使用 clickhouse-logger 插件将日志数据发送到 ClickHouse 数据库中。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`clickhouse-logger` 插件可用于将日志数据推送到 [ClickHouse](https://github.com/ClickHouse/ClickHouse) 数据库中。\n\n## 属性\n\n| 名称             | 类型    | 必选项  | 默认值              | 有效值       | 描述                                                     |\n| ---------------- | ------- | ------ | ------------------- | ----------- | -------------------------------------------------------- |\n| endpoint_addr    | 废弃    | 是     |                     |              | ClickHouse 的 `endpoints`。请使用 `endpoint_addrs` 代替。 |\n| endpoint_addrs   | array   | 是     |                     |              | ClickHouse 的 `endpoints。`。                            |\n| database         | string  | 是     |                     |              | 使用的数据库。                                            |\n| logtable         | string  | 是     |                     |              | 写入的表名。                                              |\n| user             | string  | 是     |                     |              | ClickHouse 的用户。                                       |\n| password         | string  | 是     |                     |              | ClickHouse 的密码。                                      |\n| timeout          | integer | 否     | 3                   | [1,...]      | 发送请求后保持连接活动的时间。                             |\n| name             | string  | 否     | \"clickhouse logger\" |              | 标识 logger 的唯一标识符。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。                               |\n| ssl_verify       | boolean | 否     | true                | [true,false] | 当设置为 `true` 时，验证证书。                                                |\n| log_format             | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| include_req_body       | boolean | 否     | false          | [false, true]         | 当设置为 `true` 时，包含请求体。**注意**：如果请求体无法完全存放在内存中，由于 NGINX 的限制，APISIX 无法将它记录下来。|\n| include_req_body_expr  | array   | 否     |                |                       | 当 `include_req_body` 属性设置为 `true` 时进行过滤。只有当此处设置的表达式计算结果为 `true` 时，才会记录请求体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n| include_resp_body      | boolean | 否     | false          | [false, true]         | 当设置为 `true` 时，包含响应体。 |\n| include_resp_body_expr | array   | 否     |                |                       | 当 `include_resp_body` 属性设置为 `true` 时进行过滤。只有当此处设置的表达式计算结果为 `true` 时才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。|\n\n注意：schema 中还定义了 `encrypt_fields = {\"password\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n此外：你可以使用环境变量或者 APISIX secret 来存放和引用插件配置，APISIX 当前支持通过两种方式配置 secrets - [Environment Variables and HashiCorp Vault](../terminology/secret.md)。\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认情况下批处理器每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n```json\n{\n    \"response\": {\n        \"status\": 200,\n        \"size\": 118,\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"server\": \"APISIX/3.7.0\",\n            \"content-length\": \"12\"\n        }\n    },\n    \"client_ip\": \"127.0.0.1\",\n    \"upstream_latency\": 3,\n    \"apisix_latency\": 98.999998092651,\n    \"upstream\": \"127.0.0.1:1982\",\n    \"latency\": 101.99999809265,\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"route_id\": \"1\",\n    \"start_time\": 1704507612177,\n    \"service_id\": \"\",\n    \"request\": {\n        \"method\": \"POST\",\n        \"querystring\": {\n            \"foo\": \"unknown\"\n        },\n        \"headers\": {\n            \"host\": \"localhost\",\n            \"connection\": \"close\",\n            \"content-length\": \"18\"\n        },\n        \"size\": 110,\n        \"uri\": \"/hello?foo=unknown\",\n        \"url\": \"http://localhost:1984/hello?foo=unknown\"\n    }\n}\n```\n\n## 配置插件元数据\n\n`clickhouse-logger` 也支持自定义日志格式，与 [http-logger](./http-logger.md) 插件类似。\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否   |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX](../apisix-variable.md) 或 [NGINX](http://nginx.org/en/docs/varindex.html) 变量。该配置全局生效。如果你指定了 `log_format`，该配置就会对所有绑定 `clickhouse-logger` 的路由或服务生效。|\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/clickhouse-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\"\n    }\n}'\n```\n\n您可以使用 Clickhouse docker 镜像来创建一个容器，如下所示：\n\n```shell\ndocker run -d -p 8123:8123 -p 9000:9000 -p 9009:9009 --name some-clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server\n```\n\n然后在您的 ClickHouse 数据库中创建一个表来存储日志。\n\n```shell\ncurl -X POST 'http://localhost:8123/' \\\n--data-binary 'CREATE TABLE default.test (host String, client_ip String, route_id String, service_id String, `@timestamp` String, PRIMARY KEY(`@timestamp`)) ENGINE = MergeTree()' --user default:\n```\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"clickhouse-logger\": {\n                \"user\": \"default\",\n                \"password\": \"\",\n                \"database\": \"default\",\n                \"logtable\": \"test\",\n                \"endpoint_addrs\": [\"http://127.0.0.1:8123\"]\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n:::note 注意\n\n如果配置多个 `endpoints`，日志将会随机写入到各个 `endpoints`。\n\n:::\n\n## 测试插件\n\n现在你可以向 APISIX 发起请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n现在，如果您检查表中的行，您将获得以下输出：\n\n```shell\ncurl 'http://localhost:8123/?query=select%20*%20from%20default.test'\n127.0.0.1\t127.0.0.1\t1\t\t2023-05-08T19:15:53+05:30\n```\n\n## 删除插件\n\n当你需要删除该插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/client-control.md",
    "content": "---\ntitle: client-control\nkeywords:\n  - APISIX\n  - API 网关\n  - Client Control\ndescription: 本文介绍了 Apache APISIX proxy-control 插件的相关操作，你可以使用此插件动态地控制 NGINX 处理客户端的请求的行为。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`client-control` 插件能够通过设置客户端请求体大小的上限来动态地控制 NGINX 处理客户端的请求。\n\n:::info 重要\n\n此插件需要 APISIX 在 [APISIX-Runtime](../FAQ.md#如何构建-apisix-Runtime-环境) 环境上运行。更多信息请参考 [apisix-build-tools](https://github.com/api7/apisix-build-tools)。\n\n:::\n\n## 属性\n\n| 名称      | 类型          | 必选项 | 有效值                                                                    | 描述                                                                                                                                                                                    |\n| --------- | ------------- | ----------- | ------------------------------------------------------------------------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| max_body_size | integer        | 否    | [0,...] | 设置客户端请求体的最大上限，动态调整 [`client_max_body_size`](https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size) 的大小，单位为字节。当设置 `max_body_size` 为 0 时，将不会对客户端请求体大小进行检查。 |\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用 `client-control` 插件，并设置 `max_body_size`：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"client-control\": {\n            \"max_body_size\" : 1\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n启用插件后，使用 `curl` 命令请求该路由：\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html -d '123'\n```\n\n因为在配置插件时设置了 `max_body_size` 为 `1`，所以返回的 HTTP 响应头中如果带有 `413` 状态码，则表示插件生效：\n\n```shell\nHTTP/1.1 413 Request Entity Too Large\n...\n<html>\n<head><title>413 Request Entity Too Large</title></head>\n<body>\n<center><h1>413 Request Entity Too Large</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/consumer-restriction.md",
    "content": "---\ntitle: consumer-restriction\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Consumer restriction\ndescription: Consumer Restriction 插件允许用户根据 Route、Service、Consumer 或 Consumer Group 来设置相应的访问限制。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`consumer-restriction` 插件允许用户根据 Route、Service、Consumer 或 Consumer Group 来设置相应的访问限制。\n\n## 属性\n\n| 名称                       | 类型          | 必选项 | 默认值        | 有效值                                                       | 描述                                                         |\n| -------------------------- | ------------- | ------ | ------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |\n| type                       | string        | 否     | consumer_name | [\"consumer_name\", \"consumer_group_id\", \"service_id\", \"route_id\"] | 支持设置访问限制的对象类型。                                 |\n| whitelist                  | array[string] | 是     |               |                                                              | 加入白名单的对象，优先级高于`allowed_by_methods`。           |\n| blacklist                  | array[string] | 是     |               |                                                              | 加入黑名单的对象，优先级高于`whitelist`。                    |\n| rejected_code              | integer       | 否     | 403           | [200,...]                                                    | 当请求被拒绝时，返回的 HTTP 状态码。                         |\n| rejected_msg               | string        | 否     |               |                                                              | 当请求被拒绝时，返回的错误信息。                             |\n| allowed_by_methods         | array[object] | 否     |               |                                                              | 一组为 Consumer 设置允许的配置，包括用户名和允许的 HTTP 方法列表。 |\n| allowed_by_methods.user    | string        | 否     |               |                                                              | 为 Consumer 设置的用户名。                                   |\n| allowed_by_methods.methods | array[string] | 否     |               | [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"HEAD\", \"OPTIONS\", \"CONNECT\", \"TRACE\", \"PURGE\"] | 为 Consumer 设置的允许的 HTTP 方法列表。                     |\n\n:::note\n\n不同的 `type` 属性值分别代表以下含义：\n\n- `consumer_name`：把 Consumer 的 `username` 列入白名单或黑名单来限制 Consumer 对 Route 或 Service 的访问。\n- `consumer_group_id`: 把 Consumer Group 的 `id` 列入白名单或黑名单来限制 Consumer 对 Route 或 Service 的访问。\n- `service_id`：把 Service 的 `id` 列入白名单或黑名单来限制 Consumer 对 Service 的访问，需要结合授权插件一起使用。\n- `route_id`：把 Route 的 `id` 列入白名单或黑名单来限制 Consumer 对 Route 的访问。\n\n:::\n\n## 启用并测试插件\n\n### 通过 `consumer_name` 限制访问\n\n首先，创建两个 Consumer，分别为 `jack1` 和 `jack2`：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"username\": \"jack1\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\":\"jack2019\",\n            \"password\": \"123456\"\n        }\n    }\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"username\": \"jack2\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\":\"jack2020\",\n            \"password\": \"123456\"\n        }\n    }\n}'\n```\n\n然后，在指定路由上启用并配置 `consumer-restriction` 插件，并通过将 `consumer_name` 加入 `whitelist` 来限制不同 Consumer 的访问：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {},\n        \"consumer-restriction\": {\n            \"whitelist\": [\n                \"jack1\"\n            ]\n        }\n    }\n}'\n```\n\n**测试插件**\n\n`jack1` 发出访问请求，返回 `200` HTTP 状态码，代表访问成功：\n\n```shell\ncurl -u jack2019:123456 http://127.0.0.1:9080/index.html -i\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n`jack2` 发出访问请求，返回 `403` HTTP 状态码，代表访问被限制，插件生效：\n\n```shell\ncurl -u jack2020:123456 http://127.0.0.1:9080/index.html -i\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"The consumer_name is forbidden.\"}\n```\n\n### 通过 `allowed_by_methods` 限制访问\n\n首先，创建两个 Consumer，分别为 `jack1` 和 `jack2`，创建方法请参考[通过 `consumer_name` 限制访问](#通过-consumername-限制访问)。\n\n然后，在指定路由上启用并配置 `consumer-restriction` 插件，并且仅允许 `jack1` 使用 `POST` 方法进行访问：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {},\n        \"consumer-restriction\": {\n            \"allowed_by_methods\":[{\n                \"user\": \"jack1\",\n                \"methods\": [\"POST\"]\n            }]\n        }\n    }\n}'\n```\n\n**测试插件**\n\n`jack1` 发出访问请求，返回 `403` HTTP 状态码，代表访问被限制：\n\n```shell\ncurl -u jack2019:123456 http://127.0.0.1:9080/index.html\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"The consumer_name is forbidden.\"}\n```\n\n现在更新插件配置，增加 `jack1` 的 `GET` 访问能力：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {},\n        \"consumer-restriction\": {\n            \"allowed_by_methods\":[{\n                \"user\": \"jack1\",\n                \"methods\": [\"POST\",\"GET\"]\n            }]\n        }\n    }\n}'\n```\n\n`jack1` 再次发出访问请求，返回 `200` HTTP 状态码，代表访问成功：\n\n```shell\ncurl -u jack2019:123456 http://127.0.0.1:9080/index.html\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n### 通过 `service_id` 限制访问\n\n使用 `service_id` 的方式需要与授权插件一起配合使用，这里以 [`key-auth`](./key-auth.md) 授权插件为例。\n\n首先，创建两个 Service：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/services/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"desc\": \"new service 001\"\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/services/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"desc\": \"new service 002\"\n}'\n```\n\n在指定 Consumer 上配置 `key-auth` 和 `consumer-restriction` 插件，并通过将 `service_id` 加入 `whitelist` 来限制 Consumer 对 Service 的访问：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"new_consumer\",\n    \"plugins\": {\n    \"key-auth\": {\n        \"key\": \"auth-jack\"\n    },\n    \"consumer-restriction\": {\n           \"type\": \"service_id\",\n            \"whitelist\": [\n                \"1\"\n            ],\n            \"rejected_code\": 403\n        }\n    }\n}'\n```\n\n**测试插件**\n\n在指定路由上启用并配置 `key-auth` 插件，并绑定 `service_id` 为 `1`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"service_id\": 1,\n    \"plugins\": {\n         \"key-auth\": {\n        }\n    }\n}'\n```\n\n对 Service 发出访问请求，返回 `403` HTTP 状态码，说明在白名单列中的 `service_id` 允许访问，插件生效：\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'apikey: auth-jack' -i\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n更新配置 `key-auth` 插件，并绑定 `service_id` 为 `2`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"service_id\": 2,\n    \"plugins\": {\n         \"key-auth\": {\n        }\n    }\n}'\n```\n\n再次对 Service 发出访问请求，返回 `403` HTTP 状态码，说明不在白名单列表的 `service_id` 被拒绝访问，插件生效：\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'apikey: auth-jack' -i\n```\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"The service_id is forbidden.\"}\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"basic-auth\": {}\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/cors.md",
    "content": "---\ntitle: cors\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - CORS\ndescription: 本文介绍了 Apache APISIX cors 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`cors` 插件可以让你轻松地为服务端启用 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)（Cross-Origin Resource Sharing，跨域资源共享）的返回头。\n\n## 属性\n\n| 名称             | 类型    | 必选项 | 默认值 | 描述                                                         |\n| ---------------- | ------- | ------ | ------ | ------------------------------------------------------------ |\n| allow_origins    | string  | 否   | \"*\"    | 允许跨域访问的 Origin，格式为 `scheme://host:port`，示例如 `https://somedomain.com:8081`。如果你有多个 Origin，请使用 `,` 分隔。当 `allow_credential` 为 `false` 时，可以使用 `*` 来表示允许所有 Origin 通过。你也可以在启用了 `allow_credential` 后使用 `**` 强制允许所有 Origin 均通过，但请注意这样存在安全隐患。 |\n| allow_methods    | string  | 否   | \"*\"    | 允许跨域访问的 Method，比如：`GET`，`POST` 等。如果你有多个 Method，请使用 `,` 分割。当 `allow_credential` 为 `false` 时，可以使用 `*` 来表示允许所有 Method 通过。你也可以在启用了 `allow_credential` 后使用 `**` 强制允许所有 Method 都通过，但请注意这样存在安全隐患。 |\n| allow_headers    | string  | 否   | \"*\"    | 允许跨域访问时请求方携带哪些非 `CORS 规范` 以外的 Header。如果你有多个 Header，请使用 `,` 分割。当 `allow_credential` 为 `false` 时，可以使用 `*` 来表示允许所有 Header 通过。你也可以在启用了 `allow_credential` 后使用 `**` 强制允许所有 Header 都通过，但请注意这样存在安全隐患。 |\n| expose_headers   | string  | 否   |        | 允许跨域访问时响应方携带哪些非 CORS 规范 以外的 Header。如果你有多个 Header，请使用 , 分割。当 allow_credential 为 false 时，可以使用 * 来表示允许任意 Header。如果不设置，插件不会修改 `Access-Control-Expose-Headers` 头，详情请参考 [Access-Control-Expose-Headers - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers)。 |\n| max_age          | integer | 否   | 5      | 浏览器缓存 CORS 结果的最大时间，单位为秒。在这个时间范围内，浏览器会复用上一次的检查结果，`-1` 表示不缓存。请注意各个浏览器允许的最大时间不同，详情请参考 [Access-Control-Max-Age - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#directives)。 |\n| allow_credential | boolean | 否   | false  | 是否允许跨域访问的请求方携带凭据（如 Cookie 等）。根据 CORS 规范，如果设置该选项为 `true`，那么将不能在其他属性中使用 `*`。 |\n| allow_origins_by_regex | array | 否   | nil  | 使用正则表达式数组来匹配允许跨域访问的 Origin，如 `[\".*\\.test.com$\"]` 可以匹配任何 `test.com` 的子域名。如果 `allow_origins_by_regex` 属性已经指定，则会忽略 `allow_origins` 属性。 |\n| allow_origins_by_metadata | array | 否    | nil   | 通过引用插件元数据的 `allow_origins` 配置允许跨域访问的 Origin。比如当插件元数据为 `\"allow_origins\": {\"EXAMPLE\": \"https://example.com\"}` 时，配置 `[\"EXAMPLE\"]` 将允许 Origin `https://example.com` 的访问。  |\n\n:::info IMPORTANT\n\n1. `allow_credential` 是一个很敏感的选项，请谨慎开启。开启之后，其他参数默认的 `*` 将失效，你必须显式指定它们的值。\n2. 在使用 `**` 时，需要清楚该参数引入的一些安全隐患，比如 CSRF，并确保这样的安全等级符合自己预期。\n\n:::\n\n## 元数据\n\n| 名称           | 类型    | 必选项  | 描述                       |\n| -----------   | ------  | ------ | ------------------ |\n| allow_origins | object  | 否    | 定义允许跨域访问的 Origin；它的键为 `allow_origins_by_metadata` 使用的引用键，值则为允许跨域访问的 Origin，其语义与属性中的 `allow_origins` 相同。 |\n\n## 启用插件\n\n你可以在路由或服务上启用 `cors` 插件。\n\n你可以通过如下命令在指定路由上启用 `cors` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"cors\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl http://127.0.0.1:9080/hello -v\n```\n\n如果返回结果中出现 CORS 相关的 header，则代表插件生效：\n\n```shell\n...\n< Server: APISIX web server\n< Access-Control-Allow-Origin: *\n< Access-Control-Allow-Methods: *\n< Access-Control-Allow-Headers: *\n< Access-Control-Max-Age: 5\n...\n```\n\n## 删除插件\n\n当你需要禁用 `cors` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:8080\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/csrf.md",
    "content": "---\ntitle: csrf\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 跨站请求伪造攻击\n  - Cross-site request forgery\n  - csrf\ndescription: CSRF 插件基于 Double Submit Cookie 的方式，帮助用户阻止跨站请求伪造攻击。\n\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`csrf` 插件基于 [`Double Submit Cookie`](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Double_Submit_Cookie) 的方式，保护用户的 API 免于 CSRF 攻击。\n\n在此插件运行时，`GET`、`HEAD` 和 `OPTIONS` 会被定义为 `safe-methods`，其他的请求方法则定义为 `unsafe-methods`。因此 `GET`、`HEAD` 和 `OPTIONS` 方法的调用不会被检查拦截。\n\n## 属性\n\n| 名称             | 类型    | 必选项 | 默认值 | 有效值 | 描述         |\n| ---------------- | ------- | ----------- | ------- | ----- |---------------------|\n| name   | string | 否    | `apisix-csrf-token`  |    | 生成的 Cookie 中的 Token 名称，需要使用此名称在请求头携带 Cookie 中的内容。 |\n| expires | number | 否 | `7200` | | CSRF Cookie 的过期时间，单位为秒。当设置为 `0` 时，会忽略 CSRF Cookie 过期时间检查。|\n| key | string | 是 |  |  | 加密 Token 的密钥。        |\n\n注意：schema 中还定义了 `encrypt_fields = {\"key\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用并配置 `csrf` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"uri\": \"/hello\",\n  \"plugins\": {\n    \"csrf\": {\n      \"key\": \"edd1c9f034335f136f87ad84b625c8f1\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  }\n}'\n```\n\n当你使用 `GET` 之外的方法访问被保护的路由时，请求会被拦截并返回 `401` HTTP 状态码。\n\n使用 `GET` 请求 `/hello` 时，在响应中会有一个携带了加密 Token 的 Cookie。Token 字段名称为插件配置中的 `name` 值，默认为 `apisix-csrf-token`。\n\n:::note\n\n每一个请求都会返回一个新的 Cookie。\n\n:::\n\n在后续对该路由进行的 `unsafe-methods` 请求中，需要从 Cookie 中读取加密的 Token，并在请求头中携带该 Token。请求头字段的名称为插件属性中的 `name`。\n\n## 测试插件\n\n启用插件后，使用 `curl` 命令尝试直接对该路由发起 `POST` 请求，会返回 `Unauthorized` 字样的报错提示：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello -X POST\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"error_msg\":\"no csrf token in headers\"}\n```\n\n当发起 `GET` 请求时，返回结果中会有携带 Token 的 Cookie：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nSet-Cookie: apisix-csrf-token=eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ==;path=/;Expires=Mon, 13-Dec-21 09:33:55 GMT\n```\n\n在请求之前，用户需要从 Cookie 中读取 Token，并在后续的 `unsafe-methods` 请求的请求头中携带。\n\n例如，你可以在客户端使用 [js-cookie](https://github.com/js-cookie/js-cookie) 读取 Cookie，使用 [axios](https://github.com/axios/axios) 发送请求：\n\n```js\nconst token = Cookie.get('apisix-csrf-token');\n\nconst instance = axios.create({\n  headers: {'apisix-csrf-token': token}\n});\n```\n\n使用 `curl` 命令发送请求，确保请求中携带了 Cookie 信息，如果返回 `200` HTTP 状态码则表示请求成功：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello -X POST -H 'apisix-csrf-token: eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ==' -b 'apisix-csrf-token=eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ=='\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"uri\": \"/hello\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:1980\": 1\n    }\n  }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/datadog.md",
    "content": "---\ntitle: datadog\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 简介\n\n`datadog` 是 Apache APISIX 内置的监控插件，可与 [Datadog](https://www.datadoghq.com/)（云应用最常用的监控和可观测性平台之一）无缝集成。`datadog` 插件支持对每个请求和响应周期进行多种指标参数的获取，这些指标参数基本反映了系统的行为和健康状况。\n\n`datadog` 插件通过 UDP 协议将其自定义指标推送给 DogStatsD 服务器，该服务器通过 UDP 连接与 Datadog Agent 捆绑在一起（关于如何安装 Datadog Agent，请参考[Agent](https://docs.datadoghq.com/agent/) ）。DogStatsD 基本上是 StatsD 协议的实现，它为 Apache APISIX Agent 收集自定义指标，并将其聚合成单个数据点，发送到配置的 Datadog 服务器。更多关于 DogStatsD 的信息，请参考 [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent) 。\n\n`datadog` 插件具有将多个指标参数组成一个批处理统一推送给外部 Datadog Agent 的能力，并且可以重复使用同一个数据包套接字。\n\n此功能可以有效解决日志数据发送不及时的问题。在创建批处理器之后，如果对 `inactive_timeout` 参数进行配置，那么批处理器会在配置好的时间内自动发送日志数据。如果不进行配置，时间默认为 5s。\n\n关于 Apache APISIX 的批处理程序的更多信息，请参考 [Batch-Processor](../batch-processor.md#配置)\n\n## APISIX-Datadog plugin 工作原理\n\n![APISIX-Datadog 插件架构图](https://static.apiseven.com/202108/1636685752757-d02d8305-2a68-4b3e-b2cc-9e5410c8bf11.png)\n\nAPISIX-Datadog 插件将其自定义指标推送到 DogStatsD server。而 DogStatsD server 通过 UDP 连接与 Datadog agent 捆绑在一起。DogStatsD 是 StatsD 协议的一个实现。它为 Apache APISIX agent 收集自定义指标，将其聚合成一个数据点，并将其发送到配置的 Datadog server。要了解更多关于 DogStatsD 的信息，请访问 DogStatsD 文档。\n\n当你启用 APISIX-Datadog 插件时，Apache APISIX agent 会在每个请求响应周期向 DogStatsD server 输出以下指标：\n\n| 参数名称         | StatsD 类型 | 描述                                                       |\n| ---------------- | ----------- | ---------------------------------------------------------- |\n| Request Counter  | Counter     | 收到的请求数量。                                           |\n| Request Latency  | Histogram   | 处理该请求所需的时间，以毫秒为单位。                       |\n| Upstream latency | Histogram   | 上游 server agent 请求到收到响应所需的时间，以毫秒为单位。 |\n| APISIX Latency   | Histogram   | APISIX agent 处理该请求的时间，以毫秒为单位。              |\n| Ingress Size     | Timer       | 请求体大小，以字节为单位。                                 |\n| Egress Size      | Timer       | 响应体大小，以字节为单位。                                 |\n\n这些指标将被发送到 DogStatsD agent，并带有以下标签。如果任何特定的标签没有合适的值，该标签将被直接省略。\n\n| 参数名称        | 描述                                                         |\n| --------------- | ------------------------------------------------------------ |\n| route_name      | 路由的名称，如果不存在，将显示路由 ID。                      |\n| service_id      | 如果一个路由是用服务的抽象概念创建的，那么特定的服务 ID 将被使用。 |\n| consumer        | 如果路由有一个链接的消费者，消费者的用户名将被添加为一个标签。 |\n| balancer_ip     | 处理了当前请求的上游复制均衡器的的 IP。                      |\n| response_status | HTTP 响应状态代码。                                          |\n| scheme          | 已用于提出请求的协议，如 HTTP、gRPC、gRPCs 等。              |\n\nAPISIX-Datadog 插件维护了一个带有 timer 的 buffer。当 timer 失效时，APISIX-Datadog 插件会将 buffer 的指标作为一个批量处理程序传送给本地运行的 DogStatsD server。这种方法通过重复使用相同的 UDP 套接字，对资源的占用较少，而且由于可以配置 timer，所以不会一直让网络过载。\n\n## 如何使用插件\n\n### 前提：Datadog Agent\n\n1. 首先你必须在系统中安装一个 Datadog agent。它可以是一个 docker 容器，一个 pod 或是一个二进制的包管理器。你只需要确保 Apache APISIX agent 可以到达 Datadog agent 的 8125 端口。\n2. 如果你从没使用过 Datadog\n   1. 首先访问 [www.datadoghq.com](http://www.datadoghq.com/) ，创建一个账户。\n   2. 然后按照下图标注的步骤生成 API 密钥。 ![Generate an API Key](https://static.apiseven.com/202108/1636685007445-05f134fd-e80a-4173-b1d7-f0a118087998.png)\n3. APISIX-Datadog 插件只需要依赖 `datadog/agent` 的 dogstatsd 组件即可实现，因为该插件按照 statsd 协议通过标准的 UDP 套接字向 DogStatsD server 异步发送参数。我们推荐使用独立的 `datadog/dogstatsd` 镜像，而不是使用完整的`datadog/agent` ，因为 `datadog/dogstatsd` 的组件大小只有大约 11 MB，更加轻量化。而完整的 `datadog/agent` 镜像的大小为 2.8 GB。\n\n运行以下命令，将它作为一个容器来运行：\n\n```shell\n# pull the latest image\ndocker pull datadog/dogstatsd:latest\n# run a detached container\ndocker run -d --name dogstatsd-agent -e DD_API_KEY=<Your API Key from step 2> -p 8125:8125/udp datadog/dogstatsd\n```\n\n如果你在生产环境中使用 Kubernetes，你可以将 `dogstatsd` 作为一个 `Daemonset` 或 `Multi-Container Pod` 与 Apache APISIX agent 一起部署。\n\n### 启用插件\n\n本小节介绍了如何在指定路由上启用 `datadog` 插件。进行以下操作之前请确认您的 Datadog Agent 已经启动并正常运行。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"datadog\": {}\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n现在，任何对 uri `/hello` 的请求都会生成上述指标，并推送到 Datadog Agent 的 DogStatsD 服务器。\n\n### 删除插件\n\n删除插件配置中相应的 JSON 配置以禁用 `datadog`。\nAPISIX 插件是支持热加载的，所以不用重新启动 APISIX，配置就能生效。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n### 补充：自定义配置\n\n在默认配置中，`datadog` 插件希望 Dogstatsd 服务在 `127.0.0.1:8125` 可用。如果你想更新配置，请更新插件的元数据。如果想要了解更多关于 `datadog` 插件元数据的字段，请参阅[元数据](#元数据)。\n\n向 `/apisix/admin/plugin_metadata/datadog` 发起请求，更改其元数据。操作示例如下：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/datadog -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"host\": \"172.168.45.29\",\n    \"port\": 8126,\n    \"constant_tags\": [\n        \"source:apisix\",\n        \"service:custom\"\n    ],\n    \"namespace\": \"apisix\"\n}'\n```\n\n上述命令将会更新元数据，后续各指标将通过 UDP StatsD 推送到 `172.168.45.29:8126` 上对应的服务，并且配置将被热加载，不需要重新启动 APISIX 实例，就可以使配置生效。\n\n如果你想把 `datadog` 插件的元数据 schema 恢复到默认值，只需向同一个服务地址再发出一个 Body 为空的 PUT 请求。示例如下：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/datadog \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '{}'\n```\n\n## 配置属性\n\n### 属性\n\n| 名称             | 类型   | 必选项  | 默认值      | 有效值       | 描述                                                                                |\n| -----------      | ------ | -----------  | -------      | -----       | ------------------------------------------------------------                               |\n| prefer_name      | boolean | optional    | true         | true/false  | 如果设置为 `false`，将使用路由/服务的 id 值作为插件的 `route_name`，而不是带有参数的标签名称。   |\n\n该插件支持使用批处理程序来聚集和处理条目（日志/数据）的批次。这就避免了插件频繁地提交数据，默认情况下，批处理程序每 `5` 秒或当队列中的数据达到 `1000` 时提交数据。有关信息或自定义批处理程序的参数设置，请参阅[批处理程序](../batch-processor.md#configuration) 配置部分。\n\n### 元数据\n\n| 名称        | 类型    | 必选项 |     默认值        | 描述                                                            |\n| ----------- | ------  | ----------- |      -------       | ---------------------------------------------------------------------- |\n| host        | string  | optional    |  \"127.0.0.1\"       | DogStatsD 服务器的主机地址                                      |\n| port        | integer | optional    |    8125            | DogStatsD 服务器的主机端口                                         |\n| namespace   | string  | optional    |    \"apisix\"        | 由 APISIX 代理发送的所有自定义参数的前缀。对寻找指标图的实体很有帮助，例如：apisix.request.counter。                                        |\n| constant_tags | array | optional    | [ \"source:apisix\" ] | 静态标签嵌入到生成的指标中。这对某些信号的度量进行分组很有用。 |\n\n要了解更多关于如何有效地编写标签，请访问[这里](https://docs.datadoghq.com/getting_started/tagging/#defining-tags)\n\n### 输出指标\n\n启用 datadog 插件之后，APISIX 就会按照下面的指标格式，将数据整理成数据包最终发送到 DogStatsD server。\n\n| Metric Name               | StatsD Type   | Description               |\n| -----------               | -----------   | -------                   |\n| Request Counter           | Counter       | 收到的请求数量。   |\n| Request Latency           | Histogram     | 处理该请求所需的时间（以毫秒为单位）。 |\n| Upstream latency          | Histogram     | 代理请求到上游服务器直到收到响应所需的时间（以毫秒为单位）。 |\n| APISIX Latency            | Histogram     | APISIX 代理处理该请求的时间（以毫秒为单位）。|\n| Ingress Size              | Timer         | 以字节为单位的请求体大小。 |\n| Egress Size               | Timer         | 以字节为单位的响应体大小。 |\n\n这些指标会带有以下标签，并首先被发送到本地 DogStatsD Agent。\n\n> 如果一个标签没有合适的值，该标签将被直接省略。\n\n- **route_name**：在路由模式定义中指定的名称，如果不存在或插件属性 `prefer_name` 被设置为 `false`，它将默认使用路由/服务的 id 值。\n- **service_name**：如果一个路由是用服务的抽象概念创建的，特定的服务 name/id（基于插件的 `prefer_name` 属性）将被使用。\n- **consumer**：如果路由有一个正在链接中的消费者，那么消费者的用户名将被添加为一个标签。\n- **balancer_ip**：处理当前请求的上游负载均衡器的 IP。\n- **response_status**：HTTP 响应状态代码。\n- **scheme**：已用于提出请求的协议，如 HTTP、gRPC、gRPCs 等。\n"
  },
  {
    "path": "docs/zh/latest/plugins/dubbo-proxy.md",
    "content": "---\ntitle: dubbo-proxy\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`dubbo-proxy` 插件允许将 `HTTP` 请求代理到 [**dubbo**](http://dubbo.apache.org)。\n\n## 要求\n\n如果你正在使用 `OpenResty`, 你需要编译它来支持 `dubbo`, 参考 [APISIX-Runtime](../FAQ.md#如何构建-apisix-runtime-环境)。\n\n## 运行时属性\n\n| 名称       | 类型 | 必选项 | 默认值  | 有效值       | 描述                                                          |\n| ------------ | ------ | ----------- | -------- | ------------ | -------------------------------------------------------------------- |\n| service_name    | string | 必选  |          |              | dubbo 服务名字 |\n| service_version | string | 必选    |          |              | dubbo 服务版本 |\n| method          | string | 可选    | uri 路径 |     | dubbo 服务方法 |\n\n## 静态属性\n\n| 名称       | 类型   | 必选项 | 默认值 | 有效值        | 描述                                                        |\n| ------------ | ------ | ----------- | -------- | ------------ | -------------------------------------------------------------------- |\n| upstream_multiplex_count | number | 必选    | 32        | >= 1 | 上游连接中最大的多路复用请求数 |\n\n## 如何启用\n\n首先，在 `config.yaml` 中启用 `dubbo-proxy` 插件：\n\n```\n# Add this in config.yaml\nplugins:\n  - ... # plugin you need\n  - dubbo-proxy\n```\n\n然后重载 `APISIX`。\n\n这里有个例子，在指定的路由中启用 `dubbo-proxy` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/upstreams/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"nodes\": {\n        \"127.0.0.1:20880\": 1\n    },\n    \"type\": \"roundrobin\"\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uris\": [\n        \"/hello\"\n    ],\n    \"plugins\": {\n        \"dubbo-proxy\": {\n            \"service_name\": \"org.apache.dubbo.sample.tengine.DemoService\",\n            \"service_version\": \"0.0.0\",\n            \"method\": \"tengineDubbo\"\n        }\n    },\n    \"upstream_id\": 1\n}'\n```\n\n## 测试插件\n\n你可以在 `Tengine` 提供的 [快速开始](https://github.com/alibaba/tengine/tree/master/modules/mod_dubbo#quick-start) 例子中使用上述配置进行测试。\n\n将会有同样的结果。\n\n从上游 `dubbo` 服务返回的数据一定是 `Map<String, String>` 类型。\n\n如果返回的数据如下\n\n```json\n{\n    \"status\": \"200\",\n    \"header1\": \"value1\",\n    \"header2\": \"valu2\",\n    \"body\": \"blahblah\"\n}\n```\n\n则对应的 `HTTP` 响应如下\n\n```http\nHTTP/1.1 200 OK # \"status\" will be the status code\n...\nheader1: value1\nheader2: value2\n...\n\nblahblah # \"body\" will be the body\n```\n\n## 删除插件\n\n当你想在某个路由或服务中禁用 `dubbo-proxy` 插件，非常简单，你可以直接删除插件配置中的 `json` 配置，不需要重启服务就能立即生效：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uris\": [\n        \"/hello\"\n    ],\n    \"plugins\": {\n    },\n    \"upstream_id\": 1\n    }\n}'\n```\n\n现在 `dubbo-proxy` 插件就已经被禁用了。此方法同样适用于其他插件。\n\n如果你想彻底禁用 `dubbo-proxy` 插件，\n你需要在 `config.yaml` 中注释掉以下内容：\n\n```yaml\nplugins:\n  - ... # plugin you need\n  #- dubbo-proxy\n```\n\n然后重新加载 `APISIX`。\n"
  },
  {
    "path": "docs/zh/latest/plugins/echo.md",
    "content": "---\ntitle: echo\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Echo\ndescription: 本文介绍了关于 Apache APISIX `echo` 插件的基本信息及使用方法。\n---\n\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`echo` 插件可以帮助用户尽可能地全面了解如何开发 APISIX 插件。\n\n该插件展示了如何在常见的 `phase` 中实现相应的功能，常见的 `phase` 包括：init, rewrite, access, balancer, header filter, body filter 以及 log。\n\n:::caution WARNING\n\n`echo` 插件只能用作示例，并不能处理一些特别的场景。**请勿将该插件用在生产环境中！**\n\n:::\n\n## 属性\n\n| 名称        | 类型   | 必选项  |  描述                                                                                            |\n| ----------- | ------ | ------ | ----------------------------------------------------------------------------------------------- |\n| before_body | string | 否     | 在 `body` 属性之前添加的内容，如果 `body` 属性没有指定，就会将其添加在上游 `response body` 之前。 |\n| body        | string | 否     | 返回给客户端的响应内容，它将覆盖上游返回的响应 `body`。                                        |\n| after_body  | string | 否     | 在 `body` 属性之后添加的内容，如果 body 属性没有指定将在上游响应 `body` 之后添加。              |\n| headers     | object | 否     | 返回值的 headers。                                                                                |\n\n:::note\n\n参数 `before_body`、`body` 和 `after_body` 至少要配置一个。\n\n:::\n\n## 启用插件\n\n以下示例展示了如何在指定路由中启用 `echo` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"echo\": {\n            \"before_body\": \"before the body modification \"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，你可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nbefore the body modification hello world\n```\n\n## 删除插件\n\n当你需要禁用 `echo` 插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/elasticsearch-logger.md",
    "content": "---\ntitle: elasticsearch-logger\nkeywords:\n  - APISIX\n  - API 网关\n  - 插件\n  - Elasticsearch-logger\n  - 日志\ndescription: elasticsearch-logger Plugin 将请求和响应日志批量推送到 Elasticsearch，并支持日志格式的自定义。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/elasticsearch-logger\" />\n</head>\n\n## 描述\n\n`elasticsearch-logger` 插件将请求和响应日志批量推送到 [Elasticsearch](https://www.elastic.co)，并支持自定义日志格式。启用后，插件会将请求上下文信息序列化为 [Elasticsearch Bulk 格式](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html#docs-bulk) 并将其添加到队列中，然后再推送到 Elasticsearch。有关更多详细信息，请参阅 [批处理器](../batch-processor.md)。\n\n## 属性\n\n| 名称          | 类型    | 必选项 | 默认值               | 描述                                                         |\n| ------------- | ------- | -------- | -------------------- | ------------------------------------------------------------ |\n| endup_addrs | array[string] | 是 | | Elasticsearch API 端点地址。如果配置了多个端点，则会随机写入。 |\n| field | object | 是 | | Elasticsearch `field` 配置。 |\n| field.index | string | 是 | | Elasticsearch [_index 字段](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html#mapping-index-field)。 |\n| log_format | object | 否 | | 自定义日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过 `$` 前缀引用 [APISIX](../apisix-variable.md) 或 [NGINX 变量](http://nginx.org/en/docs/varindex.html)。 |\n| auth | array | 否 | | Elasticsearch [身份验证](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html) 配置。 |\n| auth.username | string | 是 | | Elasticsearch [身份验证](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html) 用户名​​。 |\n| auth.password | string | 是 | | Elasticsearch [身份验证](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html) 密码。 |\n| headers | object | 否 | | 自定义请求标头，以键值对形式配置。例如 `{\"Authorization\": \"Bearer token\", \"X-API-Key\": \"key\"}`。 |\n| ssl_verify | boolean | 否 | true | 如果为 true，则执行 SSL 验证。 |\n| timeout | integer | 否 | 10 | Elasticsearch 发送数据超时（秒）。 |\n| include_req_body | boolean | 否 | false |如果为 true，则将请求主体包含在日志中。请注意，如果请求主体太大而无法保存在内存中，则由于 NGINX 的限制而无法记录。|\n| include_req_body_expr | array[array] | 否 | | 一个或多个条件的数组，形式为 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。在 `include_req_body` 为 true 时使用。仅当此处配置的表达式计算结果为 true 时，才会记录请求主体。|\n| include_resp_body | boolean | 否 | false | 如果为 true，则将响应主体包含在日志中。|\n| include_resp_body_expr | array[array] | 否 | | 一个或多个条件的数组，形式为 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。在 `include_resp_body` 为 true 时使用。仅当此处配置的表达式计算结果为 true 时，才会记录响应主体。|\n\n注意：schema 中还定义了 `encrypt_fields = {\"auth.password\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n本插件支持使用批处理器来聚合并批量处理条目（日志和数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解或自定义批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置) 配置部分。\n\n## Plugin Metadata\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| log_format | object | 否 |  | 自定义日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过 `$` 前缀引用 [APISIX 变量](../apisix-variable.md) 和 [NGINX 变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `elasticsearch-logger` 插件。\n\n要遵循示例，请在 Docker 中启动 Elasticsearch 实例：\n\n```shell\ndocker run -d \\\n  --name elasticsearch \\\n  --network apisix-quickstart-net \\\n  -v elasticsearch_vol:/usr/share/elasticsearch/data/ \\\n  -p 9200:9200 \\\n  -p 9300:9300 \\\n  -e ES_JAVA_OPTS=\"-Xms512m -Xmx512m\" \\\n  -e discovery.type=single-node \\\n  -e xpack.security.enabled=false \\\n  docker.elastic.co/elasticsearch/elasticsearch:7.17.1\n```\n\n在 Docker 中启动 Kibana 实例，以可视化 Elasticsearch 中的索引数据：\n\n```shell\ndocker run -d \\\n  --name kibana \\\n  --network apisix-quickstart-net \\\n  -p 5601:5601 \\\n  -e ELASTICSEARCH_HOSTS=\"http://elasticsearch:9200\" \\\n  docker.elastic.co/kibana/kibana:7.17.1\n```\n\n如果成功，您应该在 [localhost:5601](http://localhost:5601) 上看到 Kibana 仪表板。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 以默认日志格式记录\n\n以下示例演示如何在路由上启用 `elasticsearch-logger` 插件，该插件记录客户端对路由的请求和响应，并将日志推送到 Elasticsearch。\n\n使用 `elasticsearch-logger` 创建路由，将 `index` 字段配置为 `gateway`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"elasticsearch-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"elasticsearch-logger\": {\n        \"endpoint_addrs\": [\"http://elasticsearch:9200\"],\n        \"field\": {\n          \"index\": \"gateway\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n向路由发送请求以生成日志条目：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [localhost:5601](http://localhost:5601) 上的 Kibana 仪表板，并在 __Discover__ 选项卡下创建一个新的索引模式 `gateway` 以从 Elasticsearch 获取数据。配置完成后，导航回 __Discover__ 选项卡，您应该会看到生成的日志，类似于以下内容：\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"CE-JL5QBOkdYRG7kEjTJ\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"request\": {\n      \"headers\": {\n        \"host\": \"127.0.0.1:9080\",\n        \"accept\": \"*/*\",\n        \"user-agent\": \"curl/8.6.0\"\n      },\n      \"size\": 85,\n      \"querystring\": {},\n      \"method\": \"GET\",\n      \"url\": \"http://127.0.0.1:9080/anything\",\n      \"uri\": \"/anything\"\n    },\n    \"response\": {\n      \"headers\": {\n        \"content-type\": \"application/json\",\n        \"access-control-allow-credentials\": \"true\",\n        \"server\": \"APISIX/3.11.0\",\n        \"content-length\": \"390\",\n        \"access-control-allow-origin\": \"*\",\n        \"connection\": \"close\",\n        \"date\": \"Mon, 13 Jan 2025 10:18:14 GMT\"\n      },\n      \"status\": 200,\n      \"size\": 618\n    },\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"latency\": 585.00003814697,\n    \"apisix_latency\": 18.000038146973,\n    \"upstream_latency\": 567,\n    \"upstream\": \"50.19.58.113:80\",\n    \"server\": {\n      \"hostname\": \"0b9a772e68f8\",\n      \"version\": \"3.11.0\"\n    },\n    \"service_id\": \"\",\n    \"client_ip\": \"192.168.65.1\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n### 使用 Plugin Metadata 记录请求和响应标头\n\n以下示例演示了如何使用 [Plugin Metadata](../terminology/plugin-metadata.md) 和 [NGINX 变量](http://nginx.org/en/docs/varindex.html) 自定义日志格式，以记录请求和响应中的特定标头。\n\n在 APISIX 中，[Plugin Metadata](../terminology/plugin-metadata.md) 用于配置同一插件的所有插件实例的通用元数据字段。当插件在多个资源中启用并需要对其元数据字段进行通用更新时，它很有用。\n\n首先，使用 `elasticsearch-logger` 创建路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"elasticsearch-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"elasticsearch-logger\": {\n        \"endpoint_addrs\": [\"http://elasticsearch:9200\"],\n        \"field\": {\n          \"index\": \"gateway\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n接下来，配置 `elasticsearch-logger` 的 Plugin Metadata：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/elasticsearch-logger\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"log_format\": {\n      \"host\": \"$host\",\n      \"@timestamp\": \"$time_iso8601\",\n      \"client_ip\": \"$remote_addr\",\n      \"env\": \"$http_env\",\n      \"resp_content_type\": \"$sent_http_Content_Type\"\n    }\n  }'\n```\n\n使用 `env` 标头向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"env: dev\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [localhost:5601](http://localhost:5601) 上的 Kibana 仪表板，并在 __Discover__ 选项卡下创建一个新的索引模式 `gateway` 以从 Elasticsearch 获取数据（如果您尚未这样做）。配置完成后，导航回 __Discover__ 选项卡，您应该会看到生成的日志，类似于以下内容：\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"Ck-WL5QBOkdYRG7kODS0\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"client_ip\": \"192.168.65.1\",\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"@timestamp\": \"2025-01-06T10:32:36+00:00\",\n    \"host\": \"127.0.0.1\",\n    \"resp_content_type\": \"application/json\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n### 有条件地记录请求主体\n\n以下示例演示了如何有条件地记录请求主体。\n\n使用 `elasticsearch-logger` 创建路由，仅在 URL 查询字符串 `log_body` 为 `true` 时记录请求主体：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"elasticsearch-logger\": {\n        \"endpoint_addrs\": [\"http://elasticsearch:9200\"],\n        \"field\": {\n          \"index\": \"gateway\"\n        },\n        \"include_req_body\": true,\n        \"include_req_body_expr\": [[\"arg_log_body\", \"==\", \"yes\"]]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    },\n  \"uri\": \"/anything\",\n  \"id\": \"elasticsearch-logger-route\"\n}'\n```\n\n使用满足以下条件的 URL 查询字符串向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?log_body=yes\" -X POST -d '{\"env\": \"dev\"}'\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [localhost:5601](http://localhost:5601) 上的 Kibana 仪表板，并在 __Discover__ 选项卡下创建一个新的索引模式 `gateway` 以从 Elasticsearch 获取数据（如果您尚未这样做）。配置完成后，导航回 __Discover__ 选项卡，您应该会看到生成的日志，类似于以下内容：\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"Dk-cL5QBOkdYRG7k7DSW\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"request\": {\n      \"headers\": {\n        \"user-agent\": \"curl/8.6.0\",\n        \"accept\": \"*/*\",\n        \"content-length\": \"14\",\n        \"host\": \"127.0.0.1:9080\",\n        \"content-type\": \"application/x-www-form-urlencoded\"\n      },\n      \"size\": 182,\n      \"querystring\": {\n        \"log_body\": \"yes\"\n      },\n      \"body\": \"{\\\"env\\\": \\\"dev\\\"}\",\n      \"method\": \"POST\",\n      \"url\": \"http://127.0.0.1:9080/anything?log_body=yes\",\n      \"uri\": \"/anything?log_body=yes\"\n    },\n    \"start_time\": 1735965595203,\n    \"response\": {\n      \"headers\": {\n        \"content-type\": \"application/json\",\n        \"server\": \"APISIX/3.11.0\",\n        \"access-control-allow-credentials\": \"true\",\n        \"content-length\": \"548\",\n        \"access-control-allow-origin\": \"*\",\n        \"connection\": \"close\",\n        \"date\": \"Mon, 13 Jan 2025 11:02:32 GMT\"\n      },\n      \"status\": 200,\n      \"size\": 776\n    },\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"latency\": 703.9999961853,\n    \"apisix_latency\": 34.999996185303,\n    \"upstream_latency\": 669,\n    \"upstream\": \"34.197.122.172:80\",\n    \"server\": {\n      \"hostname\": \"0b9a772e68f8\",\n      \"version\": \"3.11.0\"\n    },\n    \"service_id\": \"\",\n    \"client_ip\": \"192.168.65.1\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n向路由发送一个没有任何 URL 查询字符串的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST -d '{\"env\": \"dev\"}'\n```\n\n导航到 Kibana 仪表板 __Discover__ 选项卡，您应该看到生成的日志，但没有请求正文：\n\n```json\n{\n  \"_index\": \"gateway\",\n  \"_id\": \"EU-eL5QBOkdYRG7kUDST\",\n  \"_version\": 1,\n  \"_score\": 1,\n  \"_source\": {\n    \"request\": {\n      \"headers\": {\n        \"content-type\": \"application/x-www-form-urlencoded\",\n        \"accept\": \"*/*\",\n        \"content-length\": \"14\",\n        \"host\": \"127.0.0.1:9080\",\n        \"user-agent\": \"curl/8.6.0\"\n      },\n      \"size\": 169,\n      \"querystring\": {},\n      \"method\": \"POST\",\n      \"url\": \"http://127.0.0.1:9080/anything\",\n      \"uri\": \"/anything\"\n    },\n    \"start_time\": 1735965686363,\n    \"response\": {\n      \"headers\": {\n        \"content-type\": \"application/json\",\n        \"access-control-allow-credentials\": \"true\",\n        \"server\": \"APISIX/3.11.0\",\n        \"content-length\": \"510\",\n        \"access-control-allow-origin\": \"*\",\n        \"connection\": \"close\",\n        \"date\": \"Mon, 13 Jan 2025 11:15:54 GMT\"\n      },\n      \"status\": 200,\n      \"size\": 738\n    },\n    \"route_id\": \"elasticsearch-logger-route\",\n    \"latency\": 680.99999427795,\n    \"apisix_latency\": 4.9999942779541,\n    \"upstream_latency\": 676,\n    \"upstream\": \"34.197.122.172:80\",\n    \"server\": {\n      \"hostname\": \"0b9a772e68f8\",\n      \"version\": \"3.11.0\"\n    },\n    \"service_id\": \"\",\n    \"client_ip\": \"192.168.65.1\"\n  },\n  \"fields\": {\n    ...\n  }\n}\n```\n\n:::info\n\n如果您除了将 `include_req_body` 或 `include_resp_body` 设置为 `true` 之外还自定义了 `log_format`，则插件不会在日志中包含正文。\n\n作为一种解决方法，您可以在日志格式中使用 NGINX 变量 `$request_body`，例如：\n\n```json\n{\n  \"elasticsearch-logger\": {\n    ...,\n    \"log_format\": {\"body\": \"$request_body\"}\n  }\n}\n```\n\n:::\n"
  },
  {
    "path": "docs/zh/latest/plugins/error-log-logger.md",
    "content": "---\ntitle: error-log-logger\nkeywords:\n  - APISIX\n  - API 网关\n  - 错误日志\n  - Plugin\ndescription: API 网关 Apache APISIX error-log-logger 插件用于将 APISIX 的错误日志推送到 TCP、Apache SkyWalking、Apache Kafka 或 ClickHouse 服务器。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`error-log-logger` 插件用于将 APISIX 的错误日志 (`error.log`) 推送到 TCP、Apache SkyWalking、Apache Kafka 或 ClickHouse 服务器，你还可以设置错误日志级别以将日志发送到服务器。\n\n## 属性\n\n| 名称                              | 类型    | 必选项 | 默认值                         | 有效值         | 描述                                                                             |\n| -------------------------------- | ------- | ------ | ------------------------------ | ------------- | -------------------------------------------------------------------------------- |\n| tcp.host                         | string  | 是     |                                |               | TCP 服务的 IP 地址或主机名。                                                      |\n| tcp.port                         | integer | 是     |                                | [0,...]       | 目标端口。                                                                        |\n| tcp.tls                          | boolean | 否     | false                          | [false, true] | 当设置为 `true` 时执行 SSL 验证。                                                |\n| tcp.tls_server_name              | string  | 否     |                                |               | TLS 服务名称标记。                                                                 |\n| skywalking.endpoint_addr         | string  | 否     | http://127.0.0.1:12900/v3/logs |               | SkyWalking 的 HTTP endpoint 地址，例如：http://127.0.0.1:12800。                   |\n| skywalking.service_name          | string  | 否     | APISIX                         |               | SkyWalking 上报的 service 名称。                                                   |\n| skywalking.service_instance_name | String  | 否     | APISIX Instance Name           |               | SkyWalking 上报的 service 实例名，如果希望直接获取本机主机名请设置为 `$hostname`。   |\n| clickhouse.endpoint_addr         | String  | 否     | http://127.0.0.1:8213          |               | ClickHouse 的 HTTP endpoint 地址，例如 `http://127.0.0.1:8213`。                   |\n| clickhouse.user                  | String  | 否     | default                        |               | ClickHouse 的用户名。                                                              |\n| clickhouse.password              | String  | 否     |                                |               | ClickHouse 的密码。                                                                |\n| clickhouse.database              | String  | 否     |                                |               | ClickHouse 的用于接收日志的数据库。                                                |\n| clickhouse.logtable              | String  | 否     |                                |               | ClickHouse 的用于接收日志的表。                                                    |\n| kafka.brokers                    | array   | 是     |                                |               | 需要推送的 Kafka broker 列表。\t|\n| kafka.brokers.host                  | string  | 是   |                |                       | Kafka broker 的节点 host 配置，例如 `192.168.1.1`|\n| kafka.brokers.port                  | string  | 是   |                |                       | Kafka broker 的节点端口配置  |\n| kafka.brokers.sasl_config           | object  | 否   |                |                       | Kafka broker 中的 sasl_config |\n| kafka.brokers.sasl_config.mechanism | string  | 否   | \"PLAIN\"          | [\"PLAIN\"]  | Kafka broker 中的 sasl 认证机制 |\n| kafka.brokers.sasl_config.user      | string  | 是   |                  |             | Kafka broker 中 sasl 配置中的 user，如果 sasl_config 存在，则必须填写 |\n| kafka.brokers.sasl_config.password  | string  | 是   |                  |             | Kafka broker 中 sasl 配置中的 password，如果 sasl_config 存在，则必须填写 |\n| kafka.kafka_topic                   | string  | 是   |                |                       | 需要推送的 Kafka topic。|\n| kafka.producer_type                 | string  | 否   | async          | [\"async\", \"sync\"]     | 生产者发送消息的模式。|\n| kafka.required_acks                 | integer | 否   | 1              | [0, 1, -1]            | 生产者在确认一个请求发送完成之前需要收到的反馈信息的数量。该参数是为了保证发送请求的可靠性。该属性的配置与 Kafka `acks` 属性相同，具体配置请参考 [Apache Kafka 文档](https://kafka.apache.org/documentation/#producerconfigs_acks)。 |\n| kafka.key                           | string  | 否   |                |                       | 用于消息分区而分配的密钥。 |\n| kafka.cluster_name           | integer | 否     | 1              | [0,...]               | Kafka 集群的名称，当有两个及以上 Kafka 集群时使用。只有当 `producer_type` 设为 `async` 模式时才可以使用该属性。|\n| kafka.meta_refresh_interval | integer | 否 | 30 | [1,...] | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的 `refresh_interval` 参数，用于指定自动刷新 metadata 的间隔时长，单位为秒。 |\n| timeout                          | integer | 否     | 3                              | [1,...]       | 连接和发送数据超时间，以秒为单位。                                                   |\n| keepalive                        | integer | 否     | 30                             | [1,...]       | 复用连接时，连接保持的时间，以秒为单位。                                             |\n| level                            | string  | 否     | WARN                           |               | 进行错误日志筛选的级别，默认为 `WARN`，取值 [\"STDERR\", \"EMERG\", \"ALERT\", \"CRIT\", \"ERR\", \"ERROR\", \"WARN\", \"NOTICE\", \"INFO\", \"DEBUG\"]，其中 `ERR` 与 `ERROR` 级别一致。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"clickhouse.password\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n本插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解或自定义批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置) 配置部分。\n\n### 默认日志格式示例\n\n```text\n[\"2024/01/06 16:04:30 [warn] 11786#9692271: *1 [lua] plugin.lua:205: load(): new plugins: {\"error-log-logger\":true}, context: init_worker_by_lua*\",\"\\n\",\"2024/01/06 16:04:30 [warn] 11786#9692271: *1 [lua] plugin.lua:255: load_stream(): new plugins: {\"limit-conn\":true,\"ip-restriction\":true,\"syslog\":true,\"mqtt-proxy\":true}, context: init_worker_by_lua*\",\"\\n\"]\n```\n\n## 启用插件\n\n该插件默认为禁用状态，你可以在 `./conf/config.yaml` 中启用 `error-log-logger` 插件。你可以参考如下示例启用插件：\n\n```yaml title=\"./conf/config.yaml\"\nplugins:                          # plugin list\n  ......\n  - request-id\n  - hmac-auth\n  - api-breaker\n  - error-log-logger              # enable plugin `error-log-logger\n```\n\n完成插件配置后，你需要重新加载 APISIX，插件才会生效。\n\n:::note 注意\n\n该插件属于 APISIX 全局性插件，不需要在任何路由或服务中绑定。\n\n:::\n\n### 配置 TCP 服务器地址\n\n你可以通过配置插件元数据来设置 TCP 服务器地址，如下所示：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"tcp\": {\n    \"host\": \"127.0.0.1\",\n    \"port\": 1999\n  },\n  \"inactive_timeout\": 1\n}'\n```\n\n### 配置 SkyWalking OAP 服务器地址\n\n通过以下配置插件元数据设置 SkyWalking OAP 服务器地址，如下所示：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"skywalking\": {\n    \"endpoint_addr\": \"http://127.0.0.1:12800/v3/logs\"\n  },\n  \"inactive_timeout\": 1\n}'\n```\n\n### 配置 ClickHouse 数据库\n\n该插件支持将错误日志作为字符串发送到 ClickHouse 服务器中对应表的 `data` 字段。\n\n你可以按照如下方式进行配置：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"clickhouse\": {\n      \"user\": \"default\",\n      \"password\": \"a\",\n      \"database\": \"error_log\",\n      \"logtable\": \"t\",\n      \"endpoint_addr\": \"http://127.0.0.1:8123\"\n  }\n}'\n```\n\n### 配置 Kafka\n\n该插件支持将错误日志发送到 Kafka，你可以按照如下方式进行配置：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/error-log-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n   \"kafka\":{\n      \"brokers\":[\n         {\n            \"host\":\"127.0.0.1\",\n            \"port\":9092\n         }\n      ],\n      \"kafka_topic\":\"test2\"\n   },\n   \"level\":\"ERROR\",\n   \"inactive_timeout\":1\n}'\n```\n\n## 删除插件\n\n当你不再需要该插件时，只需要在 `./conf/config.yaml` 中删除或注释该插件即可。\n\n```yaml\nplugins:                          # plugin list\n  ... ...\n  - request-id\n  - hmac-auth\n  - api-breaker\n  #- error-log-logger              # enable plugin `error-log-logger\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ext-plugin-post-req.md",
    "content": "---\ntitle: ext-plugin-post-req\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - ext-plugin-post-req\ndescription: 本文介绍了关于 Apache APISIX `ext-plugin-post-req` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ext-plugin-post-req` 插件的功能与 `ext-plugin-pre-req` 插件的不同之处在于：`ext-plugin-post-req` 插件是在内置 Lua 插件执行之后且在请求到达上游之前工作。\n\n你可以参考 [ext-plugin-pre-req](./ext-plugin-pre-req.md) 文档，学习如何配置和使用 `ext-plugin-post-req` 插件。\n"
  },
  {
    "path": "docs/zh/latest/plugins/ext-plugin-post-resp.md",
    "content": "---\ntitle: ext-plugin-post-resp\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ext-plugin-post-resp\ndescription: 本文介绍了关于 Apache APISIX `ext-plugin-post-resp` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ext-plugin-post-resp` 插件用于在执行内置 Lua 插件之前和在 Plugin Runner 内运行特定的 External Plugin。\n\n`ext-plugin-post-resp` 插件将在请求获取到上游的响应之后执行。\n\n启用本插件之后，APISIX 将使用 [lua-resty-http](https://github.com/api7/lua-resty-http) 库向上游发起请求，这会导致：\n\n- [proxy-control](./proxy-control.md) 插件不可用\n- [proxy-mirror](./proxy-mirror.md) 插件不可用\n- [proxy-cache](./proxy-cache.md) 插件不可用\n- [APISIX 与上游间的双向认证](../mtls.md#apisix-与上游间的双向认证) 功能尚不可用\n\n如果你想了解更多关于 External Plugin 的信息，请参考 [External Plugin](../external-plugin.md) 。\n\n:::note\n\nExternal Plugin 执行的结果会影响当前请求的响应。\n\n:::\n\n## 属性\n\n| 名称              | 类型    | 必选项 | 默认值  | 有效值                                                           | 描述                                                                              |\n| ----------------- | ------ | ------ | ------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| conf              | array  | 否     |         | [{\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}] | 在 Plugin Runner 内执行的插件列表的配置。                                           |\n| allow_degradation | boolean| 否     | false   | [false, true]                                                    | 当 Plugin Runner 临时不可用时是否允许请求继续，当值设置为 `true` 时则自动允许请求继续。   |\n\n## 启用插件\n\n以下示例展示了如何在指定路由中启用 `ext-plugin-post-resp` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"ext-plugin-post-resp\": {\n            \"conf\" : [\n                {\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}\n            ]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\n在返回结果中可以看到刚刚配置的 Plugin Runner 已经被触发，同时 `ext-plugin-A` 插件也已经被执行。\n\n## 删除插件\n\n当你需要禁用 `ext-plugin-post-resp` 插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ext-plugin-pre-req.md",
    "content": "---\ntitle: ext-plugin-pre-req\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - ext-plugin-pre-req\ndescription: 本文介绍了关于 Apache APISIX `ext-plugin-pre-req` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ext-plugin-pre-req` 插件用于在执行内置 Lua 插件之前和在 Plugin Runner 内运行特定的 External Plugin。\n\n如果你想了解更多关于 External Plugin 的信息，请参考 [External Plugin](../external-plugin.md) 。\n\n:::note\n\nExternal Plugin 执行的结果会影响当前请求的行为。\n\n:::\n\n## 属性\n\n| 名称              | 类型    | 必选项 | 默认值  | 有效值                                                           | 描述                                                                              |\n| ----------------- | ------ | ------ | ------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| conf              | array  | 否     |         | [{\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}] | 在 Plugin Runner 内执行的插件列表的配置。                                           |\n| allow_degradation | boolean| 否     | false   | [false, true]                                                    | 当 Plugin Runner 临时不可用时是否允许请求继续，当值设置为 true 时则自动允许请求继续。   |\n\n## 启用插件\n\n以下示例展示了如何在指定路由中启用 `ext-plugin-pre-req` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"ext-plugin-pre-req\": {\n            \"conf\" : [\n                {\"name\": \"ext-plugin-A\", \"value\": \"{\\\"enable\\\":\\\"feature\\\"}\"}\n            ]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\n在返回结果中可以看到刚刚配置的 Plugin Runner 已经被触发，同时 `ext-plugin-A` 插件也已经被执行。\n\n## 删除插件\n\n当你需要禁用 `ext-plugin-pre-req` 插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/fault-injection.md",
    "content": "---\ntitle: fault-injection\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Fault Injection\n  - fault-injection\ndescription: 本文介绍了关于 Apache APISIX `fault-injection` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`fault-injection` 插件是故障注入插件，该插件可以和其他插件一起使用，并在其他插件执行前被执行。\n\n## 属性\n\n| 名称              | 类型    | 必选项 | 有效值     | 描述                       |\n| ----------------- | ------- | ---- |  ---------- | -------------------------- |\n| abort.http_status | integer | 是   |  [200, ...] | 返回给客户端的 HTTP 状态码 |\n| abort.body        | string  | 否   |             | 返回给客户端的响应数据。支持使用 NGINX 变量，如 `client addr: $remote_addr\\n`|\n| abort.headers     | object  | 否   |            |  返回给客户端的响应头，可以包含 NGINX 变量，如 `$remote_addr` |\n| abort.percentage  | integer | 否   |  [0, 100]   | 将被中断的请求占比         |\n| abort.vars        | array[] | 否   |             | 执行故障注入的规则，当规则匹配通过后才会执行故障注。`vars` 是一个表达式的列表，来自 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)。 |\n| delay.duration    | number  | 是   |             | 延迟时间，可以指定小数     |\n| delay.percentage  | integer | 否   |  [0, 100]   | 将被延迟的请求占比         |\n| delay.vars        | array[] | 否   |             | 执行请求延迟的规则，当规则匹配通过后才会延迟请求。`vars` 是一个表达式列表，来自 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)。   |\n\n:::info IMPORTANT\n\n`abort` 属性将直接返回给客户端指定的响应码并且终止其他插件的执行。\n\n`delay` 属性将延迟某个请求，并且还会执行配置的其他插件。\n\n`abort` 和 `delay` 属性至少要配置一个。\n\n:::\n\n:::tip\n\n`vars` 是由 [`lua-resty-expr`](https://github.com/api7/lua-resty-expr) 的表达式组成的列表，它可以灵活的实现规则之间的 AND/OR 关系，示例如下：：\n\n```json\n[\n    [\n        [ \"arg_name\",\"==\",\"jack\" ],\n        [ \"arg_age\",\"==\",18 ]\n    ],\n    [\n        [ \"arg_name2\",\"==\",\"allen\" ]\n    ]\n]\n```\n\n以上示例表示前两个表达式之间的关系是 AND，而前两个和第三个表达式之间的关系是 OR。\n\n:::\n\n## 启用插件\n\n你可以在指定路由启用 `fault-injection` 插件，并指定 `abort` 属性。如下所示：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"fault-injection\": {\n           \"abort\": {\n              \"http_status\": 200,\n              \"body\": \"Fault Injection!\"\n           }\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n同样，我们也可以指定 `delay` 属性。如下所示：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"fault-injection\": {\n           \"delay\": {\n              \"duration\": 3\n           }\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n还可以同时为指定路由启用 `fault-injection` 插件，并指定 `abort` 属性和 `delay` 属性的 `vars` 规则。如下所示：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"fault-injection\": {\n            \"abort\": {\n                \"http_status\": 403,\n                \"body\": \"Fault Injection!\\n\",\n                \"vars\": [\n                    [\n                        [ \"arg_name\",\"==\",\"jack\" ]\n                    ]\n                ]\n            },\n            \"delay\": {\n                \"duration\": 2,\n                \"vars\": [\n                    [\n                        [ \"http_age\",\"==\",\"18\" ]\n                    ]\n                ]\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n通过上述示例启用插件后，可以向路由发起如下请求：\n\n```shell\ncurl http://127.0.0.1:9080/hello -i\n```\n\n```shell\nHTTP/1.1 200 OK\nDate: Mon, 13 Jan 2020 13:50:04 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\nFault Injection!\n```\n\n通过如下命令可以向配置 `delay` 属性的路由发起请求：\n\n```shell\ntime curl http://127.0.0.1:9080/hello -i\n```\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: application/octet-stream\nContent-Length: 6\nConnection: keep-alive\nServer: APISIX web server\nDate: Tue, 14 Jan 2020 14:30:54 GMT\nLast-Modified: Sat, 11 Jan 2020 12:46:21 GMT\n\nhello\n\nreal    0m3.034s\nuser    0m0.007s\nsys     0m0.010s\n```\n\n### 标准匹配的故障注入\n\n你可以在 `fault-injection` 插件中使用 `vars` 规则设置特定规则：\n\n```Shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"fault-injection\": {\n            \"abort\": {\n                    \"http_status\": 403,\n                    \"body\": \"Fault Injection!\\n\",\n                    \"vars\": [\n                        [\n                            [ \"arg_name\",\"==\",\"jack\" ]\n                        ]\n                    ]\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n使用不同的 `name` 参数测试路由：\n\n```Shell\ncurl \"http://127.0.0.1:9080/hello?name=allen\" -i\n```\n\n没有故障注入的情况下，你可以得到如下结果：\n\n```shell\nHTTP/1.1 200 OK\nContent-Type: application/octet-stream\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Wed, 20 Jan 2021 07:21:57 GMT\nServer: APISIX/2.2\n\nhello\n```\n\n如果我们将 `name` 设置为与配置相匹配的名称，`fault-injection` 插件将被执行：\n\n```Shell\ncurl \"http://127.0.0.1:9080/hello?name=jack\" -i\n```\n\n```shell\nHTTP/1.1 403 Forbidden\nDate: Wed, 20 Jan 2021 07:23:37 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/2.2\n\nFault Injection!\n```\n\n## 删除插件\n\n当你需要禁用 `fault-injection` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/file-logger.md",
    "content": "---\ntitle: file-logger\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - file-logger\ndescription: API 网关 Apache APISIX file-logger 插件可用于将日志数据存储到指定位置。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`file-logger` 插件可用于将日志数据存储到指定位置。\n\n:::tip 提示\n\n`file-logger` 插件特点如下：\n\n- 可将指定路由的日志发送到指定位置，方便你在本地统计各个路由的请求和响应数据。在使用 [debug mode](../../../en/latest/debug-mode.md) 时，你可以很轻松地将出现问题的路由的日志输出到指定文件中，从而更方便地排查问题。\n- 可以获取 [APISIX 变量](../../../en/latest/apisix-variable.md)和 [NGINX 变量](http://nginx.org/en/docs/varindex.html)，而 `access.log` 仅能使用 NGINX 变量。\n- 支持热加载，你可以在路由中随时更改其配置并立即生效。而修改 `access.log` 相关配置，则需要重新加载 APISIX。\n- 支持以 JSON 格式保存日志数据。\n- 可以在 `log phase` 阶段修改 `file-logger` 执行的函数来收集你所需要的信息。\n\n:::\n\n## 属性\n\n| 名称             | 类型     | 必选项 | 描述                                             |\n| ---------------- | ------- |-----| ------------------------------------------------ |\n| path             | string  | 是   | 自定义输出文件路径。例如：`logs/file.log`。        |\n| log_format       | object  | 否   | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| include_req_body   | boolean | 否   | 当设置为 `true` 时，日志中将包含请求体。如果请求体太大而无法在内存中保存，则由于 Nginx 的限制，无法记录请求体。|\n| include_req_body_expr | array   | 否   | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。 |\n| include_resp_body      | boolean | 否   | 当设置为 `true` 时，生成的文件包含响应体。                                                                                               |\n| include_resp_body_expr | array   | 否   | 当 `include_resp_body` 属性设置为 `true` 时，使用该属性并基于 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 进行过滤。如果存在，则仅在表达式计算结果为 `true` 时记录响应。       |\n| match        | array[array] | 否   |  当设置了这个选项后，只有匹配规则的日志才会被记录。`match` 是一个表达式列表，具体请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)。   |\n\n### 默认日志格式示例\n\n  ```json\n  {\n    \"service_id\": \"\",\n    \"apisix_latency\": 100.99999809265,\n    \"start_time\": 1703907485819,\n    \"latency\": 101.99999809265,\n    \"upstream_latency\": 1,\n    \"client_ip\": \"127.0.0.1\",\n    \"route_id\": \"1\",\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"request\": {\n        \"headers\": {\n            \"host\": \"127.0.0.1:1984\",\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"user-agent\": \"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\",\n            \"content-length\": \"12\"\n        },\n        \"method\": \"POST\",\n        \"size\": 194,\n        \"url\": \"http://127.0.0.1:1984/hello?log_body=no\",\n        \"uri\": \"/hello?log_body=no\",\n        \"querystring\": {\n            \"log_body\": \"no\"\n        }\n    },\n    \"response\": {\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"content-length\": \"12\",\n            \"server\": \"APISIX/3.7.0\"\n        },\n        \"status\": 200,\n        \"size\": 123\n    },\n    \"upstream\": \"127.0.0.1:1982\"\n }\n  ```\n\n## 插件元数据设置\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| path             | string  | 否   |  |         | 当插件配置中未指定 `path` 时使用的日志文件路径。 |\n| log_format       | object  | 可选   |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n\n:::note 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `file-logger` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/file-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"path\": \"logs/metadata-file.log\",\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你可以在日志系统中看到如下类似日志：\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"plugins\": {\n    \"file-logger\": {\n      \"path\": \"logs/file.log\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  },\n  \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n访问成功后，你可以在对应的 `logs` 目录下找到 `file.log` 文件。\n\n## 过滤日志\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"plugins\": {\n    \"file-logger\": {\n      \"path\": \"logs/file.log\",\n      \"match\": [\n        [\n          [ \"arg_name\",\"==\",\"jack\" ]\n        ]\n      ]\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"127.0.0.1:9001\": 1\n    }\n  },\n  \"uri\": \"/hello\"\n}'\n```\n\n测试：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello?name=jack\n```\n\n在 `logs/file.log` 中可以看到日志记录\n\n```shell\ncurl -i http://127.0.0.1:9080/hello?name=rose\n```\n\n在 `logs/file.log` 中看不到日志记录\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:9001\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/forward-auth.md",
    "content": "---\ntitle: forward-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Forward Authentication\n  - forward-auth\ndescription: 本文介绍了关于 Apache APISIX `forward-auth` 插件的基本信息及使用方法。\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`forward-auth` 插件使用的是经典外部认证。当身份认证失败时，可以实现自定义错误或者重定向到认证页面的场景。\n\n`forward-auth` 插件巧妙地将身份认证和授权逻辑移到了一个专门的外部服务中，APISIX 将用户的请求转发给认证服务并阻塞原始请求，然后在认证服务下以非 2xx 状态响应时进行结果替换。\n\n## 属性\n\n| 名称              | 类型           | 必选项 |  默认值 | 有效值         | 描述                                                                                                               |\n| ----------------- | ------------- | ------| ------- | -------------- | -------------------------------------------------------------------------------------------------------------------- |\n| uri               | string        | 是    |         |                | 设置 `authorization` 服务的地址 (例如：https://localhost:9188)。                                                      |\n| ssl_verify        | boolean       | 否    | true    | [true, false]  | 当设置为 `true` 时，验证 SSL 证书。                                                                                  |\n| request_method    | string        | 否    | GET     | [\"GET\",\"POST\"] | 客户端向 authorization 服务发送请求的方法。当设置为 POST 时，会将 request body 转发至 authorization 服务。         |\n| request_headers   | array[string] | 否    |         |                | 设置需要由客户端转发到 `authorization` 服务的请求头。如果没有设置，则只发送 APISIX 提供的 headers (例如：X-Forwarded-XXX)。 |\n| extra_headers   |object | False    |         |                | 以键值格式传递给授权服务的额外标头。值可以是变量，例如“$request_uri”或“$post_arg.xyz”。 |\n| upstream_headers  | array[string] | 否    |         |                | 认证通过时，设置 `authorization` 服务转发至 `upstream` 的请求头。如果不设置则不转发任何请求头。                             |\n| client_headers    | array[string] | 否    |         |                | 认证失败时，由 `authorization` 服务向 `client` 发送的响应头。如果不设置则不转发任何响应头。                                |\n| timeout           | integer       | 否    | 3000ms  | [1, 60000]ms   | `authorization` 服务请求超时时间。                                                                                     |\n| keepalive         | boolean       | 否    | true    | [true, false]  | HTTP 长连接。                                                                                                         |\n| keepalive_timeout | integer       | 否    | 60000ms | [1000, ...]ms  | 长连接超时时间。                                                                                                      |\n| keepalive_pool    | integer       | 否    | 5       | [1, ...]ms     | 长连接池大小。                                                                                                        |\n| allow_degradation | boolean       | 否    | false   |                | 当设置为 `true` 时，允许在身份验证服务器不可用时跳过身份验证。 |\n| status_on_error   | integer       | 否    | 403     | [200,...,599]   | 设置授权服务出现网络错误时返回给客户端的 HTTP 状态。默认状态为“403”。 |\n\n## 数据定义\n\nAPISIX 将生成并发送如下所示的请求头到认证服务：\n\n| Scheme            | HTTP Method        | Host              | URI             | Source IP       |\n| ----------------- | ------------------ | ----------------- | --------------- | --------------- |\n| X-Forwarded-Proto | X-Forwarded-Method | X-Forwarded-Host  | X-Forwarded-Uri | X-Forwarded-For |\n\n## 使用示例\n\n首先，你需要设置一个外部认证服务。以下示例使用的是 Apache APISIX 无服务器插件模拟服务：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/auth' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/auth\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\n                \"return function (conf, ctx)\n                    local core = require(\\\"apisix.core\\\");\n                    local authorization = core.request.header(ctx, \\\"Authorization\\\");\n                    if authorization == \\\"123\\\" then\n                        core.response.exit(200);\n                    elseif authorization == \\\"321\\\" then\n                        core.response.set_header(\\\"X-User-ID\\\", \\\"i-am-user\\\");\n                        core.response.exit(200);\n                    else core.response.set_header(\\\"Location\\\", \\\"http://example.com/auth\\\");\n                        core.response.exit(403);\n                    end\n                end\"\n            ]\n        }\n    }\n}'\n```\n\n现在你可以在指定 Route 上启用 `forward-auth` 插件：\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n    \"uri\": \"/headers\",\n    \"plugins\": {\n        \"forward-auth\": {\n            \"uri\": \"http://127.0.0.1:9080/auth\",\n            \"request_headers\": [\"Authorization\"],\n            \"upstream_headers\": [\"X-User-ID\"],\n            \"client_headers\": [\"Location\"]\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n完成上述配置后，可通过以下三种方式进行测试：\n\n- 在请求头中发送认证的详细信息：\n\n```shell\ncurl http://127.0.0.1:9080/headers -H 'Authorization: 123'\n```\n\n```\n{\n    \"headers\": {\n        \"Authorization\": \"123\",\n        \"Next\": \"More-headers\"\n    }\n}\n```\n\n- 转发认证服务响应头到 Upstream。\n\n```shell\ncurl http://127.0.0.1:9080/headers -H 'Authorization: 321'\n```\n\n```\n{\n    \"headers\": {\n        \"Authorization\": \"321\",\n        \"X-User-ID\": \"i-am-user\",\n        \"Next\": \"More-headers\"\n    }\n}\n```\n\n- 当授权失败时，认证服务可以向用户发送自定义响应：\n\n```shell\ncurl -i http://127.0.0.1:9080/headers\n```\n\n```shell\nHTTP/1.1 403 Forbidden\nLocation: http://example.com/auth\n```\n\n### Using data from POST body to make decision on Authorization service\n\n::: note\n当要根据 POST 正文做出决定时，建议使用带有 `extra_headers` 字段的 `$post_arg.*` 并根据标头对授权服务做出决定，而不是使用 POST `request_method` 将整个请求正文传递给授权服务。\n:::\n\n在 `/auth` 路由上创建一个无服务器函数，用于检查 `tenant_id` 标头是否存在。如果存在，路由会使用 HTTP 200 进行响应，并将 `X-User-ID` 标头设置为固定值 `i-am-an-user`。如果缺少 `tenant_id`，则会返回 HTTP 400 和错误消息。\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/auth' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/auth\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\n                \"return function(conf, ctx)\n                 local core = require(\\\"apisix.core\\\")\n                 local tenant_id = core.request.header(ctx, \\\"tenant_id\\\")\n                 if tenant_id == \\\"123\\\" then\n                     core.response.set_header(\\\"X-User-ID\\\", \\\"i-am-an-user\\\");\n                     core.response.exit(200);\n                else\n                    core.response.exit(400, \\\"tenant_id is \\\"..tenant_id .. \\\" but expected 123\\\");\n                end\n            end\"\n            ]\n        }\n    }\n}'\n```\n\n创建一个接受 POST 请求的路由，并使用 `forward-auth` 插件通过请求中的 `tenant_id` 调用身份验证端点。仅当身份验证检查返回 200 时，请求才会转发到上游服务。\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n    \"uri\": \"/post\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n        \"forward-auth\": {\n            \"uri\": \"http://127.0.0.1:9080/auth\",\n            \"request_method\": \"GET\",\n            \"extra_headers\": {\"tenant_id\": \"$post_arg.tenant_id\"}\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n发送带有 `tenant_id` 标头的 POST 请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/post -H \"Content-Type: application/json\" -X POST -d '{\n   \"tenant_id\": \"123\"\n}'\n```\n\n您应该收到类似以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\n   \\\"tenant_id\\\": \\\"123\\\"\\n}\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"25\",\n    \"Content-Type\": \"application/json\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.13.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-687775d8-6890073173b30c2834901e8b\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": {\n    \"tenant_id\": \"123\"\n  },\n  \"origin\": \"127.0.0.1, 106.215.82.114\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\n发送带有错误 `tenant_id` 标头的 POST 请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/post -H \"Content-Type: application/json\" -X POST -d '{\n   \"tenant_id\": \"asdfasd\"\n}'\n```\n\n您应该收到包含以下消息的 `HTTP/1.1 400 Bad Request` 响应：\n\n```shell\ntenant_id is asdfasd but expected 123\n```\n\n## 删除插件\n\n当你需要禁用 `forward-auth` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/gm.md",
    "content": "---\ntitle: GM\nkeywords:\n  - Apache APISIX\n  - Plugin\n  - GM\ndescription: 本文介绍了关于 Apache APISIX gm 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`gm` 插件能启用国密相关的功能。目前支持通过该插件动态配置国密双证书。\n\n:::note 相关介绍\n国密就是国产化的密码算法。在我们日常开发过程中会接触到各种各样的密码算法，如 RSA、SHA256 等等。为了达到更高的安全等级，许多大公司和国家会制定自己的密码算法。国密就是这样一组由中国国家密码管理局制定的密码算法。在国际形势越发复杂多变的今天，密码算法的国产化替代，在一些领域已经成为了一股势不可挡的潮流。\n:::\n\n## 启用插件\n\n**该插件要求 Apache APISIX 运行在编译了 Tongsuo 的 APISIX-Runtime 上。**\n\n首先，我们需要安装 Tongsuo（此处我们选择编译出 Tongsuo 的动态链接库）：\n\n```\n# TODO: use a fixed release once they have created one.\n# See https://github.com/Tongsuo-Project/Tongsuo/issues/318\ngit clone https://github.com/api7/tongsuo --depth 1\npushd tongsuo\n./config shared enable-ntls -g --prefix=/usr/local/tongsuo\nmake -j2\nsudo make install_sw\n```\n\n其次，我们需要构建 APISIX-Runtime，让它使用 Tongsuo 作为 SSL 库：\n\n```\nexport OR_PREFIX=/usr/local/openresty\nexport openssl_prefix=/usr/local/tongsuo\nexport zlib_prefix=$OR_PREFIX/zlib\nexport pcre_prefix=$OR_PREFIX/pcre\n\nexport cc_opt=\"-DNGX_LUA_ABORT_AT_PANIC -I${zlib_prefix}/include -I${pcre_prefix}/include -I${openssl_prefix}/include\"\nexport ld_opt=\"-L${zlib_prefix}/lib -L${pcre_prefix}/lib -L${openssl_prefix}/lib64 -Wl,-rpath,${zlib_prefix}/lib:${pcre_prefix}/lib:${openssl_prefix}/lib64\"\n./build-apisix-runtime.sh\n```\n\n该插件默认是禁用状态，你需要将其添加到配置文件（`./conf/config.yaml`）中才可以启用它：\n\n```yaml\nplugins:\n  - ...\n  - gm\n```\n\n由于 APISIX 的默认 cipher 中不包含国密 cipher，所以我们还需要在配置文件（`./conf/config.yaml`）中设置 cipher：\n\n```yaml\napisix:\n  ...\n  ssl:\n    ...\n    # 可按实际情况调整。错误的 cipher 会导致“no shared cipher”或“no ciphers available”报错。\n    ssl_ciphers: HIGH:!aNULL:!MD5\n\n```\n\n配置完成后，重新加载 APISIX，此时 APISIX 将会启用国密相关的逻辑。\n\n## 测试插件\n\n在测试插件之前，我们需要准备好国密双证书。Tongsuo 提供了生成[SM2 双证书](https://www.yuque.com/tsdoc/ts/sulazb)的教程。\n\n在下面的例子中，我们将用到如下的证书：\n\n```\n# 客户端加密证书和密钥\nt/certs/client_enc.crt\nt/certs/client_enc.key\n# 客户端签名证书和密钥\nt/certs/client_sign.crt\nt/certs/client_sign.key\n# CA 和中间 CA 打包在一起的文件，用于设置受信任的 CA\nt/certs/gm_ca.crt\n# 服务端加密证书和密钥\nt/certs/server_enc.crt\nt/certs/server_enc.key\n# 服务端签名证书和密钥\nt/certs/server_sign.crt\nt/certs/server_sign.key\n```\n\n此外，我们还需要准备 Tongsuo 命令行工具。\n\n```\n./config enable-ntls -static\nmake -j2\n# 生成的命令行工具在 apps 目录下\nmv apps/openssl ..\n```\n\n你也可以采用非静态编译的方式，不过就需要根据具体环境，自己解决动态链接库的路径问题了。\n\n以下示例展示了如何在指定域名中启用 `gm` 插件：\n\n创建对应的 SSL 对象：\n\n```python\n#!/usr/bin/env python\n# coding: utf-8\n\nimport sys\n# sudo pip install requests\nimport requests\n\nif len(sys.argv) <= 3:\n    print(\"bad argument\")\n    sys.exit(1)\nwith open(sys.argv[1]) as f:\n    enc_cert = f.read()\nwith open(sys.argv[2]) as f:\n    enc_key = f.read()\nwith open(sys.argv[3]) as f:\n    sign_cert = f.read()\nwith open(sys.argv[4]) as f:\n    sign_key = f.read()\napi_key = \"edd1c9f034335f136f87ad84b625c8f1\"\nresp = requests.put(\"http://127.0.0.1:9180/apisix/admin/ssls/1\", json={\n    \"cert\": enc_cert,\n    \"key\": enc_key,\n    \"certs\": [sign_cert],\n    \"keys\": [sign_key],\n    \"gm\": True,\n    \"snis\": [\"localhost\"],\n}, headers={\n    \"X-API-KEY\": api_key,\n})\nprint(resp.status_code)\nprint(resp.text)\n```\n\n将上面的脚本保存为 `./create_gm_ssl.py`，运行：\n\n```shell\n./create_gm_ssl.py t/certs/server_enc.crt  t/certs/server_enc.key t/certs/server_sign.crt t/certs/server_sign.key\n```\n\n输出结果如下所示：\n\n```\n200\n{\"key\":\"\\/apisix\\/ssls\\/1\",\"value\":{\"keys\":[\"Yn...\n```\n\n完成上述准备后，可以使用如下命令测试插件是否启用成功：\n\n```shell\n./openssl s_client -connect localhost:9443 -servername localhost -cipher ECDHE-SM2-WITH-SM4-SM3 -enable_ntls -ntls -verifyCAfile t/certs/gm_ca.crt -sign_cert t/certs/client_sign.crt -sign_key t/certs/client_sign.key -enc_cert t/certs/client_enc.crt -enc_key t/certs/client_enc.key\n```\n\n这里 `./openssl` 是前面得到的 Tongsuo 命令行工具。9443 是 APISIX 默认的 HTTPS 端口。\n\n如果一切正常，可以看到连接建立了起来，并输出如下信息：\n\n```\n...\nNew, NTLSv1.1, Cipher is ECDHE-SM2-SM4-CBC-SM3\n...\n```\n\n## 删除插件\n\n如果不再使用此插件，可将 `gm` 插件从 `./conf/config.yaml` 配置文件中移除，然后重启 APISIX 或者通过插件热加载的接口触发插件的卸载。\n"
  },
  {
    "path": "docs/zh/latest/plugins/google-cloud-logging.md",
    "content": "---\ntitle: google-cloud-logging\nkeywords:\n  - APISIX\n  - API 网关\n  - 插件\n  - Google Cloud logging\n  - 日志\ndescription: API 网关 Apache APISIX 的 google-cloud-logging 插件可用于将请求日志转发到 Google Cloud Logging Service 中进行分析和存储。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`google-cloud-logging` 插件可用于将请求日志发送到 [Google Cloud Logging Service](https://cloud.google.com/logging/) 进行分析和存储。\n\n## 属性\n\n| 名称                     | 必选项   | 默认值                                           | 描述                                                                                                                             |\n| ----------------------- | -------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------  |\n| auth_config             | 是       |                                                  | `auth_config` 和 `auth_file` 必须配置一个。                                                                                     |\n| auth_config.client_email | 是       |                                                  | 谷歌服务帐号的 email 参数。                                                                                                           |\n| auth_config.private_key | 是       |                                                  | 谷歌服务帐号的私钥参数。                                                                                                           |\n| auth_config.project_id  | 是       |                                                  | 谷歌服务帐号的项目 ID。                                                                                                            |\n| auth_config.token_uri   | 是       | https://oauth2.googleapis.com/token              | 请求谷歌服务帐户的令牌的 URI。                                                                                                     |\n| auth_config.entries_uri | 否       | https://logging.googleapis.com/v2/entries:write  | 谷歌日志服务写入日志条目的 API。                                                                                                   |\n| auth_config.scope       | 否       |                                                  | 谷歌服务账号的访问范围，可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#logging)。可选项：\"https://www.googleapis.com/auth/logging.read\"、\"https://www.googleapis.com/auth/logging.write\"、\"https://www.googleapis.com/auth/logging.admin\"、\"https://www.googleapis.com/auth/cloud-platform\"。|\n| auth_config.scopes      | 废弃     |                                                  | 谷歌服务账号的访问范围，推荐使用 `auth_config.scope`                                                                               |\n| auth_file               | 是       |                                                  | `auth_config` 和 `auth_file` 必须配置一个。                                                                 |\n| ssl_verify              | 否       | true                                             | 当设置为 `true` 时，启用 `SSL` 验证。                 |\n| resource                | 否       | {\"type\": \"global\"}                               | 谷歌监控资源，请参考 [MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource)。             |\n| log_id                  | 否       | apisix.apache.org%2Flogs                         | 谷歌日志 ID，请参考 [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry)。                                |\n| log_format              | 否   |       | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"auth_config.private_key\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n该插件支持使用批处理器来聚合并批量处理条目（日志和数据）。这样可以避免该插件频繁地提交数据。默认情况下每 `5` 秒钟或队列中的数据达到 `1000` 条时，批处理器会自动提交数据，如需了解更多信息或自定义配置，请参考 [Batch Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n```json\n{\n    \"insertId\": \"0013a6afc9c281ce2e7f413c01892bdc\",\n    \"labels\": {\n        \"source\": \"apache-apisix-google-cloud-logging\"\n    },\n    \"logName\": \"projects/apisix/logs/apisix.apache.org%2Flogs\",\n    \"httpRequest\": {\n        \"requestMethod\": \"GET\",\n        \"requestUrl\": \"http://localhost:1984/hello\",\n        \"requestSize\": 59,\n        \"responseSize\": 118,\n        \"status\": 200,\n        \"remoteIp\": \"127.0.0.1\",\n        \"serverIp\": \"127.0.0.1:1980\",\n        \"latency\": \"0.103s\"\n    },\n    \"resource\": {\n        \"type\": \"global\"\n    },\n    \"jsonPayload\": {\n        \"service_id\": \"\",\n        \"route_id\": \"1\"\n    },\n    \"timestamp\": \"2024-01-06T03:34:45.065Z\"\n}\n```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否    |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::info 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `google-cloud-logging` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/google-cloud-logging \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```json\n{\"partialSuccess\":false,\"entries\":[{\"jsonPayload\":{\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"},\"resource\":{\"type\":\"global\"},\"insertId\":\"942e81f60b9157f0d46bc9f5a8f0cc40\",\"logName\":\"projects/apisix/logs/apisix.apache.org%2Flogs\",\"timestamp\":\"2023-01-09T14:47:25+08:00\",\"labels\":{\"source\":\"apache-apisix-google-cloud-logging\"}}]}\n```\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用该插件：\n\n**完整配置**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"google-cloud-logging\": {\n            \"auth_config\":{\n                \"project_id\":\"apisix\",\n                \"client_email\":\"your service account email@apisix.iam.gserviceaccount.com\",\n                \"private_key\":\"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----\",\n                \"token_uri\":\"https://oauth2.googleapis.com/token\",\n                \"scope\":[\n                    \"https://www.googleapis.com/auth/logging.admin\"\n                ],\n                \"entries_uri\":\"https://logging.googleapis.com/v2/entries:write\"\n            },\n            \"resource\":{\n                \"type\":\"global\"\n            },\n            \"log_id\":\"apisix.apache.org%2Flogs\",\n            \"inactive_timeout\":10,\n            \"max_retry_count\":0,\n            \"max_retry_count\":0,\n            \"buffer_duration\":60,\n            \"retry_delay\":1,\n            \"batch_max_size\":1\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n**最小化配置**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"google-cloud-logging\": {\n            \"auth_config\":{\n                \"project_id\":\"apisix\",\n                \"client_email\":\"your service account email@apisix.iam.gserviceaccount.com\",\n                \"private_key\":\"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n访问成功后，你可以登录 [Google Cloud Logging Service](https://console.cloud.google.com/logs/viewer) 查看相关日志。\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/grpc-transcode.md",
    "content": "---\ntitle: grpc-transcode\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - gRPC Web\n  - grpc-web\ndescription: 本文介绍了关于 Apache APISIX `grpc-transcode` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n使用 `grpc-transcode` 插件可以在 HTTP 和 gRPC 请求之间进行转换。\n\nAPISIX 接收 HTTP 请求后，首先对请求进行转码，并将转码后的请求转发到 gRPC 服务，获取响应并以 HTTP 格式将其返回给客户端。\n\n<!-- TODO: use an image here to explain the concept better -->\n\n## 属性\n\n| 名称      | 类型                                                                       | 必选项 | 默认值 | 描述                               |\n| --------- | -------------------------------------------------  | ----- | ------  ------------------------------ |\n| proto_id  | string/integer                                     | 是    |        | `.proto` 内容的 id。             |\n| service   | string                                             | 是    |        | gRPC 服务名。                    |\n| method    | string                                             | 是    |        | gRPC 服务中要调用的方法名。        |\n| deadline  | number                                             | 否    | 0      | gRPC 服务的 deadline，单位为：ms。 |\n| pb_option | array[string([pb_option_def](#pb_option-的选项))]    | 否    |        | proto 编码过程中的转换选项。       |\n| show_status_in_body  | boolean                                 | 否    | false    | 是否在返回体中展示解析过的 `grpc-status-details-bin` |\n| status_detail_type | string                                    | 否    |        | `grpc-status-details-bin` 中 [details](https://github.com/googleapis/googleapis/blob/b7cb84f5d42e6dba0fdcc2d8689313f6a8c9d7b9/google/rpc/status.proto#L46) 部分对应的 message type，如果不指定，此部分不进行解码  |\n\n### pb_option 的选项\n\n| 类型            | 有效值                                                                                     |\n|-----------------|-------------------------------------------------------------------------------------------|\n| enum as result  | `enum_as_name`, `enum_as_value`                                                           |\n| int64 as result | `int64_as_number`, `int64_as_string`, `int64_as_hexstring`                                |\n| default values  | `auto_default_values`, `no_default_values`, `use_default_values`, `use_default_metatable` |\n| hooks           | `enable_hooks`, `disable_hooks`                                                           |\n\n## 启用插件\n\n在启用插件之前，你必须将 `.proto` 或 `.pb` 文件的内容添加到 APISIX。\n\n可以使用 `/admin/protos/id` 接口将文件的内容添加到 `content` 字段：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"syntax = \\\"proto3\\\";\n    package helloworld;\n    service Greeter {\n        rpc SayHello (HelloRequest) returns (HelloReply) {}\n    }\n    message HelloRequest {\n        string name = 1;\n    }\n    message HelloReply {\n        string message = 1;\n    }\"\n}'\n```\n\n如果你的 `.proto` 文件包含 `import`，或者想要把多个 `.proto` 文件合并成一个 proto，你可以生成一个 `.pb` 文件并在 APISIX 中使用它。\n\n假设已经有一个 `.proto` 文件（`proto/helloworld.proto`），它导入了另一个 `proto` 文件：\n\n```proto\nsyntax = \"proto3\";\n\npackage helloworld;\nimport \"proto/import.proto\";\n...\n```\n\n首先，让我们从 `.proto` 文件创建一个 `.pb` 文件。\n\n```shell\nprotoc --include_imports --descriptor_set_out=proto.pb proto/helloworld.proto\n```\n\n输出的二进制文件 `proto.pb` 将同时包含 `helloworld.proto` 和 `import.proto`。\n\n然后将 `proto.pb` 的内容作为 proto 的 `content` 字段提交。\n\n由于 proto 的内容是二进制的，我们需要使用以下 shell 命令将其转换成 `base64`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"'\"$(base64 -w0 /path/to/proto.pb)\"'\"\n}'\n```\n\n返回 `HTTP/1.1 201 Created` 结果如下：\n\n```\n{\"node\":{\"value\":{\"create_time\":1643879753,\"update_time\":1643883085,\"content\":\"CmgKEnByb3RvL2ltcG9ydC5wcm90bxIDcGtnIhoKBFVzZXISEgoEbmFtZRgBIAEoCVIEbmFtZSIeCghSZXNwb25zZRISCgRib2R5GAEgASgJUgRib2R5QglaBy4vcHJvdG9iBnByb3RvMwq9AQoPcHJvdG8vc3JjLnByb3RvEgpoZWxsb3dvcmxkGhJwcm90by9pbXBvcnQucHJvdG8iPAoHUmVxdWVzdBIdCgR1c2VyGAEgASgLMgkucGtnLlVzZXJSBHVzZXISEgoEYm9keRgCIAEoCVIEYm9keTI5CgpUZXN0SW1wb3J0EisKA1J1bhITLmhlbGxvd29ybGQuUmVxdWVzdBoNLnBrZy5SZXNwb25zZSIAQglaBy4vcHJvdG9iBnByb3RvMw==\"},\"key\":\"\\/apisix\\/proto\\/1\"}}\n```\n\n现在我们可以在指定路由中启用 `grpc-transcode` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/grpctest\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n         \"proto_id\": \"1\",\n         \"service\": \"helloworld.Greeter\",\n         \"method\": \"SayHello\"\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\n:::note\n\n此处使用的 Upstream 应该是 gRPC 服务。请注意，`scheme` 需要设置为 `grpc`。\n\n可以使用 [grpc_server_example](https://github.com/api7/grpc_server_example) 进行测试。\n\n:::\n\n## 测试插件\n\n通过上述示例配置插件后，你可以向 APISIX 发出请求以从 gRPC 服务（通过 APISIX）获得响应：\n\n访问上面配置的 route：\n\n```shell\ncurl -i http://127.0.0.1:9080/grpctest?name=world\n```\n\n返回结果\n\n```Shell\nHTTP/1.1 200 OK\nDate: Fri, 16 Aug 2019 11:55:36 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\nProxy-Connection: keep-alive\n\n{\"message\":\"Hello world\"}\n```\n\n你也可以配置 `pb_option`，如下所示：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/23 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/zeebe/WorkflowInstanceCreate\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n            \"proto_id\": \"1\",\n            \"service\": \"gateway_protocol.Gateway\",\n            \"method\": \"CreateWorkflowInstance\",\n            \"pb_option\":[\"int64_as_string\"]\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:26500\": 1\n        }\n    }\n}'\n```\n\n可以通过如下命令检查刚刚配置的路由：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/zeebe/WorkflowInstanceCreate?bpmnProcessId=order-process&version=1&variables=\\{\\\"orderId\\\":\\\"7\\\",\\\"ordervalue\\\":99\\}\"\n```\n\n```Shell\nHTTP/1.1 200 OK\nDate: Wed, 13 Nov 2019 03:38:27 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\ngrpc-encoding: identity\ngrpc-accept-encoding: gzip\nServer: APISIX web server\nTrailer: grpc-status\nTrailer: grpc-message\n\n{\"workflowKey\":\"#2251799813685260\",\"workflowInstanceKey\":\"#2251799813688013\",\"bpmnProcessId\":\"order-process\",\"version\":1}\n```\n\n## 在返回体中展示 `grpc-status-details-bin`\n\n如果 gRPC 服务返回了错误，返回头中可能存在 `grpc-status-details-bin` 字段对错误进行描述，你可以解码该字段，并展示在返回体中。\n\n上传 proto 文件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"syntax = \\\"proto3\\\";\n    package helloworld;\n    service Greeter {\n        rpc GetErrResp (HelloRequest) returns (HelloReply) {}\n    }\n    message HelloRequest {\n        string name = 1;\n        repeated string items = 2;\n    }\n    message HelloReply {\n        string message = 1;\n        repeated string items = 2;\n    }\"\n}'\n```\n\n启用 `grpc-transcode` 插件，并设置选项 `show_status_in_body` 为 `true`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/grpctest\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n         \"proto_id\": \"1\",\n         \"service\": \"helloworld.Greeter\",\n         \"method\": \"GetErrResp\",\n         \"show_status_in_body\": true\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\n访问上面配置的 route：\n\n```shell\ncurl -i http://127.0.0.1:9080/grpctest?name=world\n```\n\n返回结果\n\n```Shell\nHTTP/1.1 503 Service Temporarily Unavailable\nDate: Wed, 10 Aug 2022 08:59:46 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\nServer: APISIX web server\n\n{\"error\":{\"details\":[{\"type_url\":\"type.googleapis.com\\/helloworld.ErrorDetail\",\"value\":\"\\b\\u0001\\u0012\\u001cThe server is out of service\\u001a\\u0007service\"}],\"message\":\"Out of service\",\"code\":14}}\n```\n\n注意返回体中还存在未解码的字段，如果需要解码该字段，需要在上传的 proto 文件中加上该字段对应的 `message type`。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/protos/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"content\" : \"syntax = \\\"proto3\\\";\n    package helloworld;\n    service Greeter {\n        rpc GetErrResp (HelloRequest) returns (HelloReply) {}\n    }\n    message HelloRequest {\n        string name = 1;\n        repeated string items = 2;\n    }\n    message HelloReply {\n        string message = 1;\n        repeated string items = 2;\n    }\n    message ErrorDetail {\n        int64 code = 1;\n        string message = 2;\n        string type = 3;\n    }\"\n}'\n```\n\n同时配置选项 `status_detail_type` 为 `helloworld.ErrorDetail`：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/grpctest\",\n    \"plugins\": {\n        \"grpc-transcode\": {\n         \"proto_id\": \"1\",\n         \"service\": \"helloworld.Greeter\",\n         \"method\": \"GetErrResp\",\n         \"show_status_in_body\": true,\n         \"status_detail_type\": \"helloworld.ErrorDetail\"\n        }\n    },\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n\n此时就能返回完全解码后的结果\n\n```Shell\nHTTP/1.1 503 Service Temporarily Unavailable\nDate: Wed, 10 Aug 2022 09:02:46 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\nServer: APISIX web server\n\n{\"error\":{\"details\":[{\"type\":\"service\",\"message\":\"The server is out of service\",\"code\":1}],\"message\":\"Out of service\",\"code\":14}}\n```\n\n## 删除插件\n\n当你需要禁用 `grpc-transcode` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/111 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/grpctest\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"scheme\": \"grpc\",\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:50051\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/grpc-web.md",
    "content": "---\ntitle: grpc-web\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - gRPC Web\n  - grpc-web\ndescription: 本文介绍了关于 Apache APISIX `grpc-web` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`grpc-web` 插件是一个代理插件，可以处理从 JavaScript 客户端到 gRPC Service 的 [gRPC Web](https://github.com/grpc/grpc-web) 请求。\n\n## 属性\n\n| 名称                  | 类型    | 必选项 | 默认值                                     | 描述                                                             |\n|---------------------| ------- |----|-----------------------------------------|----------------------------------------------------------------|\n| cors_allow_headers  | string  | 否  | \"content-type,x-grpc-web,x-user-agent\"  | 允许跨域访问时请求方携带哪些非 `CORS 规范` 以外的 Header。如果你有多个 Header，请使用 `,` 分割。 |\n\n## 启用插件\n\n你可以通过如下命令在指定路由上启用 `gRPC-web` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\":\"/grpc/web/*\",\n    \"plugins\":{\n        \"grpc-web\":{}\n    },\n    \"upstream\":{\n        \"scheme\":\"grpc\",\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n请参考 [gRPC-Web Client Runtime Library](https://www.npmjs.com/package/grpc-web) 或 [Apache APISIX gRPC Web Test Framework](https://github.com/apache/apisix/tree/master/t/plugin/grpc-web) 了解如何配置你的 Web 客户端。\n\n运行 gRPC Web 客户端后，你可以从浏览器或通过 Node.js 向 APISIX 发出请求。\n\n:::note\n\n请求方式仅支持 `POST` 和 `OPTIONS`，详细信息请参考：[CORS support](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support) 。\n\n内容类型支持 `application/grpc-web`、`application/grpc-web-text`、`application/grpc-web+proto`、`application/grpc-web-text+proto`，详细信息请参考：[Protocol differences vs gRPC over HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2) 。\n\n:::\n\n## 删除插件\n\n当你需要禁用 `grpc-web` 插件时，可以通过如下命令删除相应的 `JSON` 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\":\"/grpc/web/*\",\n    \"plugins\":{},\n    \"upstream\":{\n        \"scheme\":\"grpc\",\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/gzip.md",
    "content": "---\ntitle: gzip\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - gzip\ndescription: 本文介绍了关于 Apache APISIX `gzip` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`gzip` 插件能动态设置 [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/compression/) 的压缩行为。\n当启用 `gzip` 插件时，客户端在发起请求时需要在请求头中添加 `Accept-Encoding: gzip`，以表明客户端支持 `gzip` 压缩。APISIX 在接收到请求后，会根据客户端的支持情况和服务器配置动态判断是否对响应内容进行 gzip 压缩。如果判定条件得到满足，APISIX 将在响应头中添加 `Content-Encoding: gzip` 字段，以指示响应内容已经通过 `gzip` 压缩。在客户端接收到响应后，根据响应头中的 `Content-Encoding` 字段使用相应的解压缩算法对响应内容进行解压，从而获取原始的响应内容。\n\n:::info IMPORTANT\n\n该插件要求 Apache APISIX 运行在 [APISIX-Runtime](../FAQ.md#如何构建-apisix-runtime-环境) 上。\n\n:::\n\n## 属性\n\n| 名称           | 类型                  | 必选项  | 默认值         | 有效值    | 描述                                                                                                                            |\n| ---------------| -------------------- | ------- | -------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| types          | array[string] or \"*\" | 否      |  [\"text/html\"] |          | 动态设置 [`gzip_types`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_types) 指令，特殊值 `\"*\"` 匹配任何 MIME 类型。 |\n| min_length     | integer              | 否      |  20            | >= 1     | 动态设置 [`gzip_min_length`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_min_length) 指令。                      |\n| comp_level     | integer              | 否      |  1             | [1, 9]   | 动态设置 [`gzip_comp_level`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_comp_level) 指令。                      |\n| http_version   | number               | 否      |  1.1           | 1.1, 1.0 | 动态设置 [`gzip_http_version`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_http_version) 指令。                  |\n| buffers.number | integer              | 否      |  32            | >= 1     | 动态设置 [`gzip_buffers`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_buffers) 指令 参数 `number`。                            |\n| buffers.size   | integer              | 否      |  4096          | >= 1     | 动态设置 [`gzip_buffers`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_buffers) 指令参数 `size`。单位为字节。                            |\n| vary           | boolean              | 否      |  false         |          | 动态设置 [`gzip_vary`](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_vary) 指令。                                  |\n\n## 启用插件\n\n以下示例展示了如何在指定路由中启用 `gzip` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"gzip\": {\n            \"buffers\": {\n                \"number\": 8\n            }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl http://127.0.0.1:9080/index.html -i -H \"Accept-Encoding: gzip\"\n```\n\n```\nHTTP/1.1 404 Not Found\nContent-Type: text/html; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nDate: Wed, 21 Jul 2021 03:52:55 GMT\nServer: APISIX/2.7\nContent-Encoding: gzip\n\nWarning: Binary output can mess up your terminal. Use \"--output -\" to tell\nWarning: curl to output it to your terminal anyway, or consider \"--output\nWarning: <FILE>\" to save to a file.\n```\n\n## 删除插件\n\n当你需要禁用 `gzip` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/hmac-auth.md",
    "content": "---\ntitle: hmac-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - HMAC Authentication\n  - hmac-auth\ndescription: hmac-auth 插件支持 HMAC 认证，保证请求的完整性，防止传输过程中的修改，增强 API 的安全性。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`hmac-auth` 插件支持 HMAC（基于哈希的消息认证码）认证，作为一种确保请求完整性的机制，防止它们在传输过程中被修改。要使用该插件，您需要在 [Consumers](../terminology/consumer.md) 上配置 HMAC 密钥，并在 Routes 或 Services 上启用该插件。\n\n当消费者成功通过身份验证后，APISIX 会在将请求代理到上游服务之前向请求添加其他标头，例如 `X-Consumer-Username`、`X-Credential-Indentifier` 和其他消费者自定义标头（如果已配置）。上游服务将能够区分消费者并根据需要实现其他逻辑。如果这些值中的任何一个不可用，则不会添加相应的标头。\n\n启用后，插件会验证请求的 `Authorization` 标头中的 HMAC 签名，并检查传入的请求是否来自受信任的来源。具体来说，当 APISIX 收到 HMAC 签名的请求时，会从 `Authorization` 标头中提取密钥 ID。然后，APISIX 会检索相应的消费者配置，包括密钥。如果密钥 ID 有效且存在，APISIX 将使用请求的 `Date` 标头和密钥生成 HMAC 签名。如果生成的签名与 `Authorization` 标头中提供的签名匹配，则请求通过身份验证并转发到上游服务。\n\n插件实现基于 [draft-cavage-http-signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt)。\n\n## 属性\n\n以下属性可用于 Consumers 或 Credentials 的配置。\n\n| 名称             | 类型          | 必选项 | 默认值        | 有效值                                      | 描述                                                                                                                                                                                      |\n| ---------------- | ------------- | ------ | ------------- | ------------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| access_key       | string        | 是   |               |                                             |  消费者的唯一标识符，用于标识相关配置，例如密钥。                                                                                   |\n| secret_key       | string        | 是   |               |                                             | 用于生成 HMAC 的密钥。此字段支持使用 [APISIX Secret](../terminology/secret.md) 资源将值保存在 Secret Manager 中。                       |\n\n以下属性可用于 Routes 或 Services 的配置。\n\n| 名称             | 类型          | 必选项 | 默认值        | 有效值                                      | 描述                                                                                                                                                                                      |\n| ---------------- | ------------- | ------ | ------------- | ------------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| allowed_algorithms             | array[string]        | 否    | [\"hmac-sha1\", \"hmac-sha256\", \"hmac-sha512\"] | \"hmac-sha1\"、\"hmac-sha256\" 和 \"hmac-sha512\" 的组合 | 允许的 HMAC 算法列表。                                                                                                                                                                                |\n| clock_skew            | integer       | 否    | 300             |                 >=1                          | 客户端请求的时间戳与 APISIX 服务器当前时间之间允许的最大时间差（以秒为单位）。这有助于解决客户端和服务器之间的时间同步差异，并防止重放攻击。时间戳将根据 Date 头中的时间（必须为 GMT 格式）进行计算。        |\n| signed_headers        | array[string] | 否    |               |                                             | 客户端请求的 HMAC 签名中应包含的 HMAC 签名头列表。  |\n| validate_request_body | boolean       | 否    | false         |                              | 如果为 true，则验证请求正文的完整性，以确保在传输过程中没有被篡改。具体来说，插件会创建一个 SHA-256 的 base64 编码 digest，并将其与 `Digest` 头进行比较。如果 `Digest` 头丢失或 digest 不匹配，验证将失败。                          |\n| hide_credentials | boolean       | 否    | false         |                              | 如果为 true，则不会将授权请求头传递给上游服务。                        |\n| anonymous_consumer | string    | 否    |          |                              | 匿名消费者名称。如果已配置，则允许匿名用户绕过身份验证。                        |\n| realm | string | 否 | hmac |在身份验证失败时，应包含在 `WWW-Authenticate` 标头中的域。|\n\n注意：schema 中还定义了 `encrypt_fields = {\"secret_key\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n## 示例\n\n下面的示例说明了如何在不同场景中使用“hmac-auth”插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 在路由上实现 HMAC 身份验证\n\n以下示例演示如何在路由上实现 HMAC 身份验证。您还将在 `Consumer-Custom-Id` 标头中将消费者自定义 ID 附加到经过身份验证的请求，该 ID 可用于根据需要实现其他逻辑。\n\n创建一个带有自定义 ID 标签的消费者 `john`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\n为消费者创建 `hmac-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\n使用 `hmac-auth` 插件的默认配置创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/get\",\n    \"methods\": [\"GET\"],\n    \"plugins\": {\n      \"hmac-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n生成签名。您可以使用以下 Python 代码片段或其他技术栈：\n\n```python title=\"hmac-sig-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                # key id\nsecret_key = b\"john-secret-key\"    # secret key\nrequest_method = \"GET\"             # HTTP method\nrequest_path = \"/get\"              # Route URI\nalgorithm= \"hmac-sha256\"           # can use other algorithms in allowed_algorithms\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s)\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n  f\"{key_id}\\n\"\n  f\"{request_method} {request_path}\\n\"\n  f\"date: {gmt_time}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# construct the request headers\nheaders = {\n  \"Date\": gmt_time,\n  \"Authorization\": (\n    f'Signature keyId=\"{key_id}\",algorithm=\"{algorithm}\",'\n    f'headers=\"@request-target date\",'\n    f'signature=\"{signature_base64}\"'\n  )\n}\n\n# print headers\nprint(headers)\n```\n\n运行脚本：\n\n```shell\npython3 hmac-sig-header-gen.py\n```\n\n您应该看到打印的请求标头：\n\n```text\n{'Date': 'Fri, 06 Sep 2024 06:41:29 GMT', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"'}\n```\n\n使用生成的标头，向路由发送请求：\n\n```shell\ncurl -X GET \"http://127.0.0.1:9080/get\" \\\n  -H \"Date: Fri, 06 Sep 2024 06:41:29 GMT\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"'\n```\n\n您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Signature keyId=\\\"john-key\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date\\\",signature=\\\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\\\"\",\n    \"Date\": \"Fri, 06 Sep 2024 06:41:29 GMT\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d96513-2e52d4f35c9b6a2772d667ea\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 34.0.34.160\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n### Hide Authorization Information From Upstream\n\nAs seen the in the [last example](#implement-hmac-authentication-on-a-route), the `Authorization` header passed to the Upstream includes the signature and all other details. This could potentially introduce security risks.\n\nThe following example demonstrates how to prevent these information from being sent to the Upstream service.\n\nUpdate the plugin configuration to set `hide_credentials` to `true`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/hmac-auth-route\" -X PATCH \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"plugins\": {\n    \"hmac-auth\": {\n      \"hide_credentials\": true\n    }\n  }\n}'\n```\n\nSend a request to the route:\n\n```shell\ncurl -X GET \"http://127.0.0.1:9080/get\" \\\n  -H \"Date: Fri, 06 Sep 2024 06:41:29 GMT\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"'\n```\n\nYou should see an `HTTP/1.1 200 OK` response and notice the `Authorization` header is entirely removed:\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d96513-2e52d4f35c9b6a2772d667ea\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 34.0.34.160\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n### Enable Body Validation\n\nThe following example demonstrates how to enable body validation to ensure the integrity of the request body.\n\nCreate a consumer `john`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\n为消费者创建 `hmac-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with the `hmac-auth` plugin as such:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/post\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"validate_request_body\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n生成签名。您可以使用以下 Python 代码片段或其他技术栈：\n\n```python title=\"hmac-sig-digest-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                 # key id\nsecret_key = b\"john-secret-key\"     # secret key\nrequest_method = \"POST\"             # HTTP method\nrequest_path = \"/post\"              # Route URI\nalgorithm= \"hmac-sha256\"            # can use other algorithms in allowed_algorithms\nbody = '{\"name\": \"world\"}'          # example request body\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s).\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n    f\"{key_id}\\n\"\n    f\"{request_method} {request_path}\\n\"\n    f\"date: {gmt_time}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# create the SHA-256 digest of the request body and base64 encode it\nbody_digest = hashlib.sha256(body.encode('utf-8')).digest()\nbody_digest_base64 = base64.b64encode(body_digest).decode('utf-8')\n\n# construct the request headers\nheaders = {\n    \"Date\": gmt_time,\n    \"Digest\": f\"SHA-256={body_digest_base64}\",\n    \"Authorization\": (\n        f'Signature keyId=\"{key_id}\",algorithm=\"hmac-sha256\",'\n        f'headers=\"@request-target date\",'\n        f'signature=\"{signature_base64}\"'\n    )\n}\n\n# print headers\nprint(headers)\n```\n\n运行脚本：\n\n```shell\npython3 hmac-sig-digest-header-gen.py\n```\n\n您应该看到打印的请求标头：\n\n```text\n{'Date': 'Fri, 06 Sep 2024 09:16:16 GMT', 'Digest': 'SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"'}\n```\n\n使用生成的标头，向路由发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Date: Fri, 06 Sep 2024 09:16:16 GMT\" \\\n  -H \"Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"' \\\n  -d '{\"name\": \"world\"}'\n```\n\n您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"{\\\"name\\\": \\\"world\\\"}\": \"\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Signature keyId=\\\"john-key\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date\\\",signature=\\\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\\\"\",\n    \"Content-Length\": \"17\",\n    \"Content-Type\": \"application/x-www-form-urlencoded\",\n    \"Date\": \"Fri, 06 Sep 2024 09:16:16 GMT\",\n    \"Digest\": \"SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d978c3-49f929ad5237da5340bbbeb4\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"origin\": \"192.168.65.1, 34.0.34.160\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\n如果您发送的请求没有摘要或摘要无效：\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Date: Fri, 06 Sep 2024 09:16:16 GMT\" \\\n  -H \"Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=\" \\\n  -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"' \\\n  -d '{\"name\": \"world\"}'\n```\n\n您应该看到一个 `HTTP/1.1 401 Unauthorized` 响应，其中包含以下消息：\n\n```text\n{\"message\":\"client request can't be validated\"}\n```\n\n### 强制签名标头\n\n以下示例演示了如何强制在请求的 HMAC 签名中对某些标头进行签名。\n\n创建消费者 `john`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\n为消费者创建 `hmac-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\n使用 `hmac-auth` 插件创建路由，该插件要求 HMAC 签名中存在三个标头：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/get\",\n    \"methods\": [\"GET\"],\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"signed_headers\": [\"date\",\"x-custom-header-a\",\"x-custom-header-b\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n生成签名。您可以使用以下 Python 代码片段或其他技术栈：\n\n```python title=\"hmac-sig-req-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                # key id\nsecret_key = b\"john-secret-key\"    # secret key\nrequest_method = \"GET\"             # HTTP method\nrequest_path = \"/get\"              # Route URI\nalgorithm= \"hmac-sha256\"           # can use other algorithms in allowed_algorithms\ncustom_header_a = \"hello123\"       # required custom header\ncustom_header_b = \"world456\"       # required custom header\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s)\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n    f\"{key_id}\\n\"\n    f\"{request_method} {request_path}\\n\"\n    f\"date: {gmt_time}\\n\"\n    f\"x-custom-header-a: {custom_header_a}\\n\"\n    f\"x-custom-header-b: {custom_header_b}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# construct the request headers\nheaders = {\n    \"Date\": gmt_time,\n    \"Authorization\": (\n        f'Signature keyId=\"{key_id}\",algorithm=\"hmac-sha256\",'\n        f'headers=\"@request-target date x-custom-header-a x-custom-header-b\",'\n        f'signature=\"{signature_base64}\"'\n    ),\n    \"x-custom-header-a\": custom_header_a,\n    \"x-custom-header-b\": custom_header_b\n}\n\n# print headers\nprint(headers)\n```\n\n运行脚本：\n\n```shell\npython3 hmac-sig-req-header-gen.py\n```\n\n您应该看到打印的请求标头：\n\n```text\n{'Date': 'Fri, 06 Sep 2024 09:58:49 GMT', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"', 'x-custom-header-a': 'hello123', 'x-custom-header-b': 'world456'}\n```\n\n使用生成的标头，向路由发送请求：\n\n```shell\ncurl -X GET \"http://127.0.0.1:9080/get\" \\\n     -H \"Date: Fri, 06 Sep 2024 09:58:49 GMT\" \\\n     -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"' \\\n     -H \"x-custom-header-a: hello123\" \\\n     -H \"x-custom-header-b: world456\"\n```\n\n您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"Signature keyId=\\\"john-key\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\\\"\",\n    \"Date\": \"Fri, 06 Sep 2024 09:58:49 GMT\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66d98196-64a58db25ece71c077999ecd\",\n    \"X-Consumer-Username\": \"john\",\n    \"X-Credential-Identifier\": \"cred-john-hmac-auth\",\n    \"X-Custom-Header-A\": \"hello123\",\n    \"X-Custom-Header-B\": \"world456\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"192.168.65.1, 103.97.2.206\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n### 匿名消费者的速率限制\n\n以下示例演示了如何为常规消费者和匿名消费者配置不同的速率限制策略，其中匿名消费者不需要进行身份验证，配额较少。\n\n创建常规消费者 `john`，并配置 `limit-count` 插件，以允许 30 秒内的配额为 3：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n为消费者 `john` 创建 `hmac-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-hmac-auth\",\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"key_id\": \"john-key\",\n        \"secret_key\": \"john-secret-key\"\n      }\n    }\n  }'\n```\n\n创建匿名用户 `anonymous`，并配置 `limit-count` 插件，以允许 30 秒内配额为 1：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n创建路由并配置 `hmac-auth` 插件以接受匿名消费者 `anonymous` 绕过身份验证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"hmac-auth-route\",\n    \"uri\": \"/get\",\n    \"methods\": [\"GET\"],\n    \"plugins\": {\n      \"hmac-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n生成签名。您可以使用以下 Python 代码片段或其他技术栈：\n\n```python title=\"hmac-sig-header-gen.py\"\nimport hmac\nimport hashlib\nimport base64\nfrom datetime import datetime, timezone\n\nkey_id = \"john-key\"                # key id\nsecret_key = b\"john-secret-key\"    # secret key\nrequest_method = \"GET\"             # HTTP method\nrequest_path = \"/get\"              # Route URI\nalgorithm= \"hmac-sha256\"           # can use other algorithms in allowed_algorithms\n\n# get current datetime in GMT\n# note: the signature will become invalid after the clock skew (default 300s)\n# you can regenerate the signature after it becomes invalid, or increase the clock\n# skew to prolong the validity within the advised security boundary\ngmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')\n\n# construct the signing string (ordered)\n# the date and any subsequent custom headers should be lowercased and separated by a\n# single space character, i.e. `<key>:<space><value>`\n# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6\nsigning_string = (\n  f\"{key_id}\\n\"\n  f\"{request_method} {request_path}\\n\"\n  f\"date: {gmt_time}\\n\"\n)\n\n# create signature\nsignature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest()\nsignature_base64 = base64.b64encode(signature).decode('utf-8')\n\n# construct the request headers\nheaders = {\n  \"Date\": gmt_time,\n  \"Authorization\": (\n    f'Signature keyId=\"{key_id}\",algorithm=\"{algorithm}\",'\n    f'headers=\"@request-target date\",'\n    f'signature=\"{signature_base64}\"'\n  )\n}\n\n# print headers\nprint(headers)\n```\n\n运行脚本：\n\n```shell\npython3 hmac-sig-header-gen.py\n```\n\n您应该看到打印的请求标头：\n\n```text\n{'Date': 'Mon, 21 Oct 2024 17:31:18 GMT', 'Authorization': 'Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"ztFfl9w7LmCrIuPjRC/DWSF4gN6Bt8dBBz4y+u1pzt8=\"'}\n```\n\n使用生成的标头发送五个连续的请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H \"Date: Mon, 21 Oct 2024 17:31:18 GMT\" -H 'Authorization: Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"ztFfl9w7LmCrIuPjRC/DWSF4gN6Bt8dBBz4y+u1pzt8=\"' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 5 个请求中，3 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200:    3, 429:    2\n```\n\n发送五个匿名请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，表明只有一个请求成功：\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/http-dubbo.md",
    "content": "---\ntitle: http-dubbo\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - http-dubbo\n  - http to dubbo\ndescription: 本文介绍了关于 Apache APISIX `http-dubbo` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`http-dubbo` 插件可以将 http 请求 encode 为 dubbo 协议转发给上游服务（注意：在 dubbo2.x 时上游服务的序列化类型必须是 fastjson)\n\n## 属性\n\n| 名称                     | 类型    | 必选项 | 默认值 | 有效值      | 描述                                                                                                                                                                                                           |\n| ------------------------ | ------- |-----| ------ | ----------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| service_name             | string  | 是   |        |             | dubbo 服务名                                                                                                                                                                                                    |\n| service_version          | string  | 否   | 0.0.0  |             | dubbo 服务版本 默认 0.0.0                                                                                                                                                                                          |\n| method                   | string  | 是   |        |             | dubbo 服务方法名                                                                                                                                                                                                  |\n| params_type_desc         | string  | 否   |        |             | dubbo 服务方法签名描述，入参如果是 void 可不填写                                                                                                                                                                               |\n| serialization_header_key | string  | 否   |        |             | 插件会读取该请求头判断 body 是否已经按照 dubbo 协议序列化完毕。如果该请求头的值为 true 则插件不会更改 body 内容，直接把他当作 dubbo 请求参数。如果为 false 则要求开发者按照 dubbo 泛化调用的格式传递参数，由插件进行序列化。注意：由于 lua 和 java 的插件序列化精度不同，可能会导致参数精度不同。 |\n| serialized               | boolean | 否   | false  | [true, false] | 和`serialization_header_key`一样。优先级低于`serialization_header_key`                                                                                                                                                |\n| connect_timeout          | number  | 否   | 6000   |             | 上游服务 tcp connect_timeout                                                                                                                                                                                     |\n| read_timeout             | number  | 否   | 6000   |             | 上游服务 tcp read_timeout                                                                                                                                                                                        |\n| send_timeout             | number  | 否   | 6000   |             | 上游服务 tcp send_timeout                                                                                                                                                                                        |\n\n## 启用插件\n\n以下示例展示了如何在指定路由中启用 `http-dubbo` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/TestService/testMethod\",\n    \"plugins\": {\n        \"http-dubbo\": {\n            \"method\": \"testMethod\",\n            \"params_type_desc\": \"Ljava/lang/Long;Ljava/lang/Integer;\",\n            \"serialized\": true,\n            \"service_name\": \"com.xxx.xxx.TestService\",\n            \"service_version\": \"0.0.0\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl --location 'http://127.0.0.1:9080/TestService/testMethod' \\\n--data '1\n2'\n```\n\n## 如何获取 params_type_desc\n\n```java\nMethod[] declaredMethods = YourService.class.getDeclaredMethods();\nString params_type_desc = ReflectUtils.getDesc(Arrays.stream(declaredMethods).filter(it->it.getName().equals(\"yourmethod\")).findAny().get().getParameterTypes());\n\n//方法重载情况下需要找自己需要暴露的方法  ReflectUtils 为 dubbo 实现\n```\n\n## 如何按照 dubbo 协议使用 json 进行序列化\n\n为了防止精度丢失。我们推荐使用序列化好的 body 进行请求。\ndubbo 的 fastjson 序列化规则如下：\n\n- 每个参数之间使用 toJSONString 转化为 JSON 字符串\n\n- 每个参数之间使用换行符 `\\n` 分隔\n\n部分语言和库在字符串或数字调用 toJSONString 后结果是不变的这可能需要你手动处理一些特殊情况例如：\n\n- 字符串 `abc\"` 需要被 encode 为 `\"abc\\\"\"`\n\n- 字符串 `123` 需要被 encode 为 `\"123\"`\n\n抽象类，父类或者泛型作为入参签名，入参需要具体类型时。序列化需要写入具体的类型信息具体参考 [WriteClassName](https://github.com/alibaba/fastjson/wiki/SerializerFeature_cn)\n\n## 删除插件\n\n当你需要禁用 `http-dubbo` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务。\n"
  },
  {
    "path": "docs/zh/latest/plugins/http-logger.md",
    "content": "---\ntitle: http-logger\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 插件\n  - HTTP Logger\n  - 日志\ndescription: 本文介绍了 API 网关 Apache APISIX 的 http-logger 插件。使用该插件可以将 APISIX 的日志数据推送到 HTTP 或 HTTPS 服务器。\n---\n\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`http-logger` 插件可以将 APISIX 的日志数据推送到 HTTP 或 HTTPS 服务器。该插件提供了将日志数据请求作为 JSON 对象发送到监控工具或者其他 HTTP 服务器的功能。\n\n## 属性\n\n| 名称                      | 类型     | 必选项 | 默认值         | 有效值               | 描述                                             |\n|-------------------------| ------- |-----| ------------- | -------------------- | ------------------------------------------------ |\n| uri                     | string  | 是   |               |                      | HTTP 或 HTTPS 服务器的 URI。                   |\n| auth_header             | string  | 否   |               |                      | 授权 header（如果需要）。                                    |\n| timeout                 | integer | 否   | 3             | [1,...]              | 发送请求后保持连接处于活动状态的时间。           |\n| log_format              | object  | 否   |               |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| include_req_body        | boolean | 否   | false         | [false, true]        | 当设置为 `true` 时，将请求体包含在日志中。如果请求体太大而无法保存在内存中，由于 NGINX 的限制，无法记录。 |\n| include_req_body_expr   | array   | 否   |               |                      | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。 |\n| include_resp_body       | boolean | 否   | false         | [false, true]        | 当设置为 `true` 时，包含响应体。                                                                                               |\n| include_resp_body_expr  | array   | 否   |               |                      | 当 `include_resp_body` 属性设置为 `true` 时，使用该属性并基于 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 进行过滤。如果存在，则仅在表达式计算结果为 `true` 时记录响应。       |\n| concat_method           | string  | 否   | \"json\"        | [\"json\", \"new_line\"] | 枚举类型： **json**：对所有待发日志使用 `json.encode` 编码。**new_line**：对每一条待发日志单独使用 `json.encode` 编码并使用 `\\n` 连接起来。 |\n| ssl_verify              | boolean | 否   | false          | [false, true]       | 当设置为 `true` 时验证证书。 |\n\n该插件支持使用批处理器来聚合并批量处理条目（日志和数据）。这样可以避免该插件频繁地提交数据。默认情况下每 `5` 秒钟或队列中的数据达到 `1000` 条时，批处理器会自动提交数据，如需了解更多信息或自定义配置，请参考 [Batch Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n  ```json\n  {\n    \"service_id\": \"\",\n    \"apisix_latency\": 100.99999809265,\n    \"start_time\": 1703907485819,\n    \"latency\": 101.99999809265,\n    \"upstream_latency\": 1,\n    \"client_ip\": \"127.0.0.1\",\n    \"route_id\": \"1\",\n    \"server\": {\n        \"version\": \"3.7.0\",\n        \"hostname\": \"localhost\"\n    },\n    \"request\": {\n        \"headers\": {\n            \"host\": \"127.0.0.1:1984\",\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"user-agent\": \"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\",\n            \"content-length\": \"12\"\n        },\n        \"method\": \"POST\",\n        \"size\": 194,\n        \"url\": \"http://127.0.0.1:1984/hello?log_body=no\",\n        \"uri\": \"/hello?log_body=no\",\n        \"querystring\": {\n            \"log_body\": \"no\"\n        }\n    },\n    \"response\": {\n        \"headers\": {\n            \"content-type\": \"text/plain\",\n            \"connection\": \"close\",\n            \"content-length\": \"12\",\n            \"server\": \"APISIX/3.7.0\"\n        },\n        \"status\": 200,\n        \"size\": 123\n    },\n    \"upstream\": \"127.0.0.1:1982\"\n }\n  ```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否    |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::info 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `http-logger` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/http-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## 启用插件\n\n你可以通过如下命令在指定路由上启用 `http-logger` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"http-logger\": {\n                \"uri\": \"http://mockbin.org/bin/:ID\"\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n[mockbin](http://mockbin.org/bin/create) 服务器用于模拟 HTTP 服务器，以方便查看 APISIX 生成的日志。\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求，访问日志将记录在你的 `mockbin` 服务器中：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ip-restriction.md",
    "content": "---\ntitle: ip-restriction\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - IP restriction\n  - ip-restriction\ndescription: ip-restriction 插件支持通过配置 IP 地址白名单或黑名单来限制 IP 地址对上游资源的访问。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ip-restriction\" />\n</head>\n\n## 描述\n\n`ip-restriction` 插件支持通过配置 IP 地址白名单或黑名单来限制 IP 地址对上游资源的访问。限制 IP 对资源的访问有助于防止未经授权的访问并加强 API 安全性。\n\n## 属性\n\n| 参数名    | 类型          | 必选项 | 默认值 | 有效值 | 描述                             |\n| --------- | ------------- | ------ | ------ | ------ | -------------------------------- |\n| whitelist | array[string] | 否   |        |        | 要列入白名单的 IP 列表。支持 IPv4、IPv6 和 CIDR 表示法。 |\n| blacklist | array[string] | 否   |        |        | 要列入黑名单的 IP 列表。支持 IPv4、IPv6 和 CIDR 表示法。 |\n| message | string | 否   | \"Your IP address is not allowed\" | [1, 1024] | 在未允许的 IP 访问的情况下返回的信息。 |\n\n:::note\n\n`whitelist` 或 `blacklist` 至少配置一个，但不能同时配置。\n\n:::\n\n## 示例\n\n以下示例演示了如何针对不同场景配置 `ip-restriction` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 通过白名单限制访问\n\n以下示例演示了如何将有权访问上游资源的 IP 地址列表列入白名单，并自定义拒绝访问的错误消息。\n\n使用 `ip-restriction` 插件创建路由，将一系列 IP 列入白名单，并自定义拒绝访问时的错误消息：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ip-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ip-restriction\": {\n        \"whitelist\": [\n          \"192.168.0.1/24\"\n        ],\n        \"message\": \"Access denied\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n如果您的 IP 被允许，您应该会收到 `HTTP/1.1 200 OK` 响应。如果不允许，您应该会收到 `HTTP/1.1 403 Forbidden` 响应，并显示以下错误消息：\n\n```text\n{\"message\":\"Access denied\"}\n```\n\n### 使用修改后的 IP 限制访问\n\n以下示例演示了如何使用 `real-ip` 插件修改用于 IP 限制的 IP。如果 APISIX 位于反向代理之后，并且 APISIX 无法获得真实客户端 IP，则此功能特别有用。\n\n使用 `ip-restriction` 插件创建路由，将特定 IP 地址列入白名单，并从 URL 参数 `realip` 获取客户端 IP 地址：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ip-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ip-restriction\": {\n        \"whitelist\": [\n          \"192.168.1.241\"\n        ]\n      },\n      \"real-ip\": {\n        \"source\": \"arg_realip\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n      \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?realip=192.168.1.241\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n使用不同的 IP 地址发送另一个请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?realip=192.168.10.24\"\n```\n\n您应该会收到 `HTTP/1.1 403 Forbidden` 响应。\n"
  },
  {
    "path": "docs/zh/latest/plugins/jwe-decrypt.md",
    "content": "---\ntitle: jwe-decrypt\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - APISIX 插件\n  - JWE Decrypt\n  - jwe-decrypt\ndescription: 本文档包含了关于 APISIX jwe-decrypt 插件的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`jwe-decrypt` 插件，用于解密 APISIX [Service](../terminology/service.md) 或者 [Route](../terminology/route.md) 请求中的 [JWE](https://datatracker.ietf.org/doc/html/rfc7516) 授权请求头。\n\n插件增加了一个 `/apisix/plugin/jwe/encrypt` 的内部 API，提供给 JWE 加密使用。解密时，秘钥应该配置在 [Consumer](../terminology/consumer.md)内。\n\n## 属性\n\nConsumer 配置：\n\n| 名称          | 类型      | 必选项   | 默认值   | 有效值 | 描述                                                          |\n|---------------|---------|-------|-------|-----|-------------------------------------------------------------|\n| key           | string  | True  |       |     | Consumer 的唯一 key                                            |\n| secret        | string  | True  |       |     | 解密密钥，必须为 32 位。秘钥可以使用 [Secret](../terminology/secret.md) 资源保存在密钥管理服务中 |\n| is_base64_encoded | boolean | False | false |     | 如果密钥是 Base64 编码，则需要配置为 `true`                               |\n\n:::note\n\n注意，在启用 `is_base64_encoded` 后，你的 `secret` 长度可能会超过 32 位，你只需要保证在 Decode 后的长度仍然是 32 位即可。\n\n:::\n\nRoute 配置：\n\n| 名称             | 类型      | 必选项   | 默认值           | 描述                                                                         |\n|----------------|---------|-------|---------------|----------------------------------------------------------------------------|\n| header         | string  | True | Authorization | 指定请求头，用于获取加密令牌                                                             |\n| forward_header | string  | True | Authorization | 传递给 Upstream 的请求头名称                                                        |\n| strict         | boolean | False | true          | 如果为配置为 true，请求中缺失 JWE token 则抛出 `403` 异常。如果为 `false`, 在缺失 JWE token 的情况下不会抛出异常 |\n\n## 启用插件\n\n首先，基于 `jwe-decrypt` 插件创建一个 Consumer，并且配置解密密钥：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"jwe-decrypt\": {\n            \"key\": \"user-key\",\n            \"secret\": \"-secret-length-must-be-32-chars-\"\n        }\n    }\n}'\n```\n\n下一步，基于 `jwe-decrypt` 插件创建一个路由，用于解密 authorization 请求头：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/anything*\",\n    \"plugins\": {\n        \"jwe-decrypt\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n\n### 使用 JWE 加密数据\n\n该插件创建了一个内部的 API `/apisix/plugin/jwe/encrypt` 以使用 JWE 进行加密。要公开它，需要创建一个对应的路由，并启用 [public-api](public-api.md) 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/jwenew -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/plugin/jwe/encrypt\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\n向 API 发送一个请求，将 Consumer 中配置的密钥，以参数的方式传递给 URI，用于加密 payload 中的一些数据。\n\n```shell\ncurl -G --data-urlencode 'payload={\"uid\":10000,\"uname\":\"test\"}' 'http://127.0.0.1:9080/apisix/plugin/jwe/encrypt?key=user-key' -i\n```\n\n您应该看到类似于如下内容的响应结果，其中 JWE 加密的数据位于响应体中：\n\n```\nHTTP/1.1 200 OK\nDate: Mon, 25 Sep 2023 02:38:16 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/3.5.0\nApisix-Plugins: public-api\n\neyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.hfzMJ0YfmbMcJ0ojgv4PYAHxPjlgMivmv35MiA.7nilnBt2dxLR_O6kf-HQUA\n```\n\n### 使用 JWE 解密数据\n\n将加密数据放在 `Authorization` 请求头中，向 API 发起请求：\n\n```shell\ncurl http://127.0.0.1:9080/anything/hello -H 'Authorization: eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.hfzMJ0YfmbMcJ0ojgv4PYAHxPjlgMivmv35MiA.7nilnBt2dxLR_O6kf-HQUA' -i\n```\n\n您应该可以看到类似于如下的响应内容，其中 `Authorization` 响应头显示了有效的解密内容：\n\n```\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 452\nConnection: keep-alive\nDate: Mon, 25 Sep 2023 02:38:59 GMT\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\nServer: APISIX/3.5.0\nApisix-Plugins: jwe-decrypt\n\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"{\\\"uid\\\":10000,\\\"uname\\\":\\\"test\\\"}\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.1.2\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6510f2c3-1586ec011a22b5094dbe1896\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 119.143.79.94\",\n  \"url\": \"http://127.0.0.1/anything/hello\"\n}\n```\n\n## 删除插件\n\n要删除 `jwe-decrypt` 插件，您可以从插件配置中删除插件对应的 JSON 配置，APISIX 会自动加载，您不需要重新启动即可生效。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/anything*\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/jwt-auth.md",
    "content": "---\ntitle: jwt-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - JWT Auth\n  - jwt-auth\ndescription: jwt-auth 插件支持使用 JSON Web Token (JWT) 作为客户端在访问上游资源之前进行身份验证的机制。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`jwt-auth` 插件支持使用 [JSON Web Token (JWT)](https://jwt.io/) 作为客户端在访问上游资源之前进行身份验证的机制。\n\n启用后，该插件会公开一个端点，供 [消费者](../terminology/consumer.md) 创建 JWT 凭据。该过程会生成一个令牌，客户端请求应携带该令牌以向 APISIX 标识自己。该令牌可以包含在请求 URL 查询字符串、请求标头或 cookie 中。然后，APISIX 将验证该令牌以确定是否应允许或拒绝请求访问上游资源。\n\n当消费者成功通过身份验证后，APISIX 会在将请求代理到上游服务之前向请求添加其他标头，例如 `X-Consumer-Username`、`X-Credential-Indentifier` 和其他消费者自定义标头（如已配置）。上游服务将能够区分消费者并根据需要实施其他逻辑。如果任何一个值不可用，则不会添加相应的标题。\n\n## 属性\n\nConsumer/Credential 端：\n\n| 名称          | 类型     | 必选项 | 默认值  | 有效值                      | 描述                                                                                                          |\n| ------------- | ------- | ----- | ------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ |\n| key           | string  | 是    |         |                             | 消费者的唯一密钥。  |\n| secret        | string  | 否    |         |                             | 当使用对称算法时，用于对 JWT 进行签名和验证的共享密钥。使用 `HS256` 或 `HS512` 作为算法时必填。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源，将值保存在 Secret Manager 中。   |\n| public_key    | string  | 否    |         |                             | RSA 或 ECDSA 公钥， `algorithm` 属性选择 `RS256` 或 `ES256` 算法时必选。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源，将值保存在 Secret Manager 中。       |\n| algorithm     | string  | 否    | \"HS256\" | [\"HS256\", \"HS384\", \"HS512\", \"RS256\", \"RS384\", \"RS512\", \"ES256\", \"ES384\", \"ES512\", \"PS256\", \"PS384\", \"PS512\", \"EdDSA\"] | 加密算法。                                                                                                      |\n| exp           | integer | 否    | 86400   | [1,...]                     | token 的超时时间。                                                                                              |\n| base64_secret | boolean | 否    | false   |                             | 当设置为 `true` 时，密钥为 base64 编码。                                                                                         |\n| lifetime_grace_period | integer | 否    | 0  | [0,...]                  | 宽限期（以秒为单位）。用于解决生成 JWT 的服务器与验证 JWT 的服务器之间的时钟偏差。 |\n| key_claim_name | string | 否                                                 | key     |                             | JWT payload 中的声明用于标识相关的秘密，例如 `iss`。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"secret\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\nRoute 端：\n\n| 名称   | 类型    | 必选项 | 默认值         | 描述                                                    |\n| ------ | ------ | ------ | ------------- |---------------------------------------------------------|\n| header | string | 否     | authorization | 设置我们从哪个 header 获取 token。                         |\n| query  | string | 否     | jwt           | 设置我们从哪个 query string 获取 token，优先级低于 header。  |\n| cookie | string | 否     | jwt           | 设置我们从哪个 cookie 获取 token，优先级低于 query。        |\n| hide_credentials | boolean | 否     | false  | 如果为 true，则不要将 header、query 或带有 JWT 的 cookie 传递给上游服务。 |\n| key_claim_name | string  | 否     | key           | 包含用户密钥（对应消费者的密钥属性）的 JWT 声明的名称。|\n| anonymous_consumer | string | 否     | false  | 匿名消费者名称。如果已配置，则允许匿名用户绕过身份验证。  |\n| store_in_ctx | boolean | 否     | false  | 设置为 `true` 将会将 JWT 负载存储在请求上下文 (`ctx.jwt_auth_payload`) 中。这允许在同一请求上随后运行的低优先级插件检索和使用 JWT 令牌。 |\n| realm | string | 否 | jwt |在身份验证失败时，应包含在 `WWW-Authenticate` 标头中的域。|\n| claims_to_verify | array[string] | 否 | [\"exp\", \"nbf\"] | [\"exp\", \"nbf\"] | 需要在 JWT 负载中验证的声明。 |\n\n您可以使用 [HashiCorp Vault](https://www.vaultproject.io/) 实施 `jwt-auth`，以从其[加密的 KV 引擎](https://developer.hashicorp.com/vault/docs/secrets/kv) 使用 [APISIX Secret](../terminology/secret.md) 资源。\n\n## 示例\n\n以下示例演示了如何在不同场景中使用 `jwt-auth` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 使用 JWT 进行消费者身份验证\n\n以下示例演示了如何使用 JWT 进行消费者密钥身份验证。\n\n创建消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `jwt-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\n使用 `jwt-auth` 插件创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n要为 `jack` 颁发 JWT，您可以使用 [JWT.io 的 JWT 编码器](https://jwt.io) 或其他实用程序。如果您使用 [JWT.io 的 JWT 编码器](https://jwt.io)，请执行以下操作：\n\n* 填写 `HS256` 作为算法。\n* 将 __Valid secret__ 部分中的密钥更新为 `jack-hs256-secret-that-is-very-long`。\n* 使用消费者密钥 `jack-key` 更新有效 payload；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n将生成的 JWT 保存到变量中：\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\n使用 `Authorization` 标头中的 JWT 向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: ${jwt_token}\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjY2NDk2NDAsImtleSI6ImphY2sta2V5In0.kdhumNWrZFxjUvYzWLt4lFr546PNsr9TXuf0Az5opoM\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66ea951a-4d740d724bd2a44f174d4daf\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-jwt-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n使用无效的令牌发送请求以验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjY2NDk2NDAsImtleSI6ImphY2sta2V5In0.kdhumNWrZFxjU_random_random\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 401 Unauthorized` 响应：\n\n```text\n{\"message\":\"failed to verify jwt\"}\n```\n\n### 在请求标头、查询字符串或 Cookie 中携带 JWT\n\n以下示例演示如何在指定的标头、查询字符串和 Cookie 中接受 JWT。\n\n创建一个消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `jwt-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `jwt-auth` 插件的路由，并指定请求可以在标头、查询或 cookie 中携带令牌：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"header\": \"jwt-auth-header\",\n        \"query\": \"jwt-query\",\n        \"cookie\": \"jwt-cookie\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n要为 `jack` 颁发 JWT，您可以使用 [JWT.io 的 JWT 编码器](https://jwt.io) 或其他实用程序。如果您使用 [JWT.io 的 JWT 编码器](https://jwt.io)，请执行以下操作：\n\n* 填写 `HS256` 作为算法。\n* 将 __Valid secret__ 部分中的密钥更新为 `jack-hs256-secret-that-is-very-long`。\n* 使用消费者密钥 `jack-key` 更新有效 payload；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n将生成的 JWT 保存到变量中：\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\n#### 使用标头中的 JWT 进行验证\n\n发送标头中包含 JWT 的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"jwt-auth-header: ${jwt_token}\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"Jwt-Auth-Header\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\",\n    ...\n  },\n  ...\n}\n```\n\n#### 在查询字符串中使用 JWT 进行验证\n\n在查询字符串中使用 JWT 发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get?jwt-query=${jwt_token}\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {\n    \"jwt-query\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    ...\n  },\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://127.0.0.1/get?jwt-query=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTY5NTEyOTA0NH0.EiktFX7di_tBbspbjmqDKoWAD9JG39Wo_CAQ1LZ9voQ\"\n}\n```\n\n#### 使用 Cookie 中的 JWT 进行验证\n\n使用 cookie 中的 JWT 发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" --cookie jwt-cookie=${jwt_token}\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Cookie\": \"jwt-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\",\n    ...\n  },\n  ...\n}\n```\n\n### 在环境变量中管理密钥\n\n以下示例演示了如何将 `jwt-auth` 消费者密钥保存到环境变量并在配置中引用它。\n\nAPISIX 支持引用通过 [NGINX `env` 指令](https://nginx.org/en/docs/ngx_core_module.html#env) 配置的系统和用户环境变量。\n\n将密钥保存到环境变量中：\n\n```shell\nexport JACK_JWT_SECRET=jack-hs256-secret-that-is-very-long\n```\n\n:::tip\n\n如果您在 Docker 中运行 APISIX，需要在启动容器时使用 `-e` flag 设置环境变量。\n\n:::\n\n创建一个消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `jwt-auth` 凭证并引用环境变量：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"$env://JACK_JWT_SECRET\"\n      }\n    }\n  }'\n```\n\n创建启用 `jwt-auth` 的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n要为 `jack` 颁发 JWT，您可以使用 [JWT.io 的 JWT 编码器](https://jwt.io) 或其他实用程序。如果您使用 [JWT.io 的 JWT 编码器](https://jwt.io)，请执行以下操作：\n\n* 填写 `HS256` 作为算法。\n* 将 __Valid secret__ 部分中的密钥更新为 `jack-hs256-secret-that-is-very-long`。\n* 使用消费者密钥 `jack-key` 更新有效 payload；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n将生成的 JWT 保存到变量中：\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\n发送标头中包含 JWT 的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Authorization: ${jwt_token}\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n### 在 Secret Manager 中管理 Secret\n\n以下示例演示了如何在 [HashiCorp Vault](https://www.vaultproject.io) 中管理 `jwt-auth` 消费者密钥，并在插件配置中引用它。\n\n在 Docker 中启动 Vault 开发服务器：\n\n```shell\ndocker run -d \\\n  --name vault \\\n  -p 8200:8200 \\\n  --cap-add IPC_LOCK \\\n  -e VAULT_DEV_ROOT_TOKEN_ID=root \\\n  -e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \\\n  vault:1.9.0 \\\n  vault server -dev\n```\n\nAPISIX 目前支持 [Vault KV 引擎版本 1](https://developer.hashicorp.com/vault/docs/secrets/kv#kv-version-1)。请在 Vault 中启用它：\n\n```shell\ndocker exec -i vault sh -c \"VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\nSuccess! Enabled the kv secrets engine at: kv/\n```\n\n创建一个 Secret，并配置 Vault 地址和其他连接信息。根据情况相应地更新 Vault 地址：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/secrets/vault/jwt\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"https://127.0.0.1:8200\",\n    \"prefix\": \"kv/apisix\",\n    \"token\": \"root\"\n  }'\n```\n\n创建消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `jwt-auth` 凭证并引用 Secret：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jwt-vault-key\",\n        \"secret\": \"$secret://vault/jwt/jack/jwt-secret\"\n      }\n    }\n  }'\n```\n\n创建启用 `jwt-auth` 的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n在 Vault 中将 `jwt-auth` 键值设置为 `vault-hs256-secret-that-is-very-long`：\n\n```shell\ndocker exec -i vault sh -c \"VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jack jwt-secret=vault-hs256-secret-that-is-very-long\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\nSuccess! Data written to: kv/apisix/jack\n```\n\n要为 `jack` 颁发 JWT，您可以使用 [JWT.io 的 JWT 编码器](https://jwt.io) 或其他实用程序。如果您使用 [JWT.io 的 JWT 编码器](https://jwt.io)，请执行以下操作：\n\n* 填写 `HS256` 作为算法。\n* 将 __Valid secret__ 部分中的密钥更新为 `vault-hs256-secret-that-is-very-long`。\n* 使用消费者密钥 `jwt-vault-key` 更新有效 payload；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jwt-vault-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n复制生成的 JWT 并保存到变量：\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqd3QtdmF1bHQta2V5IiwibmJmIjoxNzI5MTMyMjcxfQ.i2pLj7QcQvnlSjB7iV5V522tIV43boQRtee7L0rwlkQ\n```\n\n发送带有令牌作为标头的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Authorization: ${jwt_token}\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n### 使用 RS256 算法签名 JWT\n\n以下示例演示了如何在实现 JWT 消费者身份验证时使用非对称算法（例如 RS256）对 JWT 进行签名和验证。您将使用 [openssl](https://openssl-library.org/source/) 生成 RSA 密钥对，并使用 [JWT.io](https://jwt.io) 生成 JWT，以便更好地理解 JWT 的组成。\n\n生成一个 2048 位 RSA 私钥，并提取相应的 PEM 格式公钥：\n\n```shell\nopenssl genrsa -out jwt-rsa256-private.pem 2048\nopenssl rsa -in jwt-rsa256-private.pem -pubout -out jwt-rsa256-public.pem\n```\n\n您应该看到在当前工作目录中生成的 `jwt-rsa256-private.pem` 和 `jwt-rsa256-public.pem`。\n\n访问 [JWT.io 的 JWT 编码器](https://jwt.io) 并执行以下操作：\n\n* 填写 `RS256` 作为算法。\n* 将私钥内容复制并粘贴到 __SIGN JWT: PRIVATE KEY__ 部分。\n* 使用消费者密钥 `jack-key` 更新有效负载；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n复制生成的 JWT 并保存到变量：\n\n```shell\nexport jwt_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.K-I13em84kAcyH1jfIJl7ls_4jlwg1GzEzo5_xrDu-3wt3Xa3irS6naUsWpxX-a-hmcZZxRa9zqunqQjUP4kvn5e3xg2f_KyCR-_ZbwqYEPk3bXeFV1l4iypv6z5L7W1Niharun-dpMU03b1Tz64vhFx6UwxNL5UIZ7bunDAo_BXZ7Xe8rFhNHvIHyBFsDEXIBgx8lNYMq8QJk3iKxZhZZ5Om7lgYjOOKRgew4WkhBAY0v1AkO77nTlvSK0OEeeiwhkROyntggyx-S-U222ykMQ6mBLxkP4Cq5qHwXD8AUcLk5mhEij-3QhboYnt7yhKeZ3wDSpcjDvvL2aasC25ng\n```\n\n创建一个消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `jwt-auth` 凭证并配置 RSA 密钥：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"algorithm\": \"RS256\",\n        \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoTxe7ZPycrEP0SK4OBA2\\n0OUQsDN9gSFSHVvx/t++nZNrFxzZnV6q6/TRsihNXUIgwaOu5icFlIcxPL9Mf9UJ\\na5/XCQExp1TxpuSmjkhIFAJ/x5zXrC8SGTztP3SjkhYnQO9PKVXI6ljwgakVCfpl\\numuTYqI+ev7e45NdK8gJoJxPp8bPMdf8/nHfLXZuqhO/btrDg1x+j7frDNrEw+6B\\nCK2SsuypmYN+LwHfaH4Of7MQFk3LNIxyBz0mdbsKJBzp360rbWnQeauWtDymZxLT\\nATRNBVyl3nCNsURRTkc7eyknLaDt2N5xTIoUGHTUFYSdE68QWmukYMVGcEHEEPkp\\naQIDAQAB\\n-----END PUBLIC KEY-----\"\n      }\n    }\n  }'\n```\n\n:::tip\n\n您应该在起始行之后和结束行之前添加换行符，例如 `-----BEGIN PUBLIC KEY-----\\n......\\n-----END PUBLIC KEY-----`。\n\n密钥内容可以直接连接。\n\n:::\n\n使用 `jwt-auth` 插件创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-route\",\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n使用 `Authorization` 标头中的 JWT 向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: ${jwt_token}\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n### 将消费者自定义 ID 添加到标头\n\n以下示例演示了如何在 `Consumer-Custom-Id` 标头中将消费者自定义 ID 附加到已验证的请求，该 ID 可用于根据需要实现其他逻辑。\n\n创建一个带有自定义 ID 标签的消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\n为消费者创建 `jwt-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\n使用 `jwt-auth` 创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"jwt-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n要为 `jack` 颁发 JWT，您可以使用 [JWT.io 的 JWT 编码器](https://jwt.io) 或其他实用程序。如果您使用 [JWT.io 的 JWT 编码器](https://jwt.io)，请执行以下操作：\n\n* 填写 `HS256` 作为算法。\n* 将 __Valid secret__ 部分中的密钥更新为 `jack-hs256-secret-that-is-very-long`。\n* 使用消费者密钥 `jack-key` 更新有效 payload；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n复制生成的 JWT 并保存到变量：\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\n为了验证，使用 `Authorization` 标头中的 JWT 向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\" -H \"Authorization: ${jwt_token}\"\n```\n\n您应该会看到类似以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Authorization\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6873b19d-329331db76e5e7194c942b47\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-jwt-auth\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### 匿名消费者的速率限制\n\n以下示例演示了如何为普通消费者和匿名消费者配置不同的速率限制策略，其中匿名消费者无需身份验证，且配额较少。\n\n创建一个普通消费者 `jack`，并配置 `limit-count` 插件，允许在 30 秒内使用 3 个配额：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n为消费者 `jack` 创建 `jwt-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-jwt-auth\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"key\": \"jack-key\",\n        \"secret\": \"jack-hs256-secret-that-is-very-long\"\n      }\n    }\n  }'\n```\n\n创建匿名用户 `anonymous`，并配置 `limit-count` 插件，以允许 30 秒内配额为 1：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n创建一个路由并配置 `jwt-auth` 插件以接受匿名消费者 `anonymous` 绕过身份验证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"jwt-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"jwt-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n要为 `jack` 颁发 JWT，您可以使用 [JWT.io 的 JWT 编码器](https://jwt.io) 或其他实用程序。如果您使用 [JWT.io 的 JWT 编码器](https://jwt.io)，请执行以下操作：\n\n* 填写 `HS256` 作为算法。\n* 将 __Valid secret__ 部分中的密钥更新为 `jack-hs256-secret-that-is-very-long`。\n* 使用消费者密钥 `jack-key` 更新有效 payload；并添加 `exp` 或 `nbf` UNIX 时间戳。\n\n  您的 payload 应类似于以下内容：\n\n  ```json\n  {\n    \"key\": \"jack-key\",\n    \"nbf\": 1729132271\n  }\n  ```\n\n将生成的 JWT 复制到 __Encoded__ 部分并保存到变量中：\n\n```shell\nexport jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqYWNrLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.UEPXy5jpid624T1XpfjM0PLY73LZPjV3Qt8yZ92kVuU\n```\n\n为了验证速率限制，请使用 jack 的 JWT 连续发送五个请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H \"Authorization: ${jwt_token}\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 5 个请求中，3 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200:    3, 429:    2\n```\n\n发送五个匿名请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，表明只有一个请求成功：\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/kafka-logger.md",
    "content": "---\ntitle: kafka-logger\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Kafka Logger\ndescription: API 网关 Apache APISIX 的 kafka-logger 插件用于将日志作为 JSON 对象推送到 Apache Kafka 集群中。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`kafka-logger` 插件用于将日志作为 JSON 对象推送到 Apache Kafka 集群中。可用作 `ngx_lua` NGINX 模块的 Kafka 客户端驱动程序。\n\n## 属性\n\n| 名称                   | 类型    | 必选项 | 默认值          | 有效值                | 描述                                             |\n| ---------------------- | ------- | ------ | -------------- | --------------------- | ------------------------------------------------ |\n| broker_list            | object  | 是     |                |                       | 已废弃，现使用 `brokers` 属性代替。原指需要推送的 Kafka 的 broker 列表。                  |\n| brokers                | array   | 是     |                |                       | 需要推送的 Kafka 的 broker 列表。                   |\n| brokers.host           | string  | 是     |                |                       | Kafka broker 的节点 host 配置，例如 `192.168.1.1`                     |\n| brokers.port           | string  | 是     |                |                       | Kafka broker 的节点端口配置                         |\n| brokers.sasl_config    | object  | 否     |                |                       | Kafka broker 中的 sasl_config                     |\n| brokers.sasl_config.mechanism  | string  | 否     | \"PLAIN\"          | [\"PLAIN\", \"SCRAM-SHA-256\", \"SCRAM-SHA-512\"]   | Kafka broker 中的 sasl 认证机制                     |\n| brokers.sasl_config.user       | string  | 是     |                  |             | Kafka broker 中 sasl 配置中的 user，如果 sasl_config 存在，则必须填写                 |\n| brokers.sasl_config.password   | string  | 是     |                  |             | Kafka broker 中 sasl 配置中的 password，如果 sasl_config 存在，则必须填写             |\n| kafka_topic            | string  | 是     |                |                       | 需要推送的 topic。                                 |\n| producer_type          | string  | 否     | async          | [\"async\", \"sync\"]     | 生产者发送消息的模式。          |\n| required_acks          | integer | 否     | 1              | [1, -1]            | 生产者在确认一个请求发送完成之前需要收到的反馈信息的数量。该参数是为了保证发送请求的可靠性。该属性的配置与 Kafka `acks` 属性相同，具体配置请参考 [Apache Kafka 文档](https://kafka.apache.org/documentation/#producerconfigs_acks)。required_acks 还不支持为 0。  |\n| key                    | string  | 否     |                |                       | 用于消息分区而分配的密钥。                             |\n| timeout                | integer | 否     | 3              | [1,...]               | 发送数据的超时时间。                             |\n| name                   | string  | 否     | \"kafka logger\" |                       | 标识 logger 的唯一标识符。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。                     |\n| meta_format            | enum    | 否     | \"default\"      | [\"default\"，\"origin\"] | `default`：获取请求信息以默认的 JSON 编码方式。`origin`：获取请求信息以 HTTP 原始请求方式。更多信息，请参考 [meta_format](#meta_format-示例)。|\n| log_format             | object  | 否   | |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| include_req_body       | boolean | 否     | false          | [false, true]         | 当设置为 `true` 时，包含请求体。**注意**：如果请求体无法完全存放在内存中，由于 NGINX 的限制，APISIX 无法将它记录下来。|\n| include_req_body_expr  | array   | 否     |                |                       | 当 `include_req_body` 属性设置为 `true` 时进行过滤。只有当此处设置的表达式计算结果为 `true` 时，才会记录请求体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n| max_req_body_bytes     | integer | 否    | 524288         | >=1                   | 允许的最大请求正文（以字节为单位）。在此限制内的请求体将被推送到 Kafka。如果大小超过配置值，则正文在推送到 Kafka 之前将被截断。                                                                                                                                                                                                  |\n| include_resp_body      | boolean | 否     | false          | [false, true]         | 当设置为 `true` 时，包含响应体。 |\n| include_resp_body_expr | array   | 否     |                |                       | 当 `include_resp_body` 属性设置为 `true` 时进行过滤。只有当此处设置的表达式计算结果为 `true` 时才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。|\n| max_resp_body_bytes    | integer | 否    | 524288         | >=1                   | 允许的最大响应正文（以字节为单位）。低于此限制的响应主体将被推送到 Kafka。如果大小超过配置值，则正文在推送到 Kafka 之前将被截断。                                                                                                                                                                                                  |\n| cluster_name           | integer | 否     | 1              | [0,...]               | Kafka 集群的名称，当有两个及以上 Kafka 集群时使用。只有当 `producer_type` 设为 `async` 模式时才可以使用该属性。|\n| producer_batch_num     | integer | 否     | 200            | [1,...]               | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的 `batch_num` 参数，聚合消息批量提交，单位为消息条数。 |\n| producer_batch_size    | integer | 否     | 1048576        | [0,...]               | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的 `batch_size` 参数，单位为字节。 |\n| producer_max_buffering | integer | 否     | 50000          | [1,...]               | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的 `max_buffering` 参数，表示最大缓冲区，单位为条。 |\n| producer_time_linger   | integer | 否     | 1              | [1,...]               | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的 `flush_time` 参数，单位为秒。|\n| meta_refresh_interval | integer  | 否     | 30             | [1,...]               | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的 `refresh_interval` 参数，用于指定自动刷新 metadata 的间隔时长，单位为秒。 |\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置) 配置部分。\n\n:::tip 提示\n\n数据首先写入缓冲区。当缓冲区超过 `batch_max_size` 或 `buffer_duration` 设置的值时，则会将数据发送到 Kafka 服务器并刷新缓冲区。\n\n如果发送成功，则返回 `true`。如果出现错误，则返回 `nil`，并带有描述错误的字符串 `buffer overflow`。\n\n:::\n\n### meta_format 示例\n\n- `default`:\n\n    ```json\n    {\n     \"upstream\": \"127.0.0.1:1980\",\n     \"start_time\": 1619414294760,\n     \"client_ip\": \"127.0.0.1\",\n     \"service_id\": \"\",\n     \"route_id\": \"1\",\n     \"request\": {\n       \"querystring\": {\n         \"ab\": \"cd\"\n       },\n       \"size\": 90,\n       \"uri\": \"/hello?ab=cd\",\n       \"url\": \"http://localhost:1984/hello?ab=cd\",\n       \"headers\": {\n         \"host\": \"localhost\",\n         \"content-length\": \"6\",\n         \"connection\": \"close\"\n       },\n       \"body\": \"abcdef\",\n       \"method\": \"GET\"\n     },\n     \"response\": {\n       \"headers\": {\n         \"connection\": \"close\",\n         \"content-type\": \"text/plain; charset=utf-8\",\n         \"date\": \"Mon, 26 Apr 2021 05:18:14 GMT\",\n         \"server\": \"APISIX/2.5\",\n         \"transfer-encoding\": \"chunked\"\n       },\n       \"size\": 190,\n       \"status\": 200\n     },\n     \"server\": {\n       \"hostname\": \"localhost\",\n       \"version\": \"2.5\"\n     },\n     \"latency\": 0\n    }\n    ```\n\n- `origin`:\n\n    ```http\n    GET /hello?ab=cd HTTP/1.1\n    host: localhost\n    content-length: 6\n    connection: close\n\n    abcdef\n    ```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        |  描述                                             |\n| ---------------- | ------- | ------ | ------------- |------------------------------------------------ |\n| log_format       | object  | 否   |   | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::note 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `kafka-logger` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/kafka-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## 如何启用\n\n你可以通过如下命令在指定路由上启用 `kafka-logger` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"kafka-logger\": {\n            \"brokers\" : [\n              {\n               \"host\": \"127.0.0.1\",\n               \"port\": 9092\n              }\n            ],\n           \"kafka_topic\" : \"test2\",\n           \"key\" : \"key1\"\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n该插件还支持一次推送到多个 Broker，示例如下：\n\n```json\n\"brokers\" : [\n    {\n      \"host\" :\"127.0.0.1\",\n      \"port\" : 9092\n    },\n    {\n      \"host\" :\"127.0.0.1\",\n      \"port\" : 9093\n    }\n],\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/key-auth.md",
    "content": "---\ntitle: key-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Key Auth\n  - key-auth\ndescription: key-auth 插件支持使用身份验证密钥作为客户端在访问上游资源之前进行身份验证的机制。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n    <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/key-auth\" />\n</head>\n\n## 描述\n\n`key-auth` 插件支持使用身份验证密钥作为客户端在访问上游资源之前进行身份验证的机制。\n\n要使用该插件，您需要在 [Consumers](../terminology/consumer.md) 上配置身份验证密钥，并在路由或服务上启用该插件。密钥可以包含在请求 URL 查询字符串或请求标头中。然后，APISIX 将验证密钥以确定是否应允许或拒绝请求访问上游资源。\n\n当消费者成功通过身份验证后，APISIX 会在将请求代理到上游服务之前向请求添加其他标头，例如 `X-Consumer-Username`、`X-Credential-Indentifier` 和其他消费者自定义标头（如果已配置）。上游服务将能够区分消费者并根据需要实现其他逻辑。如果这些值中的任何一个不可用，则不会添加相应的标头。\n\n## 属性\n\nConsumer/Credential 端：\n\n| 名称 | 类型   | 必选项  | 描述                                                                                                          |\n| ---- | ------ | ------ | ------------------------------------------------------------------------------------------------------------- |\n| key  | string | 是     | 不同的 Consumer 应有不同的 `key`，它应当是唯一的。如果多个 Consumer 使用了相同的 `key`，将会出现请求匹配异常。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源，将值保存在 Secret Manager 中。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"key\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\nRoute 端：\n\n| 名称              | 类型   | 必选项 | 默认值 | 描述                                                                                                                                                       |\n| ----------------- | ------ | ----- | ------ |----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| header            | string | 否    | apikey | 设置我们从哪个 header 获取 key。                                                                                                                                   |\n| query             | string | 否    | apikey | 设置我们从哪个 query string 获取 key，优先级低于 `header`。                                                                                                              |\n| hide_credentials  | boolean | 否    | false  | 如果为 `true`，则不要将含有认证信息的 header 或 query string 传递给 Upstream。  |\n| realm | string | 否 | key |在身份验证失败时，应包含在 `WWW-Authenticate` 标头中的域。|\n\n## 示例\n\n以下示例演示了如何在不同场景中使用 `key-auth` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 在路由上实现密钥认证\n\n以下示例演示如何在路由上实现密钥认证并将密钥包含在请求标头中。\n\n创建一个消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\n使用 `key-auth` 创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n#### 使用有效密钥进行验证\n\n使用有效密钥发送请求至：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'apikey: jack-key'\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应。\n\n#### 使用无效密钥进行验证\n\n使用无效密钥发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'apikey: wrong-key'\n```\n\n您应该看到以下 `HTTP/1.1 401 Unauthorized` 响应：\n\n```text\n{\"message\":\"Invalid API key in request\"}\n```\n\n#### 无需密钥即可验证\n\n无需密钥即可发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该看到以下 `HTTP/1.1 401 Unauthorized` 响应：\n\n```text\n{\"message\":\"Missing API key found in request\"}\n```\n\n### 隐藏上游的身份验证信息\n\n以下示例演示如何通过配置 `hide_credentials` 来防止密钥被发送到上游服务。默认情况下，身份验证密钥被转发到上游服务，这在某些情况下可能会导致安全风险。\n\n创建一个消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\n#### 不隐藏凭据\n\n使用 `key-auth` 创建路由，并将 `hide_credentials` 配置为 `false` (默认配置)：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"id\": \"key-auth-route\",\n  \"uri\": \"/anything\",\n  \"plugins\": {\n    \"key-auth\": {\n      \"hide_credentials\": false\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n发送带有有效密钥的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?apikey=jack-key\"\n```\n\n您应该看到以下 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {\n    \"auth\": \"jack-key\"\n  },\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-key-auth\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6502d8a5-2194962a67aa21dd33f94bb2\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 103.248.35.179\",\n  \"url\": \"http://127.0.0.1/anything?apikey=jack-key\"\n}\n```\n\n注意凭证 `jack-key` 对于上游服务是可见的。\n\n#### 隐藏凭据\n\n将插件的 `hide_credentials` 更新为 `true`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/key-auth-route\" -X PATCH \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"plugins\": {\n    \"key-auth\": {\n      \"hide_credentials\": true\n    }\n  }\n}'\n```\n\n发送带有有效密钥的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?apikey=jack-key\"\n```\n\n您应该看到以下 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-key-auth\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6502d85c-16f34dbb5629a5960183e803\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 103.248.35.179\",\n  \"url\": \"http://127.0.0.1/anything\"\n}\n```\n\n注意凭证 `jack-key` 对上游服务不再可见。\n\n### 演示标头和查询中的密钥优先级\n\n以下示例演示了如何在路由上实现消费者的密钥身份验证，并自定义应包含密钥的 URL 参数。该示例还显示，当在标头和查询字符串中都配置了 API 密钥时，请求标头具有更高的优先级。\n\n创建消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\n使用 `key-auth` 创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n-H \"X-API-KEY: ${admin_key}\" \\\n-d '{\n  \"id\": \"key-auth-route\",\n  \"uri\": \"/anything\",\n  \"plugins\": {\n    \"key-auth\": {\n      \"query\": \"auth\"\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n#### 使用有效密钥进行验证\n\n使用有效密钥发送请求至：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=jack-key\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n#### 使用无效密钥进行验证\n\n使用无效密钥发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=wrong-key\"\n```\n\n您应该看到以下 `HTTP/1.1 401 Unauthorized` 响应：\n\n```text\n{\"message\":\"Invalid API key in request\"}\n```\n\n#### 使用查询字符串中的有效密钥进行验证\n\n但是，如果您在标头中包含有效密钥，而 URL 查询字符串中仍包含无效密钥：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=wrong-key\" -H 'apikey: jack-key'\n```\n\n您应该会看到 `HTTP/1.1 200 OK` 响应。这表明标头中包含的密钥始终具有更高的优先级。\n\n### 将消费者自定义 ID 添加到标头\n\n以下示例演示了如何在 `Consumer-Custom-Id` 标头中将消费者自定义 ID 附加到经过身份验证的请求，该 ID 可用于根据需要实现其他逻辑。\n\n创建一个带有自定义 ID 标签的消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"labels\": {\n      \"custom_id\": \"495aec6a\"\n    }\n  }'\n```\n\nCreate `key-auth` credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\nCreate a Route with `key-auth`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\nTo verify, send a request to the Route with the valid key:\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?auth=jack-key\"\n```\n\nYou should see an `HTTP/1.1 200 OK` response similar to the following:\n\n```json\n{\n  \"args\": {\n    \"auth\": \"jack-key\"\n  },\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-66ea8d64-33df89052ae198a706e18c2a\",\n    \"X-Consumer-Username\": \"jack\",\n    \"X-Credential-Identifier\": \"cred-jack-key-auth\",\n    \"X-Consumer-Custom-Id\": \"495aec6a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"192.168.65.1, 205.198.122.37\",\n  \"url\": \"http://127.0.0.1/anything?apikey=jack-key\"\n}\n```\n\n### 匿名消费者的速率限制\n\n以下示例演示了如何为常规消费者和匿名消费者配置不同的速率限制策略，其中匿名消费者不需要进行身份验证，并且配额较少。\n\n创建常规消费者 `jack` 并配置 `limit-count` 插件以允许 30 秒内的配额为 3：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n为消费者 `jack` 创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\n创建匿名用户 `anonymous`，并配置 `limit-count`插件，以允许 30 秒内配额为 1：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n创建路由并配置 `key-auth` 插件以接受匿名消费者 `anonymous` 绕过身份验证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n为了验证，请使用 `jack` 的密钥发送五个连续的请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: jack-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 5 个请求中，3 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200:    3, 429:    2\n```\n\n发送五个匿名请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，表明只有一个请求成功：\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ldap-auth.md",
    "content": "---\ntitle: ldap-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - LDAP Authentication\n  - ldap-auth\ndescription: 本篇文档介绍了 Apache APISIX ldap-auth 插件的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ldap-auth` 插件可用于给路由或服务添加 LDAP 身份认证，该插件使用 [lua-resty-ldap](https://github.com/api7/lua-resty-ldap) 连接 LDAP 服务器。\n\n该插件需要与 Consumer 一起配合使用，API 的调用方可以使用 [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) 与 LDAP 服务器进行认证。\n\n## 属性\n\nConsumer 端：\n\n| 名称    | 类型   | 必选项 | 描述                                                                      |\n| ------- | ------ | -------- | -------------------------------------------------------------------------------- |\n| user_dn | string | 是     | LDAP 客户端的 dn，例如：`cn=user01,ou=users,dc=example,dc=org`。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源，将值保存在 Secret Manager 中。 |\n\nRoute 端：\n\n| 名称     | 类型    | 必选项 | 默认值 | 描述                                                            |\n|----------|---------|----------|---------|------------------------------------------------------------------------|\n| base_dn  | string  | 是     |         | LDAP 服务器的 dn，例如：`ou=users,dc=example,dc=org`。|\n| ldap_uri | string  | 是     |         | LDAP 服务器的 URI。                                                |\n| use_tls  | boolean | 否    | false  | 如果设置为 `true` 则表示启用 TLS。                                             |\n| tls_verify| boolean  | 否     | false        | 是否校验 LDAP 服务器的证书。如果设置为 `true`，你必须设置 `config.yaml` 里面的 `ssl_trusted_certificate`，并且确保 `ldap_uri` 里的 host 和服务器证书中的 host 匹配。 |\n| uid      | string  | 否    | cn    | UID 属性。                                                         |\n| realm | string | 否 | ldap |在身份验证失败时，应包含在 `WWW-Authenticate` 标头中的域。|\n\n## 启用插件\n\n首先，你需要创建一个 Consumer 并在其中配置该插件，具体代码如下：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"foo\",\n    \"plugins\": {\n        \"ldap-auth\": {\n            \"user_dn\": \"cn=user01,ou=users,dc=example,dc=org\"\n        }\n    }\n}'\n```\n\n然后就可以在指定路由或服务中启用该插件，具体代码如下：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"ldap-auth\": {\n            \"base_dn\": \"ou=users,dc=example,dc=org\",\n            \"ldap_uri\": \"localhost:1389\",\n            \"uid\": \"cn\"\n        },\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述方法配置插件后，可以通过以下命令测试插件：\n\n```shell\ncurl -i -uuser01:password1 http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n如果授权信息请求头丢失或无效，则请求将被拒绝（如下展示了几种返回结果）：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Missing authorization in request\"}\n```\n\n```shell\ncurl -i -uuser:password1 http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Invalid user authorization\"}\n```\n\n```shell\ncurl -i -uuser01:passwordfalse http://127.0.0.1:9080/hello\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Invalid user authorization\"}\n```\n\n## 删除插件\n\n当你需要禁用 `ldap-auth` 插件时，可以通过以下命令删除相应的 JSON 配置。APISIX 将自动重新加载，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/limit-conn.md",
    "content": "---\ntitle: limit-conn\nkeywords:\n  - APISIX\n  - API 网关\n  - Limit Connection\ndescription: limit-conn 插件通过管理并发连接来限制请求速率。超过阈值的请求可能会被延迟或拒绝，以确保 API 使用受控并防止过载。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/limit-conn\" />\n</head>\n\n## 描述\n\n`limit-conn` 插件通过并发连接数来限制请求速率。超过阈值的请求将根据配置被延迟或拒绝，从而确保可控的资源使用并防止过载。\n\n## 属性\n\n| 名称        | 类型    | 必选项    | 默认值 | 有效值                      | 描述              |\n|------------|---------|----------|-------|----------------------------|------------------|\n| conn | integer | 否 | | > 0 | 允许的最大并发请求数。超过配置的限制且低于`conn + burst`的请求将被延迟。如果未配置 `rules`,则为必填项。|\n| burst | integer | 否 | | >= 0 | 每秒允许延迟的过多并发请求数。超过限制的请求将被立即拒绝。如果未配置 `rules`,则为必填项。|\n| default_conn_delay | number | 是 | | > 0 | 允许超过`conn + burst`的并发请求的处理延迟（秒），可根据`only_use_default_delay`设置动态调整。|\n| only_use_default_delay | boolean | 否 | false | | 如果为 false，则根据请求超出`conn`限制的程度按比例延迟请求。拥塞越严重，延迟就越大。例如，当 `conn` 为 `5`、`burst` 为 `3` 且 `default_conn_delay` 为 `1` 时，6 个并发请求将导致 1 秒的延迟，7 个请求将导致 2 秒的延迟，8 个请求将导致 3 秒的延迟，依此类推，直到达到 `conn + burst` 的总限制，超过此限制的请求将被拒绝。如果为 true，则使用 `default_conn_delay` 延迟 `burst` 范围内的所有超额请求。超出 `conn + burst` 的请求将被立即拒绝。例如，当 `conn` 为 `5`、`burst` 为 `3` 且 `default_conn_delay` 为 `1` 时，6、7 或 8 个并发请求都将延迟 1 秒。|\n| rules                    | array[object] | 否    |       |                   | 连接限制规则列表。每个规则是一个包含 `conn`、`burst` 和 `key` 的对象。如果配置了此项，则优先于 `conn`、`burst` 和 `key`。 |\n| rules.conn               | integer 或 string | 是 |       | > 0 或变量表达式 | 允许的最大并发请求数。可以是静态整数或变量表达式，如 `$http_custom_conn`。 |\n| rules.burst              | integer 或 string | 是 |       | >= 0 或变量表达式 | 允许延迟的过多并发请求数。可以是静态整数或变量表达式。 |\n| rules.key                | string  | 是     |       |                   | 用于计数请求的键。如果配置的键不存在，则不会执行该规则。`key` 被解释为变量组合，例如：`$http_custom_a $http_custom_b`。 |\n| key_type | string | 否 | var | [\"var\",\"var_combination\"] | key 的类型。如果`key_type` 为 `var`，则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`，则 `key` 将被解释为变量的组合。 |\n| key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`，则 `key` 将被解释为变量。变量不需要以美元符号（`$`）为前缀。如果 `key_type` 为 `var_combination`，则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如，要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合，则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果未配置 `rules`，则为必填项。|\n| key_ttl | integer | 否 | 3600 | | Redis 键的 TTL（以秒为单位）。当 `policy` 为 `redis` 或 `redis-cluster` 时使用。 |\n| rejected_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。|\n| rejection_msg | string | 否 | | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。|\n| allow_degradation | boolean | 否 | false | | 如果为 true，则允许 APISIX 在插件或其依赖项不可用时继续处理没有插件的请求。|\n| policy | string | 否 | local | [\"local\",\"re​​dis\",\"re​​dis-cluster\"] | 速率限制计数器的策略。如果是 `local`，则计数器存储在本地内存中。如果是 `redis`，则计数器存储在 Redis 实例上。如果是 `redis-cluster`，则计数器存储在 Redis 集群中。|\n| redis_host | string | 否 | | | Redis 节点的地址。当 `policy` 为 `redis` 时必填。 |\n| redis_port | integer | 否 | 6379 | [1,...] | 当 `policy` 为 `redis` 时，Redis 节点的端口。 |\n| redis_username | string | 否 | | | 如果使用 Redis ACL，则为 Redis 的用户名。如果使用旧式身份验证方法 `requirepass`，则仅配置 `redis_password`。当 `policy` 为 `redis` 时使用。 |\n| redis_password | string | 否 | | | 当 `policy` 为 `redis` 或 `redis-cluster` 时，Redis 节点的密码。 |\n| redis_ssl | boolean | 否 | false |如果为 true，则在 `policy` 为 `redis` 时使用 SSL 连接到 Redis 集群。|\n| redis_ssl_verify | boolean | 否 | false | | 如果为 true，则在 `policy` 为 `redis` 时验证服务器 SSL 证书。|\n| redis_database | integer | 否 | 0 | >= 0 | 当 `policy` 为 `redis` 时，Redis 中的数据库编号。|\n| redis_timeout | integer | 否 | 1000 | [1,...] | 当 `policy` 为 `redis` 或 `redis-cluster` 时，Redis 超时值（以毫秒为单位）。 |\n| redis_keepalive_timeout | integer | 否 | 10000 | ≥ 1000 | 当 `policy` 为 `redis` 或 `redis-cluster` 时，与 `redis` 或 `redis-cluster` 的空闲连接超时时间，单位为毫秒。|\n| redis_keepalive_pool | integer | 否 | 100 | ≥ 1 | 当 `policy` 为 `redis` 或 `redis-cluster` 时，与 `redis` 或 `redis-cluster` 的连接池最大连接数。|\n| redis_cluster_nodes | array[string] | 否 | | | 具有至少两个地址的 Redis 群集节点列表。当 policy 为 redis-cluster 时必填。 |\n| redis_cluster_name | string | 否 | | | Redis 集群的名称。当 `policy` 为 `redis-cluster` 时必须使用。|\n| redis_cluster_ssl | boolean | 否 | false | | 如果为 `true`，当 `policy` 为 `redis-cluster`时，使用 SSL 连接 Redis 集群。|\n| redis_cluster_ssl_verify | boolean | 否 | false | | 如果为 `true`，当 `policy` 为 `redis-cluster` 时，验证服务器 SSL 证书。  |\n\n## 示例\n\n以下示例演示了如何在不同场景中配置 `limit-conn`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 通过远程地址应用速率限制\n\n以下示例演示如何使用 `limit-conn` 通过 `remote_addr` 限制请求速率，并附带示例连接和突发阈值。\n\n使用 `limit-conn` 插件创建路由，以允许 2 个并发请求和 1 个过多的并发请求。此外：\n\n* 配置插件，允许超过 `conn + burst` 的并发请求有 0.1 秒的处理延迟。\n* 将密钥类型设置为 `vars`，以将 `key` 解释为变量。\n* 根据请求的 `remote_address` 计算速率限制计数。\n* 将 `policy` 设置为 `local`，以使用内存中的本地计数器。\n* 将 `rejected_code` 自定义为 `429`。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-conn\": {\n        \"conn\": 2,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\",\n        \"policy\": \"local\",\n        \"rejected_code\": 429\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送五个并发请求：\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\"'\n```\n\n您应该会看到类似以下内容的响应，其中超过阈值的请求被拒绝：\n\n```text\nResponse: 200\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\n```\n\n### 通过远程地址和消费者名称应用速率限制\n\n以下示例演示如何使用 `limit-conn` 通过变量组合 `remote_addr` 和 `consumer_name` 对请求进行速率限制。\n\n创建消费者 `john`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建第二个消费者 `jane`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `key-auth` 和 `limit-conn` 插件的路由，并在 `limit-conn` 插件中指定使用变量组合作为速率限制 key：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"limit-conn\": {\n        \"conn\": 2,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"rejected_code\": 429,\n        \"key_type\": \"var_combination\",\n        \"key\": \"$remote_addr $consumer_name\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n作为消费者 `john` 发送五个并发请求：\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\" -H \"apikey: john-key\"'\n```\n\n您应该会看到类似以下内容的响应，其中超过阈值的请求被拒绝：\n\n```text\nResponse: 200\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\n```\n\n接下来立刻以消费者 `jane` 的身份发送五个并发请求：\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\" -H \"apikey: jane-key\"'\n```\n\n您还应该看到类似以下内容的响应，其中过多的请求被拒绝：\n\n```text\nResponse: 200\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\n```\n\n### 限制 WebSocket 连接速率\n\n以下示例演示了如何使用 `limit-conn` 插件来限制并发 WebSocket 连接的数量。\n\n启动 [上游 WebSocket 服务器](https://hub.docker.com/r/jmalloc/echo-server)：\n\n```shell\ndocker run -d \\\n  -p 8080:8080 \\\n  --name websocket-server \\\n  --network=apisix-quickstart-net \\\n  jmalloc/echo-server\n```\n\n创建到服务器 WebSocket 端点的路由，并为路由启用 WebSocket。相应地调整 WebSocket 服务器地址。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"ws-route\",\n  \"uri\": \"/.ws\",\n  \"plugins\": {\n    \"limit-conn\": {\n      \"conn\": 2,\n      \"burst\": 1,\n      \"default_conn_delay\": 0.1,\n      \"key_type\": \"var\",\n      \"key\": \"remote_addr\",\n      \"rejected_code\": 429\n    }\n  },\n  \"enable_websocket\": true,\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"websocket-server:8080\": 1\n    }\n  }\n}'\n```\n\n安装 WebSocket 客户端，例如 [websocat](https://github.com/vi/websocat)，通过以下路由与 WebSocket 服务器建立连接：\n\n```shell\nwebsocat \"ws://127.0.0.1:9080/.ws\"\n```\n\n在终端中发送 `hello` 消息，您应该会看到 WebSocket 服务器回显相同的消息：\n\n```text\nRequest served by 1cd244052136\nhello\nhello\n```\n\n再打开三个终端会话并运行：\n\n```shell\nwebsocat \"ws://127.0.0.1:9080/.ws\"\n```\n\n由于速率限制的影响，当您尝试与服务器建立 WebSocket 连接时，您应该会看到最后一个终端会话打印 `429 Too Many Requests`。\n\n### 使用 Redis 服务器在 APISIX 节点之间共享配额\n\n以下示例演示了使用 Redis 服务器对多个 APISIX 节点之间的请求进行速率限制，以便不同的 APISIX 节点共享相同的速率限制配额。\n\n在每个 APISIX 实例上，使用以下配置创建路由。相应地调整管理 API、Redis 主机、端口、密码和数据库的地址。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-conn\": {\n        \"conn\": 1,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"rejected_code\": 429,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis\",\n        \"redis_host\": \"192.168.xxx.xxx\",\n        \"redis_port\": 6379,\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_database\": 1\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送五个并发请求：\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\"'\n```\n\n您应该会看到类似以下内容的响应，其中超过阈值的请求被拒绝：\n\n```text\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\nResponse: 429\n```\n\n这表明在不同 APISIX 实例中配置的两个路由共享相同的配额。\n\n### 使用 Redis 集群在 APISIX 节点之间共享配额\n\n您还可以使用 Redis 集群在多个 APISIX 节点之间应用相同的配额，以便不同的 APISIX 节点共享相同的速率限制配额。\n\n确保您的 Redis 实例在 [集群模式](https://redis.io/docs/management/scaling/#create-and-use-a-redis-cluster) 下运行。`limit-conn` 插件配置至少需要两个节点。\n\n在每个 APISIX 实例上，使用以下配置创建一个路由。相应地调整管理 API 的地址、Redis 集群节点、密码、集群名称和 SSL 验证。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-conn-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-conn\": {\n        \"conn\": 1,\n        \"burst\": 1,\n        \"default_conn_delay\": 0.1,\n        \"rejected_code\": 429,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis-cluster\",\n        \"redis_cluster_nodes\": [\n          \"192.168.xxx.xxx:6379\",\n          \"192.168.xxx.xxx:16379\"\n        ],\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_cluster_name\": \"redis-cluster-1\",\n        \"redis_cluster_ssl\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送五个并发请求：\n\n```shell\nseq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w \"Response: %{http_code}\\n\" \"http://127.0.0.1:9080/get\"'\n```\n\n您应该会看到类似以下内容的响应，其中超过阈值的请求被拒绝：\n\n```text\nResponse: 200\nResponse: 200\nResponse: 429\nResponse: 429\nResponse: 429\n```\n\n这表明在不同的 APISIX 实例中配置的两条路由共享相同的配额。\n"
  },
  {
    "path": "docs/zh/latest/plugins/limit-count.md",
    "content": "---\ntitle: limit-count\nkeywords:\n  - APISIX\n  - API 网关\n  - Limit Count\n  - 速率限制\ndescription: limit-count 插件使用固定窗口算法，通过给定时间间隔内的请求数量来限制请求速率。超过配置配额的请求将被拒绝。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/limit-count\" />\n</head>\n\n## 描述\n\n`limit-count` 插件使用固定窗口算法，通过给定时间间隔内的请求数量来限制请求速率。超过配置配额的请求将被拒绝。\n\n您可能会在响应中看到以下速率限制标头：\n\n* `X-RateLimit-Limit`：总配额\n* `X-RateLimit-Remaining`：剩余配额\n* `X-RateLimit-Reset`：计数器重置的剩余秒数\n\n## 属性\n\n| 名称                | 类型    | 必选项      | 默认值        | 有效值                                   | 描述                                                                                                                                                                                                                                 |\n| ------------------- | ------- | ---------- | ------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| count | integer | 否 | | > 0 | 给定时间间隔内允许的最大请求数。如果未配置 `rules`，则此项必填。 |\n| time_window | integer | 否 | | > 0 | 速率限制 `count` 对应的时间间隔（以秒为单位）。如果未配置 `rules`，则此项必填。 |\n| rules | array[object] | 否 | | | 速率限制规则列表。每个规则是一个包含 `count`、`time_window` 和 `key` 的对象。如果配置了 `rules`，则顶层的 `count` 和 `time_window` 将被忽略。 |\n| rules.count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 |\n| rules.time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔（以秒为单位）。 |\n| rules.key | string | 是 | | | 用于统计请求的键。如果配置的键不存在，则不会执行该规则。`key` 被解释为变量的组合，例如：`$http_custom_a $http_custom_b`。|\n| rules.header_prefix | string | 否 | | | 速率限制标头的前缀。如果已配置，响应将包含 `X-{header_prefix}-RateLimit-Limit`、`X-{header_prefix}-RateLimit-Remaining` 和 `X-{header_prefix}-RateLimit-Reset` 标头。如果未配置，则使用规则数组中规则的索引作为前缀。例如，第一个规则的标头将是 `X-1-RateLimit-Limit`、`X-1-RateLimit-Remaining` 和 `X-1-RateLimit-Reset`。|\n| key_type | string | 否 | var | [\"var\",\"var_combination\",\"constant\"] | key 的类型。如果`key_type` 为 `var`，则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`，则 `key` 将被解释为变量的组合。如果 `key_type` 为 `constant`，则 `key` 将被解释为常量。 |\n| key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`，则 `key` 将被解释为变量。变量不需要以美元符号（`$`）为前缀。如果 `key_type` 为 `var_combination`，则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如，要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合，则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`，则 `key` 会被解释为常量值。|\n| rejected_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。|\n| rejection_msg | string | 否 | | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。|\n| policy | string | 否 | local | [\"local\",\"re​​dis\",\"re​​dis-cluster\"] | 速率限制计数器的策略。如果是 `local`，则计数器存储在本地内存中。如果是 `redis`，则计数器存储在 Redis 实例上。如果是 `redis-cluster`，则计数器存储在 Redis 集群中。|\n| allow_degradation | boolean | 否 | false | | 如果为 true，则允许 APISIX 在插件或其依赖项不可用时继续处理没有插件的请求。|\n| show_limit_quota_header | boolean | 否 | true | | 如果为 true，则在响应标头中包含 `X-RateLimit-Limit` 以显示总配额和 `X-RateLimit-Remaining` 以显示剩余配额。|\n| group | string | 否 | | 非空 | 插件的 `group` ID，以便同一 `group` 的路由可以共享相同的速率限制计数器。 |\n| redis_host | string | 否 | | | Redis 节点的地址。当 `policy` 为 `redis` 时必填。 |\n| redis_port | integer | 否 | 6379 | [1,...] | 当 `policy` 为 `redis` 时，Redis 节点的端口。 |\n| redis_username | string | 否 | | | 如果使用 Redis ACL，则为 Redis 的用户名。如果使用旧式身份验证方法 `requirepass`，则仅配置 `redis_password`。当 `policy` 为 `redis` 时使用。 |\n| redis_password | string | 否 | | | 当 `policy` 为 `redis` 或 `redis-cluster` 时，Redis 节点的密码。 |\n| redis_ssl | boolean | 否 | false |如果为 true，则在 `policy` 为 `redis` 时使用 SSL 连接到 Redis 集群。|\n| redis_ssl_verify | boolean | 否 | false | | 如果为 true，则在 `policy` 为 `redis` 时验证服务器 SSL 证书。|\n| redis_database | integer | 否 | 0 | >= 0 | 当 `policy` 为 `redis` 时，Redis 中的数据库编号。|\n| redis_timeout | integer | 否 | 1000 | [1,...] | 当 `policy` 为 `redis` 或 `redis-cluster` 时，Redis 超时值（以毫秒为单位）。 |\n| redis_keepalive_timeout | integer | 否 | 10000 | ≥ 1000 | 当 `policy` 为 `redis` 或 `redis-cluster` 时，与 `redis` 或 `redis-cluster` 的空闲连接超时时间，单位为毫秒。|\n| redis_keepalive_pool | integer | 否 | 100 | ≥ 1 | 当 `policy` 为 `redis` 或 `redis-cluster` 时，与 `redis` 或 `redis-cluster` 的连接池最大连接数。|\n| redis_cluster_nodes | array[string] | 否 | | | 具有至少两个地址的 Redis 群集节点列表。当 policy 为 redis-cluster 时必填。 |\nredis_cluster_name | string | 否 | | | | Redis 集群的名称。当 `policy` 为 `redis-cluster` 时必须使用。|\n| redis_cluster_ssl | boolean | 否 | false | | 如果为 `true`，当 `policy` 为 `redis-cluster`时，使用 SSL 连接 Redis 集群。|\n| redis_cluster_ssl_verify | boolean | 否 | false | | 如果为 `true`，当 `policy` 为 `redis-cluster` 时，验证服务器 SSL 证书。  |\n\n## 示例\n\n下面的示例演示了如何在不同情况下配置 `limit-count` 。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 按远程地址应用速率限制\n\n下面的示例演示了通过单一变量 `remote_addr` 对请求进行速率限制。\n\n创建一个带有 `limit-count` 插件的路由，允许在 30 秒窗口内为每个远程地址设置 1 个配额：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key_type\": \"var\",\n        \"key\": \"remote_addr\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送验证请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该会看到 `HTTP/1.1 200 OK` 响应。\n\n该请求已消耗了时间窗口允许的所有配额。如果您在相同的 30 秒时间间隔内再次发送该请求，您应该会收到 `HTTP/1.1 429 Too Many Requests` 响应，表示该请求超出了配额阈值。\n\n### 通过远程地址和消费者名称应用速率限制\n\n以下示例演示了通过变量 `remote_addr` 和 `consumer_name` 的组合对请求进行速率限制。它允许每个远程地址和每个消费者在 30 秒窗口内有 1 个配额。\n\n创建消费者 `john`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建第二个消费者 `jane`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `key-auth` 和 `limit-count` 插件的路由，并在 `limit-count` 插件中指定使用变量组合作为速率限制键：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key_type\": \"var_combination\",\n        \"key\": \"$remote_addr $consumer_name\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n以消费者 `jane` 的身份发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: jane-key'\n```\n\n您应该会看到一个 `HTTP/1.1 200 OK` 响应以及相应的响应主体。\n\n此请求已消耗了为时间窗口设置的所有配额。如果您在相同的 30 秒时间间隔内向消费者 `jane` 发送相同的请求，您应该会收到一个 `HTTP/1.1 429 Too Many Requests` 响应，表示请求超出了配额阈值。\n\n在相同的 30 秒时间间隔内向消费者 `john` 发送相同的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key'\n```\n\n您应该看到一个 `HTTP/1.1 200 OK` 响应和相应的响应主体，表明请求不受速率限制。\n\n在相同的 30 秒时间间隔内再次以消费者 `john` 的身份发送相同的请求，您应该收到一个 `HTTP/1.1 429 Too Many Requests` 响应。\n\n这通过变量 `remote_addr` 和 `consumer_name` 的组合验证了插件速率限制。\n\n### 在路由之间共享配额\n\n以下示例通过配置 `limit-count` 插件的 `group` 演示了在多个路由之间共享速率限制配额。\n\n请注意，同一 `group` 的 `limit-count` 插件的配置应该相同。为了避免更新异常和重复配置，您可以创建一个带有 `limit-count` 插件和上游的服务，以供路由连接。\n\n创建服务：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/services\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-service\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"group\": \"srv1\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n创建两个路由，并将其 `service_id` 配置为 `limit-count-service`，以便它们对插件和上游共享相同的配置：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route-1\",\n    \"service_id\": \"limit-count-service\",\n    \"uri\": \"/get1\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/get\"\n      }\n    }\n  }'\n```\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route-2\",\n    \"service_id\": \"limit-count-service\",\n    \"uri\": \"/get2\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/get\"\n      }\n    }\n  }'\n```\n\n:::note\n\n[`proxy-rewrite`](./proxy-rewrite.md) 插件用于将 URI 重写为 `/get`，以便将请求转发到正确的端点。\n\n:::\n\n向路由 `/get1` 发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get1\"\n```\n\n您应该会看到一个 `HTTP/1.1 200 OK` 响应以及相应的响应主体。\n\n在相同的 30 秒时间间隔内向路由 `/get2` 发送相同的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get2\"\n```\n\n您应该收到 `HTTP/1.1 429 Too Many Requests` 响应，这验证两个路由共享相同的速率限制配额。\n\n### 使用 Redis 服务器在 APISIX 节点之间共享配额\n\n以下示例演示了使用 Redis 服务器对多个 APISIX 节点之间的请求进行速率限制，以便不同的 APISIX 节点共享相同的速率限制配额。\n\n在每个 APISIX 实例上，使用以下配置创建一个路由。相应地调整管理 API 的地址、Redis 主机、端口、密码和数据库。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis\",\n        \"redis_host\": \"192.168.xxx.xxx\",\n        \"redis_port\": 6379,\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_database\": 1\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向 APISIX 实例发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该会看到一个 `HTTP/1.1 200 OK` 响应以及相应的响应主体。\n\n在相同的 30 秒时间间隔内向不同的 APISIX 实例发送相同的请求，您应该会收到一个 `HTTP/1.1 429 Too Many Requests` 响应，验证在不同 APISIX 节点中配置的路由是否共享相同的配额。\n\n### 使用 Redis 集群在 APISIX 节点之间共享配额\n\n您还可以使用 Redis 集群在多个 APISIX 节点之间应用相同的配额，以便不同的 APISIX 节点共享相同的速率限制配额。\n\n确保您的 Redis 实例在 [集群模式](https://redis.io/docs/management/scaling/#create-and-use-a-redis-cluster) 下运行。`limit-count` 插件配置至少需要两个节点。\n\n在每个 APISIX 实例上，使用以下配置创建路由。相应地调整管理 API 的地址、Redis 集群节点、密码、集群名称和 SSL 验证。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-count-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429,\n        \"key\": \"remote_addr\",\n        \"policy\": \"redis-cluster\",\n        \"redis_cluster_nodes\": [\n          \"192.168.xxx.xxx:6379\",\n          \"192.168.xxx.xxx:16379\"\n        ],\n        \"redis_password\": \"p@ssw0rd\",\n        \"redis_cluster_name\": \"redis-cluster-1\",\n        \"redis_cluster_ssl\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向 APISIX 实例发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该会看到一个 `HTTP/1.1 200 OK` 响应以及相应的响应主体。\n\n在相同的 30 秒时间间隔内向不同的 APISIX 实例发送相同的请求，您应该会收到一个 `HTTP/1.1 429 Too Many Requests` 响应，验证在不同 APISIX 节点中配置的路由是否共享相同的配额。\n\n### 使用匿名消费者进行速率限制\n\n以下示例演示了如何为常规和匿名消费者配置不同的速率限制策略，其中匿名消费者不需要进行身份验证并且配额较少。虽然此示例使用 [`key-auth`](./key-auth.md) 进行身份验证，但匿名消费者也可以使用 [`basic-auth`](./basic-auth.md)、[`jwt-auth`](./jwt-auth.md) 和 [`hmac-auth`](./hmac-auth.md) 进行配置。\n\n创建一个消费者 `john`，并配置 `limit-count` 插件，以允许 30 秒内配额为 3：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 3,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n为消费者 `john` 创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建匿名用户 `anonymous`，并配置 `limit-count` 插件，以允许 30 秒内配额为 1：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"anonymous\",\n    \"plugins\": {\n      \"limit-count\": {\n        \"count\": 1,\n        \"time_window\": 30,\n        \"rejected_code\": 429\n      }\n    }\n  }'\n```\n\n创建路由并配置 `key-auth` 插件以接受匿名消费者 `anonymous` 绕过身份验证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"key-auth-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"anonymous_consumer\": \"anonymous\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n使用 `john` 的密钥发送五个连续的请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: john-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 5 个请求中，3 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200:    3, 429:    2\n```\n\n发送五个匿名请求：\n\n```shell\nresp=$(seq 5 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，表明只有一个请求成功：\n\n```text\n200:    1, 429:    4\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/limit-req.md",
    "content": "---\ntitle: limit-req\nkeywords:\n  - APISIX\n  - API 网关\n  - Limit Request\n  - limit-req\ndescription: limit-req 插件使用漏桶算法来限制请求的数量并允许节流。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/limit-req\" />\n</head>\n\n## 描述\n\n`limit-req` 插件使用 [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket) 算法来限制请求的数量并允许节流。\n\n## 属性\n\n| 名称          | 类型    | 必选项 | 默认值 | 有效值                                                                                  | 描述                                                                                                                                              |\n| ------------- | ------- | ------ | ------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| rate | integer | True | | > 0 | 每秒允许的最大请求数。超过速率且低于突发的请求将被延迟。|\n| bust | integer | True | | >= 0 | 每秒允许延迟的请求数，以进行限制。超过速率和突发的请求将被拒绝。|\n| key_type | string | 否 | var | [\"var\",\"var_combination\"] | key 的类型。如果 `key_type` 为 `var`，则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`，则 `key` 将被解释为变量的组合。 |\n| key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`，则 `key` 将被解释为变量。变量不需要以美元符号（`$`）为前缀。如果 `key_type` 为 `var_combination`，则 `key` 会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如，要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 的组合，则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`，则 `key` 会被解释为常量值。|\n| rejected_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 状态代码。|\n| rejection_msg | string | 否 | | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。|\n| nodelay           | boolean | 否    | false   |                            | 如果为 true，则不要延迟突发阈值内的请求。                                                                        |\n| allow_degradation | boolean | 否 | false | | 如果为 true，则允许 APISIX 在插件或其依赖项不可用时继续处理没有插件的请求。|\n| policy | string | 否 | local | [\"local\",\"re​​dis\",\"re​​dis-cluster\"] | 速率限制计数器的策略。如果是 `local`，则计数器存储在本地内存中。如果是 `redis`，则计数器存储在 Redis 实例上。如果是 `redis-cluster`，则计数器存储在 Redis 集群中。|\n| allow_degradation | boolean | 否 | false | | 如果为 true，则允许 APISIX 在插件或其依赖项不可用时继续处理没有插件的请求。|\n| show_limit_quota_header | boolean | 否 | true | | 如果为 true，则在响应标头中包含 `X-RateLimit-Limit` 以显示总配额和 `X-RateLimit-Remaining` 以显示剩余配额。|\n| redis_host | string | 否 | | | Redis 节点的地址。当 `policy` 为 `redis` 时必填。 |\n| redis_port | integer | 否 | 6379 | [1,...] | 当 `policy` 为 `redis` 时，Redis 节点的端口。 |\n| redis_username | string | 否 | | | 如果使用 Redis ACL，则为 Redis 的用户名。如果使用旧式身份验证方法 `requirepass`，则仅配置 `redis_password`。当 `policy` 为 `redis` 时使用。 |\n| redis_password | string | 否 | | | 当 `policy` 为 `redis` 或 `redis-cluster` 时，Redis 节点的密码。 |\n| redis_ssl | boolean | 否 | false |如果为 true，则在 `policy` 为 `redis` 时使用 SSL 连接到 Redis 集群。|\n| redis_ssl_verify | boolean | 否 | false | | 如果为 true，则在 `policy` 为 `redis` 时验证服务器 SSL 证书。|\n| redis_database | integer | 否 | 0 | >= 0 | 当 `policy` 为 `redis` 时，Redis 中的数据库编号。|\n| redis_timeout | integer | 否 | 1000 | [1,...] | 当 `policy` 为 `redis` 或 `redis-cluster` 时，Redis 超时值（以毫秒为单位）。 |\n| redis_keepalive_timeout | integer | 否 | 10000 | ≥ 1000 | 当 `policy` 为 `redis` 或 `redis-cluster` 时，与 `redis` 或 `redis-cluster` 的空闲连接超时时间，单位为毫秒。|\n| redis_keepalive_pool | integer | 否 | 100 | ≥ 1 | 当 `policy` 为 `redis` 或 `redis-cluster` 时，与 `redis` 或 `redis-cluster` 的连接池最大连接数。|\n| redis_cluster_nodes | array[string] | 否 | | | 具有至少两个地址的 Redis 群集节点列表。当 policy 为 redis-cluster 时必填。 |\nredis_cluster_name | string | 否 | | | | Redis 集群的名称。当 `policy` 为 `redis-cluster` 时必须使用。|\n| redis_cluster_ssl | boolean | 否 | false | | 如果为 `true`，当 `policy` 为 `redis-cluster`时，使用 SSL 连接 Redis 集群。|\n| redis_cluster_ssl_verify | boolean | 否 | false | | 如果为 `true`，当 `policy` 为 `redis-cluster` 时，验证服务器 SSL 证书。  |\n\n## 示例\n\n以下示例演示了如何在不同场景中配置 `limit-req`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 通过远程地址应用速率限制\n\n以下示例演示了通过单个变量 `remote_addr` 对 HTTP 请求进行速率限制。\n\n使用 `limit-req` 插件创建允许每个远程地址 1 QPS 的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '\n  {\n    \"id\": \"limit-req-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-req\": {\n        \"rate\": 1,\n        \"burst\": 0,\n        \"key\": \"remote_addr\",\n        \"key_type\": \"var\",\n        \"rejected_code\": 429,\n        \"nodelay\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求以验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该会看到一个 `HTTP/1.1 200 OK` 响应。\n\n该请求已消耗了时间窗口允许的所有配额。如果您在同一秒内再次发送请求，您应该会收到 `HTTP/1.1 429 Too Many Requests` 响应，表示请求超出了配额阈值。\n\n### 允许速率限制阈值\n\n以下示例演示了如何配置 `burst` 以允许速率限制阈值超出配置的值并实现请求限制。您还将看到与未实施限制时的比较。\n\n使用 `limit-req` 插件创建一个路由，允许每个远程地址 1 QPS，并将 `burst` 设置为 1，以允许 1 个超过 `rate` 的请求延迟处理：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-req-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"limit-req\": {\n        \"rate\": 1,\n        \"burst\": 1,\n        \"key\": \"remote_addr\",\n        \"rejected_code\": 429\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n生成三个对路由的请求：\n\n```shell\nresp=$(seq 3 | xargs -I{} curl -i \"http://127.0.0.1:9080/get\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200 responses: $count_200 ; 429 responses: $count_429\"\n```\n\n您可能会看到所有三个请求都成功：\n\n```text\n200 responses: 3 ; 429 responses: 0\n```\n\n现在，将 `burst` 更新为 0 或将 `nodelay` 设置为 `true`，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/limit-req-route\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"limit-req\": {\n        \"nodelay\": true\n      }\n    }\n  }'\n```\n\n再次向路由生成三个请求：\n\n```shell\nresp=$(seq 3 | xargs -I{} curl -i \"http://127.0.0.1:9080/get\" -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200 responses: $count_200 ; 429 responses: $count_429\"\n```\n\n您应该会看到类似以下内容的响应，表明超出速率的请求已被拒绝：\n\n```text\n200 responses: 1 ; 429 responses: 2\n```\n\n### 通过远程地址和消费者名称应用速率限制\n\n以下示例演示了通过变量组合 `remote_addr` 和 `consumer_name` 来限制请求的速率。\n\n使用 `limit-req` 插件创建一个路由，允许每个远程地址和每个消费者 有 1 QPS。\n\n创建消费者 `john`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建第二个消费者 `jane`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `key-auth` 和 `limit-req` 插件的路由，并在 `limit-req` 插件中指定使用变量组合作为速率限制 key：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"limit-req-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"limit-req\": {\n        \"rate\": 1,\n        \"burst\": 0,\n        \"key\": \"$remote_addr $consumer_name\",\n        \"key_type\": \"var_combination\",\n        \"rejected_code\": 429\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n同时发送两个请求，每个请求针对一个消费者：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: jane-key' & \\\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key' &\n```\n\n您应该会收到两个请求的 `HTTP/1.1 200 OK`，表明请求未超过每个消费者的阈值。\n\n如果您在同一秒内以任一消费者身份发送更多请求，应该会收到 `HTTP/1.1 429 Too Many Requests` 响应。\n\n这验证了插件速率限制是通过变量 `remote_addr` 和 `consumer_name` 的来实现的。\n"
  },
  {
    "path": "docs/zh/latest/plugins/log-rotate.md",
    "content": "---\ntitle: log-rotate\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - 日志切分\ndescription: 云原生 API 网关 Apache APISIX log-rotate 插件用于定期切分日志目录下的访问日志和错误日志。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`log-rotate` 插件用于定期切分日志目录下的访问日志和错误日志。\n\n你可以自定义日志轮换的频率以及要保留的日志数量。当日志数量超过限制时，旧的日志会被自动删除。\n\n## 参数\n\n| 名称               | 类型     | 必选项 | 默认值  | 有效值        | 描述                                                                          |\n| ------------------ | ------- | ------ | ------- | ------------- | ---------------------------------------------------------------------------- |\n| interval           | integer | 是     | 60 * 60 |               | 每间隔多长时间切分一次日志，以秒为单位。                                        |\n| max_kept           | integer | 是     | 24 * 7  |               | 最多保留多少份历史日志，超过指定数量后，自动删除老文件。                         |\n| max_size           | integer | 否     | -1      |               | 日志文件超过指定大小时进行切分，单位为 Byte。如果 `max_size` 小于 0 或者根据 `interval` 计算的时间到达时，将不会根据 `max_size` 切分日志。 |\n| enable_compression | boolean | 否     | false   | [false, true] | 当设置为 `true` 时，启用日志文件压缩。该功能需要在系统中安装 `tar` 。     |\n\n开启该插件后，就会按照参数自动切分日志文件了。比如以下示例是根据 `interval: 10` 和 `max_kept: 10` 得到的样本。\n\n```shell\nll logs\n```\n\n```\ntotal 44K\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-40_access.log\n-rw-r--r--. 1 resty resty 2.8K Mar 20 20:33 2020-03-20_20-33-40_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-33-50_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:33 2020-03-20_20-33-50_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:33 2020-03-20_20-34-00_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:34 2020-03-20_20-34-00_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:34 2020-03-20_20-34-10_access.log\n-rw-r--r--. 1 resty resty 2.4K Mar 20 20:34 2020-03-20_20-34-10_error.log\n-rw-r--r--. 1 resty resty    0 Mar 20 20:34 access.log\n-rw-r--r--. 1 resty resty 1.5K Mar 20 21:31 error.log\n```\n\n当开启日志文件压缩时，日志文件名称如下所示：\n\n```shell\nll logs\n```\n\n```shell\ntotal 10.5K\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:33 2020-03-20_20-33-50_access.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:33 2020-03-20_20-33-50_error.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:33 2020-03-20_20-34-00_access.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:34 2020-03-20_20-34-00_error.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:34 2020-03-20_20-34-10_access.log.tar.gz\n-rw-r--r--. 1 resty resty  1.5K Mar 20 20:34 2020-03-20_20-34-10_error.log.tar.gz\n-rw-r--r--. 1 resty resty    0 Mar 20 20:34 access.log\n-rw-r--r--. 1 resty resty 1.5K Mar 20 21:31 error.log\n```\n\n## 启用插件\n\n**该插件默认为禁用状态**，你可以在 `./conf/config.yaml` 中启用 `log-rotate` 插件，不需要在任何路由或服务中绑定。\n\n```yaml title=\"./conf/config.yaml\"\nplugins:\n    # the plugins you enabled\n    - log-rotate\n\nplugin_attr:\n    log-rotate:\n        interval: 3600    # rotate interval (unit: second)\n        max_kept: 168     # max number of log files will be kept\n        max_size: -1      # max size of log files will be kept\n        enable_compression: false    # enable log file compression(gzip) or not, default false\n```\n\n配置完成，你需要重新加载 APISIX。\n\n## 删除插件\n\n当你不再需要该插件时，只需要在 `./conf/config.yaml` 中删除或注释该插件即可。\n\n```yaml\nplugins:\n    # the plugins you enabled\n    # - log-rotate\n\nplugin_attr:\n\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/loggly.md",
    "content": "---\ntitle: loggly\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - SolarWinds Loggly\ndescription: API 网关 Apache APISIX loggly 插件可用于将日志转发到 SolarWinds Loggly 进行分析和存储。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`loggly` 插件可用于将日志转发到 [SolarWinds Loggly](https://www.solarwinds.com/loggly) 进行分析和存储。\n\n当启用插件时，APISIX 会将请求上下文信息序列化为符合 [Loggly Syslog](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/streaming-syslog-without-using-files.htm?cshid=loggly_streaming-syslog-without-using-files) 的数据格式，即具有 [RFC5424](https://datatracker.ietf.org/doc/html/rfc5424) 兼容标头的 Syslog。\n\n## 属性\n\n| 名称                   | 类型          | 必选项 | 默认值 | 描述                                                                                                                                                                                                              |\n|------------------------|---------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------|\n| customer_token         | string        | 是      |         | 将日志发送到 Loggly 时使用的唯一标识符，以确保将日志发送到正确的组织帐户。                                                                                                       |\n| severity               | string (enum) | 否      | INFO    | Syslog 日志事件的严重性级别。包括：`DEBUG`、`INFO`、`NOTICE`、`WARNING`、`ERR`、`CRIT`、`ALERT` 和 `EMEGR`。                                         |\n| severity_map           | object        | 否      | nil     | 一种将上游 HTTP 响应代码映射到 Syslog 中的方法。 `key-value`，其中 `key` 是 HTTP 响应代码，`value`是 Syslog 严重级别。例如`{\"410\": \"CRIT\"}`。                |\n| tags                   | array         | 否      |         | 元数据将包含在任何事件日志中，以帮助进行分段和过滤。                                                                                                        |\n| log_format             | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| include_req_body       | boolean       | 否      | false   | 当设置为 `true` 时，包含请求体。**注意**：如果请求体无法完全存放在内存中，由于 NGINX 的限制，APISIX 无法将它记录下来。               |\n| include_req_body_expr   | array         | 否   |       | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。 |\n| include_resp_body      | boolean       | 否      | false   | 当设置为 `true` 时，包含响应体。                                            |\n| include_resp_body_expr | array         | 否      |         | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n\n该插件支持使用批处理器来聚合并批量处理条目（日志或数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n如果要生成用户令牌，请在 Loggly 系统中的 `<your assigned subdomain>/loggly.com/tokens` 设置，或者在系统中单击 `Logs > Source setup > Customer tokens`。\n\n### 默认日志格式示例\n\n```text\n<10>1 2024-01-06T06:50:51.739Z 127.0.0.1 apisix 58525 - [token-1@41058 tag=\"apisix\"] {\"service_id\":\"\",\"server\":{\"version\":\"3.7.0\",\"hostname\":\"localhost\"},\"apisix_latency\":100.99985313416,\"request\":{\"url\":\"http://127.0.0.1:1984/opentracing\",\"headers\":{\"content-type\":\"application/x-www-form-urlencoded\",\"user-agent\":\"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\",\"host\":\"127.0.0.1:1984\"},\"querystring\":{},\"uri\":\"/opentracing\",\"size\":155,\"method\":\"GET\"},\"response\":{\"headers\":{\"content-type\":\"text/plain\",\"server\":\"APISIX/3.7.0\",\"transfer-encoding\":\"chunked\",\"connection\":\"close\"},\"size\":141,\"status\":200},\"route_id\":\"1\",\"latency\":103.99985313416,\"upstream_latency\":3,\"client_ip\":\"127.0.0.1\",\"upstream\":\"127.0.0.1:1982\",\"start_time\":1704523851634}\n```\n\n## 插件元数据设置\n\n你还可以通过插件元数据配置插件。详细配置如下：\n\n| 名称       | 类型    | 必选项 | 默认值               | 有效值                           | 描述                                                                |\n|------------|---------|-------|----------------------|--------------------------------|---------------------------------------------------------------------|\n| host       | string  | 否    | \"logs-01.loggly.com\" |                                | 发送日志的主机的端点。                                                |\n| port       | integer | 否    | 514                  |                                | 要连接的 Loggly 端口。仅用于 `syslog` 协议。                         |\n| timeout    | integer | 否    | 5000                 |                                | 发送数据请求超时时间（以毫秒为单位）。                                 |\n| protocol   | string  | 否    | \"syslog\"             | [ \"syslog\", \"http\", \"https\" ]  | 将日志发送到 Loggly 的协议。                                          |\n| log_format | object  | 否    | nil                  |                                | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n\nAPISIX 支持 [Syslog](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/streaming-syslog-without-using-files.htm)、[HTTP/S](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/http-bulk-endpoint.htm)（批量端点）协议将日志事件发送到 Loggly。**默认情况下 `protocol` 的值为 `syslog`**。该协议允许你通过一些细粒度的控制（基于上游 HTTP 响应代码的日志严重性映射）发送符合 RFC5424 的系统日志事件。但是 HTTP/S 批量端点非常适合以更快的传输速度发送更大量的日志事件。\n\n:::note 注意\n\nSyslog 协议允许你发送符合 RFC5424 的 syslog 事件并进行细粒度控制。但是在以快速传输速度发送大量日志时，使用 HTTP/S 批量端点会更好。你可以通过以下方式更新元数据以更新使用的协议：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/loggly \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n   \"protocol\": \"http\"\n}'\n```\n\n:::\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用该插件：\n\n**完整配置**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"loggly\":{\n            \"customer_token\":\"0e6fe4bf-376e-40f4-b25f-1d55cb29f5a2\",\n            \"tags\":[\"apisix\", \"testroute\"],\n            \"severity\":\"info\",\n            \"severity_map\":{\n                \"503\": \"err\",\n                \"410\": \"alert\"\n            },\n            \"buffer_duration\":60,\n            \"max_retry_count\":0,\n            \"retry_delay\":1,\n            \"inactive_timeout\":2,\n            \"batch_max_size\":10\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:80\":1\n        }\n    },\n    \"uri\":\"/index.html\"\n}'\n```\n\n**最小化配置**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"loggly\":{\n            \"customer_token\":\"0e6fe4bf-376e-40f4-b25f-1d55cb29f5a2\",\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:80\":1\n        }\n    },\n    \"uri\":\"/index.html\"\n}'\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\n发出请求后，你就可以在 Loggly 仪表盘上查看相关日志：\n\n![Loggly Dashboard](../../../assets/images/plugin/loggly-dashboard.png)\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/loki-logger.md",
    "content": "---\ntitle: loki-logger\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Loki-logger\n  - Grafana Loki\ndescription: loki-logger 插件通过 Loki HTTP API /loki/api/v1/push 将请求和响应日志批量推送到 Grafana Loki。该插件还支持自定义日志格式。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/loki-logger\" />\n</head>\n\n## 描述\n\n`loki-logger` 插件通过 [Loki HTTP API](https://grafana.com/docs/loki/latest/reference/loki-http-api/#loki-http-api) `/loki/api/v1/push` 将请求和响应日志批量推送到 [Grafana Loki](https://grafana.com/oss/loki/)。该插件还支持自定义日志格式。\n\n启用后，插件会将请求上下文信息序列化为 [JSON object](https://grafana.com/docs/loki/latest/api/#push-log-entries-to-loki) 并将其添加到队列中，然后再将其推送到 Loki。有关更多详细信息，请参阅批处理处理器 [batch processor](../batch-processor.md)。\n\n## 属性\n\n| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |\n|--|---|---|---|---|\n| end_addrs | array[string] | 是 | | | Loki API URL，例如 `http://127.0.0.1:3100`。如果配置了多个端点，日志将被推送到列表中随机确定的端点。 |\n| end_uri | string | 否 | /loki/api/v1/push | | Loki 提取端点的 URI 路径。 |\n| tenant_id | string | 否 | fake | | Loki 租户 ID。根据 Loki 的 [多租户文档](https://grafana.com/docs/loki/latest/operations/multi-tenancy/#multi-tenancy)，在单租户下默认值设置为 `fake`。 |\n| headers | object | 否 |  |  | 请求头键值对（对 `X-Scope-OrgID` 和 `Content-Type` 的设置将会被忽略）。 |\n| log_labels | object | 否 | {job = \"apisix\"} | | Loki 日志标签。支持 [NGINX 变量](https://nginx.org/en/docs/varindex.html) 和值中的常量字符串。变量应以 `$` 符号为前缀。例如，标签可以是 `{\"origin\" = \"apisix\"}` 或 `{\"origin\" = \"$remote_addr\"}`。|\n| ssl_verify | boolean | 否 | true | | 如果为 true，则验证 Loki 的 SSL 证书。|\n| timeout | integer | 否 | 3000 | [1, 60000] | Loki 服务 HTTP 调用的超时时间（以毫秒为单位）。|\n| keepalive | boolean | 否 | true | | 如果为 true，则保持连接以应对多个请求。|\n| keepalive_timeout | integer | 否 | 60000 | >=1000 | Keepalive 超时时间（以毫秒为单位）。|\n| keepalive_pool | integer | 否 | 5 | >=1 | 连接池中的最大连接数。|\n| log_format | object | 否 | | | 自定义日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过 `$` 前缀引用 [APISIX 变量](../apisix-variable.md) 和 [NGINX 变量](http://nginx.org/en/docs/varindex.html)。 |\n| name | string | 否 | loki-logger | | 批处理器插件的唯一标识符。如果使用 [Prometheus](./prometheus.md) 监控 APISIX 指标，则名称会导出到 `apisix_batch_process_entries`。 |\n| include_req_body | boolean | 否 | false | | 如果为 true，则将请求正文包含在日志中。请注意，如果请求正文太大而无法保存在内存中，则由于 NGINX 的限制而无法记录。 |\n| include_req_body_expr | array[array] | 否 | | |一个或多个 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 形式条件的数组。在 `include_req_body` 为 true 时使用。仅当此处配置的表达式计算结果为 true 时，才会记录请求正文。|\n| include_resp_body | boolean | 否 | false | | 如果为 true，则将响应正文包含在日志中。|\n| include_resp_body_expr | array[array] | 否 | | | 一个或多个 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 形式条件的数组。在 `include_resp_body` 为 true 时使用。仅当此处配置的表达式计算结果为 true 时，才会记录响应正文。|\n\n该插件支持使用批处理器对条目（日志/数据）进行批量聚合和处理，避免了频繁提交数据的需求。批处理器每隔 `5` 秒或当队列中的数据达到 `1000` 时提交数据。有关更多信息或设置自定义配置，请参阅 [批处理器](../batch-processor.md#configuration)。\n\n## Plugin Metadata\n\n您还可以使用 [Plugin Metadata](../terminology/plugin-metadata.md) 全局配置日志格式，该 Plugin Metadata 配置所有 `loki-logger` 插件实例的日志格式。如果在单个插件实例上配置的日志格式与在 Plugin Metadata 上配置的日志格式不同，则在单个插件实例上配置的日志格式优先。\n\n| 名称 | 类型 | 必选项 | 默认值 | 描述 |\n|------|------|----------|--|-------------|\n| log_format | object | 否 |  | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 和 [NGINX 变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n## 示例\n\n下面的示例演示了如何为不同场景配置 `loki-logger` 插件。\n\n为了遵循示例，请在 Docker 中启动一个示例 Loki 实例：\n\n```shell\nwget https://raw.githubusercontent.com/grafana/loki/v3.0.0/cmd/loki/loki-local-config.yaml -O loki-config.yaml\ndocker run --name loki -d -v $(pwd):/mnt/config -p 3100:3100 grafana/loki:3.2.1 -config.file=/mnt/config/loki-config.yaml\n```\n\n此外，启动 Grafana 实例来查看和可视化日志：\n\n```shell\ndocker run -d --name=apisix-quickstart-grafana \\\n  -p 3000:3000 \\\n  grafana/grafana-oss\n```\n\n要连接 Loki 和 Grafana，请访问 Grafana，网址为 [`http://localhost:3000`](http://localhost:3000)。在 __Connections > Data sources__ 下，添加新数据源并选择 Loki。您的连接 URL 应遵循 `http://{your_ip_address}:3100` 的格式。保存新数据源时，Grafana 还应测试连接，您应该会看到 Grafana 通知数据源已成功连接。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 以默认日志格式记录请求和响应\n\n以下示例演示了如何在路由上配置 `loki-logger` 插件以记录通过路由的请求和响应。\n\n使用 `loki-logger` 插件创建路由并配置 Loki 的地址：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"loki-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"loki-logger\": {\n        \"endpoint_addrs\": [\"http://192.168.1.5:3100\"]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n向路由发送一些请求以生成日志条目：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到所有请求的“HTTP/1.1 200 OK”响应。\n\n导航到 [Grafana explore view](http://localhost:3000/explore) 并运行查询 `job = apisix`。您应该会看到与您的请求相对应的许多日志，例如以下内容：\n\n```json\n{\n  \"route_id\": \"loki-logger-route\",\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"date\": \"Fri, 03 Jan 2025 03:54:26 GMT\",\n      \"server\": \"APISIX/3.11.0\",\n      \"access-control-allow-credentials\": \"true\",\n      \"content-length\": \"391\",\n      \"access-control-allow-origin\": \"*\",\n      \"content-type\": \"application/json\",\n      \"connection\": \"close\"\n    },\n    \"size\": 619\n  },\n  \"start_time\": 1735876466,\n  \"client_ip\": \"192.168.65.1\",\n  \"service_id\": \"\",\n  \"apisix_latency\": 5.0000038146973,\n  \"upstream\": \"34.197.122.172:80\",\n  \"upstream_latency\": 666,\n  \"server\": {\n    \"hostname\": \"0b9a772e68f8\",\n    \"version\": \"3.11.0\"\n  },\n  \"request\": {\n    \"headers\": {\n      \"user-agent\": \"curl/8.6.0\",\n      \"accept\": \"*/*\",\n      \"host\": \"127.0.0.1:9080\"\n    },\n    \"size\": 85,\n    \"method\": \"GET\",\n    \"url\": \"http://127.0.0.1:9080/anything\",\n    \"querystring\": {},\n    \"uri\": \"/anything\"\n  },\n  \"latency\": 671.0000038147\n}\n```\n\n这验证了 Loki 已从 APISIX 接收日志。您还可以在 Grafana 中创建仪表板，以进一步可视化和分析日志。\n\n### 使用 Plugin Metadata 自定义日志格式\n\n以下示例演示了如何使用 [Plugin Metadata](../terminology/plugin-metadata.md) 自定义日志格式。\n\n使用 `loki-logger` 插件创建路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"loki-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"loki-logger\": {\n        \"endpoint_addrs\": [\"http://192.168.1.5:3100\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n为 `loki-logger` 配置 Plugin Metadata，它将更新所有需要记录请求的路由的日志格式：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/loki-logger\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"log_format\": {\n      \"host\": \"$host\",\n      \"client_ip\": \"$remote_addr\",\n      \"route_id\": \"$route_id\",\n      \"@timestamp\": \"$time_iso8601\"\n    }\n  }'\n```\n\n向路由发送请求以生成新的日志条目：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [Grafana explore view](http://localhost:3000/explore) 并运行查询 `job = apisix`。您应该会看到与您的请求相对应的日志条目，类似于以下内容：\n\n```json\n{\n  \"@timestamp\":\"2025-01-03T21:11:34+00:00\",\n  \"client_ip\":\"192.168.65.1\",\n  \"route_id\":\"loki-logger-route\",\n  \"host\":\"127.0.0.1\"\n}\n```\n\n如果路由上的插件指定了特定的日志格式，它将优先于 Plugin Metadata 中指定的日志格式。例如，按如下方式更新上一个路由上的插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/loki-logger-route\" -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"loki-logger\": {\n        \"log_format\": {\n          \"route_id\": \"$route_id\",\n          \"client_ip\": \"$remote_addr\",\n          \"@timestamp\": \"$time_iso8601\"\n        }\n      }\n    }\n  }'\n```\n\n向路由发送请求以生成新的日志条目：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [Grafana explore view](http://localhost:3000/explore) 并重新运行查询 `job = apisix`。您应该会看到与您的请求相对应的日志条目，与路由上配置的格式一致，类似于以下内容：\n\n```json\n{\n  \"client_ip\":\"192.168.65.1\",\n  \"route_id\":\"loki-logger-route\",\n  \"@timestamp\":\"2025-01-03T21:19:45+00:00\"\n}\n```\n\n### 有条件地记录请求主体\n\n以下示例演示了如何有条件地记录请求主体。\n\n使用 `loki-logger` 创建路由，仅在 URL 查询字符串 `log_body` 为 `yes` 时记录请求主体：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"loki-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"loki-logger\": {\n        \"endpoint_addrs\": [\"http://192.168.1.5:3100\"],\n        \"include_req_body\": true,\n        \"include_req_body_expr\": [[\"arg_log_body\", \"==\", \"yes\"]]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n使用满足条件的 URL 查询字符串向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?log_body=yes\" -X POST -d '{\"env\": \"dev\"}'\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [Grafana explore view](http://localhost:3000/explore) 并重新运行查询 `job = apisix`。您应该会看到与您的请求相对应的日志条目，与路由上配置的格式一致，类似于以下内容：\n\n```json\n{\n  \"route_id\": \"loki-logger-route\",\n  ...,\n  \"request\": {\n    \"headers\": {\n      ...\n    },\n    \"body\": \"{\\\"env\\\": \\\"dev\\\"}\",\n    \"size\": 182,\n    \"method\": \"POST\",\n    \"url\": \"http://127.0.0.1:9080/anything?log_body=yes\",\n    \"querystring\": {\n      \"log_body\": \"yes\"\n    },\n    \"uri\": \"/anything?log_body=yes\"\n  },\n  \"latency\": 809.99994277954\n}\n```\n\n向路由发送一个没有任何 URL 查询字符串的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST -d '{\"env\": \"dev\"}'\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n导航到 [Grafana explore view](http://localhost:3000/explore) 并重新运行查询 `job = apisix`。您应该会看到与您的请求相对应的日志条目，与路由上配置的格式一致，类似于以下内容：\n\n```json\n{\n  \"route_id\": \"loki-logger-route\",\n  ...,\n  \"request\": {\n    \"headers\": {\n      ...\n    },\n    \"size\": 169,\n    \"method\": \"POST\",\n    \"url\": \"http://127.0.0.1:9080/anything\",\n    \"querystring\": {},\n    \"uri\": \"/anything\"\n  },\n  \"latency\": 557.00016021729\n}\n```\n\n:::info\n\n如果您除了将 `include_req_body` 或 `include_resp_body` 设置为 `true` 之外还自定义了 `log_format`，则插件不会在日志中包含正文。\n\n作为一种解决方法，您可以在日志格式中使用 NGINX 变量 `$request_body`，例如：\n\n```json\n{\n  \"kafka-logger\": {\n    ...,\n    \"log_format\": {\"body\": \"$request_body\"}\n  }\n}\n```\n\n:::\n\n## FAQ\n\n### 日志未正确推送\n\n请查看 `error.log` 文件以获取此类日志。\n\n```text\n2023/04/30 13:45:46 [error] 19381#19381: *1075673 [lua] batch-processor.lua:95: Batch Processor[loki logger] failed to process entries: loki server returned status: 401, body: no org id, context: ngx.timer, client: 127.0.0.1, server: 0.0.0.0:9081\n```\n\n可以根据错误代码 `failed to process entries: loki server returned status: 401, body: no org id` 和 loki 服务器的响应正文来诊断错误。\n\n### 当请求每秒 (RPS) 较高时出现错误？\n\n- 请确保 `keepalive` 相关的配置已正确设置。有关更多信息，请参阅[属性](#属性) 。\n- 请检查 `error.log` 中的日志，查找此类日志。\n\n    ```text\n    2023/04/30 13:49:34 [error] 19381#19381: *1082680 [lua] batch-processor.lua:95: Batch Processor[loki logger] failed to process entries: loki server returned status: 429, body: Ingestion rate limit exceeded for user tenant_1 (limit: 4194304 bytes/sec) while attempting to ingest '1000' lines totaling '616307' bytes, reduce log volume or contact your Loki administrator to see if the limit can be increased, context: ngx.timer, client: 127.0.0.1, server: 0.0.0.0:9081\n    ```\n\n  - 通常与高 QPS 相关的日志如上所示。错误信息为：`Ingestion rate limit exceeded for user tenant_1 (limit: 4194304 bytes/sec) while attempting to ingest '1000' lines totaling '616307' bytes, reduce log volume or contact your Loki administrator to see if the limit can be increased`。\n  - 请参考 [Loki 文档](https://grafana.com/docs/loki/latest/configuration/#limits_config) ，添加默认日志量和突发日志量的限制，例如 `ingestion_rate_mb` 和 `ingestion_burst_size_mb`。\n\n    在开发过程中进行测试时，将 `ingestion_burst_size_mb` 设置为 100 可以确保 APISIX 以至少 10000 RPS 的速率正确推送日志。\n"
  },
  {
    "path": "docs/zh/latest/plugins/mocking.md",
    "content": "---\ntitle: mocking\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Mocking\ndescription: 本文介绍了关于 Apache APISIX `mocking` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`mocking` 插件用于模拟 API。当执行该插件时，它将随机返回指定格式的模拟数据，并且请求不会转发到上游。\n\n## 属性\n\n| 名称            | 类型    | 必选项 | 默认值           |  描述                                                           |\n| -------------   | -------| ----- | ---------------- | --------------------------------------------------------------------------- |\n| delay           | integer| 否    |                  | 延时返回的时间，单位为秒。                                            |\n| response_status | integer| 否    | 200              | 返回响应的 HTTP 状态码。                                            |\n| content_type    | string | 否    | application/json | 返回响应的 Header `Content-Type`。                                            |\n| response_example| string | 否    |                  | 返回响应的 Body，支持使用变量，例如 `$remote_addr $consumer_name`，与 `response_schema` 字段二选一。 |\n| response_schema | object | 否    |                  | 指定响应的 `jsonschema` 对象，未指定 `response_example` 字段时生效。                        |\n| with_mock_header| boolean| 否    | true             | 当设置为 `true` 时，将添加响应头 `x-mock-by: APISIX/{version}`。设置为 `false` 时则不添加该响应头。   |\n| response_headers| object | 否    |                  | 要在模拟响应中添加的标头。示例：`{\"X-Foo\": \"bar\", \"X-Few\": \"baz\"}`                               |\n\nJSON Schema 在其字段中支持以下类型：\n\n- `string`\n- `number`\n- `integer`\n- `boolean`\n- `object`\n- `array`\n\n以下是一个 JSON Schema 示例：\n\n```json\n{\n    \"properties\":{\n        \"field0\":{\n            \"example\":\"abcd\",\n            \"type\":\"string\"\n        },\n        \"field1\":{\n            \"example\":123.12,\n            \"type\":\"number\"\n        },\n        \"field3\":{\n            \"properties\":{\n                \"field3_1\":{\n                    \"type\":\"string\"\n                },\n                \"field3_2\":{\n                    \"properties\":{\n                        \"field3_2_1\":{\n                            \"example\":true,\n                            \"type\":\"boolean\"\n                        },\n                        \"field3_2_2\":{\n                            \"items\":{\n                                \"example\":155.55,\n                                \"type\":\"integer\"\n                            },\n                            \"type\":\"array\"\n                        }\n                    },\n                    \"type\":\"object\"\n                }\n            },\n            \"type\":\"object\"\n        },\n        \"field2\":{\n            \"items\":{\n                \"type\":\"string\"\n            },\n            \"type\":\"array\"\n        }\n    },\n    \"type\":\"object\"\n}\n```\n\n以下为上述 JSON Schema 可能生成的返回对象：\n\n```json\n{\n    \"field1\": 123.12,\n    \"field3\": {\n        \"field3_1\": \"LCFE0\",\n        \"field3_2\": {\n            \"field3_2_1\": true,\n            \"field3_2_2\": [\n                155,\n                155\n            ]\n        }\n    },\n    \"field0\": \"abcd\",\n    \"field2\": [\n        \"sC\"\n    ]\n}\n```\n\n## 启用插件\n\n你可以通过如下命令在指定路由上启用 `mocking` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"mocking\": {\n            \"delay\": 1,\n            \"content_type\": \"application/json\",\n            \"response_status\": 200,\n            \"response_schema\": {\n               \"properties\":{\n                   \"field0\":{\n                       \"example\":\"abcd\",\n                       \"type\":\"string\"\n                   },\n                   \"field1\":{\n                       \"example\":123.12,\n                       \"type\":\"number\"\n                   },\n                   \"field3\":{\n                       \"properties\":{\n                           \"field3_1\":{\n                               \"type\":\"string\"\n                           },\n                           \"field3_2\":{\n                               \"properties\":{\n                                   \"field3_2_1\":{\n                                       \"example\":true,\n                                       \"type\":\"boolean\"\n                                   },\n                                   \"field3_2_2\":{\n                                       \"items\":{\n                                           \"example\":155.55,\n                                           \"type\":\"integer\"\n                                       },\n                                       \"type\":\"array\"\n                                   }\n                               },\n                               \"type\":\"object\"\n                           }\n                       },\n                       \"type\":\"object\"\n                   },\n                   \"field2\":{\n                       \"items\":{\n                           \"type\":\"string\"\n                       },\n                       \"type\":\"array\"\n                   }\n               },\n               \"type\":\"object\"\n           }\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下方式测试插件是否启用成功：\n\n当 `mocking` 插件配置如下：\n\n```JSON\n{\n  \"delay\":0,\n  \"content_type\":\"\",\n  \"with_mock_header\":true,\n  \"response_status\":201,\n  \"response_example\":\"{\\\"a\\\":1,\\\"b\\\":2}\"\n}\n```\n\n通过如下命令进行测试：\n\n```shell\ncurl http://127.0.0.1:9080/test-mock -i\n```\n\n```Shell\nHTTP/1.1 201 Created\nDate: Fri, 14 Jan 2022 11:49:34 GMT\nContent-Type: application/json;charset=utf8\nTransfer-Encoding: chunked\nConnection: keep-alive\nx-mock-by: APISIX/2.10.0\nServer: APISIX/2.10.0\n\n{\"a\":1,\"b\":2}\n```\n\n## 删除插件\n\n当你需要禁用 `mocking` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/mqtt-proxy.md",
    "content": "---\ntitle: mqtt-proxy\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - MQTT Proxy\ndescription: 本文档介绍了 Apache APISIX mqtt-proxy 插件的信息，通过 `mqtt-proxy` 插件可以使用 MQTT 的 `client_id` 进行动态负载平衡。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n通过 `mqtt-proxy` 插件可以使用 MQTT 的 `client_id` 进行动态负载平衡。它仅适用于 `stream` 模式。\n\n这个插件支持 MQTT [3.1.*](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) 及 [5.0]( https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html ) 两个协议。\n\n## 属性\n\n| 名称           | 类型     | 必选项 | 描述                                                 |\n| -------------- | ------- | ----- | --------------------------------------------------- |\n| protocol_name  | string  | 否    | 协议名称，默认为 `MQTT`。                               |\n| protocol_level | integer | 是    | 协议级别，MQTT `3.1.*` 为 `4`，MQTT `5.0` 应是`5`。     |\n\n## 启用插件\n\n为了启用该插件，需要先在配置文件（`./conf/config.yaml`）中加载 `stream_proxy` 相关配置。以下配置代表监听 `9100` TCP 端口：\n\n```yaml title=\"./conf/config.yaml\"\n    ...\n    router:\n        http: 'radixtree_uri'\n        ssl: 'radixtree_sni'\n    proxy_mode: http&stream\n    stream_proxy:                 # TCP/UDP proxy\n      tcp:                        # TCP proxy port list\n        - 9100\n    dns_resolver:\n    ...\n```\n\n现在你可以将请求发送到 `9100` 端口。\n\n你可以创建一个 stream 路由并启用 `mqtt-proxy` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"mqtt-proxy\": {\n            \"protocol_name\": \"MQTT\",\n            \"protocol_level\": 4\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": [{\n            \"host\": \"127.0.0.1\",\n            \"port\": 1980,\n            \"weight\": 1\n        }]\n    }\n}'\n```\n\n如果你在 macOS 中使用 Docker，则 `host.docker.internal` 是 `host` 的正确属性。\n\n该插件暴露了一个变量 `mqtt_client_id`，你可以使用它来通过客户端 ID 进行负载均衡。比如：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"mqtt-proxy\": {\n            \"protocol_name\": \"MQTT\",\n            \"protocol_level\": 4\n        }\n    },\n    \"upstream\": {\n        \"type\": \"chash\",\n        \"key\": \"mqtt_client_id\",\n        \"nodes\": [\n        {\n            \"host\": \"127.0.0.1\",\n            \"port\": 1995,\n            \"weight\": 1\n        },\n        {\n            \"host\": \"127.0.0.2\",\n            \"port\": 1995,\n            \"weight\": 1\n        }\n        ]\n    }\n}'\n```\n\n不同客户端 ID 的 MQTT 连接将通过一致性哈希算法被转发到不同的节点。如果客户端 ID 为空，将会通过客户端 IP 进行均衡。\n\n## 使用 mqtt-proxy 插件启用 mTLS\n\nStream 代理可以使用 TCP 连接并且支持 TLS。请参考 [如何通过 tcp 连接接受 tls](../stream-proxy.md/#accept-tls-over-tcp-connection) 打开启用了 TLS 的 stream 代理。\n\n`mqtt-proxy` 插件通过 Stream 代理的指定端口的 TCP 通信启用，如果 `tls` 设置为 `true`，则还要求客户端通过 TLS 进行身份验证。\n\n配置 `ssl` 提供 CA 证书和服务器证书，以及 SNI 列表。使用 `ssl` 保护 `stream_routes` 的步骤等同于 [protect Routes](../mtls.md/#protect-route)。\n\n### 创建 stream_route 并配置 mqtt-proxy 插件和 mTLS\n\n通过以下示例可以创建一个配置了 `mqtt-proxy` 插件的 `stream_route`，需要提供 CA 证书、客户端证书和客户端密钥（对于不受主机信任的自签名证书，请使用 -k 选项）：\n\n```shell\ncurl 127.0.0.1:9180/apisix/admin/stream_routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"mqtt-proxy\": {\n            \"protocol_name\": \"MQTT\",\n            \"protocol_level\": 4\n        }\n    },\n    \"sni\": \"${your_sni_name}\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n:::note 注意\n\n`sni` 名称必须与提供的 CA 和服务器证书创建的 SSL 对象的一个​​或多个 SNI 匹配。\n\n:::\n\n## 删除插件\n\n当你需要删除该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X DELETE\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/multi-auth.md",
    "content": "---\ntitle: multi-auth\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Multi Auth\n  - multi-auth\ndescription: 本文档包含有关 Apache APISIX multi-auth 插件的信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n插件 `multi-auth` 用于向 `Route` 或者 `Service` 中，添加多种身份验证方式。它支持 `auth` 类型的插件。您可以使用 `multi-auth` 插件，来组合不同的身份认证方式。\n\n插件通过迭代 `auth_plugins` 属性指定的插件列表，提供了灵活的身份认证机制。它允许多个 `Consumer` 在使用不同身份验证方式时共享相同的 `Route` ，同时。例如：一个 Consumer 使用 basic 认证，而另一个消费者使用 JWT 认证。\n\n## 属性\n\nFor Route:\n\n| 名称           | 类型    | 必选项  | 默认值 | 描述                      |\n|--------------|-------|------|-----|-------------------------|\n| auth_plugins | array | True | -   | 添加需要支持的认证插件。至少需要 2 个插件。 |\n\n## 启用插件\n\n要启用插件，您必须创建两个或多个具有不同身份验证插件配置的 Consumer：\n\n首先创建一个 Consumer 使用 basic-auth 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"foo1\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\": \"foo1\",\n            \"password\": \"bar1\"\n        }\n    }\n}'\n```\n\n然后再创建一个 Consumer 使用 key-auth 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"foo2\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    }\n}'\n```\n\n创建 Consumer 之后，您可以配置一个路由或服务来验证请求：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"multi-auth\":{\n         \"auth_plugins\":[\n            {\n               \"basic-auth\":{ }\n            },\n            {\n               \"key-auth\":{\n                  \"query\":\"apikey\",\n                  \"hide_credentials\":true,\n                  \"header\":\"apikey\"\n               }\n            }\n         ]\n      }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 使用示例\n\n如上所述配置插件后，您可以向对应的 API 发起一个请求，如下所示：\n\n请求开启 basic-auth 插件的 API\n\n```shell\ncurl -i -ufoo1:bar1 http://127.0.0.1:9080/hello\n```\n\n请求开启 key-auth 插件的 API\n\n```shell\ncurl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -i\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n如果请求未授权，将会返回 `401 Unauthorized` 错误：\n\n```json\n{\"message\":\"Authorization Failed\"}\n```\n\n## 删除插件\n\n要删除 `multi-auth` 插件，您可以从插件配置中删除插件对应的 JSON 配置，APISIX 会自动加载，您不需要重新启动即可生效。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/node-status.md",
    "content": "---\ntitle: node-status\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 插件\n  - Node status\ndescription: 本文介绍了 API 网关 Apache APISIX node-status 插件的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`node-status` 插件可用于通过暴露的 API 查询 APISIX 的请求状态，并返回基本的状态信息。\n\n## 插件属性\n\n无。\n\n## 插件接口\n\n该插件将会增加 `/apisix/status` 的接口用来暴露 APISIX 的状态，你需要通过 [public-api](public-api.md) 插件来暴露该接口。\n\n## 启用插件\n\n`node-status` 插件默认为禁用状态，如果你需要使用该插件，请在配置文件 `./conf/config.yaml` 中启用它：\n\n``` yaml title=\"./conf/config.yaml\"\nplugins:\n  - limit-req\n  - node-status\n  - jwt-auth\n  - zipkin\n  ......\n```\n\n你需要为 `/apisix/status` API 配置路由，并使用 [public-api](public-api.md) 插件暴露它。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/ns -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/status\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\n## 测试插件\n\n完成上述配置后，你可以通过以下命令向 `/apisix/status` 发送请求以获取状态信息。\n\n```shell\ncurl http://127.0.0.1:9080/apisix/status -i\n```\n\n```shell\nHTTP/1.1 200 OK\nDate: Tue, 03 Nov 2020 11:12:55 GMT\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\n{\"status\":{\"total\":\"23\",\"waiting\":\"0\",\"accepted\":\"22\",\"writing\":\"1\",\"handled\":\"22\",\"active\":\"1\",\"reading\":\"0\"},\"id\":\"6790a064-8f61-44ba-a6d3-5df42f2b1bb3\"}\n```\n\n返回结果中的参数释义如下：\n\n| 参数         | 说明                                                                    |\n| ------------ | ---------------------------------------------------------------------- |\n| status       | APISIX 的状态信息。                                                     |\n| total        | 客户端请求总数。                                                        |\n| waiting      | 当前等待客户端请求的空闲连接数。                                          |\n| accepted     | 当前已经接受的客户端连接总数。                                            |\n| writing      | 当前正在写给客户端响应的连接数。                                          |\n| handled      | 当前已经处理的连接总数，除非达到其他资源的限制，否则此值与 `accepted` 相同。 |\n| active       | 当前活跃的客户端连接数。                                                 |\n| reading      | 当前正在读取请求头的连接数。                                              |\n| id           | APISIX UID 信息，保存在 `./conf/apisix.uid` 文件中。                |\n\n## 删除插件\n\n如果你不再需要该插件，可以从配置文件 (`./conf/config.yaml`) 中删除它：\n\n``` yaml title=\"conf/config.yaml\"\n  - limit-req\n  - jwt-auth\n  - zipkin\n  ......\n```\n\n你也可以移除暴露 `/apisix/status` 接口的路由。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/ns -H \"X-API-KEY: $admin_key\" -X DELETE\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ocsp-stapling.md",
    "content": "---\ntitle: ocsp-stapling\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 插件\n  - ocsp-stapling\ndescription: 本文介绍了 API 网关 Apache APISIX ocsp-stapling 插件的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`ocsp-stapling` 插件可以动态地设置 Nginx 中 [OCSP stapling](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_stapling) 的相关行为。\n\n## 启用插件\n\n这个插件是默认禁用的，通过修改配置文件 `./conf/config.yaml` 来启用它：\n\n```yaml\nplugins:\n  - ...\n  - ocsp-stapling\n```\n\n修改配置文件之后，重启 APISIX 或者通过插件热加载接口来使配置生效：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n\n## 属性\n\n插件属性存储在 SSL 资源的 `ocsp_stapling` 字段中。\n\n| 名称           | 类型                 | 必选项   | 默认值          | 有效值       | 描述                                                                  |\n|----------------|----------------------|----------|---------------|--------------|-----------------------------------------------------------------------|\n| enabled        | boolean              | False    | false         |              | 与 `ssl_stapling` 指令类似，用于启用或禁用 OCSP stapling 特性            |\n| skip_verify    | boolean              | False    | false         |              | 与 `ssl_stapling_verify` 指令类似，用于启用或禁用对于 OCSP 响应结果的校验 |\n| cache_ttl      | integer              | False    | 3600          | >= 60        | 指定 OCSP 响应结果的缓存时间                                            |\n\n## 使用示例\n\n首先您应该创建一个 SSL 资源，并且证书资源中应该包含颁发者的证书。通常情况下，全链路证书就可以正常工作。\n\n如下示例中，生成相关的 SSL 资源：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\" : \"'\"$(cat server.crt)\"'\",\n    \"key\": \"'\"$(cat server.key)\"'\",\n    \"snis\": [\"test.com\"],\n    \"ocsp_stapling\": {\n        \"enabled\": true\n    }\n}'\n```\n\n通过上述命令生成 SSL 资源后，可以通过以下方法测试：\n\n```shell\necho -n \"Q\" | openssl s_client -status -connect localhost:9443 -servername test.com 2>&1 | cat\n```\n\n```\n...\nCONNECTED(00000003)\nOCSP response:\n======================================\nOCSP Response Data:\n    OCSP Response Status: successful (0x0)\n...\n```\n\n可以通过以下方法禁用插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\" : \"'\"$(cat server.crt)\"'\",\n    \"key\": \"'\"$(cat server.key)\"'\",\n    \"snis\": [\"test.com\"],\n    \"ocsp_stapling\": {\n        \"enabled\": false\n    }\n}'\n```\n\n## 删除插件\n\n在删除插件之前，需要确保所有的 SSL 资源都已经移除 `ocsp_stapling` 字段，可以通过以下命令实现对单个 SSL 资源的对应字段移除：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PATCH -d '\n{\n    \"ocsp_stapling\": null\n}'\n```\n\n通过修改配置文件 `./conf/config.yaml` 来禁用它：\n\n```yaml\nplugins:\n  - ...\n  # - ocsp-stapling\n```\n\n修改配置文件之后，重启 APISIX 或者通过插件热加载接口来使配置生效：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/opa.md",
    "content": "---\ntitle: opa\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Open Policy Agent\n  - opa\ndescription: 本篇文档介绍了 Apache APISIX 通过 opa 插件与 Open Policy Agent 对接的相关信息。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`opa` 插件可用于与 [Open Policy Agent](https://www.openpolicyagent.org) 进行集成，实现后端服务的认证授权与访问服务等功能解耦，减少系统复杂性。\n\n## 属性\n\n| 名称              | 类型    | 必选项 | 默认值 | 有效值  | 描述                                                                                                                                                                                |\n|-------------------|---------|----------|---------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| host              | string  | 是     |         |               | OPA 服务的主机地址，例如 `https://localhost:8181`。                                                                                                                   |\n| ssl_verify        | boolean | 否    | true    |               | 当设置为 `true` 时，将验证 SSL 证书。                                                                                                                                          |\n| policy            | string  | 是     |         |               | OPA 策略路径，是 `package` 和 `decision` 配置的组合。当使用高级功能（如自定义响应）时，你可以省略 `decision` 配置。指定命名空间时，请使用斜杠格式（例如 `examples/echo`），而不是点号格式（例如 `examples.echo`）。        |\n| timeout           | integer | 否    | 3000ms  | [1, 60000]ms  | 设置 HTTP 调用超时时间。                                                                                                                                                                |\n| keepalive         | boolean | 否    | true    |               | 当设置为 `true` 时，将为多个请求保持连接并处于活动状态。                                                                                                                               |\n| keepalive_timeout | integer | 否    | 60000ms | [1000, ...]ms | 连接断开后的闲置时间。                                                                                                                                                                        |\n| keepalive_pool    | integer | 否    | 5       | [1, ...]ms    | 连接池限制。                                                                                                                                                                    |\n| with_route        | boolean | 否    | false   |               | 当设置为 `true` 时，发送关于当前 Route 的信息。                                                                                                                              |\n| with_service      | boolean | 否    | false   |               | 当设置为 `true` 时，发送关于当前 Service 的信息。                                                                                                                            |\n| with_consumer     | boolean | 否    | false   |               | 当设置为 `true` 时，发送关于当前 Consumer 的信息。注意，这可能会发送敏感信息，如 API key。请确保在安全的情况下才打开它。 |\n\n## 数据定义\n\n### APISIX 向 OPA 发送信息\n\n下述示例代码展示了如何通过 APISIX 向 OPA 服务发送数据：\n\n```json\n{\n    \"type\": \"http\",\n    \"request\": {\n        \"scheme\": \"http\",\n        \"path\": \"\\/get\",\n        \"headers\": {\n            \"user-agent\": \"curl\\/7.68.0\",\n            \"accept\": \"*\\/*\",\n            \"host\": \"127.0.0.1:9080\"\n        },\n        \"query\": {},\n        \"port\": 9080,\n        \"method\": \"GET\",\n        \"host\": \"127.0.0.1\"\n    },\n    \"var\": {\n        \"timestamp\": 1701234567,\n        \"server_addr\": \"127.0.0.1\",\n        \"server_port\": \"9080\",\n        \"remote_port\": \"port\",\n        \"remote_addr\": \"ip address\"\n    },\n    \"route\": {},\n    \"service\": {},\n    \"consumer\": {}\n}\n```\n\n上述代码具体释义如下：\n\n- `type` 代表请求类型（如 `http` 或 `stream`）；\n- `request` 则需要在 `type` 为 `http` 时使用，包含基本的请求信息（如 URL、头信息等）；\n- `var` 包含关于请求连接的基本信息（如 IP、端口、请求时间戳等）；\n- `route`、`service` 和 `consumer` 包含的数据与 APISIX 中存储的数据相同，只有当这些对象上配置了 `opa` 插件时才会发送。\n\n### OPA 向 APISIX 返回数据\n\n下述示例代码展示了 OPA 服务对 APISIX 发送请求后的响应数据：\n\n```json\n{\n    \"result\": {\n        \"allow\": true,\n        \"reason\": \"test\",\n        \"headers\": {\n            \"an\": \"header\"\n        },\n        \"status_code\": 401\n    }\n}\n```\n\n上述响应中的代码释义如下：\n\n- `allow` 配置是必不可少的，它表示请求是否允许通过 APISIX 进行转发；\n- `reason`、`headers` 和 `status_code` 是可选的，只有当你配置一个自定义响应时才会返回这些选项信息，具体使用方法可查看后续测试用例。\n\n## 测试插件\n\n首先启动 OPA 环境：\n\n```shell\ndocker run -d --name opa -p 8181:8181 openpolicyagent/opa:0.35.0 run -s\n```\n\n### 基本用法\n\n一旦你运行了 OPA 服务，就可以进行基本策略的创建：\n\n```shell\ncurl -X PUT '127.0.0.1:8181/v1/policies/example1' \\\n    -H 'Content-Type: text/plain' \\\n    -d 'package example1\n\nimport input.request\n\ndefault allow = false\n\nallow {\n    # HTTP method must GET\n    request.method == \"GET\"\n}'\n```\n\n然后在指定路由上配置 `opa` 插件：\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"opa\": {\n            \"host\": \"http://127.0.0.1:8181\",\n            \"policy\": \"example1\"\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n使用如下命令进行测试：\n\n```shell\ncurl -i -X GET 127.0.0.1:9080/get\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n如果尝试向不同的端点发出请求，会出现请求失败的状态：\n\n```shell\ncurl -i -X POST 127.0.0.1:9080/post\n```\n\n```shell\nHTTP/1.1 403 FORBIDDEN\n```\n\n### 使用自定义响应\n\n除了基础用法外，你还可以为更复杂的使用场景配置自定义响应，参考示例如下：\n\n```shell\ncurl -X PUT '127.0.0.1:8181/v1/policies/example2' \\\n    -H 'Content-Type: text/plain' \\\n    -d 'package example2\n\nimport input.request\n\ndefault allow = false\n\nallow {\n    request.method == \"GET\"\n}\n\n# custom response body (Accepts a string or an object, the object will respond as JSON format)\nreason = \"test\" {\n    not allow\n}\n\n# custom response header (The data of the object can be written in this way)\nheaders = {\n    \"Location\": \"http://example.com/auth\"\n} {\n    not allow\n}\n\n# custom response status code\nstatus_code = 302 {\n    not allow\n}'\n```\n\n同时，你可以将 `opa` 插件的策略参数调整为 `example2`，然后发出请求进行测试：\n\n```shell\ncurl -i -X GET 127.0.0.1:9080/get\n```\n\n```shell\nHTTP/1.1 200 OK\n```\n\n此时如果你发出一个失败请求，将会收到来自 OPA 服务的自定义响应反馈，如下所示：\n\n```shell\ncurl -i -X POST 127.0.0.1:9080/post\n```\n\n```shell\nHTTP/1.1 302 FOUND\nLocation: http://example.com/auth\n\ntest\n```\n\n### 发送 APISIX 数据\n\n如果你的 OPA 服务需要根据 APISIX 的某些数据（如 Route 和 Consumer 的详细信息）来进行后续操作时，则可以通过配置插件来实现。\n\n下述示例展示了一个简单的 `echo` 策略，它将原样返回 APISIX 发送的数据：\n\n```shell\ncurl -X PUT '127.0.0.1:8181/v1/policies/echo' \\\n    -H 'Content-Type: text/plain' \\\n    -d 'package echo\n\nallow = false\nreason = input'\n```\n\n现在就可以在路由上配置插件来发送 APISIX 数据：\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \\\n    -H 'X-API-KEY: <api-key>' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"opa\": {\n            \"host\": \"http://127.0.0.1:8181\",\n            \"policy\": \"echo\",\n            \"with_route\": true\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n此时如果你提出一个请求，则可以通过自定义响应看到来自路由的数据：\n\n```shell\ncurl -X GET 127.0.0.1:9080/get\n```\n\n```shell\n{\n    \"type\": \"http\",\n    \"request\": {\n        xxx\n    },\n    \"var\": {\n        xxx\n    },\n    \"route\": {\n        xxx\n    }\n}\n```\n\n## 删除插件\n\n当你需要禁用 `opa` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/openfunction.md",
    "content": "---\ntitle: openfunction\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - OpenFunction\ndescription: 本文介绍了 API 网关 Apache APISIX 的 openfunction 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`openfunction` 插件用于将开源的分布式无服务器平台 [CNCF OpenFunction](https://openfunction.dev/) 作为动态上游集成至 APISIX。\n\n启用 `openfunction` 插件后，该插件会终止对已配置 URI 的请求，并代表客户端向 OpenFunction 的 function 发起一个新的请求，然后 `openfunction` 插件会将响应信息返回至客户端。\n\n## 属性\n\n| 名称                         | 类型    | 必选项 | 默认值  | 有效值       | 描述                                                         |\n| --------------------------- | ------- | ------ | ------- | ------------ | ------------------------------------------------------------ |\n| function_uri                | string  | 是     |         |              | OpenFunction function uri，例如 `https://localhost:30858/default/function-sample`。     |\n| ssl_verify                  | boolean | 否     | true    |              | 当设置为 `true` 时执行 SSL 验证。                            |\n| authorization               | object  | 否     |         |              | 访问 OpenFunction 的函数的授权凭证。|\n| authorization.service_token | string  | 否     |         |              | OpenFunction service token，其格式为 `xxx:xxx`，支持函数入口的 basic auth 认证方式。 |\n| timeout                     | integer | 否     | 3000 ms | [100,...] ms | OpenFunction action 和 HTTP 调用超时时间，以毫秒为单位。          |\n| keepalive                   | boolean | 否     | true    |              | 当设置为 `true` 时，保持连接的活动状态以便重复使用。         |\n| keepalive_timeout           | integer | 否     | 60000 ms| [1000,...] ms| 当连接空闲时，保持该连接处于活动状态的时间，以毫秒为单位。               |\n| keepalive_pool              | integer | 否     | 5       | [1,...]      | 连接断开之前，可接收的最大请求数。                           |\n\n:::note 注意\n\n`timeout` 字段规定了 OpenFunction function 的最大执行时间，以及 APISIX 中 HTTP 客户端的请求超时时间。\n\n因为 OpenFunction function 调用可能会耗费很长时间来拉取容器镜像和启动容器，如果 `timeout` 字段的值设置太小，可能会导致大量请求失败。\n\n:::\n\n## 前提条件\n\n在使用 `openfunction` 插件之前，你需要通过以下命令运行 OpenFunction。详情参考 [OpenFunction 安装指南](https://openfunction.dev/docs/getting-started/installation/) 。\n\n请确保当前环境中已经安装对应版本的 Kubernetes 集群。\n\n### 创建并推送函数\n\n你可以参考 [OpenFunction 官方示例](https://github.com/OpenFunction/samples) 创建函数。构建函数时，你需要使用以下命令为容器仓库生成一个密钥，才可以将函数容器镜像推送到容器仓库 ( 例如 Docker Hub 或 Quay.io）。\n\n```shell\nREGISTRY_SERVER=https://index.docker.io/v1/ REGISTRY_USER=<your_registry_user> REGISTRY_PASSWORD=<your_registry_password>\nkubectl create secret docker-registry push-secret \\\n    --docker-server=$REGISTRY_SERVER \\\n    --docker-username=$REGISTRY_USER \\\n    --docker-password=$REGISTRY_PASSWORD\n```\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"openfunction\": {\n            \"function_uri\": \"http://localhost:3233/default/function-sample/test\",\n            \"authorization\": {\n                \"service_token\": \"test:test\"\n            }\n        }\n    }\n}'\n```\n\n## 测试插件\n\n使用 `curl` 命令测试：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello -X POST -d'test'\n```\n\n正常返回结果：\n\n```\nhello, test!\n```\n\n### 配置路径转发\n\n`OpenFunction` 插件还支持 URL 路径转发，同时将请求代理到上游的 OpenFunction API 端点。基本请求路径的扩展（如路由 `/hello/*` 中 `*` 的部分）会被添加到插件配置中指定的 `function_uri`。\n\n:::info 重要\n\n路由上配置的 `uri` 必须以 `*` 结尾，此功能才能正常工作。APISIX 路由是严格匹配的，`*` 表示此 URI 的任何子路径都将匹配到同一路由。\n\n:::\n\n下面的示例配置了此功能：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello/*\",\n    \"plugins\": {\n        \"openfunction\": {\n            \"function_uri\": \"http://localhost:3233/default/function-sample\",\n            \"authorization\": {\n                \"service_token\": \"test:test\"\n            }\n        }\n    }\n}'\n```\n\n现在，对路径 `hello/123` 的任何请求都将调用 OpenFunction 插件设置的对应的函数，并转发添加的路径：\n\n```shell\ncurl  http://127.0.0.1:9080/hello/123\n```\n\n```shell\nHello, 123!\n```\n\n## 删除插件\n\n当你需要禁用 `openfunction` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/openid-connect.md",
    "content": "---\ntitle: openid-connect\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - OpenID Connect\n  - OIDC\ndescription: openid-connect 插件支持与 OpenID Connect (OIDC) 身份提供商集成，例如 Keycloak、Auth0、Microsoft Entra ID、Google、Okta 等。它允许 APISIX 对客户端进行身份验证并从身份提供商处获取其信息，然后允许或拒绝其访问上游受保护资源。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/openid-connect\" />\n</head>\n\n## 描述\n\n`openid-connect` 插件支持与 [OpenID Connect (OIDC)](https://openid.net/connect/) 身份提供商集成，例如 Keycloak、Auth0、Microsoft Entra ID、Google、Okta 等。它允许 APISIX 对客户端进行身份验证，并从身份提供商处获取其信息，然后允许或拒绝其访问上游受保护资源。\n\n## 属性\n\n| 名称                                 | 类型     | 必选项 | 默认值                | 有效值         | 描述                                                                                             |\n| ------------------------------------ | ------- | ------ | --------------------- | ------------- | ------------------------------------------------------------------------------------------------ |\n| client_id                            | string  | 是     |                       |               | OAuth 客户端 ID。                                                                                 |\n| client_secret                        | string  | 是     |                       |               | OAuth 客户端 secret。                                                                            |\n| discovery | string | 是 | | | OpenID 提供商的知名发现文档的 URL，其中包含 OP API 端点列表。插件可以直接利用发现文档中的端点。您也可以单独配置这些端点，这优先于发现文档中提供的端点。 |\n| scope | string | 否 | openid | | 与应返回的有关经过身份验证的用户的信息相对应的 OIDC 范围，也称为 [claim](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims)。这用于向用户授权适当的权限。默认值为 `openid`，这是 OIDC 返回唯一标识经过身份验证的用户的 `sub` 声明所需的范围。可以附加其他范围并用空格分隔，例如 `openid email profile`。 |\n| required_scopes | array[string] | 否 | | | 访问令牌中必须存在的范围。当 `bearer_only` 为 `true` 时与自省端点结合使用。如果缺少任何必需的范围，插件将以 403 禁止错误拒绝请求。|\n| realm | string | 否 | apisix | | 由于持有者令牌无效，[`WWW-Authenticate`](https://www.rfc-editor.org/rfc/rfc6750#section-3) 响应标头中的领域伴随 401 未经授权的请求。 |\n| bearer_only | boolean | 否 | false | | 如果为 true，则严格要求在身份验证请求中使用持有者访问令牌。 |\n| logout_path | string | 否 | /logout | | 激活注销的路径。 |\n| post_logout_redirect_uri | string | 否 | | | `logout_path` 收到注销请求后将用户重定向到的 URL。|\n| redirect_uri | string | 否 | | | 通过 OpenID 提供商进行身份验证后重定向到的 URI。请注意，重定向 URI 不应与请求 URI 相同，而应为请求 URI 的子路径。例如，如果路由的 `uri` 是 `/api/v1/*`，则 `redirect_uri` 可以配置为 `/api/v1/redirect`。如果未配置 `redirect_uri`，APISIX 将在请求 URI 后附加 `/.apisix/redirect` 以确定 `redirect_uri` 的值。|\n| timeout | integer | 否 | 3 | [1,...] | 请求超时时间（秒）。|\n| ssl_verify | boolean | 否 | true | | 如果为 true，则验证 OpenID 提供商的 SSL 证书。|\n| introspection_endpoint | string | 否 | | |用于自检访问令牌的 OpenID 提供程序的 [令牌自检](https://datatracker.ietf.org/doc/html/rfc7662) 端点的 URL。如果未设置，则将使用众所周知的发现文档中提供的自检端点[作为后备](https://github.com/zmartzone/lua-resty-openidc/commit/cdaf824996d2b499de4c72852c91733872137c9c)。|\n| introspection_endpoint_auth_method | string | 否 | client_secret_basic | | 令牌自检端点的身份验证方法。该值应为 `introspection_endpoint_auth_methods_supported` [授权服务器元数据](https://www.rfc-editor.org/rfc/rfc8414.html) 中指定的身份验证方法之一，如众所周知的发现文档中所示，例如 `client_secret_basic`、`client_secret_post`、`private_key_jwt` 和 `client_secret_jwt`。|\n| token_endpoint_auth_method | string | 否 | client_secret_basic | | 令牌端点的身份验证方法。该值应为 `token_endpoint_auth_methods_supported` [授权服务器元数据](https://www.rfc-editor.org/rfc/rfc8414.html) 中指定的身份验证方法之一，如众所周知的发现文档中所示，例如 `client_secret_basic`、`client_secret_post`、`private_key_jwt` 和 `client_secret_jwt`。如果配置的方法不受支持，则回退到 `token_endpoint_auth_methods_supported` 数组中的第一个方法。|\n| public_key | string | 否 | | | 用于验证 JWT 签名 id 的公钥使用非对称算法。提供此值来执行令牌验证将跳过客户端凭据流中的令牌自检。您可以以 `-----BEGIN PUBLIC KEY-----\\\\n……\\\\n-----END PUBLIC KEY-----` 格式传递公钥。|\n| use_jwks | boolean | 否 | false | | 如果为 true 并且未设置 `public_key`，则使用 JWKS 验证 JWT 签名并跳过客户端凭据流中的令牌自检。JWKS 端点是从发现文档中解析出来的。|\n| use_pkce | boolean | 否 | false | | 如果为 true，则使用 [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) 中定义的授权码流的代码交换证明密钥 (PKCE)。|\n| token_signing_alg_values_expected | string | 否 | | | 用于签署 JWT 的算法，例如 `RS256`。 |\n| set_access_token_header | boolean | 否 | true | | 如果为 true，则在请求标头中设置访问令牌。默认情况下，使用 `X-Access-Token` 标头。|\n| access_token_in_authorization_header | boolean | 否 | false | | 如果为 true 并且 `set_access_token_header` 也为 true，则在 `Authorization` 标头中设置访问令牌。 |\n| set_id_token_header | boolean | 否 | true | | 如果为 true 并且 ID 令牌可用，则在 `X-ID-Token` 请求标头中设置值。 |\n| set_userinfo_header | boolean | 否 | true | | 如果为 true 并且用户信息数据可用，则在 `X-Userinfo` 请求标头中设置值。 |\n| set_refresh_token_header | boolean | 否 | false | | 如果为 true 并且刷新令牌可用，则在 `X-Refresh-Token` 请求标头中设置值。 |\n| session | object | 否 | | | 当 `bearer_only` 为 `false` 且插件使用 Authorization Code 流程时使用的 Session 配置。 |\n| session.secret | string | 是 | | 16 个字符以上 | 当 `bearer_only` 为 `false` 时，用于 session 加密和 HMAC 运算的密钥。|\n| session.cookie | object | 否 | | | Cookie 配置。 |\n| session.cookie.lifetime | integer | 否 | 3600 | | Cookie 生存时间（秒）。|\n| session.storage | string | 否 | cookie | [\"cookie\", \"redis\"] | 会话存储方式。 |\n| session.redis | object | 否 | | | 当 `storage` 为 `redis` 时的 Redis 配置。 |\n| session.redis.host | string | 否 | 127.0.0.1 | | Redis 主机地址。 |\n| session.redis.port | integer | 否 | 6379 | | Redis 端口。 |\n| session.redis.password | string | 否 | | | Redis 密码。 |\n| session.redis.username | string | 否 | | | Redis 用户名。 |\n| session.redis.database | integer | 否 | 0 | | Redis 数据库索引。 |\n| session.redis.prefix | string | 否 | sessions | | Redis 键前缀。 |\n| session.redis.ssl    | boolean   | 否    | false |             |   启用 Redis SSL 连接。    |\n| session.redis.ssl_verify | boolean   | 否    | true |             |   验证 SSL 证书。    |\n| session.redis.server_name | string   | 否    |     |             |   Redis SNI 服务器名称。    |\n| session.redis.connect_timeout | integer   | 否    | 1000 |             |   连接超时时间（毫秒）。    |\n| session.redis.send_timeout   | integer   | 否    | 1000 |             |   发送超时时间（毫秒）。    |\n| session.redis.read_timeout   | integer   | 否    | 1000 |             |   读取超时时间（毫秒）。    |\n| session.redis.keepalive_timeout | integer   | 否    | 10000 |             |   Keepalive 超时时间（毫秒）。    |\n| unauth_action | string | 否 | auth | [\"auth\",\"deny\",\"pass\"] | 未经身份验证的请求的操作。设置为 `auth` 时，重定向到 OpenID 提供程序的身份验证端点。设置为 `pass` 时，允许请求而无需身份验证。设置为 `deny` 时，返回 401 未经身份验证的响应，而不是启动授权代码授予流程。|\n| session_contents   | object   | 否    |       |        | 会话内容配置。如果未配置，将把所有数据存储在会话中。 |\n| session_contents.access_token   | boolean   | 否    |          |        | 若为 true，则将访问令牌存储在会话中。 |\n| session_contents.id_token   | boolean   | 否    |          |       | 若为 true，则将 ID 令牌存储在会话中。 |\n| session_contents.enc_id_token   | boolean   | 否    |          |        | 若为 true，则将加密的 ID 令牌存储在会话中。 |\n| session_contents.user   | boolean   | 否    |          |        | 若为 true，则将用户信息存储在会话中。 |\n| proxy_opts | object | 否 | | | OpenID 提供程序背后的代理服务器的配置。|\n| proxy_opts.http_proxy | string | 否 | |  | HTTP 请求的代理服务器地址，例如 `http://<proxy_host>:<proxy_port>`。|\n| proxy_opts.https_proxy | string | 否 | | | HTTPS 请求的代理服务器地址，例如 `http://<proxy_host>:<proxy_port>`。 |\n| proxy_opts.http_proxy_authorization | string | 否 | | Basic [base64 用户名：密码] | 与 `http_proxy` 一起使用的默认 `Proxy-Authorization` 标头值。可以用自定义的 `Proxy-Authorization` 请求标头覆盖。 |\n| proxy_opts.https_proxy_authorization | string | 否 | | Basic [base64 用户名：密码] | 与 `https_proxy` 一起使用的默认 `Proxy-Authorization` 标头值。不能用自定义的 `Proxy-Authorization` 请求标头覆盖，因为使用 HTTPS 时，授权在连接时完成。 |\n| proxy_opts.no_proxy | string | 否 | | | 不应代理的主机的逗号分隔列表。|\n| authorization_params | object | 否 | | | 在请求中发送到授权端点的附加参数。 |\n| client_rsa_private_key | string | 否 | | | 用于签署 JWT 以向 OP 进行身份验证的客户端 RSA 私钥。当 `token_endpoint_auth_method` 为 `private_key_jwt` 时必需。 |\n| client_rsa_private_key_id | string | 否 | | | 用于计算签名的 JWT 的客户端 RSA 私钥 ID。当 `token_endpoint_auth_method` 为 `private_key_jwt` 时可选。 |\n| client_jwt_assertion_expires_in | integer | 否 | 60 | | 用于向 OP 进行身份验证的签名 JWT 的生命周期，以秒为单位。当 `token_endpoint_auth_method` 为 `private_key_jwt` 或 `client_secret_jwt` 时使用。 |\n| renew_access_token_on_expiry | boolean | 否 | true | | 如果为 true，则在访问令牌过期或刷新令牌可用时尝试静默更新访问令牌。如果令牌无法更新，则重定向用户进行重新身份验证。|\n| access_token_expires_in | integer | 否 | | | 如果令牌端点响应中不存在 `expires_in` 属性，则访问令牌的有效期（以秒为单位）。 |\n| refresh_session_interval | integer | 否 | | | 刷新用户 ID 令牌而无需重新认证的时间间隔。如果未设置，则不会检查网关向客户端发出的会话的到期时间。如果设置为 900，则表示在 900 秒后刷新用户的 `id_token`（或浏览器中的会话），而无需重新认证。 |\n| iat_slack | integer | 否 | 120 | | ID 令牌中 `iat` 声明的时钟偏差容忍度（以秒为单位）。 |\n| accept_none_alg | boolean | 否 | false | | 如果 OpenID 提供程序未签署其 ID 令牌（例如当签名算法设置为`none` 时），则设置为 true。 |\n| accept_unsupported_alg | boolean | 否 | true | | 如果为 true，则忽略 ID 令牌签名以接受不支持的签名算法。 |\n| access_token_expires_leeway | integer | 否 | 0 | | 访问令牌续订的过期余地（以秒为单位）。当设置为大于 0 的值时，令牌续订将在令牌过期前设定的时间内进行。这样可以避免访问令牌在到达资源服务器时刚好过期而导致的错误。|\n| force_reauthorize | boolean | 否 | false | | 如果为 true，即使令牌已被缓存，也执行授权流程。 |\n| use_nonce | boolean | 否 | false | | 如果为 true，在授权请求中启用 nonce 参数。 |\n| revoke_tokens_on_logout | boolean | 否 | false | | 如果为 true，则通知授权服务器，撤销端点不再需要先前获得的刷新或访问令牌。 |\n| jwk_expires_in | integer | 否 | 86400 | | JWK 缓存的过期时间（秒）。 |\n| jwt_verification_cache_ignore | boolean | 否 | false | | 如果为 true，则强制重新验证承载令牌并忽略任何现有的缓存验证结果。 |\n| cache_segment | string | 否 | | | 缓存段的可选名称，用于分隔和区分令牌自检或 JWT 验证使用的缓存。|\n| introspection_interval | integer | 否 | 0 | | 缓存和自省访问令牌的 TTL（以秒为单位）。默认值为 0，这意味着不使用此选项，插件默认使用 `introspection_expiry_claim` 中定义的到期声明传递的 TTL。如果`introspection_interval` 大于 0 且小于 `introspection_expiry_claim` 中定义的到期声明传递的 TTL，则使用`introspection_interval`。|\n| introspection_expiry_claim | string | 否 | exp | | 到期声明的名称，它控制缓存和自省访问令牌的 TTL。|\n| introspection_addon_headers | array[string] | 否 | | | 用于将其他标头值附加到自省 HTTP 请求。如果原始请求中不存在指定的标头，则不会附加值。|\n| claim_validator | object | 否 |  |  | JWT 声明（claim）验证的相关配置。 |\n| claim_validator.issuer.valid_issuers | array[string] | 否 |  |  | 可信任的 JWT 发行者（issuer）列表。如果未配置，将使用发现端点返回的发行者；如果两者都不可用，将不会验证发行者。 |\n| claim_validator.audience | object | 否 |  |  | [Audience 声明](https://openid.net/specs/openid-connect-core-1_0.html) 验证的相关配置。 |\n| claim_validator.audience.claim | string | 否 | aud |  | 包含受众（audience）的声明名称。 |\n| claim_validator.audience.required | boolean | 否 | false |  | 若为 `true`，则要求必须存在受众声明，其名称为 `claim` 中定义的值。 |\n| claim_validator.audience.match_with_client_id | boolean | 否 | false |  | 若为 `true`，则要求受众（audience）必须与客户端 ID 匹配。若受众为字符串，则必须与客户端 ID 完全一致；若受众为字符串数组，则至少有一个值需与客户端 ID 匹配。若未找到匹配项，将返回 `mismatched audience` 错误。此要求来自 OpenID Connect 规范，用于确保令牌仅用于指定的客户端。 |\n| claim_schema | object | 否 |  |  | OIDC 响应 claim 的 JSON schema。示例：`{\"type\":\"object\",\"properties\":{\"access_token\":{\"type\":\"string\"}},\"required\":[\"access_token\"]}` - 验证响应中包含必需的字符串字段 `access_token`。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"client_secret\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n此外：你可以使用环境变量或者 APISIX secret 来存放和引用插件配置，APISIX 当前支持通过两种方式配置 secrets - [Environment Variables and HashiCorp Vault](../terminology/secret.md)。\n\n例如：你可以使用以下方式来设置环境变量\n`export keycloak_secret=abc`\n\n并且像下面这样在插件里使用\n\n`\"client_secret\": \"$ENV://keycloak_secret\"`\n\n## 示例\n\n以下示例演示了如何针对不同场景配置 `openid-connect` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### Authorization Code Flow\n\nAuthorization Code Flow 在 [RFC 6749，第 4.1 节](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) 中定义。它涉及用临时授权码交换访问令牌，通常由机密和公共客户端使用。\n\n下图说明了实施 Authorization Code Flow 时不同实体之间的交互：\n\n![授权码流程图](https://static.api7.ai/uploads/2023/11/27/Ga2402sb_oidc-code-auth-flow-revised.png)\n\n当传入请求的标头中或适当的会话 cookie 中不包含访问令牌时，插件将充当依赖方并重定向到授权服务器以继续授权码流程。\n\n成功验证后，插件将令牌保留在会话 cookie 中，后续请求将使用存储在 cookie 中的令牌。\n\n请参阅 [实现 Authorization Code Flow](../tutorials/keycloak-oidc.md#实现-authorization-code-grant)以获取使用`openid-connect`插件通过授权码流与 Keycloak 集成的示例。\n\n### Proof Key for Code Exchange (PKCE)\n\nProof Key for Code Exchange (PKCE) 在 [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) 中定义。PKCE 通过添加代码质询和验证器来增强授权码流程，以防止授权码拦截攻击。\n\n下图说明了使用 PKCE 实现授权码流程时不同实体之间的交互：\n\n![使用 PKCE 的授权码流程图](https://static.api7.ai/uploads/2024/11/04/aJ2ZVuTC_auth-code-with-pkce.png)\n\n请参阅 [实现 Authorization Code Grant](../tutorials/keycloak-oidc.md#实现-authorization-code-grant)，了解使用 `openid-connect` 插件通过 PKCE 授权码流程与 Keycloak 集成的示例。\n\n### Client Credential Flow\n\nClient Credential Flow 在 [RFC 6749，第 4.4 节](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) 中定义。它涉及客户端使用自己的凭证请求访问令牌以访问受保护的资源，通常用于机器对机器身份验证，并不代表特定用户。\n\n下图说明了实施 Client Credential Flow 时不同实体之间的交互：\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/28/sbHxqnOz_client-credential-no-introspect.png\" alt=\"Client credential flow diagram\" style={{width: '70%'}} />\n</div>\n<br />\n\n请参阅[实现 Client Credentials Grant](../tutorials/keycloak-oidc.md#实现-client-credentials-grant) 获取使用 `openid-connect` 插件通过客户端凭证流与 Keycloak 集成的示例。\n\n### Introspection Flow\n\nIntrospection Flow 在 [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) 中定义。它涉及通过查询授权服务器的自省端点来验证访问令牌的有效性和详细信息。\n\n在此流程中，当客户端向资源服务器出示访问令牌时，资源服务器会向授权服务器的自省端点发送请求，如果令牌处于活动状态，则该端点会响应令牌详细信息，包括令牌到期时间、相关范围以及它所属的用户或客户端等信息。\n\n下图说明了使用令牌自省实现 Introspection Flow 时不同实体之间的交互：\n\n<br />\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/29/Y2RWIUV9_client-cred-flow-introspection.png\" alt=\"Client credential with introspection diagram\" style={{width: '55%'}} />\n</div>\n<br />\n\n请参阅 [实现 Client Credentials Grant](../tutorials/keycloak-oidc.md#实现-client-credentials-grant) 以获取使用 `openid-connect` 插件通过带有令牌自省的客户端凭据流与 Keycloak 集成的示例。\n\n### Password Flow\n\nPassword Flow 在 [RFC 6749，第 4.3 节](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) 中定义。它专为受信任的应用程序而设计，允许它们使用用户的用户名和密码直接获取访问令牌。在此授权类型中，客户端应用程序将用户的凭据连同其自己的客户端 ID 和密钥一起发送到授权服务器，然后授权服务器对用户进行身份验证，如果有效，则颁发访问令牌。\n\n虽然高效，但此流程仅适用于高度受信任的第一方应用程序，因为它要求应用程序直接处理敏感的用户凭据，如果在第三方环境中使用，则会带来重大安全风险。\n\n下图说明了实施 Password Flow 时不同实体之间的交互：\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/30/njkWZVgX_pass-grant.png\" alt=\"Password flow diagram\" style={{width: '70%'}} />\n</div>\n<br />\n\n请参阅 [实现 Password Grant](../tutorials/keycloak-oidc.md#实现-password-grant) 获取使用 `openid-connect` 插件通过密码流与 Keycloak 集成的示例。\n\n### Refresh Token Grant\n\nRefresh Token Grant 在 [RFC 6749，第 6 节](https://datatracker.ietf.org/doc/html/rfc6749#section-6) 中定义。它允许客户端使用之前颁发的刷新令牌请求新的访问令牌，而无需用户重新进行身份验证。此流程通常在访问令牌过期时使用，允许客户端无需用户干预即可持续访问资源。刷新令牌与某些 OAuth 流程中的访问令牌一起颁发，其使用寿命和安全要求取决于授权服务器的配置。\n\n下图说明了在实施 Password Grant 和 Refresh Token Grant 时不同实体之间的交互：\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/10/30/YBF7rI6M_password-with-refresh-token.png\" alt=\"Password grant with refresh token flow diagram\" style={{width: '100%'}} />\n</div>\n<br />\n\n请参阅 [Refresh Token](../tutorials/keycloak-oidc.md#refresh-token) 获取使用 `openid-connect` 插件通过带令牌刷新的密码流与 Keycloak 集成的示例。\n\n## 故障排除\n\n本节介绍使用此插件时的一些常见问题，以帮助您排除故障。\n\n### APISIX 无法连接到 OpenID 提供商\n\n如果 APISIX 无法解析或无法连接到 OpenID 提供商，请仔细检查配置文件 `config.yaml` 中的 DNS 设置并根据需要进行修改。\n\n### `No Session State Found`\n\n如果您在使用[授权码流](#authorization-code-flow) 时遇到 500 内部服务器错误并在日志中显示以下消息，则可能有多种原因。\n\n```text\nthe error request to the redirect_uri path, but there's no session state found\n```\n\n#### 1. 重定向 URI 配置错误\n\n一个常见的错误配置是将 `redirect_uri` 配置为与路由的 URI 相同。当用户发起访问受保护资源的请求时，请求直接命中重定向 URI，且请求中没有 session cookie，从而导致 no session state found 错误。\n\n要正确配置重定向 URI，请确保 `redirect_uri` 与配置插件的路由匹配，但不要完全相同。例如，正确的配置是将路由的 `uri` 配置为 `/api/v1/*`，并将 `redirect_uri` 的路径部分配置为 `/api/v1/redirect`。\n\n您还应该确保 `redirect_uri` 包含 scheme，例如 `http` 或 `https` 。\n\n#### 2. Cookie 未发送或不存在\n\n检查 [`SameSite`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) cookie 属性是否已正确设置（即您的应用程序是否需要跨站点发送 cookie），看看这是否会成为阻止 cookie 保存到浏览器的 cookie jar 或从浏览器发送的因素。\n\n#### 3. 上游发送的标头太大\n\n如果您有 NGINX 位于 APISIX 前面来代理客户端流量，请查看 NGINX 的 `error.log` 中是否观察到以下错误：\n\n```text\nupstream sent too big header while reading response header from upstream\n```\n\n如果是这样，请尝试将 `proxy_buffers` 、 `proxy_buffer_size` 和 `proxy_busy_buffers_size` 调整为更大的值。\n\n另一个选项是配置 `session_content` 属性来调整在会话中存储哪些数据。例如，你可以将 `session_content.access_token` 设置为 `true`。\n\n#### 4. 无效的客户端密钥\n\n验证 `client_secret` 是否有效且正确。无效的 `client_secret` 将导致身份验证失败，并且不会返回任何令牌并将其存储在 session 中。\n\n#### 5. PKCE IdP 配置\n\n如果您使用授权码流程启用 PKCE，请确保您已将 IdP 客户端配置为使用 PKCE。例如，在 Keycloak 中，您应该在客户端的高级设置中配置 PKCE 质询方法：\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/11/04/xvnCNb20_pkce-keycloak-revised.jpeg\" alt=\"PKCE keycloak configuration\" style={{width: '70%'}} />\n</div>\n"
  },
  {
    "path": "docs/zh/latest/plugins/opentelemetry.md",
    "content": "---\ntitle: opentelemetry\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - OpenTelemetry\ndescription: opentelemetry 插件可用于根据 OpenTelemetry 协议规范上报 Traces 数据，该插件仅支持二进制编码的 OLTP over HTTP。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/opentelemetry\" />\n</head>\n\n## 描述\n\n`opentelemetry` 插件可用于根据 [OpenTelemetry Specification](https://opentelemetry.io/docs/reference/specification/) 协议规范上报 Traces 数据。该插件仅支持二进制编码的 OLTP over HTTP，即请求类型为 `application/x-protobuf` 的数据上报。\n\n## 配置\n\n默认情况下，服务名称、租户 ID、collector 和 batch span processor 的配置已预配置在[默认配置](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua)中。\n\n您可以通过端点 `apisix/admin/plugin_metadata/opentelemetry` 更改插件的配置，例如：\n\n:::note\n您可以从“config.yaml”获取“admin_key”,并使用以下命令保存到环境变量中：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/opentelemetry -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"trace_id_source\": \"x-request-id\",\n    \"resource\": {\n      \"service.name\": \"APISIX\"\n    },\n    \"collector\": {\n      \"address\": \"127.0.0.1:4318\",\n      \"request_timeout\": 3,\n      \"request_headers\": {\n        \"Authorization\": \"token\"\n      }\n    },\n    \"batch_span_processor\": {\n      \"drop_on_queue_full\": false,\n      \"max_queue_size\": 1024,\n      \"batch_timeout\": 2,\n      \"inactive_timeout\": 1,\n      \"max_export_batch_size\": 16\n    },\n    \"set_ngx_var\": false\n}'\n```\n\n## 属性\n\n| 名称                                  | 类型           | 必选项    | 默认值        | 有效值        | 描述 |\n|---------------------------------------|---------------|----------|--------------|--------------|-------------|\n| sampler                               | object        | 否       | -            | -            | 采样策略。    |\n| sampler.name                          | string        | 否       | `always_off` | [\"always_on\", \"always_off\", \"trace_id_ratio\", \"parent_base\"]  | 采样策略。<br />`always_on`：全采样；`always_off`：不采样；`trace_id_ratio`：基于 trace id 的百分比采样；`parent_base`：如果存在 tracing 上游，则使用上游的采样决定，否则使用配置的采样策略决策。|\n| sampler.options                       | object        | 否       | -            | -            | 采样策略参数。 |\n| sampler.options.fraction              | number        | 否       | 0            | [0, 1]       | `trace_id_ratio`：采样策略的百分比。 |\n| sampler.options.root                  | object        | 否       | -            | -            | `parent_base`：采样策略在没有上游 tracing 时，会使用 root 采样策略做决策。|\n| sampler.options.root.name             | string        | 否       | -            | [\"always_on\", \"always_off\", \"trace_id_ratio\"] | root 采样策略。 |\n| sampler.options.root.options          | object        | 否       | -            | -            | root 采样策略参数。 |\n| sampler.options.root.options.fraction | number        | 否       | 0            | [0, 1]       | `trace_id_ratio` root 采样策略的百分比|\n| additional_attributes                 | array[string] | 否       | -            | -            | 追加到 trace span 的额外属性，支持内置 NGINX 或 [APISIX 变量](https://apisix.apache.org/docs/apisix/apisix-variable/)。|\n| additional_header_prefix_attributes   | array[string] | 否       | -            | -            | 附加到跟踪范围属性的标头或标头前缀。例如，使用 `x-my-header\"` 或 `x-my-headers-*` 来包含带有前缀 `x-my-headers-` 的所有标头。 |\n\n## 示例\n\n以下示例展示了如何在不同场景下使用 `opentelemetry` 插件。\n\n### 启用全面的请求生命周期追踪\n\n:::note\n\n开启全面追踪会在请求生命周期的各个阶段引入 span 的创建与上报开销，会对 APISIX 吞吐量和延迟产生影响。\n\n:::\n\n要在请求生命周期的各个阶段（包括 SSL/SNI、rewrite、access、header_filter、body_filter、log）启用全面追踪，请在配置文件中将 `tracing` 字段设置为 `true`：\n\n```yaml title=\"config.yaml\"\napisix:\n  tracing: true\n```\n\n### 启用 opentelemetry 插件\n\n默认情况下，APISIX 中的 `opentelemetry` 插件是禁用的。要启用它，请将插件添加到配置文件中，如下所示：\n\n```yaml title=\"config.yaml\"\nplugins:\n  - ...\n  - opentelemetry\n```\n\n重新加载 APISIX 以使更改生效。\n\n### 将 Traces 上报到 OpenTelemetry\n\n以下示例展示了如何追踪对路由的请求并将 traces 发送到 OpenTelemetry。\n\n在 Docker 启动一个 OpenTelemetry collector 实例：\n\n```shell\ndocker run -d --name otel-collector -p 4318:4318 otel/opentelemetry-collector-contrib\n```\n\n创建一个开启了 `opentelemetry` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"otel-tracing-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"opentelemetry\": {\n        \"sampler\": {\n          \"name\": \"always_on\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\n你应该收到一个 `HTTP/1.1 200 OK` 响应。\n\n在 OpenTelemetry collector 的日志中，你应该看到类似以下的信息：\n\n```text\ninfo\tResourceSpans #0\nResource SchemaURL:\nResource attributes:\n     -> telemetry.sdk.language: Str(lua)\n     -> telemetry.sdk.name: Str(opentelemetry-lua)\n     -> telemetry.sdk.version: Str(0.1.1)\n     -> hostname: Str(RC)\n     -> service.name: Str(APISIX)\nScopeSpans #0\nScopeSpans SchemaURL:\nInstrumentationScope opentelemetry-lua\nSpan #0\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0adf392b5c84111\n    ID             : d9816bbaef5ee63d\n    Name           : http_router_match\n    Kind           : Internal\n    Start time     : 2026-02-04 05:57:04.846881024 +0000 UTC\n    End time       : 2026-02-04 05:57:04.846951936 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #1\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : d0adf392b5c84111\n    Name           : apisix.phase.access\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.846562048 +0000 UTC\n    End time       : 2026-02-04 05:57:04.84724608 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #2\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : 4eb72d55359331fa\n    Name           : resolve_dns\n    Kind           : Internal\n    Start time     : 2026-02-04 05:57:04.847251968 +0000 UTC\n    End time       : 2026-02-04 05:57:04.84726912 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #3\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : de572aad9bad3b47\n    Name           : apisix.phase.header_filter\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.84793088 +0000 UTC\n    End time       : 2026-02-04 05:57:04.848005888 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #4\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : 0baddeee6e5d500d\n    Name           : apisix.phase.body_filter\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.848007936 +0000 UTC\n    End time       : 2026-02-04 05:57:04.848103936 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #5\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      : d0c33adf97b099f3\n    ID             : d57d53882c40612a\n    Name           : apisix.phase.log.plugins.opentelemetry\n    Kind           : Internal\n    Start time     : 2026-02-04 05:57:04.84823296 +0000 UTC\n    End time       : 2026-02-04 05:57:04.848385024 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nSpan #6\n    Trace ID       : a5499493b517a3333578c2ac4fad3f4d\n    Parent ID      :\n    ID             : d0c33adf97b099f3\n    Name           : GET /anything\n    Kind           : Server\n    Start time     : 2026-02-04 05:57:04.84655488 +0000 UTC\n    End time       : 2026-02-04 05:57:04.84839296 +0000 UTC\n    Status code    : Unset\n    Status message :\n    DroppedAttributesCount: 0\n    DroppedEventsCount: 0\n    DroppedLinksCount: 0\nAttributes:\n     -> net.host.name: Str(localhost)\n     -> http.method: Str(GET)\n     -> http.scheme: Str(http)\n     -> http.target: Str(/anything)\n     -> http.user_agent: Str(curl/7.81.0)\n     -> http.request.method: Str(GET)\n     -> url.scheme: Str(http)\n     -> uri.path: Str(/anything)\n     -> user_agent.original: Str(curl/7.81.0)\n     -> apisix.route_id: Str(otel-tracing-route)\n     -> apisix.route_name: Empty()\n     -> http.route: Str(/anything)\n     -> http.status_code: Int(200)\n     -> http.response.status_code: Int(200)\n{\"resource\": {\"service.instance.id\": \"ed436c1a-6ee7-46b0-ad58-527d0aaf4ade\", \"service.name\": \"otelcol-contrib\", \"service.version\": \"0.144.0\"}, \"otelcol.component.id\": \"debug\", \"otelcol.component.kind\": \"exporter\", \"otelcol.signal\": \"traces\"}\n```\n\n要可视化这些追踪，你可以将 traces 导出到后端服务，例如 Zipkin 和 Prometheus。有关更多详细信息，请参阅[exporters](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter)。\n\n### 在日志中使用 trace 变量\n\n以下示例展示了如何配置 `opentelemetry` 插件以设置以下内置变量，这些变量可以在日志插件或访问日志中使用：\n\n- `opentelemetry_context_traceparent`:  [W3C trace context](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format)\n- `opentelemetry_trace_id`: 当前 span 的 trace_id\n- `opentelemetry_span_id`: 当前 span 的 span_id\n\n配置插件元数据以将 `set_ngx_var` 设置为 true：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/opentelemetry -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"set_ngx_var\": true\n}'\n```\n\n如下更新配置文件。你应该自定义访问日志格式以使用 `opentelemetry` 插件变量，并在 `set_ngx_var` 字段中设置 `opentelemetry` 变量。\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    enable_access_log: true\n    access_log_format: '{\"time\": \"$time_iso8601\",\"opentelemetry_context_traceparent\": \"$opentelemetry_context_traceparent\",\"opentelemetry_trace_id\": \"$opentelemetry_trace_id\",\"opentelemetry_span_id\": \"$opentelemetry_span_id\",\"remote_addr\": \"$remote_addr\"}'\n    access_log_format_escape: json\n```\n\n重新加载 APISIX 以使配置更改生效。\n\n```text\n{\"time\": \"18/Feb/2024:15:09:00 +0000\",\"opentelemetry_context_traceparent\": \"00-fbd0a38d4ea4a128ff1a688197bc58b0-8f4b9d9970a02629-01\",\"opentelemetry_trace_id\": \"fbd0a38d4ea4a128ff1a688197bc58b0\",\"opentelemetry_span_id\": \"af3dc7642104748a\",\"remote_addr\": \"172.10.0.1\"}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/openwhisk.md",
    "content": "---\ntitle: openwhisk\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - OpenWhisk\ndescription: 本文介绍了关于 Apache APISIX openwhisk 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`openwhisk` 插件用于将开源的分布式无服务器平台 [Apache OpenWhisk](https://openwhisk.apache.org) 作为动态上游集成至 APISIX。\n\n启用 `openwhisk` 插件后，该插件会终止对已配置 URI 的请求，并代表客户端向 OpenWhisk 的 API Host 端点发起一个新的请求，然后 `openwhisk` 插件会将响应信息返回至客户端。\n\n## 属性\n\n| 名称              | 类型    | 必选项 | 默认值  | 有效值       | 描述                                                         |\n| ----------------- | ------- | ------ | ------- | ------------ | ------------------------------------------------------------ |\n| api_host          | string  | 是     |         |              | OpenWhisk API Host 地址，例如 `https://localhost:3233`。     |\n| ssl_verify        | boolean | 否     | true    |              | 当设置为 `true` 时执行 SSL 验证。                            |\n| service_token     | string  | 是     |         |              | OpenWhisk service token，其格式为 `xxx:xxx` ，用于 API 调用时的身份认证。 |\n| namespace         | string  | 是     |         |              | OpenWhisk namespace，例如 `guest`。                          |\n| action            | string  | 是     |         |              | OpenWhisk action，例如 `hello`。                              |\n| result            | boolean | 否     | true    |              | 当设置为 `true` 时，获得 action 元数据（执行函数并获得响应结果）。 |\n| timeout           | integer | 否     | 60000ms | [1,60000]ms  | OpenWhisk action 和 HTTP 调用超时时间（以毫秒为单位）。          |\n| keepalive         | boolean | 否     | true    |              | 当设置为 `true` 时，保持连接的活动状态以便重复使用。         |\n| keepalive_timeout | integer | 否     | 60000ms | [1000,...]ms | 当连接空闲时，保持该连接处于活动状态的时间（以毫秒为单位）。               |\n| keepalive_pool    | integer | 否     | 5       | [1,...]      | 连接断开之前，可接收的最大请求数。                           |\n\n:::note 注意\n\n`timeout` 字段规定了 OpenWhisk action 的最大执行时间，以及 APISIX 中 HTTP 客户端的请求超时时间。\n\n因为 OpenWhisk action 调用可能会耗费很长时间来拉取容器镜像和启动容器，所以如果 `timeout` 字段值设置太小，可能会导致大量的失败请求。\n\n在 OpenWhisk 中 `timeout` 字段的值设置范围从 1 ms 到 60000 ms，建议用户将 `timeout` 字段的值至少设置为 1000ms。\n\n:::\n\n## 启用插件\n\n### 搭建 Apache OpenWhisk 测试环境\n\n1. 在使用 `openwhisk` 插件之前，你需要通过以下命令运行 OpenWhisk standalone 模式。请确保当前环境中已经安装 Docker 软件。\n\n```shell\ndocker run --rm -d \\\n  -h openwhisk --name openwhisk \\\n  -p 3233:3233 -p 3232:3232 \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  openwhisk/standalone:nightly\ndocker exec openwhisk waitready\n```\n\n2. 安装 [openwhisk-cli](https://github.com/apache/openwhisk-cli) 工具：\n\n你可以在 [openwhisk-cli](https://github.com/apache/openwhisk-cli) 仓库下载已发布的适用于 Linux 系统的可执行二进制文件 wsk。\n\n3. 在 OpenWhisk 中注册函数：\n\n```shell\nwsk property set --apihost \"http://localhost:3233\" --auth \"${service_token}\"\nwsk action update test <(echo 'function main(){return {\"ready\":true}}') --kind nodejs:14\n```\n\n### 创建路由\n\n你可以通过以下命令在指定路由中启用该插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"openwhisk\": {\n            \"api_host\": \"http://localhost:3233\",\n            \"service_token\": \"${service_token}\",\n            \"namespace\": \"guest\",\n            \"action\": \"test\"\n        }\n    }\n}'\n```\n\n### 测试请求\n\n使用 `curl` 命令测试：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n正常返回结果：\n\n```json\n{ \"ready\": true }\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/prometheus.md",
    "content": "---\ntitle: prometheus\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Prometheus\ndescription:  本文将介绍 prometheus 插件，以及将 APISIX 与 Prometheus 集成以进行指标收集和持续监控。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/prometheus\" />\n</head>\n\n## 描述\n\n`prometheus` 插件提供将 APISIX 与 Prometheus 集成的能力。\n\n启用该插件后，APISIX 将开始收集相关指标，例如 API 请求和延迟，并以[基于文本的展示格式](https://prometheus.io/docs/instrumenting/exposition_formats/#exposition-formats)导出到 Prometheus。然后，您可以在 Prometheus 中创建事件监控和警报，以监控 API 网关和 API 的健康状况。\n\n## 静态配置\n\n默认情况下，已在默认配置文件 [`config.lua`](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua) 中对 `prometheus` 进行预配置。\n\n要自定义这些值，请将相应的配置添加到 config.yaml 中。例如：\n\n```yaml\nplugin_attr:\n  prometheus:                               # 插件：prometheus 属性\n    export_uri: /apisix/prometheus/metrics  # 设置 Prometheus 指标端点的 URI。\n    metric_prefix: apisix_                  # 设置 APISIX 生成的 Prometheus 指标的前缀。\n    enable_export_server: true              # 启用 Prometheus 导出服务器。\n    export_addr:                            # 设置 Prometheus 导出服务器的地址。\n      ip: 127.0.0.1                         # 设置 IP。\n      port: 9091                            # 设置端口。\n    # metrics:                              # 为指标创建额外的标签。\n    #  http_status:                         # 这些指标将以 `apisix_` 为前缀。\n    #    extra_labels:                      # 设置 http_status 指标的额外标签。\n    #      - upstream_addr: $upstream_addr\n    #      - status: $upstream_status\n    #    expire: 0                          # 指标的过期时间（秒）。\n                                            # 0 表示指标不会过期。\n    #  http_latency:\n    #    extra_labels:                      # 设置 http_latency 指标的额外标签。\n    #      - upstream_addr: $upstream_addr\n    #    expire: 0                          # 指标的过期时间（秒）。\n                                            # 0 表示指标不会过期。\n    #  bandwidth:\n    #    extra_labels:                      # 设置 bandwidth 指标的额外标签。\n    #      - upstream_addr: $upstream_addr\n    #    expire: 0                          # 指标的过期时间（秒）。\n                                            # 0 表示指标不会过期。\n    # default_buckets:                      # 设置 `http_latency` 指标直方图的默认桶。\n    #   - 10\n    #   - 50\n    #   - 100\n    #   - 200\n    #   - 500\n    #   - 1000\n    #   - 2000\n    #   - 5000\n    #   - 10000\n    #   - 30000\n    #   - 60000\n    #   - 500\n```\n\n您可以使用 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)创建 `extra_labels`。请参见[为指标添加额外标签](#为指标添加额外标签)。\n\n重新加载 APISIX 以使更改生效。\n\n## 属性\n\n| 名称         | 类型     | 必选项 | 默认值 |  描述                                                  |\n| ------------ | --------| ------ | ------ | ----------------------------------------------------- |\n|prefer_name | boolean | 否     | False  | 当设置为 `true` 时，则在`prometheus` 指标中导出路由/服务名称而非它们的 `id`。 |\n\n## 指标\n\nPrometheus 中有不同类型的指标。要了解它们之间的区别，请参见[指标类型](https://prometheus.io/docs/concepts/metric_types/)。\n\n以下是 `prometheus` 插件默认导出的指标。有关示例，请参见[获取 APISIX 指标](#获取 APISIX 指标)。请注意，一些指标，例如 `apisix_batch_process_entries`，如果没有数据，将不可见。\n\n| 名称                    | 类型      | 描述                                                                                                                                                                   |\n| ----------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| apisix_bandwidth        | counter   | APISIX 中每个服务消耗的总流量（字节）。                                                                                                                               |\n| apisix_etcd_modify_indexes | gauge     | APISIX 键的 etcd 修改次数。                                                                                                                                          |\n| apisix_batch_process_entries | gauge     | 发送数据时批处理中的剩余条目数，例如使用 `http logger` 和其他日志插件。                                                                                             |\n| apisix_etcd_reachable   | gauge     | APISIX 是否可以访问 etcd。值为 `1` 表示可达，`0` 表示不可达。                                                                                                      |\n| apisix_http_status      | counter   | 从上游服务返回的 HTTP 状态代码。                                                                                                                                     |\n| apisix_http_requests_total | gauge     | 来自客户端的 HTTP 请求数量。                                                                                                                                         |\n| apisix_nginx_http_current_connections | gauge     | 当前与客户端的连接数量。                                                                                                                                             |\n| apisix_nginx_metric_errors_total | counter   | `nginx-lua-prometheus` 错误的总数。                                                                                                                                 |\n| apisix_http_latency     | histogram | HTTP 请求延迟（毫秒）。                                                                                                                                               |\n| apisix_node_info        | gauge     | APISIX 节点的信息，例如主机名和当前的 APISIX 版本号。                                                                                                                                       |\n| apisix_shared_dict_capacity_bytes | gauge     | [NGINX 共享字典](https://github.com/openresty/lua-nginx-module#ngxshareddict) 的总容量。                                                                                     |\n| apisix_shared_dict_free_space_bytes | gauge     | [NGINX 共享字典](https://github.com/openresty/lua-nginx-module#ngxshareddict) 中剩余的空间。                                                                                   |\n| apisix_upstream_status   | gauge     | 上游节点的健康检查状态，如果在上游配置了健康检查，则可用。值为 `1` 表示健康，`0` 表示不健康。                                                                                   |\n| apisix_stream_connection_total | counter   | 每个 Stream Route 处理的总连接数。                                                                                                                                         |\n\n## 标签\n\n[标签](https://prometheus.io/docs/practices/naming/#labels) 是指标的属性，用于区分指标。\n\n例如，`apisix_http_status` 指标可以使用 `route` 信息进行标记，以识别 HTTP 状态的来源路由。\n\n以下是 APISIX 指标的非详尽标签及其描述。\n\n### `apisix_http_status` 的标签\n\n以下标签用于区分 `apisix_http_status` 指标。\n\n| 名称   | 描述                                                                                                                   |\n| ------ | ---------------------------------------------------------------------------------------------------------------------- |\n| code   | 上游节点返回的 HTTP 响应代码。                                                                                       |\n| route  | HTTP 状态来源的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。 |\n| matched_uri | 匹配请求的路由 URI。如果请求不匹配任何路由，则默认为空字符串。                                                       |\n| matched_host | 匹配请求的路由主机。如果请求不匹配任何路由，或路由未配置主机，则默认为空字符串。                                     |\n| service | HTTP 状态来源的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer | 与请求关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                                             |\n| node   | 上游节点的 IP 地址。                                                                                                   |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n### `apisix_bandwidth` 的标签\n\n以下标签用于区分 `apisix_bandwidth` 指标。\n\n| 名称   | 描述                                                                                                                   |\n| ------ | ---------------------------------------------------------------------------------------------------------------------- |\n| type   | 流量类型，`egress` 或 `ingress`。                                                                                     |\n| route  | 带宽对应的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。 |\n| service | 带宽对应的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer | 与请求关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                                             |\n| node   | 上游节点的 IP 地址。                                                                                                   |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n### Labels for `apisix_llm_latency`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |                                                                                             |\n| route_id      | 带宽对应的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。                        |\n| service_id    | 带宽对应的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer   | 与请求关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                       |\n| node       | 上游节点的 IP 地址。                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n### Labels for `apisix_llm_active_connections`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| route      | Name of the Route that bandwidth corresponds to. Default to an empty string if a request does not match any Route.                                                                                 |\n| route_id      | 带宽对应的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。                         |\n| matched_uri | 匹配请求的路由 URI。如果请求不匹配任何路由，则默认为空字符串。                                                       |\n| matched_host | 匹配请求的路由主机。如果请求不匹配任何路由，或路由未配置主机，则默认为空字符串。                                     |\n| service    | Name of the Service that bandwidth corresponds to. Default to the configured value of host on the Route if the matched Route does not belong to any Service. |\n| service_id    |  带宽对应的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer   | 与请求关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                       |\n| node       | 上游节点的 IP 地址。                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n### Labels for `apisix_llm_completion_tokens`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |                                                                                             |\n| route_id      | 带宽对应的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。                         |\n| service_id    |  带宽对应的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer   | 与请求关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                       |\n| node       | 上游节点的 IP 地址。                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n### Labels for `apisix_llm_prompt_tokens`\n\n| Name | Description                                                                                                                   |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |                                                                                             |\n| route_id      | 带宽对应的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。                         |\n| service_id    |  带宽对应的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer   | 与请求关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                       |\n| node       | 上游节点的 IP 地址。                                                                                          |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n### `apisix_http_latency` 的标签\n\n以下标签用于区分 `apisix_http_latency` 指标。\n\n| 名称   | 描述                                                                                                                   |\n| ------ | ---------------------------------------------------------------------------------------------------------------------- |\n| type   | 延迟类型。有关详细信息，请参见 [延迟类型](#延迟类型)。                                                            |\n| route  | 延迟对应的路由 ID，当 `prefer_name` 为 `false`（默认）时，使用路由 ID，当 `prefer_name` 为 `true` 时，使用路由名称。如果请求不匹配任何路由，则默认为空字符串。 |\n| service | 延迟对应的服务 ID，当 `prefer_name` 为 `false`（默认）时，使用服务 ID，当 `prefer_name` 为 `true` 时，使用服务名称。如果匹配的路由不属于任何服务，则默认为路由上配置的主机值。 |\n| consumer | 与延迟关联的消费者名称。如果请求没有与之关联的消费者，则默认为空字符串。                                             |\n| node   | 与延迟关联的上游节点的 IP 地址。                                                                                     |\n| request_type       | traditional_http / ai_chat / ai_stream                                                                                          |\n| llm_model       | 对于非传统的 http 请求，llm 模型的名称                                                                                          |\n\n#### 延迟类型\n\n`apisix_http_latency` 可以标记为以下三种类型之一：\n\n* `request` 表示从客户端读取第一个字节到最后一个字节发送到客户端之间的时间。\n\n* `upstream` 表示等待上游服务响应的时间。\n\n* `apisix` 表示 `request` 延迟与 `upstream` 延迟之间的差异。\n\n换句话说，APISIX 延迟不仅归因于 Lua 处理。应理解为：\n\n```text\nAPISIX 延迟\n  = 下游请求时间 - 上游响应时间\n  = 下游流量延迟 + NGINX 延迟\n```\n\n### `apisix_upstream_status` 的标签\n\n以下标签用于区分 `apisix_upstream_status` 指标。\n\n| 名称   | 描述                                                                                                                   |\n| ------ | ---------------------------------------------------------------------------------------------------------------------- |\n| name   | 与健康检查配置的上游对应的资源 ID，例如 `/apisix/routes/1` 和 `/apisix/upstreams/1`。                              |\n| ip     | 上游节点的 IP 地址。                                                                                                   |\n| port   | 节点的端口号。                                                                                                         |\n\n## 示例\n\n以下示例演示如何在不同场景中使用 `prometheus` 插件。\n\n### 获取 APISIX 指标\n\n以下示例演示如何从 APISIX 获取指标。\n\n默认的 Prometheus 指标端点和其他与 Prometheus 相关的配置可以在 [静态配置](#静态配置) 中找到。如果您希望自定义这些配置，更新 `config.yaml` 并重新加载 APISIX。\n\n如果您在容器化环境中部署 APISIX，并希望外部访问 Prometheus 指标端点，请按如下方式更新配置文件并重新加载 APISIX：\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:\n    export_addr:\n      ip: 0.0.0.0\n```\n\n向 APISIX Prometheus 指标端点发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\n您应该看到类似以下的输出：\n\n```text\n# HELP apisix_bandwidth Total bandwidth in bytes consumed per Service in Apisix\n# TYPE apisix_bandwidth counter\napisix_bandwidth{type=\"egress\",route=\"\",service=\"\",consumer=\"\",node=\"\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 8417\napisix_bandwidth{type=\"egress\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 1420\napisix_bandwidth{type=\"egress\",route=\"2\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 1420\napisix_bandwidth{type=\"ingress\",route=\"\",service=\"\",consumer=\"\",node=\"\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 189\napisix_bandwidth{type=\"ingress\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 332\napisix_bandwidth{type=\"ingress\",route=\"2\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"} 332\n# HELP apisix_etcd_modify_indexes Etcd modify index for APISIX keys\n# TYPE apisix_etcd_modify_indexes gauge\napisix_etcd_modify_indexes{key=\"consumers\"} 0\napisix_etcd_modify_indexes{key=\"global_rules\"} 0\n...\n```\n\n### 在公共 API 端点上公开 APISIX 指标\n\n以下示例演示如何禁用默认情况下在端口 `9091` 上公开的 Prometheus 导出服务器，并在 APISIX 用于监听其他客户端请求的公共 API 端点上公开 APISIX Prometheus 指标。\n\n在配置文件中禁用 Prometheus 导出服务器，并重新加载 APISIX 以使更改生效：\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:\n    enable_export_server: false\n```\n\n接下来，使用 [`public-api`](../../../zh/latest/plugins/public-api.md) 插件创建一个路由，并为 APISIX 指标公开一个公共 API 端点：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/prometheus-metrics\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"uri\": \"/apisix/prometheus/metrics\",\n    \"plugins\": {\n      \"public-api\": {}\n    }\n  }'\n```\n\n向新指标端点发送请求以进行验证：\n\n```shell\ncurl \"http://127.0.0.1:9080/apisix/prometheus/metrics\"\n```\n\n您应该看到类似以下的输出：\n\n```text\n# HELP apisix_http_requests_total 自 APISIX 启动以来客户端请求的总数。\n# TYPE apisix_http_requests_total gauge\napisix_http_requests_total 1\n# HELP apisix_nginx_http_current_connections 当前 HTTP 连接数量。\n# TYPE apisix_nginx_http_current_connections gauge\napisix_nginx_http_current_connections{state=\"accepted\"} 1\napisix_nginx_http_current_connections{state=\"active\"} 1\napisix_nginx_http_current_connections{state=\"handled\"} 1\napisix_nginx_http_current_connections{state=\"reading\"} 0\napisix_nginx_http_current_connections{state=\"waiting\"} 0\napisix_nginx_http_current_connections{state=\"writing\"} 1\n...\n```\n\n### 监控上游健康状态\n\n以下示例演示如何监控上游节点的健康状态。\n\n使用 `prometheus` 插件创建一个路由，并配置上游的主动健康检查：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"prometheus-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"prometheus\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1,\n        \"127.0.0.1:20001\": 1\n      },\n      \"checks\": {\n        \"active\": {\n          \"timeout\": 5,\n          \"http_path\": \"/status\",\n          \"healthy\": {\n            \"interval\": 2,\n            \"successes\": 1\n          },\n          \"unhealthy\": {\n            \"interval\": 1,\n            \"http_failures\": 2\n          }\n        },\n        \"passive\": {\n          \"healthy\": {\n            \"http_statuses\": [200, 201],\n            \"successes\": 3\n          },\n          \"unhealthy\": {\n            \"http_statuses\": [500],\n            \"http_failures\": 3,\n            \"tcp_failures\": 3\n          }\n        }\n      }\n    }\n  }'\n```\n\n向 APISIX Prometheus 指标端点发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\n您应该看到类似以下的输出：\n\n```text\n# HELP apisix_upstream_status 上游健康检查的状态\n# TYPE apisix_upstream_status gauge\napisix_upstream_status{name=\"/apisix/routes/1\",ip=\"54.237.103.220\",port=\"80\"} 1\napisix_upstream_status{name=\"/apisix/routes/1\",ip=\"127.0.0.1\",port=\"20001\"} 0\n```\n\n这显示上游节点 `httpbin.org:80` 是健康的，而上游节点 `127.0.0.1:20001` 是不健康的。\n\n### 为指标添加额外标签\n\n以下示例演示如何为指标添加额外标签，并在标签值中使用 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。\n\n目前，仅以下指标支持额外标签：\n\n* apisix_http_status\n* apisix_http_latency\n* apisix_bandwidth\n\n在配置文件中包含以下配置以为指标添加标签，并重新加载 APISIX 以使更改生效：\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  prometheus:                                # 插件：prometheus\n    metrics:                                 # 根据 NGINX 变量创建额外标签。\n      http_status:\n        extra_labels:                        # 设置 `http_status` 指标的额外标签。\n          - upstream_addr: $upstream_addr    # 添加一个额外的 `upstream_addr` 标签，其值为 NGINX 变量 $upstream_addr。\n          - route_name: $route_name          # 添加一个额外的 `route_name` 标签，其值为 APISIX 变量 $route_name。\n```\n\n请注意，如果您在标签值中定义了一个变量，但它与任何现有的 [APISIX 变量](https://apisix.apache.org/zh/docs/apisix/apisix-variable/) 和 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html) 不对应，则标签值将默认为空字符串。\n\n使用 `prometheus` 插件创建一个路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"prometheus-route\",\n    \"name\": \"extra-label\",\n    \"plugins\": {\n      \"prometheus\": {}\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求以进行验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该看到 `HTTP/1.1 200 OK` 的响应。\n\n向 APISIX Prometheus 指标端点发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\n您应该看到类似以下的输出：\n\n```text\n# HELP apisix_http_status APISIX 中每个服务的 HTTP 状态代码\n# TYPE apisix_http_status counter\napisix_http_status{code=\"200\",route=\"1\",matched_uri=\"/get\",matched_host=\"\",service=\"\",consumer=\"\",node=\"54.237.103.220\",upstream_addr=\"54.237.103.220:80\",route_name=\"extra-label\"} 1\n```\n\n### 使用 Prometheus 监控 TCP/UDP 流量\n\n以下示例演示如何在 APISIX 中收集 TCP/UDP 流量指标。\n\n在 `config.yaml` 中包含以下配置以启用 Stream proxy 和 `prometheus` 插件。重新加载 APISIX 以使更改生效：\n\n```yaml title=\"conf/config.yaml\"\napisix:\n  proxy_mode: http&stream   # 启用 L4 和 L7 代理\n  stream_proxy:             # 配置 L4 代理\n    tcp:\n      - 9100                # 设置 TCP 代理监听端口\n    udp:\n      - 9200                # 设置 UDP 代理监听端口\n\nstream_plugins:\n  - prometheus              # 为 stream proxy 启用 prometheus\n```\n\n使用 `prometheus` 插件创建一个 Stream Route：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/stream_routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"prometheus\": {}\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向该 Stream Route 发送请求以进行验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9100\"\n```\n\n您应该看到 `HTTP/1.1 200 OK` 的响应。\n\n向 APISIX Prometheus 指标端点发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9091/apisix/prometheus/metrics\"\n```\n\n您应该看到类似以下的输出：\n\n```text\n# HELP apisix_stream_connection_total APISIX 中每个 Stream Route 处理的总连接数\n# TYPE apisix_stream_connection_total counter\napisix_stream_connection_total{route=\"1\"} 1\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/proxy-cache.md",
    "content": "---\ntitle: proxy-cache\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Proxy Cache\ndescription: proxy-cache 插件根据键缓存响应，支持 GET、POST 和 HEAD 请求的磁盘和内存缓存，从而增强 API 性能。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/proxy-cache\" />\n</head>\n\n## 描述\n\n`proxy-cache` 插件提供了根据缓存键缓存响应的功能。该插​​件支持基于磁盘和基于内存的缓存选项，用于缓存 [GET](https://anything.org/learn/serving-over-http/#get-request)、[POST](https://anything.org/learn/serving-over-http/#post-request) 和 [HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) 请求。\n\n可以根据请求 HTTP 方法、响应状态代码、请求标头值等有条件地缓存响应。\n\n## 属性\n\n| 名称               | 类型           | 必选项 | 默认值                    | 有效值                                                                          | 描述                                                                                                                               |\n| ------------------ | -------------- | ------ | ------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| cache_strategy | string | 否 | disk | [\"disk\",\"memory\"] | 缓存策略。缓存在磁盘还是内存中。 |\n| cache_zone | string | 否 | disk_cache_one | | 与缓存策略一起使用的缓存区域。该值应与[配置文件](#static-configurations)中定义的缓存区域之一匹配，并与缓存策略相对应。例如，当使用内存缓存策略时，应该使用内存缓存区域。 |\n| cache_key | array[string] | 否 | [\"$host\", \"$request_uri\"] | | 用于缓存的键。支持[NGINX 变量](https://nginx.org/en/docs/varindex.html)和值中的常量字符串。变量应该以 `$` 符号为前缀。 |\n| cache_bypass | array[string] | 否 | | |一个或多个用于解析值的参数，如果任何值不为空且不等于 `0`，则不会从缓存中检索响应。支持值中的 [NGINX 变量](https://nginx.org/en/docs/varindex.html) 和常量字符串。变量应该以 `$` 符号为前缀。|\n| cache_method | array[string] | 否 | [\"GET\", \"HEAD\"] | [\"GET\", \"POST\", \"HEAD\"] | 应缓存响应的请求方法。|\n| cache_http_status | array[integer] | 否 | [200, 301, 404] | [200, 599] | 应缓存响应的响应 HTTP 状态代码。|\n| hide_cache_headers | boolean | 否 | false | | 如果为 true，则隐藏 `Expires` 和 `Cache-Control` 响应标头。|\n| cache_control | boolean | 否 | false | | 如果为 true，则遵守 HTTP 规范中的 `Cache-Control` 行为。仅对内存中策略有效。 |\n| no_cache | array[string] | 否 | | | 用于解析值的一个或多个参数，如果任何值不为空且不等于 `0`，则不会缓存响应。支持 [NGINX 变量](https://nginx.org/en/docs/varindex.html) 和值中的常量字符串。变量应以 `$` 符号为前缀。 |\n| cache_ttl | integer | 否 | 300 | >=1 | 在内存中缓存时的缓存生存时间 (TTL)，以秒为单位。要调整在磁盘上缓存时的 TTL，请更新[配置文件](#static-configurations) 中的 `cache_ttl`。TTL 值与从上游服务收到的响应标头 [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) 和 [`Expires`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires) 中的值一起评估。|\n\n## 静态配置\n\n默认情况下，磁盘缓存时的 `cache_ttl` 和缓存 `zones` 等值已在 [默认配置](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua) 中预先配置。\n\n要自定义这些值，请将相应的配置添加到 `config.yaml`。例如：\n\n```yaml\napisix:\n  proxy_cache:\n    cache_ttl: 10s  # 仅当 `Expires` 和 `Cache-Control` 响应标头均不存在，或者 APISIX 返回\n                    # 由于上游不可用导致 `502 Bad Gateway` 或 `504 Gateway Timeout` 时\n                    # 才会在磁盘上缓存时使用默认缓存 TTL\n    zones:\n      - name: disk_cache_one\n        memory_size: 50m\n        disk_size: 1G\n        disk_path: /tmp/disk_cache_one\n        cache_levels: 1:2\n      # - name: disk_cache_two\n      #   memory_size: 50m\n      #   disk_size: 1G\n      #   disk_path: \"/tmp/disk_cache_two\"\n      #   cache_levels: \"1:2\"\n      - name: memory_cache\n        memory_size: 50m\n```\n\n重新加载 APISIX 以使更改生效。\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `proxy-cache`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 在磁盘上缓存数据\n\n磁盘缓存策略具有系统重启时数据持久性以及与内存缓存相比具有更大存储容量的优势。它适用于优先考虑耐用性且可以容忍稍大的缓存访问延迟的应用程序。\n\n以下示例演示了如何在路由上使用 `proxy-cache` 插件将数据缓存在磁盘上。\n\n使用磁盘缓存策略时，缓存 TTL 由响应标头 `Expires` 或 `Cache-Control` 中的值确定。如果这些标头均不存在，或者 APISIX 由于上游不可用而返回 `502 Bad Gateway` 或 `504 Gateway Timeout`，则缓存 TTL 默认为 [配置文件](#static-configuration) 中配置的值。\n\n使用 `proxy-cache` 插件创建路由以将数据缓存在磁盘上：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"cache_strategy\": \"disk\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应，表明插件已成功启用：\n\n```text\nApisix-Cache-Status: MISS\n```\n\n由于在第一次响应之前没有可用的缓存，因此显示 `Apisix-Cache-Status: MISS`。\n\n在缓存 TTL 窗口内再次发送相同的请求。您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应，显示缓存已命中：\n\n```text\nApisix-Cache-Status: HIT\n```\n\n等待缓存在 TTL 之后过期，然后再次发送相同的请求。您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应，表明缓存已过期：\n\n```text\nApisix-Cache-Status: EXPIRED\n```\n\n### 在内存中缓存数据\n\n内存缓存策略具有低延迟访问缓存数据的优势，因为从 RAM 检索数据比从磁盘存储检索数据更快。它还适用于存储不需要长期保存的临时数据，从而可以高效缓存频繁更改的数据。\n\n以下示例演示了如何在路由上使用 `proxy-cache` 插件在内存中缓存数据。\n\n使用 `proxy-cache` 创建路由并将其配置为使用基于内存的缓存：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"cache_strategy\": \"memory\",\n        \"cache_zone\": \"memory_cache\",\n        \"cache_ttl\": 10\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应，表明插件已成功启用：\n\n```text\nApisix-Cache-Status: MISS\n```\n\n由于在第一次响应之前没有可用的缓存，因此显示 `Apisix-Cache-Status: MISS`。\n\n在缓存 TTL 窗口内再次发送相同的请求。您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应，显示缓存已命中：\n\n```text\nApisix-Cache-Status: HIT\n```\n\n### 有条件地缓存响应\n\n以下示例演示了如何配置 `proxy-cache` 插件以有条件地缓存响应。\n\n使用 `proxy-cache` 插件创建路由并配置 `no_cache` 属性，这样如果 URL 参数 `no_cache` 和标头 `no_cache` 的值中至少有一个不为空且不等于 `0`，则不会缓存响应：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"no_cache\": [\"$arg_no_cache\", \"$http_no_cache\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送一些请求，其中 URL 参数的 `no_cache` 值表示绕过缓存：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?no_cache=1\"\n```\n\n您应该收到所有请求的 `HTTP/1.1 200 OK` 响应，并且每次都观察到以下标头：\n\n```text\nApisix-Cache-Status: EXPIRED\n```\n\n向路由发送一些其他请求，其中 URL 参数 `no_cache` 值为零：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?no_cache=0\"\n```\n\n您应该收到所有请求的 `HTTP/1.1 200 OK` 响应，并开始看到缓存被命中：\n\n```text\nApisix-Cache-Status: HIT\n```\n\n您还可以在 `no_cache` 标头中指定以下值：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"no_cache: 1\"\n```\n\n响应不应该被缓存：\n\n```text\nApisix-Cache-Status: EXPIRED\n```\n\n### 有条件地从缓存中检索响应\n\n以下示例演示了如何配置 `proxy-cache` 插件以有条件地从缓存中检索响应。\n\n使用 `proxy-cache` 插件创建路由并配置 `cache_bypass` 属性，这样如果 URL 参数 `bypass` 和标头 `bypass` 的值中至少有一个不为空且不等于 `0`，则不会从缓存中检索响应：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"proxy-cache\": {\n        \"cache_bypass\": [\"$arg_bypass\", \"$http_bypass\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送一个请求，其中 URL 参数值为 `bypass`，表示绕过缓存：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?bypass=1\"\n```\n\n您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应：\n\n```text\nApisix-Cache-Status: BYPASS\n```\n\n向路由发送另一个请求，其中 URL 参数 `bypass` 值为零：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?bypass=0\"\n```\n\n您应该看到带有以下标头的 `HTTP/1.1 200 OK` 响应：\n\n```text\nApisix-Cache-Status: MISS\n```\n\n您还可以在 `bypass` 标头中指定以下值：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"bypass: 1\"\n```\n\n响应应该显示绕过缓存：\n\n```text\nApisix-Cache-Status: BYPASS\n```\n\n### 缓存 502 和 504 错误响应代码\n\n当上游服务返回 500 范围内的服务器错误时，`proxy-cache` 插件将缓存响应，当且仅当返回的状态为 `502 Bad Gateway` 或 `504 Gateway Timeout`。\n\n以下示例演示了当上游服务返回 `504 Gateway Timeout` 时 `proxy-cache` 插件的行为。\n\n使用 `proxy-cache` 插件创建路由并配置虚拟上游服务：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-cache-route\",\n    \"uri\": \"/timeout\",\n    \"plugins\": {\n      \"proxy-cache\": { }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"12.34.56.78\": 1\n      }\n    }\n  }'\n```\n\n生成一些对路由的请求：\n\n```shell\nseq 4 | xargs -I{} curl -I \"http://127.0.0.1:9080/timeout\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: MISS\n\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: HIT\n\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: HIT\n\nHTTP/1.1 504 Gateway Time-out\n...\nApisix-Cache-Status: HIT\n```\n\n但是，如果上游服务返回 `503 Service Temporarily Unavailable`，则响应将不会被缓存。\n"
  },
  {
    "path": "docs/zh/latest/plugins/proxy-control.md",
    "content": "---\ntitle: proxy-control\nkeywords:\n  - APISIX\n  - API 网关\n  - Proxy Control\ndescription: 本文介绍了 Apache APISIX proxy-control 插件的相关操作，你可以使用此插件动态地控制 NGINX 代理的行为。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n使用 `proxy-control` 插件能够动态地控制 NGINX 代理的相关行为。\n\n:::info 重要\n\n此插件需要 APISIX 在 [APISIX-Runtime](../FAQ.md#如何构建-apisix-runtime-环境) 环境上运行。更多信息请参考 [apisix-build-tools](https://github.com/api7/apisix-build-tools)。\n\n:::\n\n## 属性\n\n| 名称      | 类型          | 必选项 | 默认值    | 有效值                                                                    | 描述                                                                                                                                         |\n| --------- | ------------- | ----------- | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |\n| request_buffering | boolean        | 否    |  true            |  | 如果设置为 `true`，插件将动态设置 [`proxy_request_buffering`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering)。 |\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用 `proxy-control` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/upload\",\n    \"plugins\": {\n        \"proxy-control\": {\n            \"request_buffering\": false\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n启用插件后，使用 `curl` 命令请求该路由进行一个大文件的上传测试：\n\n```shell\ncurl -i http://127.0.0.1:9080/upload -d @very_big_file\n```\n\n如果在错误日志中没有找到关于 \"a client request body is buffered to a temporary file\" 的信息，则说明插件生效。\n\n## 删除插件\n\n当你需要删除该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n  -H \"X-API-KEY: $admin_key\" -X PUT -d\n{\n    \"uri\": \"/upload\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/proxy-mirror.md",
    "content": "---\ntitle: proxy-mirror\nkeywords:\n  - APISIX\n  - API 网关\n  - Proxy Mirror\ndescription: proxy-mirror 插件将入口流量复制到 APISIX 并将其转发到指定的上游，而不会中断常规服务。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/proxy-mirror\" />\n</head>\n\n## 描述\n\n`proxy-mirror` 插件将传入流量复制到 APISIX 并将其转发到指定的上游，而不会中断常规服务。您可以将插件配置为镜像所有流量或仅镜像一部分流量。该机制有利于一些用例，包括故障排除、安全检查、分析等。\n\n请注意，APISIX 会忽略接收镜像流量的上游主机的任何响应。\n\n## 参数\n\n| 名称 | 类型   | 必选项 | 默认值 | 有效值 | 描述                                                                                                    |\n| ---- | ------ | ------ | ------ | ------ | ------------------------------------------------------------------------------------------------------- |\n| host | string | 是 | | | 将镜像流量转发到的主机的地址。该地址应包含方案但不包含路径，例如 `http://127.0.0.1:8081`。 |\n| path | string | 否 | | | 将镜像流量转发到的主机的路径。如果未指定，则默认为路由的当前 URI 路径。如果插件正在镜像 gRPC 流量，则不适用。 |\n| path_concat_mode | string | 否 | replace | [\"replace\", \"prefix\"] | 指定 `path` 时的连接模式。设置为 `replace` 时，配置的 `path` 将直接用作将镜像流量转发到的主机的路径。设置为 `prefix` 时，转发到的路径将是配置的 `path`，附加路由的请求 URI 路径。如果插件正在镜像 gRPC 流量，则不适用。 |\n| sample_ratio | number | 否 | 1 | [0.00001, 1] | 将被镜像的请求的比例。默认情况下，所有流量都会被镜像。|\n\n## 静态配置\n\n默认情况下，插件的超时值在[默认配置](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua)中预先配置。\n\n要自定义这些值，请将相应的配置添加到 `config.yaml`。例如：\n\n```yaml\nplugin_attr:\n  proxy-mirror:\n    timeout:\n      connect: 60s\n      read: 60s\n      send: 60s\n```\n\n重新加载 APISIX 以使更改生效。\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `proxy-mirror`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 镜像部分流量\n\n以下示例演示了如何配置 `proxy-mirror` 以将 50% 的流量镜像到路由并将其转发到另一个上游服务。\n\n启动一个示例 NGINX 服务器以接收镜像流量：\n\n```shell\ndocker run -p 8081:80 --name nginx nginx\n```\n\n您应该在终端会话中看到 NGINX 访问日志和错误日志。\n\n打开一个新的终端会话并使用 `proxy-mirror` 创建一个路由来镜像 50% 的流量：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"traffic-mirror-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"proxy-mirror\": {\n        \"host\": \"http://127.0.0.1:8081\",\n        \"sample_ratio\": 0.5\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n发送生成几个请求到路由：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该会收到所有请求的 `HTTP/1.1 200 OK` 响应。\n\n导航回 NGINX 终端会话，您应该会看到一些访问日志条目，大约是生成的请求数量的一半：\n\n```text\n172.17.0.1 - - [29/Jan/2024:23:11:01 +0000] \"GET /get HTTP/1.1\" 404 153 \"-\" \"curl/7.64.1\" \"-\"\n```\n\n这表明 APISIX 已将请求镜像到 NGINX 服务器。此处，HTTP 响应状态为 `404`，因为示例 NGINX 服务器未实现路由。\n\n### 配置镜像超时\n\n以下示例演示了如何更新插件的默认连接、读取和发送超时。当将流量镜像到非常慢的后端服务时，这可能很有用。\n\n由于请求镜像是作为子请求实现的，子请求中的过度延迟可能导致原始请求被阻止。默认情况下，连接、读取和发送超时设置为 60 秒。要更新这些值，您可以在配置文件的 `plugin_attr` 部分中配置它们，如下所示：\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  proxy-mirror:\n    timeout:\n      connect: 2000ms\n      read: 2000ms\n      send: 2000ms\n```\n\n重新加载 APISIX 以使更改生效。\n"
  },
  {
    "path": "docs/zh/latest/plugins/proxy-rewrite.md",
    "content": "---\ntitle: proxy-rewrite\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Proxy Rewrite\n  - proxy-rewrite\ndescription: proxy-rewrite 插件支持重写 APISIX 转发到上游服务的请求。使用此插件，您可以修改 HTTP 方法、请求目标上游地址、请求标头等。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/proxy-rewrite\" />\n</head>\n\n## 描述\n\n`proxy-rewrite` 插件支持重写 APISIX 转发到上游服务的请求。使用此插件，您可以修改 HTTP 方法、请求目标上游地址、请求标头等。\n\n## 属性\n\n| 名称 | 类型 | 必需 | 默认值 | 有效值 | 描述 |\n|-----------------------------|-----------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| uri | string | 否 | | | 新的上游 URI 路径。值支持 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。例如，`$arg_name`。 |\n| method | string | 否 | | [\"GET\", \"POST\", \"PUT\", \"HEAD\", \"DELETE\", \"OPTIONS\",\"MKCOL\", \"COPY\", \"MOVE\", \"PROPFIND\", \"PROPFIND\",\"LOCK\", \"UNLOCK\", \"PATCH\", \"TRACE\"] | 要使用的重写请求的 HTTP 方法。 |\n| regex_uri | array[string] | 否 | | | 用于匹配客户端请求的 URI 路径并组成新的上游 URI 路径的正则表达式。当同时配置 `uri` 和 `regex_uri` 时，`uri` 具有更高的优先级。该数组应包含一个或多个 **键值对**，其中键是用于匹配 URI 的正则表达式，值是新的上游 URI 路径。例如，对于 `[\"^/iresty/(. *)/(. *)\", \"/$1-$2\", ^/theothers/*\", \"/theothers\"]`，如果请求最初发送到 `/iresty/hello/world`，插件会将上游 URI 路径重写为 `/iresty/hello-world`；如果请求最初发送到 `/theothers/hello/world`，插件会将上游 URI 路径重写为 `/theothers`。|\n| host | string | 否 | | | 设置 [`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) 请求标头。|\n| headers | object | 否 | | | 要执行的标头操作。可以设置为动作动词 `add`、`remove` 和/或 `set` 的对象；或由要 `set` 的标头组成的对象。当配置了多个动作动词时，动作将按照“添加”、“删除”和“设置”的顺序执行。|\n| headers.add | object | 否 | | | 要附加到请求的标头。如果请求中已经存在标头，则会附加标头值。标头值可以设置为常量、一个或多个 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)，或者 `regex_uri` 的匹配结果（使用变量，例如 `$1-$2-$3`）。|\n| headers.set | object | 否 | | | 要设置请求的标头。如果请求中已经存在标头，则会覆盖标头值。标头值可以设置为常量、一个或多个 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)，或者 `regex_uri` 的匹配结果（使用变量，例如 `$1-$2-$3`）。不应将其用于设置 `Host`。|\n| headers.remove | array[string] | 否 | | | 从请求中删除的标头。\n| use_real_request_uri_unsafe | boolean | 否 | false | | 如果为 True，则绕过 URI 规范化并允许完整的原始请求 URI。启用此选项被视为不安全。|\n\n## 示例\n\n下面的示例说明如何在不同场景中在路由上配置 `proxy-rewrite`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 重写主机标头\n\n以下示例演示了如何修改请求中的 `Host` 标头。请注意，您不应使用 `headers.set` 来设置 `Host` 标头。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"host\": \"myapisix.demo\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向 `/headers` 发送请求以检查发送到上游的所有请求标头：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\n您应该看到类似于以下内容的响应：\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"myapisix.demo\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef198-29da0970383150175bd2d76d\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### 重写 URI 并设置标头\n\n以下示例演示了如何重写请求上游 URI 并设置其他标头值。如果客户端请求中存在相同的标头，则插件中设置的相应标头值将覆盖客户端请求中存在的值。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/anything\",\n        \"headers\": {\n          \"set\": {\n            \"X-Api-Version\": \"v1\",\n            \"X-Api-Engine\": \"apisix\"\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求以验证：\n\n```shell\ncurl \"http://127.0.0.1:9080/\" -H '\"X-Api-Version\": \"v2\"'\n```\n\n您应该看到类似于以下内容的响应：\n\n```text\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fed73a-59cd3bd640d76ab16c97f1f1\",\n    \"X-Api-Engine\": \"apisix\",\n    \"X-Api-Version\": \"v1\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"::1, 103.248.35.179\",\n  \"url\": \"http://localhost/anything\"\n}\n```\n\n注意到其中两个标头都存在，以及插件中配置的 `X-Api-Version` 标头值覆盖了请求中传递的标头值。\n\n### 重写 URI 并附加标头\n\n以下示例演示了如何重写请求上游 URI 并附加其他标头值。如果客户端请求中存在相同的标头，则它们的标头值将附加到插件中配置的标头值。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/headers\",\n        \"headers\": {\n          \"add\": {\n            \"X-Api-Version\": \"v1\",\n            \"X-Api-Engine\": \"apisix\"\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求以验证：\n\n```shell\ncurl \"http://127.0.0.1:9080/\" -H '\"X-Api-Version\": \"v2\"'\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fed73a-59cd3bd640d76ab16c97f1f1\",\n    \"X-Api-Engine\": \"apisix\",\n    \"X-Api-Version\": \"v1,v2\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n请注意，两个标头均存在，并且插件中配置的 `X-Api-Version` 标头值均附加在请求中传递的标头值上。\n\n### 删除现有标头\n\n以下示例演示了如何删除现有标头 `User-Agent`。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"headers\": {\n          \"remove\":[\n            \"User-Agent\"\n          ]\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求来验证指定的标头是否被删除：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\n您应该看到类似以下的响应，其中 `User-Agen` 标头已被移除：\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef302-07f2b13e0eb006ba776ad91d\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### 使用 RegEx 重写 URI\n\n以下示例演示了如何解析原始上游 URI 路径中的文本并使用它们组成新的上游 URI 路径。在此示例中，APISIX 配置为将所有请求从 `/test/user/agent` 转发到 `/user-agent`。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"uri\": \"/test/*\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"regex_uri\": [\"^/test/(.*)/(.*)\", \"/$1-$2\"]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求到 `/test/user/agent`，检查是否被重定向到 `/user-agent`：\n\n```shell\ncurl \"http://127.0.0.1:9080/test/user/agent\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\n{\n  \"user-agent\": \"curl/8.2.1\"\n}\n```\n\n### 添加 URL 参数\n\n以下示例演示了如何向请求添加 URL 参数。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/get?arg1=apisix&arg2=plugin\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求来验证 URL 参数是否也转发给了上游：\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\n{\n  \"args\": {\n    \"arg1\": \"apisix\",\n    \"arg2\": \"plugin\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef6dc-2b0e09591db7353a275cdae4\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"127.0.0.1, 103.248.35.148\",\n  \"url\": \"http://127.0.0.1/get?arg1=apisix&arg2=plugin\"\n}\n```\n\n### 重写 HTTP 方法\n\n以下示例演示如何将 GET 请求重写为 POST 请求。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"proxy-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"proxy-rewrite\": {\n        \"uri\": \"/anything\",\n        \"method\":\"POST\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向 `/get` 发送 GET 请求，以验证它是否转换为向 `/anything` 发送 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-64fef7de-0c63387645353998196317f2\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"json\": null,\n  \"method\": \"POST\",\n  \"origin\": \"::1, 103.248.35.179\",\n  \"url\": \"http://localhost/anything\"\n}\n```\n\n### 将消费者名称转发到上游\n\n以下示例演示了如何将成功验证的消费者名称转发到上游服务。例如，您将使用 `key-auth` 作为身份验证方法。\n\n创建消费者 `JohnDoe`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"JohnDoe\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/JohnDoe/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n接下来，创建一个启用密钥认证的路由，配置 `proxy-rewrite` 以将消费者名称添加到标头，并删除认证密钥，以使其对上游服务不可见：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"consumer-restricted-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"proxy-rewrite\": {\n        \"headers\": {\n          \"set\": {\n            \"X-Apisix-Consumer\": \"$consumer_name\"\n          },\n          \"remove\": [ \"Apikey\" ]\n        }\n      }\n    },\n    \"upstream\" : {\n      \"nodes\": {\n        \"httpbin.org\":1\n      }\n    }\n  }'\n```\n\n以消费者 `JohnDoe` 的身份向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: john-key'\n```\n\n您应该收到一个包含以下主体的 `HTTP/1.1 200 OK` 响应：\n\n```text\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.4.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-664b01a6-2163c0156ed4bff51d87d877\",\n    \"X-Apisix-Consumer\": \"JohnDoe\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  \"origin\": \"172.19.0.1, 203.12.12.12\",\n  \"url\": \"http://127.0.0.1/get\"\n}\n```\n\n向路由发送另一个请求，不带有有效凭证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该收到 `HTTP/1.1 403 Forbidden` 响应。\n"
  },
  {
    "path": "docs/zh/latest/plugins/public-api.md",
    "content": "---\ntitle: public-api\nkeywords:\n  - APISIX\n  - API 网关\n  - Public API\ndescription: public-api 插件公开了一个内部 API 端点，使其可被公开访问。该插件的主要用途之一是公开由其他插件创建的内部端点。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/public-api\" />\n</head>\n\n## 描述\n\n`public-api` 插件公开了一个内部 API 端点，使其可被公开访问。该插件的主要用途之一是公开由其他插件创建的内部端点。\n\n## 属性\n\n| 名称  | 类型   | 必选项 | 默认值 | 有效值 | 描述 |\n|------|--------|-------|-------|------|------|\n| uri  | string | 否    |    |   | 内部端点的 URI。如果未配置，则暴露路由的 URI。|\n\n## 示例\n\n以下示例展示了如何在不同场景中配置 `public-api`。\n\n### 在自定义端点暴露 Prometheus 指标\n\n以下示例演示如何禁用默认在端口 `9091` 上暴露端点的 Prometheus 导出服务器，并在 APISIX 用于监听其他客户端请求的端口 `9080` 上，通过新的公共 API 端点暴露 APISIX 的 Prometheus 指标。\n\n此外，还会配置路由，使内部端点 `/apisix/prometheus/metrics` 通过自定义端点对外公开。\n\n:::caution\n\n如果收集了大量指标，插件可能会占用大量 CPU 资源用于计算，从而影响正常请求的处理。\n\n为了解决这个问题，APISIX 使用 [特权代理进程](https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/process.md#enable_privileged_agent) ，并将指标计算卸载至独立进程。如果使用配置文件中 `plugin_attr.prometheus.export_addr` 设定的指标端点，该优化将自动生效。但如果通过 `public-api` 插件暴露指标端点，则不会受益于此优化。\n\n:::\n\n在配置文件中禁用 Prometheus 导出服务器，并重新加载 APISIX 以使更改生效：\n\n```yaml\nplugin_attr:\n  prometheus:\n    enable_export_server: false\n```\n\n接下来，创建一个带有 `public-api` 插件的路由，并为 APISIX 指标暴露一个公共 API 端点。你应将路由的 `uri` 设置为自定义端点路径，并将插件的 `uri` 设置为要暴露的内部端点。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H 'X-API-KEY: ${admin_key}' \\\n  -d '{\n    \"id\": \"prometheus-metrics\",\n    \"uri\": \"/prometheus_metrics\",\n    \"plugins\": {\n      \"public-api\": {\n        \"uri\": \"/apisix/prometheus/metrics\"\n      }\n    }\n  }'\n```\n\n向自定义指标端点发送请求：\n\n```shell\ncurl http://127.0.0.1:9080/prometheus_metrics\n```\n\n你应看到类似以下的输出：\n\n```text\n# HELP apisix_http_requests_total The total number of client requests since APISIX started\n# TYPE apisix_http_requests_total gauge\napisix_http_requests_total 1\n# HELP apisix_nginx_http_current_connections Number of HTTP connections\n# TYPE apisix_nginx_http_current_connections gauge\napisix_nginx_http_current_connections{state=\"accepted\"} 1\napisix_nginx_http_current_connections{state=\"active\"} 1\napisix_nginx_http_current_connections{state=\"handled\"} 1\napisix_nginx_http_current_connections{state=\"reading\"} 0\napisix_nginx_http_current_connections{state=\"waiting\"} 0\napisix_nginx_http_current_connections{state=\"writing\"} 1\n...\n```\n\n### 暴露批量请求端点\n\n以下示例展示了如何使用 `public-api` 插件来暴露 `batch-requests` 插件的端点，该插件用于将多个请求组合成一个请求，然后将它们发送到网关。\n\n创建一个样本路由到 httpbin 的 `/anything` 端点，用于验证目的：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"httpbin-anything\",\n    \"uri\": \"/anything\",\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n创建一个带有 `public-api` 插件的路由，并将路由的 `uri` 设置为要暴露的内部端点：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"batch-requests\",\n    \"uri\": \"/apisix/batch-requests\",\n    \"plugins\": {\n      \"public-api\": {}\n    }\n  }'\n```\n\n向暴露的批量请求端点发送一个包含 GET 和 POST 请求的流水线请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/apisix/batch-requests\" -X POST -d '\n{\n  \"pipeline\": [\n    {\n      \"method\": \"GET\",\n      \"path\": \"/anything\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/anything\",\n      \"body\": \"a post request\"\n    }\n  ]\n}'\n```\n\n您应该会收到两个请求的响应，类似于以下内容：\n\n```json\n[\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-5a30174f5534287928c54ca9\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"GET\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  },\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"a post request\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Content-Length\\\": \\\"14\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-0eddcec07f154dac0d77876f\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"POST\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  }\n]\n```\n\n如果您希望在自定义端点处暴露批量请求端点，请创建一个带有 `public-api` 插件的路由。您应该将路由的 `uri` 设置为自定义端点路径，并将插件的 uri 设置为要暴露的内部端点。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"batch-requests\",\n    \"uri\": \"/batch-requests\",\n    \"plugins\": {\n      \"public-api\": {\n        \"uri\": \"/apisix/batch-requests\"\n      }\n    }\n  }'\n```\n\n现在批量请求端点应该被暴露为 `/batch-requests`，而不是 `/apisix/batch-requests`。\n向暴露的批量请求端点发送一个包含 GET 和 POST 请求的流水线请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/batch-requests\" -X POST -d '\n{\n  \"pipeline\": [\n    {\n      \"method\": \"GET\",\n      \"path\": \"/anything\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/anything\",\n      \"body\": \"a post request\"\n    }\n  ]\n}'\n```\n\n您应该会收到两个请求的响应，类似于以下内容：\n\n```json\n[\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-5a30174f5534287928c54ca9\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"GET\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  },\n  {\n    \"reason\": \"OK\",\n    \"body\": \"{\\n  \\\"args\\\": {}, \\n  \\\"data\\\": \\\"a post request\\\", \\n  \\\"files\\\": {}, \\n  \\\"form\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Content-Length\\\": \\\"14\\\", \\n    \\\"Host\\\": \\\"127.0.0.1\\\", \\n    \\\"User-Agent\\\": \\\"curl/8.6.0\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-67b6e33b-0eddcec07f154dac0d77876f\\\", \\n    \\\"X-Forwarded-Host\\\": \\\"127.0.0.1\\\"\\n  }, \\n  \\\"json\\\": null, \\n  \\\"method\\\": \\\"POST\\\", \\n  \\\"origin\\\": \\\"192.168.107.1, 43.252.208.84\\\", \\n  \\\"url\\\": \\\"http://127.0.0.1/anything\\\"\\n}\\n\",\n    \"headers\": {\n      ...\n    },\n    \"status\": 200\n  }\n]\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/real-ip.md",
    "content": "---\ntitle: real-ip\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Real IP\ndescription: real-ip 插件允许 Apache APISIX 通过 HTTP 请求头或 HTTP 查询字符串中传递的 IP 地址设置客户端的真实 IP。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/real-ip\" />\n</head>\n\n## 描述\n\n`real-ip` 插件允许 APISIX 通过 HTTP 请求头或 HTTP 查询字符串中传递的 IP 地址设置客户端的真实 IP。当 APISIX 位于反向代理之后时，此功能尤其有用，因为在这种情况下，代理可能会被视为请求发起客户端。\n\n该插件在功能上类似于 NGINX 的 [ngx_http_realip_module](https://nginx.org/en/docs/http/ngx_http_realip_module.html)，但提供了更多的灵活性。\n\n## 属性\n\n| 名称              | 类型          | 是否必需 | 默认值 | 有效值                     | 描述                                                                 |\n|-------------------|---------------|----------|--------|----------------------------|----------------------------------------------------------------------|\n| source            | string        | 是       |        |                            | 内置变量，例如 `http_x_forwarded_for` 或 `arg_realip`。变量值应为一个有效的 IP 地址，表示客户端的真实 IP 地址，可选地包含端口。 |\n| trusted_addresses | array[string] | 否       |        | IPv4 或 IPv6 地址数组（接受 CIDR 表示法） | 已知会发送正确替代地址的可信地址。此配置设置 [`set_real_ip_from`](https://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from) 指令。 |\n| recursive         | boolean       | 否       | false  |                            | 如果为 false，则将匹配可信地址之一的原始客户端地址替换为配置的 `source` 中发送的最后一个地址。<br />如果为 true，则将匹配可信地址之一的原始客户端地址替换为配置的 `source` 中发送的最后一个非可信地址。 |\n\n:::note\n只有发送自 `apisix.trusted_addresses` 配置（支持 IP 和 CIDR）地址的 `X-Forwarded-*` 头才会被信任，并传递给插件或上游。如果未配置 `apisix.trusted_addresses` 或 ip 不在配置地址范围内的，`X-Forwarded-*` 头将全部被可信值覆盖。\n:::\n\n:::note\n如果 `source` 属性中设置的地址丢失或者无效，该插件将不会更改客户端地址。\n:::\n\n## 示例\n\n以下示例展示了如何在不同场景中配置 `real-ip`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 从 URI 参数获取真实客户端地址\n\n以下示例演示了如何使用 URI 参数更新客户端 IP 地址。\n\n创建如下路由。您应配置 `source` 以使用 [APISIX 变量](https://apisix.apache.org/docs/apisix/apisix-variable/)或者 [NGINX 变量](https://nginx.org/en/docs/varindex.html)从 URL 参数 `realip` 获取值。使用 `response-rewrite` 插件设置响应头，以验证客户端 IP 和端口是否实际更新。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"real-ip-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"real-ip\": {\n        \"source\": \"arg_realip\",\n        \"trusted_addresses\": [\"127.0.0.0/24\"]\n      },\n      \"response-rewrite\": {\n        \"headers\": {\n          \"remote_addr\": \"$remote_addr\",\n          \"remote_port\": \"$remote_port\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送带有 URL 参数中的真实 IP 和端口的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get?realip=1.2.3.4:9080\"\n```\n\n您应看到响应包含以下头：\n\n```text\nremote-addr: 1.2.3.4\nremote-port: 9080\n```\n\n### 从请求头获取真实客户端地址\n\n以下示例展示了当 APISIX 位于反向代理（例如负载均衡器）之后时，如何设置真实客户端 IP，此时代理在 [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) 请求头中暴露了真实客户端 IP。\n\n创建如下路由。您应配置 `source` 以使用 [APISIX 变量](https://apisix.apache.org/docs/apisix/apisix-variable/)或者 [NGINX 变量](https://nginx.org/en/docs/varindex.html)从请求头 `X-Forwarded-For` 获取值。使用 response-rewrite 插件设置响应头，以验证客户端 IP 是否实际更新。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"real-ip-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"real-ip\": {\n        \"source\": \"http_x_forwarded_for\",\n        \"trusted_addresses\": [\"127.0.0.0/24\"]\n      },\n      \"response-rewrite\": {\n        \"headers\": {\n          \"remote_addr\": \"$remote_addr\"\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应看到响应包含以下头：\n\n```text\nremote-addr: 10.26.3.19\n```\n\nIP 地址应对应于请求发起客户端的 IP 地址。\n\n### 在多个代理之后获取真实客户端地址\n\n以下示例展示了当 APISIX 位于多个代理之后时，如何获取真实客户端 IP，此时 [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) 请求头包含了一系列代理 IP 地址。\n\n创建如下路由。您应配置 `source` 以使用 [APISIX 变量](https://apisix.apache.org/docs/apisix/apisix-variable/)或者 [NGINX 变量](https://nginx.org/en/docs/varindex.html)从请求头 `X-Forwarded-For` 获取值。将 `recursive` 设置为 `true`，以便将匹配可信地址之一的原始客户端地址替换为配置的 `source` 中发送的最后一个非可信地址。然后，使用 `response-rewrite` 插件设置响应头，以验证客户端 IP 是否实际更新。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n  \"id\": \"real-ip-route\",\n  \"uri\": \"/get\",\n  \"plugins\": {\n    \"real-ip\": {\n      \"source\": \"http_x_forwarded_for\",\n      \"recursive\": true,\n      \"trusted_addresses\": [\"192.128.0.0/16\", \"127.0.0.0/24\"]\n    },\n    \"response-rewrite\": {\n      \"headers\": {\n        \"remote_addr\": \"$remote_addr\"\n      }\n    }\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" \\\n  -H \"X-Forwarded-For: 127.0.0.2, 192.128.1.1, 127.0.0.1\"\n```\n\n您应看到响应包含以下头：\n\n```text\nremote-addr: 127.0.0.2\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/redirect.md",
    "content": "---\ntitle: redirect\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Redirect\ndescription: 本文介绍了关于 Apache APISIX `redirect` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`redirect` 插件可用于配置 URI 重定向。\n\n## 属性\n\n| 名称                  | 类型            | 必选项 | 默认值   | 有效值          | 描述                                                                                                                                                                                                  |\n|---------------------|---------------|-----|-------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| http_to_https       | boolean       | 否   | false | [true,false] | 当设置为 `true` 并且请求是 HTTP 时，它将被重定向具有相同 URI 和 301 状态码的 HTTPS，原 URI 的查询字符串也将包含在 Location 头中。                                                                                                                                           |\n| uri                 | string        | 否   |       |              | 要重定向到的 URI，可以包含 NGINX 变量。例如：`/test/index.htm`，`$uri/index.html`，`${uri}/index.html`，`https://example.com/foo/bar`。如果你引入了一个不存在的变量，它不会报错，而是将其视为一个空变量。                                              |\n| regex_uri           | array[string] | 否   |       |              | 将来自客户端的 URL 与正则表达式匹配并重定向。当匹配成功后使用模板替换发送重定向到客户端，如果未匹配成功会将客户端请求的 URI 转发至上游。和 `regex_uri` 不可以同时存在。例如：[\"^/iresty/(.)/(.)/(.*)\",\"/$1-$2-$3\"] 第一个元素代表匹配来自客户端请求的 URI 正则表达式，第二个元素代表匹配成功后发送重定向到客户端的 URI 模板。 |\n| ret_code            | integer       | 否   | 302   | [200, ...]   | HTTP 响应码                                                                                                                                                                                            |\n| encode_uri          | boolean       | 否   | false | [true,false] | 当设置为 `true` 时，对返回的 `Location` Header 按照 [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986) 的编码格式进行编码。                                                                                          |\n| append_query_string | boolean       | 否   | false | [true,false] | 当设置为 `true` 时，将原始请求中的查询字符串添加到 `Location` Header。如果已配置 `uri` 或 `regex_uri` 已经包含查询字符串，则请求中的查询字符串将附加一个`&`。如果你已经处理过查询字符串（例如，使用 NGINX 变量 `$request_uri`），请不要再使用该参数以避免重复。                                 |\n\n:::note\n\n* `http_to_https`、`uri` 和 `regex_uri` 只能配置其中一个属性。\n* `http_to_https`、和 `append_query_string` 只能配置其中一个属性。\n* 当开启 `http_to_https` 时，重定向 URL 中的端口将按如下顺序选取一个值（按优先级从高到低排列）\n  * 从配置文件（`conf/config.yaml`）中读取 `plugin_attr.redirect.https_port`。\n  * 如果 `apisix.ssl` 处于开启状态，读取 `apisix.ssl.listen` 并从中随机选一个 `port`。\n  * 使用 443 作为默认 `https port`。\n\n:::\n\n## 启用插件\n\n以下示例展示了如何在指定路由中启用 `redirect` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/test/index.html\",\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"/test/default.html\",\n            \"ret_code\": 301\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n\n你也可以在新的 URI 中使用 NGINX 内置的任意变量：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/test\",\n    \"plugins\": {\n        \"redirect\": {\n            \"uri\": \"$uri/index.html\",\n            \"ret_code\": 301\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，可以使用如下命令测试插件是否启用成功：\n\n```shell\ncurl http://127.0.0.1:9080/test/index.html -i\n```\n\n```\nHTTP/1.1 301 Moved Permanently\nDate: Wed, 23 Oct 2019 13:48:23 GMT\nContent-Type: text/html\nContent-Length: 166\nConnection: keep-alive\nLocation: /test/default.html\n...\n```\n\n通过上述返回结果，可以看到响应码和响应头中的 `Location` 参数，它表示该插件已启用。\n\n以下示例展示了如何将 HTTP 重定向到 HTTPS：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"redirect\": {\n            \"http_to_https\": true\n        }\n    }\n}'\n```\n\n基于上述例子进行测试：\n\n```shell\ncurl http://127.0.0.1:9080/hello -i\n```\n\n```\nHTTP/1.1 301 Moved Permanently\n...\nLocation: https://127.0.0.1:9443/hello\n...\n```\n\n## 删除插件\n\n当你需要禁用 `redirect` 插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/test/index.html\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/referer-restriction.md",
    "content": "---\ntitle: referer-restriction\nkeywords:\n  - APISIX\n  - API 网关\n  - Referer restriction\ndescription: 本文介绍了 Apache APISIX referer-restriction 插件的使用方法，通过该插件可以将 referer 请求头中的域名加入黑名单或者白名单来限制其对服务或路由的访问。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`referer-restriction` 插件允许用户将 `Referer` 请求头中的域名列入白名单或黑名单来限制该域名对服务或路由的访问。\n\n## 属性\n\n| 名称    | 类型          | 必选项 | 默认值 | 有效值 | 描述                             |\n| --------- | ------------- | ------ | ------ | ------ | -------------------------------- |\n| whitelist | array[string] | 否    |         |       | 白名单域名列表。域名开头可以用 `*` 作为通配符。 |\n| blacklist | array[string] | 否    |         |       | 黑名单域名列表。域名开头可以用 `*` 作为通配符。 |\n| message | string | 否    | \"Your referer host is not allowed\" | [1, 1024] | 在未允许访问的情况下返回的信息。 |\n| bypass_missing  | boolean       | 否    | false   |       | 当设置为 `true` 时，如果 `Referer` 请求头不存在或格式有误，将绕过检查。 |\n\n:::info IMPORTANT\n\n`whitelist` 和 `blacklist` 属性无法同时在同一个服务或路由上使用，只能使用其中之一。\n\n:::\n\n## 启用插件\n\n以下示例展示了如何在特定路由上启用 `referer-restriction` 插件，并配置 `whitelist` 和 `bypass_missing` 属性：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"plugins\": {\n        \"referer-restriction\": {\n            \"bypass_missing\": true,\n            \"whitelist\": [\n                \"xx.com\",\n                \"*.xx.com\"\n            ]\n        }\n    }\n}'\n```\n\n## 测试插件\n\n通过上述命令启用插件后，你可以在请求中添加 `Referer: http://xx.com/x` 测试插件：\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'Referer: http://xx.com/x'\n```\n\n返回的 HTTP 响应头中带有 `200` 状态码则表示访问成功：\n\n```shell\nHTTP/1.1 200 OK\n...\n```\n\n接下来，将请求设置为 `Referer: http://yy.com/x`：\n\n```shell\ncurl http://127.0.0.1:9080/index.html -H 'Referer: http://yy.com/x'\n```\n\n返回的 HTTP 响应头中带有 `403` 状态码，并在响应体中带有 `message` 属性值，代表访问被阻止：\n\n```shell\nHTTP/1.1 403 Forbidden\n...\n{\"message\":\"Your referer host is not allowed\"}\n```\n\n因为启用插件时会将属性 `bypass_missing` 设置为 `true`，所以未指定 `Refer` 请求头的请求将跳过检查：\n\n```shell\ncurl http://127.0.0.1:9080/index.html\n```\n\n返回的 HTTP 响应头中带有 `200` 状态码，代表访问成功：\n\n```shell\nHTTP/1.1 200 OK\n...\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/request-id.md",
    "content": "---\ntitle: request-id\nkeywords:\n  - APISIX\n  - API 网关\n  - Request ID\ndescription: request-id 插件为通过 APISIX 代理的每个请求添加一个唯一的 ID，可用于跟踪 API 请求。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`request-id` 插件为每个通过 APISIX 代理的请求添加一个唯一 ID，可用于跟踪 API 请求。如果请求在 `header_name` 对应的 header 中带有 ID，则插件将使用 header 值作为唯一 ID，而不会用自动生成的 ID 进行覆盖。\n\n## 属性\n\n| 名称                | 类型    | 必选项   | 默认值         | 有效值 | 描述                           |\n| ------------------- | ------- | -------- | -------------- | ------ | ------------------------------ |\n| header_name | string | 否 | \"X-Request-Id\" | | 携带请求唯一 ID 的标头的名称。请注意，如果请求在 `header_name` 标头中携带 ID，则插件将使用标头值作为唯一 ID，并且不会用生成的 ID 覆盖它。|\n| include_in_response | 布尔值 | 否 | true | | 如果为 true，则将生成的请求 ID 包含在响应标头中，其中标头的名称是 `header_name` 值。|\n| algorithm | string | 否 | \"uuid\" | [\"uuid\",\"nanoid\",\"range_id\",\"ksuid\"] | 用于生成唯一 ID 的算法。设置为 `uuid` 时，插件会生成一个通用唯一标识符。设置为 `nanoid` 时，插件会生成一个紧凑的、URL 安全的 ID。设置为 `range_id` 时，插件会生成具有特定参数的连续 ID。设置为 `ksuid` 时，插件会生成具有时间戳和随机值的连续 ID。|\n| range_id | object | 否 | | |使用 `range_id` 算法生成请求 ID 的配置。|\n| range_id.char_set | string | 否 | \"abcdefghijklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ0123456789\" | 最小长度 6 | 用于 `range_id` 算法的字符集。|\n| range_id.length | integer | 否 | 16 | >=6 | 用于 `range_id` 算法的生成的 ID 的长度。|\n\n## 示例\n\n以下示例演示了如何在不同场景中配置“request-id”。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 将请求 ID 附加到默认响应标头\n\n以下示例演示了如何在路由上配置 `request-id`，如果请求中未传递标头值，则将生成的请求 ID 附加到默认的 `X-Request-Id` 响应标头。当在请求中设置 `X-Request-Id` 标头时，插件将把请求标头中的值作为请求 ID。\n\n使用其默认配置（明确定义）创建带有 `request-id` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"X-Request-Id\",\n        \"include_in_response\": true,\n        \"algorithm\": \"uuid\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到一个 `HTTP/1.1 200 OK` 响应，并且会看到响应包含 `X-Request-Id` 标头和生成的 ID：\n\n```text\nX-Request-Id: b9b2c0d4-d058-46fa-bafc-dd91a0ccf441\n```\n\n使用标头中的自定义请求 ID 向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'X-Request-Id: some-custom-request-id'\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应，并看到响应包含带有自定义请求 ID 的 `X-Request-Id` 标头：\n\n```text\nX-Request-Id：some-custom-request-id\n```\n\n### 将请求 ID 附加到自定义响应标头\n\n以下示例演示如何在路由上配置 `request-id`，将生成的请求 ID 附加到指定的标头。\n\n使用 `request-id` 插件创建路由，以定义带有请求 ID 的自定义标头，并将请求 ID 包含在响应标头中：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"X-Req-Identifier\",\n        \"include_in_response\": true\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到一个 `HTTP/1.1 200 OK` 响应，并看到响应包含带有生成 ID 的 `X-Req-Identifier` 标头：\n\n```text\nX-Req-Identifier：1c42ff59-ee4c-4103-a980-8359f4135b21\n```\n\n### 在响应标头中隐藏请求 ID\n\n以下示例演示如何在路由上配置 `request-id`，将生成的请求 ID 附加到指定的标头。包含请求 ID 的标头应转发到上游服务，但不会在响应标头中返回。\n\n使用 `request-id` 插件创建路由，以定义带有请求 ID 的自定义标头，而不在响应标头中包含请求 ID：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"X-Req-Identifier\",\n        \"include_in_response\": false\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应，并在响应标头中看到 `X-Req-Identifier` 标头。在响应主体中，您应该看到：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.6.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6752748c-7d364f48564508db1e8c9ea8\",\n    \"X-Forwarded-Host\": \"127.0.0.1\",\n    \"X-Req-Identifier\": \"268092bc-15e1-4461-b277-bf7775f2856f\"\n  },\n  ...\n}\n```\n\n这表明请求 ID 已转发到上游服务，但未在响应标头中返回。\n\n### 使用 `nanoid` 算法\n\n以下示例演示如何在路由上配置 `request-id` 并使用 `nanoid` 算法生成请求 ID。\n\n使用 `request-id` 插件创建路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"algorithm\": \"nanoid\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到一个 `HTTP/1.1 200 OK` 响应，并看到响应包含 `X-Req-Identifier` 标头，其中的 ID 使用 `nanoid` 算法生成：\n\n```text\nX-Request-Id: kepgHWCH2ycQ6JknQKrX2\n```\n\n### 使用 `ksuid` 算法\n\n以下示例演示如何在路由上配置 `request-id` 并使用 `ksuid` 算法生成请求 ID。\n\n使用 `request-id` 插件创建路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"algorithm\": \"ksuid\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到一个 `HTTP/1.1 200 OK` 响应，并看到响应包含 `X-Request-Id` 标头，其中的 ID 使用 `ksuid` 算法生成：\n\n```text\nX-Request-Id: 325ghCANEKjw6Jsfejg5p6QrLYB\n```\n\n如果装有[ksuid](https://github.com/segmentio/ksuid?tab=readme-ov-file#command-line-tool)命令工具，此 ID 可以通过`ksuid -f inspect 325ghCANEKjw6Jsfejg5p6QrLYB`查看：\n\n``` text\nREPRESENTATION:\n\n    String: 325ghCANEKjw6Jsfejg5p6QrLYB\n    Raw: 15430DBBD7F68AD7CA0AE277772AB36DDB1A3C13\n\nCOMPONENTS:\n\n    Time: 2025-09-01 16:39:23 +0800 CST\n    Timestamp: 356715963\n    Payload: D7F68AD7CA0AE277772AB36DDB1A3C13\n```\n\n### 全局和在路由上附加请求 ID\n\n以下示例演示如何将 `request-id` 配置为全局插件并在路由上附加两个 ID。\n\n为 `request-id` 插件创建全局规则，将请求 ID 添加到自定义标头：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/global_rules\" -X PUT -d '{\n  \"id\": \"rule-for-request-id\",\n  \"plugins\": {\n    \"request-id\": {\n      \"header_name\": \"Global-Request-ID\"\n    }\n  }\n}'\n```\n\n使用 `request-id` 插件创建路由，将请求 ID 添加到不同的自定义标头：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"id\": \"request-id-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"request-id\": {\n        \"header_name\": \"Route-Request-ID\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应，并看到响应包含以下标头：\n\n```text\nGlobal-Request-ID：2e9b99c1-08ed-4a74-b347-49c0891b07ad\nRoute-Request-ID：d755666b-732c-4f0e-a30e-a7a71ace4e26\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/request-validation.md",
    "content": "---\ntitle: request-validation\nkeywords:\n  - APISIX\n  - API 网关\n  - Request Validation\ndescription: request-validation 插件会在将请求转发到上游服务之前对其进行验证。此插件使用 JSON Schema 进行验证，并且可以验证请求的标头和正文。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/request-validation\" />\n</head>\n\n## 描述\n\n`request-validation` 插件会在将请求转发到上游服务之前对其进行验证。此插件使用 [JSON Schema](https://github.com/api7/jsonschema) 进行验证，并且可以验证请求的标头和正文。\n\n请参阅 [JSON Schema 规范](https://json-schema.org/specification) 了解有关语法的更多信息。\n\n## 属性\n\n| 名称             | 类型   | 必选项 | 默认值 | 有效值 | 描述                       |\n| ---------------- | ------ | ----------- | ------- | ----- | --------------------------------- |\n| header_schema    | object | 否        |         |       | `header` 数据的 `schema` 数据结构。 |\n| body_schema      | object | 否        |         |       | `body` 数据的 `schema` 数据结构。   |\n| rejected_code | integer | 否        | 400      | [200,...,599]   | 当请求被拒绝时要返回的状态码。 |\n| rejected_msg | string | 否        |         |       | 当请求被拒绝时返回的信息。 |\n\n:::note\n\n`header_schema` 和 `body_schema` 属性至少需要配置其一。\n\n:::\n\n## 示例\n\n以下示例演示了如何针对不同场景配置 `request-validation`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 验证请求标头\n\n下面的示例演示如何根据定义的 JSON Schema 验证请求标头，该模式需要两个特定的标头和标头值符合指定的要求。\n\n使用 `request-validation` 插件创建路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"User-Agent\", \"Host\"],\n          \"properties\": {\n            \"User-Agent\": {\n              \"type\": \"string\",\n              \"pattern\": \"^curl\\/\"\n            },\n            \"Host\": {\n              \"type\": \"string\",\n              \"enum\": [\"httpbin.org\", \"httpbin\"]\n            }\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n#### 使用符合架构的请求进行验证\n\n发送带有标头 `Host: httpbin` 的请求，该请求符合架构：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Host: httpbin\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin\",\n    \"User-Agent\": \"curl/7.74.0\",\n    \"X-Amzn-Trace-Id\": \"Root=1-6509ae35-63d1e0fd3934e3f221a95dd8\",\n    \"X-Forwarded-Host\": \"httpbin\"\n  },\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://httpbin/get\"\n}\n```\n\n#### 验证请求是否符合架构\n\n发送不带任何标头的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您应该收到 `HTTP/1.1 400 Bad Request` 响应，表明请求未能通过验证：\n\n```text\nproperty \"Host\" validation failed: matches none of the enum value\n```\n\n发送具有所需标头但标头值不符合的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Host: httpbin\" -H \"User-Agent: cli-mock\"\n```\n\n您应该收到一个 `HTTP/1.1 400 Bad Request` 响应，显示 `User-Agent` 标头值与预期模式不匹配：\n\n```text\nproperty \"User-Agent\" validation failed: failed to match pattern \"^curl/\" with \"cli-mock\"\n```\n\n### 自定义拒绝消息和状态代码\n\n以下示例演示了如何在验证失败时自定义响应状态和消息。\n\n使用 `request-validation` 配置路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"Host\"],\n          \"properties\": {\n            \"Host\": {\n              \"type\": \"string\",\n              \"enum\": [\"httpbin.org\", \"httpbin\"]\n            }\n          }\n        },\n        \"rejected_code\": 403,\n        \"rejected_msg\": \"Request header validation failed.\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送一个在标头中配置错误的 `Host` 的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H \"Host: httpbin2\"\n```\n\n您应该收到带有自定义消息的 `HTTP/1.1 403 Forbidden` 响应：\n\n```text\nRequest header validation failed.\n```\n\n### 验证请求主体\n\n以下示例演示如何根据定义的 JSON Schema 验证请求主体。\n\n`request-validation` 插件支持两种媒体类型的验证：\n\n* `application/json`\n* `application/x-www-form-urlencoded`\n\n#### 验证 JSON 请求主体\n\n使用 `request-validation` 插件创建路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"Content-Type\"],\n          \"properties\": {\n            \"Content-Type\": {\n            \"type\": \"string\",\n            \"pattern\": \"^application\\/json$\"\n            }\n          }\n        },\n        \"body_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"required_payload\"],\n          \"properties\": {\n            \"required_payload\": {\"type\": \"string\"},\n            \"boolean_payload\": {\"type\": \"boolean\"},\n            \"array_payload\": {\n              \"type\": \"array\",\n              \"minItems\": 1,\n              \"items\": {\n                \"type\": \"integer\",\n                \"minimum\": 200,\n                \"maximum\": 599\n              },\n              \"uniqueItems\": true,\n              \"default\": [200]\n            }\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送符合架构的 JSON Schema 的请求以验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"required_payload\":\"hello\", \"array_payload\":[301]}'\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"{\\\"array_payload\\\":[301],\\\"required_payload\\\":\\\"hello\\\"}\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    ...\n  },\n  \"json\": {\n    \"array_payload\": [\n      301\n    ],\n    \"required_payload\": \"hello\"\n  },\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\n如果你发送请求时没有指定 `Content-Type：application/json`：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -d '{\"required_payload\":\"hello,world\"}'\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 400 Bad Request` 响应：\n\n```text\nproperty \"Content-Type\" validation failed: failed to match pattern \"^application/json$\" with \"application/x-www-form-urlencoded\"\n```\n\n如果你发送的请求没有必需的 JSON 字段 `required_pa​​yload`：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n```\n\n您应该收到 `HTTP/1.1 400 Bad Request` 响应：\n\n```text\nproperty \"required_payload\" is required\n```\n\n#### 验证 URL 编码的表单主体\n\n使用 `request-validation` 插件创建路由，如下所示：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"request-validation-route\",\n    \"uri\": \"/post\",\n    \"plugins\": {\n      \"request-validation\": {\n        \"header_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"Content-Type\"],\n          \"properties\": {\n            \"Content-Type\": {\n              \"type\": \"string\",\n              \"pattern\": \"^application\\/x-www-form-urlencoded$\"\n            }\n          }\n        },\n        \"body_schema\": {\n          \"type\": \"object\",\n          \"required\": [\"required_payload\",\"enum_payload\"],\n          \"properties\": {\n            \"required_payload\": {\"type\": \"string\"},\n            \"enum_payload\": {\n              \"type\": \"string\",\n              \"enum\": [\"enum_string_1\", \"enum_string_2\"],\n              \"default\": \"enum_string_1\"\n            }\n          }\n        }\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送带有 URL 编码的表单数据的请求来验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"required_payload=hello&enum_payload=enum_string_1\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 400 Bad Request` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"enum_payload\": \"enum_string_1\",\n    \"required_payload\": \"hello\"\n  },\n  \"headers\": {\n    ...\n  },\n  \"json\": null,\n  \"origin\": \"127.0.0.1, 183.17.233.107\",\n  \"url\": \"http://127.0.0.1/post\"\n}\n```\n\n发送不带 URL 编码字段 `enum_payload` 的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/post\" -X POST \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"required_payload=hello\"\n```\n\n您应该收到以下 `HTTP/1.1 400 Bad Request`：\n\n```text\nproperty \"enum_payload\" is required\n```\n\n## 附录：JSON 模式\n\n以下部分提供了样板 JSON 模式，供您调整、组合和使用此插件。有关完整参考，请参阅 [JSON 模式规范](https://json-schema.org/specification)。\n\n### 枚举值\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"enum_payload\"],\n    \"properties\": {\n      \"enum_payload\": {\n        \"type\": \"string\",\n        \"enum\": [\"enum_string_1\", \"enum_string_2\"],\n        \"default\": \"enum_string_1\"\n      }\n    }\n  }\n}\n```\n\n### 布尔值\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"bool_payload\"],\n    \"properties\": {\n      \"bool_payload\": {\n        \"type\": \"boolean\",\n        \"default\": true\n      }\n    }\n  }\n}\n```\n\n### 数值\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"integer_payload\"],\n    \"properties\": {\n      \"integer_payload\": {\n        \"type\": \"integer\",\n        \"minimum\": 1,\n        \"maximum\": 65535\n      }\n    }\n  }\n}\n```\n\n### 字符串\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"string_payload\"],\n    \"properties\": {\n      \"string_payload\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 32\n      }\n    }\n  }\n}\n```\n\n### 字符串的正则表达式\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"regex_payload\"],\n    \"properties\": {\n      \"regex_payload\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 32,\n        \"pattern\": \"[[^[a-zA-Z0-9_]+$]]\"\n      }\n    }\n  }\n}\n```\n\n### 数组\n\n```json\n{\n  \"body_schema\": {\n    \"type\": \"object\",\n    \"required\": [\"array_payload\"],\n    \"properties\": {\n      \"array_payload\": {\n        \"type\": \"array\",\n        \"minItems\": 1,\n        \"items\": {\n          \"type\": \"integer\",\n          \"minimum\": 200,\n          \"maximum\": 599\n        },\n        \"uniqueItems\": true,\n        \"default\": [200, 302]\n      }\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/response-rewrite.md",
    "content": "---\ntitle: response-rewrite\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Response Rewrite\n  - response-rewrite\ndescription: response-rewrite 插件提供了重写 APISIX 及其上游服务返回给客户端的响应的选项。使用该插件，您可以修改 HTTP 状态代码、请求标头、响应正文等。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/response-rewrite\" />\n</head>\n\n## 描述\n\n`response-rewrite` 插件提供了重写 APISIX 及其上游服务返回给客户端的响应的选项。使用此插件，您可以修改 HTTP 状态代码、请求标头、响应正文等。\n\n例如，您可以使用此插件来：\n\n- 通过设置 `Access-Control-Allow-*` 标头来支持 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)。\n- 通过设置 HTTP 状态代码和 `Location` 标头来指示重定向。\n\n:::tip\n\n如果你仅需要重定向功能，建议使用 [redirect](redirect.md) 插件。\n\n:::\n\n## 属性\n\n| 名称            | 类型    | 必选项 | 默认值 | 有效值          | 描述                                                                                                                                                                                                                      |\n|-----------------|---------|--------|--------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| status_code     | integer | 否     |        | [200, 598]      | 修改上游返回状态码，默认保留原始响应代码。                                                                                                                                                                                |\n| body            | string  | 否     |        |                 | 修改上游返回的 `body` 内容，如果设置了新内容，header 里面的 `Content-Length` 字段也会被去掉。                                                                                                                               |\n| body_base64     | boolean | 否     | false  |                 | 如果为 true，则在发送到客户端之前解码`body` 中配置的响应主体，这对于图像和 protobuf 解码很有用。请注意，此配置不能用于解码上游响应。                                                                                                                                 |\n| headers | object | 否 | | | 按照 `add`、`remove` 和 `set` 的顺序执行的操作。 |\n| headers.add | array[string] | 否 | | | 要附加到请求的标头。如果请求中已经存在标头，则会附加标头值。标头值可以设置为常量，也可以设置为一个或多个 [Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。 |\n| headers.set | object | 否 | | |要设置到请求的标头。如果请求中已经存在标头，则会覆盖标头值。标头值可以设置为常量，也可以设置为一个或多个[Nginx 变量](https://nginx.org/en/docs/http/ngx_http_core_module.html)。 |\n| headers.remove | array[string] | 否 | | | 要从请求中删除的标头。 |\n| vars | array[array] | 否 | | | 以 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) 的形式包含一个或多个匹配条件的数组。 |\n| filters | array[object] | 否 | | | 通过将一个指定字符串替换为另一个指定字符串来修改响应主体的过滤器列表。不应与 `body` 一起配置。 |\n| filters.regex | string | True | | | 用于匹配响应主体的 RegEx 模式。 |\n| filters.scope | string | 否 | \"once\" | [\"once\",\"global\"] | 替换范围。`once` 替换第一个匹配的实例，`global` 全局替换。 |\n| filters.replace | string | True | | | 要替换的内容。 |\n| filters.options | string | 否 | \"jo\" | | 用于控制如何执行匹配操作的 RegEx 选项。请参阅[Lua NGINX 模块](https://github.com/openresty/lua-nginx-module#ngxrematch)以了解可用选项。|\n\n## 示例\n\n以下示例演示了如何在不同场景中在路由上配置 `response-rewrite`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 重写标头和正文\n\n以下示例演示了如何添加响应正文和标头，仅适用于具有 `200` HTTP 状态代码的响应。\n\n创建一个带有 `response-rewrite` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"response-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\": {\n      \"response-rewrite\": {\n        \"body\": \"{\\\"code\\\":\\\"ok\\\",\\\"message\\\":\\\"new json body\\\"}\",\n        \"headers\": {\n          \"set\": {\n            \"X-Server-id\": 3,\n            \"X-Server-status\": \"on\",\n            \"X-Server-balancer-addr\": \"$balancer_ip:$balancer_port\"\n          }\n        },\n        \"vars\": [\n          [ \"status\",\"==\",200 ]\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求以验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\"\n```\n\n您应该收到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```text\n...\nX-Server-id: 3\nX-Server-status: on\nX-Server-balancer-addr: 50.237.103.220:80\n\n{\"code\":\"ok\",\"message\":\"new json body\"}\n```\n\n### 使用 RegEx 过滤器重写标头\n\n以下示例演示如何使用 RegEx 过滤器匹配替换响应中的 `X-Amzn-Trace-Id`。\n\n创建一个带有 `response-rewrite` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"response-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/headers\",\n    \"plugins\":{\n      \"response-rewrite\":{\n        \"filters\":[\n          {\n            \"regex\":\"X-Amzn-Trace-Id\",\n            \"scope\":\"global\",\n            \"replace\":\"X-Amzn-Trace-Id-Replace\"\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求以验证：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/headers\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/8.2.1\",\n    \"X-Amzn-Trace-Id-Replace\": \"Root=1-6500095d-1041b05e2ba9c6b37232dbc7\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  }\n}\n```\n\n### 从 Base64 解码正文\n\n以下示例演示如何从 Base64 格式解码正文。\n\n创建一个带有 `response-rewrite` 插件的路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"response-rewrite-route\",\n    \"methods\": [\"GET\"],\n    \"uri\": \"/get\",\n    \"plugins\":{\n      \"response-rewrite\": {\n        \"body\": \"SGVsbG8gV29ybGQ=\",\n        \"body_base64\": true\n        }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n发送请求以验证：\n\n```shell\ncurl \"http://127.0.0.1:9080/get\"\n```\n\n您应该看到以下响应：\n\n```text\nHello World\n```\n\n### 重写响应及其与执行阶段的联系\n\n以下示例通过使用 `key-auth` 插件配置插件，演示了 `response-rewrite` 插件与 [执行阶段](/apisix/key-concepts/plugins#plugins-execution-lifecycle) 之间的联系，并查看在未经身份验证的请求的情况下，响应仍如何重写为 `200 OK`。\n\n创建消费者 `jack`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jack\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jack-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jack-key\"\n      }\n    }\n  }'\n```\n\n创建一个带有 `key-auth` 的路由，并配置 `response-rewrite` 来重写响应状态码和主体：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n   -H \"X-API-KEY: ${admin_key}\" \\\n   -d '{\n    \"id\": \"response-rewrite-route\",\n    \"uri\": \"/get\",\n    \"plugins\": {\n      \"key-auth\": {},\n      \"response-rewrite\": {\n        \"status_code\": 200,\n        \"body\": \"{\\\"code\\\": 200, \\\"msg\\\": \\\"success\\\"}\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n使用有效密钥向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\" -H 'apikey: jack-key'\n```\n\n您应该收到以下 `HTTP/1.1 200 OK` 响应：\n\n```text\n{\"code\": 200, \"msg\": \"success\"}\n```\n\n向路由发送一个没有任何键的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/get\"\n```\n\n您仍应收到相同的 `HTTP/1.1 200 OK` 响应，而不是来自 `key-auth` 插件的 `HTTP/1.1 401 Unauthorized`。这表明 `response-rewrite` 插件仍在重写响应。\n\n这是因为 `response-rewrite` 插件的 **header_filter** 和 **body_filter** 阶段逻辑将在 [`ngx.exit`](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/#ngxexit) 之后在其他插件的 **access** 或 **rewrite** 阶段继续运行。\n\n下表总结了 `ngx.exit` 对执行阶段的影响。\n\n| 阶段         | rewrite  | access   | header_filter | body_filter |\n|---------------|----------|----------|---------------|-------------|\n| **rewrite**       | ngx.exit |          |               |           |\n| **access**        | ×        | ngx.exit |               |           |\n| **header_filter** | ✓        | ✓        | ngx.exit      |           |\n| **body_filter**   | ✓        | ✓        | ×             | ngx.exit  |\n\n例如，如果 `ngx.exit` 发生在 **rewrite** 阶段，它将中断 **access** 阶段的执行，但不会干扰 **header_filter** 和 **body_filter** 阶段。\n"
  },
  {
    "path": "docs/zh/latest/plugins/rocketmq-logger.md",
    "content": "---\ntitle: rocketmq-logger\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - RocketMQ\ndescription: API 网关 Apache APISIX 的 rocketmq-logger 插件用于将日志作为 JSON 对象推送到 Apache RocketMQ 集群中。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`rocketmq-logger` 插件可以将日志以 JSON 的形式推送给外部 RocketMQ 集群。\n\n## 属性\n\n| 名称                   | 类型     | 必选项 | 默认值            | 有效值                 | 描述                                              |\n| ---------------------- | ------- | ------ | ----------------  | ------------- ------- | ------------------------------------------------ |\n| nameserver_list        | object  | 是     |                   |                       | RocketMQ 的 nameserver 列表。                     |\n| topic                  | string  | 是     |                   |                       | 要推送的 topic 名称。                             |\n| key                    | string  | 否     |                   |                       | 发送消息的 keys。                                 |\n| tag                    | string  | 否     |                   |                       | 发送消息的 tags。                                 |\n| log_format             | object  | 否     |                   |                       | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| timeout                | integer | 否     | 3                 | [1,...]               | 发送数据的超时时间。                              |\n| use_tls                | boolean | 否     | false             |                       | 当设置为 `true` 时，开启 TLS 加密。               |\n| access_key             | string  | 否     | \"\"                |                       | ACL 认证的 Access key，空字符串表示不开启 ACL。    |\n| secret_key             | string  | 否     | \"\"                |                       | ACL 认证的 Secret key。                           |\n| name                   | string  | 否     | \"rocketmq logger\" |                       | 标识 logger 的唯一标识符。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。               |\n| meta_format            | enum    | 否     | \"default\"         | [\"default\"，\"origin\"] | `default`：获取请求信息以默认的 JSON 编码方式。`origin`：获取请求信息以 HTTP 原始请求方式。更多信息，请参考 [meta_format](#meta_format-示例)。|\n| include_req_body       | boolean | 否     | false             | [false, true]         | 当设置为 `true` 时，包含请求体。**注意**：如果请求体无法完全存放在内存中，由于 NGINX 的限制，APISIX 无法将它记录下来。|\n| include_req_body_expr  | array   | 否     |                   |                       | 当 `include_req_body` 属性设置为 `true` 时进行过滤请求体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录请求体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n| include_resp_body      | boolean | 否     | false             | [false, true]         | 当设置为 `true` 时，包含响应体。 |\n| include_resp_body_expr | array   | 否     |                   |                       | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"secret_key\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n:::tip 提示\n\n数据首先写入缓冲区。当缓冲区超过 `batch_max_size` 或 `buffer_duration` 设置的值时，则会将数据发送到 RocketMQ 服务器并刷新缓冲区。\n\n如果发送成功，则返回 `true`。如果出现错误，则返回 `nil`，并带有描述错误的字符串 `buffer overflow`。\n\n:::\n\n### meta_format 示例\n\n- default:\n\n```json\n    {\n     \"upstream\": \"127.0.0.1:1980\",\n     \"start_time\": 1619414294760,\n     \"client_ip\": \"127.0.0.1\",\n     \"service_id\": \"\",\n     \"route_id\": \"1\",\n     \"request\": {\n       \"querystring\": {\n         \"ab\": \"cd\"\n       },\n       \"size\": 90,\n       \"uri\": \"/hello?ab=cd\",\n       \"url\": \"http://localhost:1984/hello?ab=cd\",\n       \"headers\": {\n         \"host\": \"localhost\",\n         \"content-length\": \"6\",\n         \"connection\": \"close\"\n       },\n       \"method\": \"GET\"\n     },\n     \"response\": {\n       \"headers\": {\n         \"connection\": \"close\",\n         \"content-type\": \"text/plain; charset=utf-8\",\n         \"date\": \"Mon, 26 Apr 2021 05:18:14 GMT\",\n         \"server\": \"APISIX/2.5\",\n         \"transfer-encoding\": \"chunked\"\n       },\n       \"size\": 190,\n       \"status\": 200\n     },\n     \"server\": {\n       \"hostname\": \"localhost\",\n       \"version\": \"2.5\"\n     },\n     \"latency\": 0\n    }\n```\n\n- origin:\n\n```http\n    GET /hello?ab=cd HTTP/1.1\n    host: localhost\n    content-length: 6\n    connection: close\n\n    abcdef\n```\n\n## 插件元数据设置\n\n| 名称         | 类型     | 必选项 | 默认值                                                                           | 描述                                                                                                                                                               |\n|------------|--------|-----|-------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| log_format | object | 否   |  | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::note 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `rocketmq-logger` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/rocketmq-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n在日志收集处，将得到类似下面的日志：\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## 启用插件\n\n你可以通过如下命令在指定路由上启用 `rocketmq-logger` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n       \"rocketmq-logger\": {\n           \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n           \"topic\" : \"test2\",\n           \"batch_max_size\": 1,\n           \"name\": \"rocketmq logger\"\n       }\n    },\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n该插件还支持一次推送到多个 `nameserver`，示例如下：\n\n```json\n[\n    \"127.0.0.1:9876\",\n    \"127.0.0.2:9876\"\n]\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/server-info.md",
    "content": "---\ntitle: server-info\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Server info\n  - server-info\ndescription: 本文介绍了关于 Apache APISIX `server-info` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`server-info` 插件可以定期将服务基本信息上报至 etcd。\n\n:::warning\n\n`server-info` 插件已弃用，将在未来的版本中被移除。更多关于弃用和移除计划的信息，请参考[这个讨论](https://github.com/apache/apisix/discussions/12298)。\n\n:::\n\n服务信息中每一项的含义如下：\n\n| 名称             | 类型    | 描述                                                                                                                   |\n| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |\n| boot_time        | integer | APISIX 服务实例的启动时间（UNIX 时间戳），如果对 APISIX 进行热更新操作，该值将被重置。普通的 reload 操作不会影响该值。         |\n| id               | string  | APISIX 服务实例 id。                                                                                                   |\n| etcd_version     | string  | etcd 集群的版本信息，如果 APISIX 和 etcd 集群之间存在网络分区，该值将设置为 `\"unknown\"`。                                   |\n| version          | string  | APISIX 版本信息。                                                                                                       |\n| hostname         | string  | 部署 APISIX 的主机或 Pod 的主机名信息。                                                                                  |\n\n## 属性\n\n无。\n\n## 插件接口\n\n该插件在 [Control API](../control-api.md) 下暴露了一个 API 接口 `/v1/server_info`。\n\n## 启用插件\n\n该插件默认是禁用状态，你可以在配置文件（`./conf/config.yaml`）中添加如下配置启用 `server-info` 插件。\n\n```yaml title=\"conf/config.yaml\"\nplugins:                          # plugin list\n  - ...\n  - server-info\n```\n\n## 自定义服务信息上报配置\n\n我们可以在 `./conf/config.yaml` 文件的 `plugin_attr` 部分修改上报配置。\n\n下表是可以自定义配置的参数：\n\n| 名称            | 类型    | 默认值  | 描述                                                               |\n| --------------- | ------- | ------ | --------------------------------------------------------------- |\n| report_ttl      | integer | 36     | etcd 中服务信息保存的 TTL（单位：秒，最大值：86400，最小值：3）。|\n\n以下是示例是通过修改配置文件（`conf/config.yaml`）中的 `plugin_attr` 部分将 `report_ttl` 设置为 1 分钟：\n\n```yaml title=\"conf/config.yaml\"\nplugin_attr:\n  server-info:\n    report_ttl: 60\n```\n\n## 测试插件\n\n在启用 `server-info` 插件后，可以通过插件的 Control API 来访问到这些数据：\n\n```shell\ncurl http://127.0.0.1:9090/v1/server_info -s | jq .\n```\n\n```JSON\n{\n  \"etcd_version\": \"3.5.0\",\n  \"id\": \"b7ce1c5c-b1aa-4df7-888a-cbe403f3e948\",\n  \"hostname\": \"fedora32\",\n  \"version\": \"2.1\",\n  \"boot_time\": 1608522102\n}\n```\n\n:::tip\n\n你可以通过 [APISIX Dashboard](/docs/dashboard/USER_GUIDE) 查看服务信息报告。\n\n:::\n\n## 删除插件\n\n如果你想禁用插件，可以将 `server-info` 从配置文件中的插件列表删除，重新加载 APISIX 后即可生效。\n\n```yaml title=\"conf/config.yaml\"\nplugins:    # plugin list\n  - ...\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/serverless.md",
    "content": "---\ntitle: serverless\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Serverless\ndescription: 本文介绍了关于 API 网关 Apache APISIX serverless-pre-function 和 serverless-post-function 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nAPISIX 有两个 `serverless` 插件：`serverless-pre-function` 和 `serverless-post-function`。\n\n`serverless-pre-function` 插件会在指定阶段开始时运行，`serverless-post-function` 插件会在指定阶段结束时运行。这两个插件使用相同的属性。\n\n## 属性\n\n| 名称      | 类型          | 必选项   | 默认值     | 有效值                                                                       | 描述                                                                            |\n| --------- | ------------- | ------- | ---------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| phase     | string        | 否      | [\"access\"] | [\"rewrite\", \"access\", \"header_filter\", \"body_filter\", \"log\", \"before_proxy\"] | 执行 serverless 函数的阶段。                                                     |\n| functions | array[string] | 是      |            |                                                                              | 指定运行的函数列表。该属性可以包含一个函数，也可以是多个函数，按照先后顺序执行。    |\n\n:::info 重要\n\n此处仅接受函数，不接受其他类型的 Lua 代码。\n\n比如匿名函数是合法的：\n\n```lua\nreturn function()\n    ngx.log(ngx.ERR, 'one')\nend\n```\n\n闭包也是合法的：\n\n```lua\nlocal count = 1\nreturn function()\n    count = count + 1\n    ngx.say(count)\nend\n```\n\n但不是函数类型的代码就是非法的：\n\n```lua\nlocal count = 1\nngx.say(count)\n```\n\n:::\n\n:::note 注意\n\n从 `v2.6` 版本开始，`conf` 和 `ctx` 作为前两个参数传递给 `serverless` 函数。\n\n在 `v2.12.0` 版本之前，`before_proxy` 阶段曾被称作 `balancer`。考虑到这一方法是在 `access` 阶段之后、请求到上游之前运行，并且与 `balancer` 没有关联，因此已经更新为 `before_proxy`。\n\n:::\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\" : [\"return function() ngx.log(ngx.ERR, \\\"serverless pre function\\\"); end\"]\n        },\n        \"serverless-post-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.ERR, \\\"match uri \\\", ctx.curr_req_matched and ctx.curr_req_matched._path); end\"]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/index.html\n```\n\n如果你在 `./logs/error.log` 中发现 `serverless pre function` 和 `match uri /index.html` 两个 error 级别的日志，表示指定的函数已经生效。\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/skywalking-logger.md",
    "content": "---\ntitle: skywalking-logger\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - SkyWalking\ndescription: skywalking-logger 将请求和响应日志作为 JSON 对象批量推送到 SkyWalking OAP 服务器，并支持日志格式的自定义。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/skywalking-logger\" />\n</head>\n\n## 描述\n\n`skywalking-logger` 插件将请求和响应日志作为 JSON 对象批量推送到 SkyWalking OAP 服务器，并支持日志格式的自定义。\n\n如果存在现有的跟踪上下文，它会自动设置跟踪日志关联并依赖于 [SkyWalking 跨进程传播标头协议](https://skywalking.apache.org/docs/main/next/en/api/x-process-propagation-headers-v3/)。\n\n## 属性\n\n| 名称                    | 类型    | 必选项 | 默认值                | 有效值        | 描述                                                               |\n| ---------------------- | ------- | ------ | -------------------- | ------------- | ---------------------------------------------------------------- |\n| endpoint_addr          | string  | 是     |                      |               | SkyWalking OAP 服务器的 URI。                                      |\n| service_name           | string  | 否     |\"APISIX\"              |               | SkyWalking 服务名称。                                              |\n| service_instance_name  | string  | 否     |\"APISIX Instance Name\"|               | SkyWalking 服务的实例名称。当设置为 `$hostname` 会直接获取本地主机名。 |\n| log_format             | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| timeout                | integer | 否     | 3                    | [1,...]       | 发送请求后保持连接活动的时间。                                       |\n| name                   | string  | 否     | \"skywalking logger\"  |               | 标识 logger 的唯一标识符。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。                                         |\n| include_req_body | boolean | 否 | false |如果为 true，则将请求主体包含在日志中。请注意，如果请求主体太大而无法保存在内存中，则由于 NGINX 的限制而无法记录。|\n| include_req_body_expr | array[array] | 否 | | 一个或多个条件的数组，形式为 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。在 `include_req_body` 为 true 时使用。仅当此处配置的表达式计算结果为 true 时，才会记录请求主体。|\n| include_resp_body | boolean | 否 | false | 如果为 true，则将响应主体包含在日志中。|\n| include_resp_body_expr | array[array] | 否 | | 一个或多个条件的数组，形式为 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。在 `include_resp_body` 为 true 时使用。仅当此处配置的表达式计算结果为 true 时，才会记录响应主体。|\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n## 元数据\n\n您还可以通过配置插件元数据来设置日志的格式。可用的配置如下：\n\n| 名称                    | 类型    | 必选项 | 默认值                | 有效值        | 描述                                                               |\n| ---------------------- | ------- | ------ | -------------------- | ------------- | ---------------------------------------------------------------- |\n| log_format | object | 否    |  | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n## 示例\n\n以下示例演示了如何为不同场景配置 `skywalking-logger` 插件。\n\n要按照示例操作，请按照 [Skywalking 的文档](https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-docker/) 使用 Docker Compose 启动存储、OAP 和 Booster UI。设置完成后，OAP 服务器应在 `12800` 上监听，并且您应该能够通过 [http://localhost:8080](http://localhost:8080) 访问 UI。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 以默认日志格式记录请求\n\n以下示例演示了如何在路由上配置 `skywalking-logger` 插件，以记录到达路由的请求信息。\n\n使用 `skywalking-logger` 插件创建路由，并使用 OAP 服务器 URI 配置插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n在 [Skywalking UI](http://localhost:8080) 中，导航至 __General Service__ > __Services__。您应该会看到一个名为 `APISIX` 的服务，其中包含与您的请求相对应的日志条目：\n\n```json\n{\n  \"upstream_latency\": 674,\n  \"request\": {\n    \"method\": \"GET\",\n    \"headers\": {\n      \"user-agent\": \"curl/8.6.0\",\n      \"host\": \"127.0.0.1:9080\",\n      \"accept\": \"*/*\"\n    },\n    \"url\": \"http://127.0.0.1:9080/anything\",\n    \"size\": 85,\n    \"querystring\": {},\n    \"uri\": \"/anything\"\n  },\n  \"client_ip\": \"192.168.65.1\",\n  \"route_id\": \"skywalking-logger-route\",\n  \"start_time\": 1736945107345,\n  \"upstream\": \"3.210.94.60:80\",\n  \"server\": {\n    \"version\": \"3.11.0\",\n    \"hostname\": \"7edbcebe8eb3\"\n  },\n  \"service_id\": \"\",\n  \"response\": {\n    \"size\": 619,\n    \"status\": 200,\n    \"headers\": {\n      \"content-type\": \"application/json\",\n      \"date\": \"Thu, 16 Jan 2025 12:45:08 GMT\",\n      \"server\": \"APISIX/3.11.0\",\n      \"access-control-allow-origin\": \"*\",\n      \"connection\": \"close\",\n      \"access-control-allow-credentials\": \"true\",\n      \"content-length\": \"391\"\n    }\n  },\n  \"latency\": 764.9998664856,\n  \"apisix_latency\": 90.999866485596\n}\n```\n\n### 使用插件元数据记录请求和响应标头\n\n以下示例演示了如何使用插件元数据和内置变量自定义日志格式，以记录来自请求和响应的特定标头。\n\n在 APISIX 中，插件元数据用于配置同一插件的所有插件实例的通用元数据字段。当插件在多个资源中启用并需要对其元数据字段进行通用更新时，它很有用。\n\n首先，使用 `skywalking-logger` 插件创建路由，并使用您的 OAP 服务器 URI 配置插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n接下来，配置 `skywalking-logger` 的插件元数据，以记录自定义请求头 `env` 和响应头 `Content-Type`:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/plugin_metadata/skywalking-logger\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"log_format\": {\n      \"host\": \"$host\",\n      \"@timestamp\": \"$time_iso8601\",\n      \"client_ip\": \"$remote_addr\",\n      \"env\": \"$http_env\",\n      \"resp_content_type\": \"$sent_http_Content_Type\"\n    }\n  }'\n```\n\n使用 `env` 标头向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H \"env: dev\"\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应。在 [Skywalking UI](http://localhost:8080) 中，导航至 __General Service__ > __Services__。您应该会看到一个名为 `APISIX` 的服务，其中包含与您的请求相对应的日志条目：\n\n```json\n[\n  {\n    \"route_id\": \"skywalking-logger-route\",\n    \"client_ip\": \"192.168.65.1\",\n    \"@timestamp\": \"2025-01-16T12:51:53+00:00\",\n    \"host\": \"127.0.0.1\",\n    \"env\": \"dev\",\n    \"resp_content_type\": \"application/json\"\n  }\n]\n```\n\n### 有条件地记录请求主体\n\n以下示例演示了如何有条件地记录请求主体。\n\n使用 `skywalking-logger` 插件创建一个路由，仅当 URL 查询字符串 `log_body` 为 `yes` 时才包含请求主体：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\",\n        \"include_req_body\": true,\n        \"include_req_body_expr\": [[\"arg_log_body\", \"==\", \"yes\"]]\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n使用满足以下条件的 URL 查询字符串向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything?log_body=yes\" -X POST -d '{\"env\": \"dev\"}'\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应。在 [Skywalking UI](http://localhost:8080) 中，导航到 __General Service__ > __Services__。您应该看到一个名为 `APISIX` 的服务，其中包含与您的请求相对应的日志条目，并记录了请求正文：\n\n```json\n[\n  {\n    \"request\": {\n      \"url\": \"http://127.0.0.1:9080/anything?log_body=yes\",\n      \"querystring\": {\n        \"log_body\": \"yes\"\n      },\n      \"uri\": \"/anything?log_body=yes\",\n      ...,\n      \"body\": \"{\\\"env\\\": \\\"dev\\\"}\",\n    },\n    ...\n  }\n]\n```\n\n向路由发送一个没有任何 URL 查询字符串的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -X POST -d '{\"env\": \"dev\"}'\n```\n\n您不应该观察到没有请求正文的日志条目。\n\n:::info\n\n如果您除了将 `include_req_body` 或 `include_resp_body` 设置为 `true` 之外还自定义了 `log_format`，则插件不会在日志中包含正文。\n\n作为一种解决方法，您可以在日志格式中使用 NGINX 变量 `$request_body`，例如：\n\n```json\n{\n  \"skywalking-logger\": {\n    ...,\n    \"log_format\": {\"body\": \"$request_body\"}\n  }\n}\n```\n\n:::\n\n### 将跟踪与日志关联\n\n以下示例演示了如何在路由上配置 `skywalking-logger` 插件，以记录到达路由的请求信息。\n\n使用 `skywalking-logger` 插件创建路由，并使用 OAP 服务器 URI 配置插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking\": {\n        \"sample_ratio\": 1\n      },\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n生成几个对路由的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n在 [Skywalking UI](http://localhost:8080) 中，导航到 __General Service__ > __Services__。您应该会看到一个名为 `APISIX` 的服务，其中包含与您的请求相对应的跟踪，您可以在其中查看相关日志：\n\n![trace context](https://static.apiseven.com/uploads/2025/01/16/soUpXm6b_trace-view-logs.png)\n\n![associated log](https://static.apiseven.com/uploads/2025/01/16/XD934LvU_associated-logs.png)\n"
  },
  {
    "path": "docs/zh/latest/plugins/skywalking.md",
    "content": "---\ntitle: skywalking\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - SkyWalking\ndescription: skywalking 插件支持与 Apache SkyWalking 集成以进行请求跟踪。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/skywalking\" />\n</head>\n\n## 描述\n\n`skywalking` 插件支持与 [Apache SkyWalking](https://skywalking.apache.org) 集成以进行请求跟踪。\n\nSkyWalking 使用其原生的 Nginx Lua 跟踪器从服务和 URI 角度提供跟踪、拓扑分析和指标。APISIX 支持 HTTP 协议与 SkyWalking 服务器交互。\n\n服务端目前支持 HTTP 和 gRPC 两种协议，在 APISIX 中目前只支持 HTTP 协议。\n\n## 静态配置\n\n默认情况下，插件的服务名称和端点地址已在[默认配置](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua)中预先配置。\n\n要自定义这些值，请将相应的配置添加到 `config.yaml`。例如：\n\n```yaml\nplugin_attr:\n  skywalking:\n    report_interval: 3      # 上报间隔时间（秒）。\n    service_name: APISIX    # SkyWalking 记者的服务名称。\n    service_instance_name: \"APISIX Instance Name\"   # SkyWalking 记者的服务实例名称。\n                                                    # 设置为 $hostname 可获取本地主机名。\n    endpoint_addr: http://127.0.0.1:12800           # SkyWalking HTTP 端点。\n```\n\n重新加载 APISIX 以使更改生效。\n\n## 属性\n\n| 名称         | 类型    | 必选项 | 默认值  | 有效值       | 描述                                                  |\n| ------------ | ------ | ------ | ------ | ------------ | ----------------------------------------------------- |\n| sample_ratio | number | 是     | 1      | [0.00001, 1] | 请求采样频率。将采样率设置为 `1` 表示对所有请求进行采样。 |\n\n## 示例\n\n要遵循示例，请按照 [Skywalking 的文档](https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-docker/) 使用 Docker Compose 启动存储、OAP 和 Booster UI。设置完成后，OAP 服务器应监听 `12800`，您应该能够通过 [http://localhost:8080](http://localhost:8080) 访问 UI。\n\n更新 APISIX 配置文件以启用 `skywalking` 插件（默认情况下处于禁用状态），并更新端点地址：\n\n```yaml title=\"config.yaml\"\nplugins:\n  - skywalking\n  - ...\n\nplugin_attr:\n  skywalking:\n    report_interval: 3\n    service_name: APISIX\n    service_instance_name: APISIX Instance\n    endpoint_addr: http://192.168.2.103:12800\n```\n\n重新加载 APISIX 以使配置更改生效。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 跟踪所有请求\n\n以下示例演示了如何跟踪通过路由的所有请求。\n\n使用 `skywalking` 创建路由，并将采样率配置为 1 以跟踪所有请求：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking\": {\n        \"sample_ratio\": 1\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n向路由发送几个请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应。\n\n在 [Skywalking UI](http://localhost:8080) 中，导航到 __General Service__ > __Services__。您应该看到一个名为 `APISIX` 的服务，其中包含与您的请求相对应的跟踪：\n\n![SkyWalking APISIX 跟踪](https://static.apiseven.com/uploads/2025/01/15/UdwiO8NJ_skywalking-traces.png)\n\n### 将跟踪与日志关联\n\n以下示例演示了如何在路由上配置 `skywalking-logger` 插件，以记录到达路由的请求信息。\n\n使用 `skywalking-logger` 插件创建路由，并使用你的 OAP 服务器 URI 配置该插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"skywalking-logger-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"skywalking\": {\n        \"sample_ratio\": 1\n      },\n      \"skywalking-logger\": {\n        \"endpoint_addr\": \"http://192.168.2.103:12800\"\n      }\n    },\n    \"upstream\": {\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  }'\n```\n\n生成几个对路由的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该会收到 `HTTP/1.1 200 OK` 响应。\n\n在 [Skywalking UI](http://localhost:8080) 中，导航到 __General Service__ > __Services__。您应该会看到一个名为 `APISIX` 的服务，其中包含与您的请求相对应的跟踪，您可以在其中查看相关日志：\n\n![trace context](https://static.apiseven.com/uploads/2025/01/16/soUpXm6b_trace-view-logs.png)\n\n![associated log](https://static.apiseven.com/uploads/2025/01/16/XD934LvU_associated-logs.png)\n"
  },
  {
    "path": "docs/zh/latest/plugins/sls-logger.md",
    "content": "---\ntitle: sls-logger\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`sls-logger` 是使用 [RF5424](https://tools.ietf.org/html/rfc5424) 标准将日志数据以 JSON 格式发送到 [阿里云日志服务](https://help.aliyun.com/document_detail/112903.html?spm=a2c4g.11186623.6.763.21321b47wcwt1u)。\n\n该插件提供了将 Log Data 作为批处理推送到阿里云日志服务器的功能。如果您没有收到日志数据，请放心一些时间，它会在我们的批处理处理器中的计时器功能到期后自动发送日志。\n\n有关 Apache APISIX 中 Batch-Processor 的更多信息，请参考：\n[Batch-Processor](../batch-processor.md)\n\n## 属性\n\n| 属性名称          | 必选项  | 描述 |\n|---------     |--------|-----------|\n| host | 必要的 | TCP 服务的 IP 地址或主机名，请参考：[阿里云日志服务列表](https://help.aliyun.com/document_detail/29008.html?spm=a2c4g.11186623.2.14.49301b4793uX0z#reference-wgx-pwq-zdb)，建议配置 IP 取代配置域名。|\n| port | 必要的 | 目标端口，阿里云日志服务默认端口为 10009。|\n| timeout | 可选的 | 发送数据超时间。|\n| log_format             | 可选的  | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| project | 必要的 | 日志服务 Project 名称，请提前在阿里云日志服务中创建 Project。|\n| logstore | 必须的 | 日志服务 Logstore 名称，请提前在阿里云日志服务中创建 Logstore。|\n| access_key_id | 必须的 | AccessKey ID。建议使用阿里云子账号 AK，详情请参见 [授权](https://help.aliyun.com/document_detail/47664.html?spm=a2c4g.11186623.2.15.49301b47lfvxXP#task-xsk-ttc-ry)。|\n| access_key_secret | 必须的 | AccessKey Secret。建议使用阿里云子账号 AK，详情请参见 [授权](https://help.aliyun.com/document_detail/47664.html?spm=a2c4g.11186623.2.15.49301b47lfvxXP#task-xsk-ttc-ry)。|\n| include_req_body | 可选的 | 是否包含请求体。|\n| include_req_body_expr   | 可选的 | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。    |\n| include_resp_body       | 可选的 | 当设置为 `true` 时，日志中将包含响应体。                                                     |\n| include_resp_body_expr  | 可选的 | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n|name| 可选的 | 批处理名字。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。|\n\n注意：schema 中还定义了 `encrypt_fields = {\"access_key_secret\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n本插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解或自定义批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置) 配置部分。\n\n### 默认日志格式示例\n\n```json\n{\n    \"route_conf\": {\n        \"host\": \"100.100.99.135\",\n        \"buffer_duration\": 60,\n        \"timeout\": 30000,\n        \"include_req_body\": false,\n        \"logstore\": \"your_logstore\",\n        \"log_format\": {\n            \"vip\": \"$remote_addr\"\n        },\n        \"project\": \"your_project\",\n        \"inactive_timeout\": 5,\n        \"access_key_id\": \"your_access_key_id\",\n        \"access_key_secret\": \"your_access_key_secret\",\n        \"batch_max_size\": 1000,\n        \"max_retry_count\": 0,\n        \"retry_delay\": 1,\n        \"port\": 10009,\n        \"name\": \"sls-logger\"\n    },\n    \"data\": \"<46>1 2024-01-06T03:29:56.457Z localhost apisix 28063 - [logservice project=\\\"your_project\\\" logstore=\\\"your_logstore\\\" access-key-id=\\\"your_access_key_id\\\" access-key-secret=\\\"your_access_key_secret\\\"] {\\\"vip\\\":\\\"127.0.0.1\\\",\\\"route_id\\\":\\\"1\\\"}\\n\"\n}\n```\n\n## 插件元数据设置\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 可选   |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的，**该设置是全局生效的**，意味着指定 log_format 后，将对所有绑定 sls-logger 的 Route 或 Service 生效。 |\n\n### 设置日志格式示例\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/sls-logger -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n在日志收集处，将得到类似下面的日志：\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## 如何开启\n\n1. 下面例子展示了如何为指定路由开启 `sls-logger` 插件的。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"sls-logger\": {\n            \"host\": \"100.100.99.135\",\n            \"port\": 10009,\n            \"project\": \"your_project\",\n            \"logstore\": \"your_logstore\",\n            \"access_key_id\": \"your_access_key_id\",\n            \"access_key_secret\": \"your_access_key_secret\",\n            \"timeout\": 30000\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n```\n注释：这里的 100.100.99.135 是阿里云华北 3 内外地址。\n```\n\n## 测试插件\n\n* 成功的情况：\n\n```shell\n$ curl -i http://127.0.0.1:9080/hello\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n* 查看阿里云日志服务上传记录\n![sls logger view](../../../assets/images/plugin/sls-logger-1.png \"阿里云日志服务预览\")\n\n## 删除插件\n\n想要禁用“sls-logger”插件，是非常简单的，将对应的插件配置从 json 配置删除，就会立即生效，不需要重新启动服务：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/splunk-hec-logging.md",
    "content": "---\ntitle: splunk-hec-logging\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 插件\n  - Splunk\n  - 日志\ndescription: API 网关 Apache APISIX 的 splunk-hec-logging 插件可用于将请求日志转发到 Splunk HTTP 事件收集器（HEC）中进行分析和存储。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`splunk-hec-logging` 插件可用于将请求日志转发到 Splunk HTTP 事件收集器（HEC）中进行分析和存储。\n\n启用该插件后，APISIX 将在 `Log Phase` 获取请求上下文信息，并将其序列化为 [Splunk Event Data 格式](https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector#Event_metadata) 后提交到批处理队列中，当触发批处理队列每批次最大处理容量或刷新缓冲区的最大时间时会将队列中的数据提交到 `Splunk HEC` 中。\n\n## 属性\n\n| 名称                | 必选项  | 默认值 | 描述                                                                                                                                                               |\n| ------------------  | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| endpoint            | 是     |        | Splunk HEC 端点配置信息。                                                                                                                                            |\n| endpoint.uri        | 是     |        | Splunk HEC 事件收集 API。                                                                                                                                            |\n| endpoint.token      | 是     |        | Splunk HEC 身份令牌。                                                                                                                                                |\n| endpoint.channel    | 否     |        | Splunk HEC 发送渠道标识，更多信息请参考 [About HTTP Event Collector Indexer Acknowledgment](https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/AboutHECIDXAck)。 |\n| endpoint.timeout    | 否     | 10     | Splunk HEC 数据提交超时时间（以秒为单位）。                                                                                                                             |\n| ssl_verify          | 否     | true   | 当设置为 `true` 时，启用 `SSL` 验证。                                                                                                                                 |\n| log_format              | 否   |                   | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n\n本插件支持使用批处理器来聚合并批量处理条目（日志和数据）。这样可以避免该插件频繁地提交数据。默认情况下每 `5` 秒钟或队列中的数据达到 `1000` 条时，批处理器会自动提交数据，如需了解更多信息或自定义配置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n```json\n{\n  \"sourcetype\": \"_json\",\n  \"time\": 1704513555.392,\n  \"event\": {\n    \"upstream\": \"127.0.0.1:1980\",\n    \"request_url\": \"http://localhost:1984/hello\",\n    \"request_query\": {},\n    \"request_size\": 59,\n    \"response_headers\": {\n      \"content-length\": \"12\",\n      \"server\": \"APISIX/3.7.0\",\n      \"content-type\": \"text/plain\",\n      \"connection\": \"close\"\n    },\n    \"response_status\": 200,\n    \"response_size\": 118,\n    \"latency\": 108.00004005432,\n    \"request_method\": \"GET\",\n    \"request_headers\": {\n      \"connection\": \"close\",\n      \"host\": \"localhost\"\n    }\n  },\n  \"source\": \"apache-apisix-splunk-hec-logging\",\n  \"host\": \"localhost\"\n}\n```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否    |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::info 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `splunk-hec-logging` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/splunk-hec-logging \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```json\n[{\"time\":1673976669.269,\"source\":\"apache-apisix-splunk-hec-logging\",\"event\":{\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"request\":{\"method\":\"GET\",\"uri\":\"/splunk.do\"},\"response\":{\"status\":200},\"route_id\":\"1\"},\"host\":\"DESKTOP-2022Q8F-wsl\",\"sourcetype\":\"_json\"}]\n```\n\n## 启用插件\n\n以下示例展示了如何在指定路由上启用该插件：\n\n**完整配置**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"splunk-hec-logging\":{\n            \"endpoint\":{\n                \"uri\":\"http://127.0.0.1:8088/services/collector\",\n                \"token\":\"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                \"channel\":\"FE0ECFAD-13D5-401B-847D-77833BD77131\",\n                \"timeout\":60\n            },\n            \"buffer_duration\":60,\n            \"max_retry_count\":0,\n            \"retry_delay\":1,\n            \"inactive_timeout\":2,\n            \"batch_max_size\":10\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    },\n    \"uri\":\"/splunk.do\"\n}'\n```\n\n**最小化配置**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\":{\n        \"splunk-hec-logging\":{\n            \"endpoint\":{\n                \"uri\":\"http://127.0.0.1:8088/services/collector\",\n                \"token\":\"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n            }\n        }\n    },\n    \"upstream\":{\n        \"type\":\"roundrobin\",\n        \"nodes\":{\n            \"127.0.0.1:1980\":1\n        }\n    },\n    \"uri\":\"/splunk.do\"\n}'\n```\n\n## 测试插件\n\n你可以通过以下命令向 APISIX 发出请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/splunk.do?q=hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n访问成功后，你可以登录 Splunk 控制台检索查看日志：\n\n![splunk hec search view](../../../assets/images/plugin/splunk-hec-admin-cn.png)\n\n## 删除插件\n\n当你需要删除该插件时，可以通过如下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/syslog.md",
    "content": "---\ntitle: syslog\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - syslog\ndescription: API 网关 Apache APISIX syslog 插件可用于将日志推送到 Syslog 服务器。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`syslog` 插件可用于将日志推送到 Syslog 服务器。\n\n该插件还实现了将日志数据以 JSON 格式发送到 Syslog 服务的能力。\n\n## 属性\n\n| 名称             | 类型     | 必选项 | 默认值       | 有效值        | 描述                                                                                                                                 |\n| ---------------- | ------- | ------ | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| host             | string  | 是     |              |               | IP 地址或主机名。                                                                                                                      |\n| port             | integer | 是     |              |               | 目标上游端口。                                                                                                                         |\n| name             | string  | 否     | \"sys logger\" |               | 标识 logger 的唯一标识符。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。                                                                                                                |\n| timeout          | integer | 否     | 3000         | [1, ...]      | 上游发送数据超时（以毫秒为单位）。                                                                                                       |\n| tls              | boolean | 否     | false        |               | 当设置为 `true` 时执行 SSL 验证。                                                                                                       |\n| flush_limit      | integer | 否     | 4096         | [1, ...]      | 如果缓冲的消息的大小加上当前消息的大小达到（> =）此限制（以字节为单位），则缓冲的日志消息将被写入日志服务器，默认为 4096（4KB）。              |\n| drop_limit       | integer | 否     | 1048576      |               | 如果缓冲的消息的大小加上当前消息的大小大于此限制（以字节为单位），则由于缓冲区大小有限，当前的日志消息将被丢弃，默认为 1048576（1MB）。        |\n| sock_type        | string  | 否     | \"tcp\"        | [\"tcp\",\"udp\"] | 用于传输层的 IP 协议类型。                                                                                                               |\n| max_retry_count  | integer | 否     |              | [1, ...]      | 连接到日志服务器失败或将日志消息发送到日志服务器失败后的最大重试次数。                                                                      |\n| retry_delay      | integer | 否     |              | [0, ...]      | 重试连接到日志服务器或重试向日志服务器发送日志消息之前的时间延迟（以毫秒为单位）。                                                           |\n| pool_size        | integer | 否     | 5            | [5, ...]      | `sock：keepalive` 使用的 Keepalive 池大小。                                                                                              |\n| log_format             | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| include_req_body | boolean | 否     | false        |  [false, true]    | 当设置为 `true` 时包括请求体。                                                                                                        |\n| include_req_body_expr   | array         | 否   |       |               | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。    |\n| include_resp_body       | boolean       | 否   | false | [false, true] | 当设置为 `true` 时，包含响应体。                                                                                                                               |\n| include_resp_body_expr  | array         | 否   |       |               | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认情况下批处理器每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n```text\n\"<46>1 2024-01-06T02:30:59.145Z 127.0.0.1 apisix 82324 - - {\\\"response\\\":{\\\"status\\\":200,\\\"size\\\":141,\\\"headers\\\":{\\\"content-type\\\":\\\"text/plain\\\",\\\"server\\\":\\\"APISIX/3.7.0\\\",\\\"transfer-encoding\\\":\\\"chunked\\\",\\\"connection\\\":\\\"close\\\"}},\\\"route_id\\\":\\\"1\\\",\\\"server\\\":{\\\"hostname\\\":\\\"baiyundeMacBook-Pro.local\\\",\\\"version\\\":\\\"3.7.0\\\"},\\\"request\\\":{\\\"uri\\\":\\\"/opentracing\\\",\\\"url\\\":\\\"http://127.0.0.1:1984/opentracing\\\",\\\"querystring\\\":{},\\\"method\\\":\\\"GET\\\",\\\"size\\\":155,\\\"headers\\\":{\\\"content-type\\\":\\\"application/x-www-form-urlencoded\\\",\\\"host\\\":\\\"127.0.0.1:1984\\\",\\\"user-agent\\\":\\\"lua-resty-http/0.16.1 (Lua) ngx_lua/10025\\\"}},\\\"upstream\\\":\\\"127.0.0.1:1982\\\",\\\"apisix_latency\\\":100.99999809265,\\\"service_id\\\":\\\"\\\",\\\"upstream_latency\\\":1,\\\"start_time\\\":1704508259044,\\\"client_ip\\\":\\\"127.0.0.1\\\",\\\"latency\\\":101.99999809265}\\n\"\n```\n\n## 插件元数据\n\n| 名称         | 类型     | 必选项 | 默认值 | 描述                                                                                                                                                             |\n|------------|--------|-----|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| log_format | object | 否   |     | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n\n:::info 重要\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `syslog` 的路由或服务都将使用该日志格式。\n\n:::\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"syslog\": {\n            \"host\" : \"127.0.0.1\",\n            \"port\" : 5044,\n            \"flush_limit\" : 1\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n现在你可以向 APISIX 发起请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n## 删除插件\n\n当你需要删除该插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/tcp-logger.md",
    "content": "---\ntitle: tcp-logger\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - TCP Logger\ndescription: 本文介绍了 API 网关 Apache APISIX 如何使用 tcp-logger 插件将日志数据发送到 TCP 服务器。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`tcp-logger` 插件可用于将日志数据发送到 TCP 服务器。\n\n该插件还实现了将日志数据以 JSON 格式发送到监控工具或其它 TCP 服务的能力。\n\n## 属性\n\n| 名称             | 类型     | 必选项  | 默认值 | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------ | ------- | ------------------------------------------------ |\n| host             | string  | 是     |        |         | TCP 服务器的 IP 地址或主机名。                     |\n| port             | integer | 是     |        | [0,...] | 目标端口。                                        |\n| timeout          | integer | 否     | 1000   | [1,...] | 发送数据超时间。                                   |\n| log_format       | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| tls              | boolean | 否     | false  |         | 用于控制是否执行 SSL 验证。                        |\n| tls_options      | string  | 否     |        |         | TLS 选项。                                        |\n| include_req_body | boolean | 否     |        | [false, true] | 当设置为 `true` 时，日志中将包含请求体。           |\n| include_req_body_expr | array | 否 |       |           | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。    |\n| include_resp_body | boolean | 否     | false | [false, true]| 当设置为 `true` 时，日志中将包含响应体。                                                     |\n| include_resp_body_expr | array | 否 |       |           | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认情况下批处理器每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n```json\n{\n  \"response\": {\n    \"status\": 200,\n    \"headers\": {\n      \"server\": \"APISIX/3.7.0\",\n      \"content-type\": \"text/plain\",\n      \"content-length\": \"12\",\n      \"connection\": \"close\"\n    },\n    \"size\": 118\n  },\n  \"server\": {\n    \"version\": \"3.7.0\",\n    \"hostname\": \"localhost\"\n  },\n  \"start_time\": 1704527628474,\n  \"client_ip\": \"127.0.0.1\",\n  \"service_id\": \"\",\n  \"latency\": 102.9999256134,\n  \"apisix_latency\": 100.9999256134,\n  \"upstream_latency\": 2,\n  \"request\": {\n    \"headers\": {\n      \"connection\": \"close\",\n      \"host\": \"localhost\"\n    },\n    \"size\": 59,\n    \"method\": \"GET\",\n    \"uri\": \"/hello\",\n    \"url\": \"http://localhost:1984/hello\",\n    \"querystring\": {}\n  },\n  \"upstream\": \"127.0.0.1:1980\",\n  \"route_id\": \"1\"\n}\n```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否    |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::info 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `tcp-logger` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/tcp-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```json\n{\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"route_id\":\"1\",\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200}}\n```\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"tcp-logger\": {\n                 \"host\": \"127.0.0.1\",\n                 \"port\": 5044,\n                 \"tls\": false,\n                 \"batch_max_size\": 1,\n                 \"name\": \"tcp logger\"\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n现在你可以向 APISIX 发起请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n## 删除插件\n\n当你需要删除该插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/tencent-cloud-cls.md",
    "content": "---\ntitle: tencent-cloud-cls\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - CLS\n  - 腾讯云\ndescription: API 网关 Apache APISIX tencent-cloud-cls 插件可用于将日志推送到[腾讯云日志服务](https://cloud.tencent.com/document/product/614)。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`tencent-cloud-cls` 插件可用于将 APISIX 日志使用[腾讯云日志服务](https://cloud.tencent.com/document/product/614) API 推送到您的日志主题。\n\n## 属性\n\n| 名称              | 类型    | 必选项 | 默认值   | 有效值       | 描述                                                                           |\n| ----------------- | ------- | ------ |-------| ------------ |------------------------------------------------------------------------------|\n| cls_host          | string  | 是     |       |              | CLS API 域名，参考[使用 API 上传日志](https://cloud.tencent.com/document/api/614/16873)。|\n| cls_topic         | string  | 是     |       |              | CLS 日志主题 id。                                                                 |\n| scheme            | string  | 否     | https | [\"http\", \"https\"] | 连接 CLS 时使用的协议方案。默认为 `https` 以实现安全连接。                                      |\n| secret_id         | string  | 是     |       |              | 云 API 密钥的 id。                                                                |\n| secret_key        | string  | 是     |       |              | 云 API 密钥的 key。                                                               |\n| sample_ratio      | number  | 否     | 1     | [0.00001, 1] | 采样的比例。设置为 `1` 时，将对所有请求进行采样。                                                  |\n| include_req_body  | boolean | 否     | false | [false, true]| 当设置为 `true` 时，日志中将包含请求体。                                                     |\n| include_req_body_expr | array | 否 |       |           | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。    |\n| include_resp_body | boolean | 否     | false | [false, true]| 当设置为 `true` 时，日志中将包含响应体。                                                     |\n| include_resp_body_expr | array | 否 |       |           | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n| global_tag        | object  | 否     |       |              | kv 形式的 JSON 数据，可以写入每一条日志，便于在 CLS 中检索。                                        |\n| log_format        | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n\n注意：schema 中还定义了 `encrypt_fields = {\"secret_key\"}`，这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。\n\n该插件支持使用批处理器来聚合并批量处理条目（日志/数据）。这样可以避免插件频繁地提交数据，默认情况下批处理器每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n### 默认日志格式示例\n\n```json\n{\n  \"response\": {\n    \"headers\": {\n      \"content-type\": \"text/plain\",\n      \"connection\": \"close\",\n      \"server\": \"APISIX/3.7.0\",\n      \"transfer-encoding\": \"chunked\"\n    },\n    \"size\": 136,\n    \"status\": 200\n  },\n  \"route_id\": \"1\",\n  \"upstream\": \"127.0.0.1:1982\",\n  \"client_ip\": \"127.0.0.1\",\n  \"apisix_latency\": 100.99985313416,\n  \"service_id\": \"\",\n  \"latency\": 103.99985313416,\n  \"start_time\": 1704525145772,\n  \"server\": {\n    \"version\": \"3.7.0\",\n    \"hostname\": \"localhost\"\n  },\n  \"upstream_latency\": 3,\n  \"request\": {\n    \"headers\": {\n      \"connection\": \"close\",\n      \"host\": \"localhost\"\n    },\n    \"url\": \"http://localhost:1984/opentracing\",\n    \"querystring\": {},\n    \"method\": \"GET\",\n    \"size\": 65,\n    \"uri\": \"/opentracing\"\n  }\n}\n```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否    |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::info 重要\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `tencent-cloud-cls` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/tencent-cloud-cls \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```shell\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200},\"route_id\":\"1\"}\n```\n\n## 启用插件\n\n你可以通过以下命令在指定路由中启用该插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"tencent-cloud-cls\": {\n            \"cls_host\": \"ap-guangzhou.cls.tencentyun.com\",\n            \"cls_topic\": \"${your CLS topic name}\",\n            \"global_tag\": {\n                \"module\": \"cls-logger\",\n                \"server_name\": \"YourApiGateWay\"\n            },\n            \"include_req_body\": true,\n            \"include_resp_body\": true,\n            \"secret_id\": \"${your secret id}\",\n            \"secret_key\": \"${your secret key}\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    },\n    \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n现在你可以向 APISIX 发起请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n## 删除插件\n\n当你需要删除该插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/traffic-split.md",
    "content": "---\ntitle: traffic-split\nkeywords:\n  - APISIX\n  - API 网关\n  - Traffic Split\n  - 灰度发布\n  - 蓝绿发布\ndescription: traffic-split 插件根据条件和/或权重将流量引导至各种上游服务。它提供了一种动态灵活的方法来实施发布策略和管理流量。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/traffic-split\" />\n</head>\n\n## 描述\n\n`traffic-split` 插件根据条件和/或权重将流量引导至各种上游服务。它提供了一种动态且灵活的方法来实施发布策略和管理流量。\n\n:::note 注意\n\n由于该插件使用了加权循环算法（特别是在重置 `wrr` 状态时），因此在使用该插件时，可能会存在上游服务之间的流量比例不精准现象。\n\n:::\n\n## 属性\n\n| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |\n| ---------------------- | --------------| ------ | ------ | ------ |-------------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------------------------------------------|\n| rules.match | array[object] | 否 | | | 要执行的一对或多对匹配条件和操作的数组。 |\n| rules.match | array[object] | 否 | | | 条件流量分割的匹配规则。 |\n| rules.match.vars | array[array] | 否 | | | 以 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) 形式包含一个或多个匹配条件的数组，用于有条件地执行插件。 |\n| rules.weighted_upstreams | array[object] | 否 | | | 上游配置列表。 |\n| rules.weighted_upstreams.upstream_id | 字符串/整数 | 否 | | | 配置的上游对象的 ID。 |\n| rules.weighted_upstreams.weight | 整数 | 否 | weight = 1 | | 每个上游的权重。 |\n| rules.weighted_upstreams.upstream | object | 否 | | | 上游配置。此处不支持某些上游配置选项。这些字段为 `service_name`、`discovery_type`、`checks`、`retries`、`retry_timeout`、`desc` 和 `labels`。作为解决方法，您可以创建一个上游对象并在 `upstream_id` 中配置它。|\n| rules.weighted_upstreams.upstream.type | array | 否 | roundrobin | [roundrobin, chash] | 流量分割算法。`roundrobin` 用于加权循环，`chash` 用于一致性哈希。|\n| rules.weighted_upstreams.upstream.hash_on | array | 否 | vars | | 当 `t​​ype` 为 `chash` 时使用。支持对 [NGINX 变量](https://nginx.org/en/docs/varindex.html)、headers、cookie、Consumer 或 [Nginx 变量](https://nginx.org/en/docs/varindex.html) 的组合进行哈希处理。 |\n| rules.weighted_upstreams.upstream.key | string | 否 | | | 当 `t​​ype` 为 `chash` 时使用。当 `hash_on` 设置为 `header` 或 `cookie` 时，需要 `key`。当 `hash_on` 设置为 `consumer` 时，不需要 `key`，因为消费者名称将自动用作密钥。 |\n| rules.weighted_upstreams.upstream.nodes | object | 否 | | | 上游节点的地址。 |\n| rules.weighted_upstreams.upstream.timeout | object | 否 | 15 | | 连接、发送和接收消息的超时时间（秒）。 |\n| rules.weighted_upstreams.upstream.pass_host | array | 否 | \"pass\" | [\"pass\", \"node\", \"rewrite\"] | 决定如何传递主机名的模式。`pass` 将客户端的主机名传递给上游。`node` 传递上游节点中配置的主机。`rewrite` 传递 `upstream_host` 中配置的值。|\n| rules.weighted_upstreams.upstream.name | string | 否 | | | 用于指定服务名称、使用场景等的上游标识符。|\n| rules.weighted_upstreams.upstream.upstream_host | string | 否 | | | 当 `pass_host` 为 `rewrite` 时使用。上游的主机名。|\n\n## 示例\n\n以下示例展示了使用 `traffic-split` 插件的不同用例。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 实现 Canary 发布\n\n以下示例演示了如何使用此插件实现 Canary 发布。\n\nCanary 发布是一种逐步部署，其中越来越多的流量被定向到新版本，从而实现受控和受监控的发布。此方法可确保在完全重定向所有流量之前，尽早识别和解决新版本中的任何潜在问题或错误。\n\n创建路由并使用以下规则配置 `traffic-split` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 3\n              },\n              {\n                \"weight\": 2\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\n每个 Upstream 的流量比例由该 Upstream 的权重占所有 Upstream 总权重的比例决定，这里总权重计算为：3 + 2 = 5。\n\n因此，60% 的流量要转发到 `httpbin.org`，另外 40% 的流量要转发到 `mock.api7.ai`。\n\n向路由发送 10 个连续请求来验证：\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers\" -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\nhttpbin.org: 6, mock.api7.ai: 4\n```\n\n相应地调整上游权重以完成金丝雀发布。\n\n### 实现蓝绿部署\n\n以下示例演示如何使用此插件实现蓝绿部署。\n\n蓝绿部署是一种部署策略，涉及维护两个相同的环境：蓝色和绿色。蓝色环境指的是当前的生产部署，绿色环境指的是新的部署。一旦绿色环境经过测试可以投入生产，流量将被路由到绿色环境，使其成为新的生产部署。\n\n创建路由并配置 `traffic-split` 插件，以便仅当请求包含标头 `release: new_release` 时才执行插件以重定向流量：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"http_release\",\"==\",\"new_release\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                }\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\n向路由发送一个带有 `release` 标头的请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\" -H 'release: new_release'\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    ...\n  }\n}\n```\n\n向路由发送一个不带任何附加标头的请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"mock.api7.ai\",\n    ...\n  }\n}\n```\n\n### 使用 APISIX 表达式定义 POST 请求的匹配条件\n\n以下示例演示了如何在规则中使用 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)，在满足 POST 请求的某些条件时有条件地执行插件。\n\n创建路由并使用以下规则配置 `traffic-split` 插件：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/post\",\n    \"methods\": [\"POST\"],\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"post_arg_id\", \"==\", \"1\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                }\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\n发送主体为 `id=1` 的 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'id=1'\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {\n    \"id\": \"1\"\n  },\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Content-Length\": \"4\",\n    \"Content-Type\": \"application/x-www-form-urlencoded\",\n    \"Host\": \"httpbin.org\",\n    ...\n  },\n  ...\n}\n```\n\n发送主体中不包含 `id=1` 的 POST 请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/post\" -X POST \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'random=string'\n```\n\n您应该看到请求已转发到 `mock.api7.ai`。\n\n### 使用 APISIX 表达式定义 AND 匹配条件\n\n以下示例演示了如何在规则中使用 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)，在满足多个条件时有条件地执行插件。\n\n创建路由并配置 `traffic-split` 插件，以便仅在满足所有三个条件时重定向流量：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"arg_name\",\"==\",\"jack\"],\n                  [\"http_user-id\",\">\",\"23\"],\n                  [\"http_apisix-key\",\"~~\",\"[a-z]+\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 3\n              },\n              {\n                \"weight\": 2\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\n如果满足条件，则 60% 的流量应定向到 `httpbin.org`，另外 40% 的流量应定向到 `mock.api7.ai`。如果不满足条件，则所有流量都应定向到 `mock.api7.ai`。\n\n发送 10 个满足所有条件的连续请求以验证：\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name=jack\" -H 'user-id: 30' -H 'apisix-key: helloapisix' -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\nhttpbin.org: 6, mock.api7.ai: 4\n```\n\n连续发送 10 个不满足条件的请求进行验证：\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name=random\" -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\n您应该会看到类似以下内容的响应：\n\n```text\nhttpbin.org: 0, mock.api7.ai: 10\n```\n\n### 使用 APISIX 表达式定义或匹配条件\n\n以下示例演示了如何在规则中使用 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)，在满足任一条件集时有条件地执行插件。\n\n创建路由并配置 `traffic-split` 插件，以在满足任一配置条件集时重定向流量：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"arg_name\",\"==\",\"jack\"],\n                  [\"http_user-id\",\">\",\"23\"],\n                  [\"http_apisix-key\",\"~~\",\"[a-z]+\"]\n                ]\n              },\n              {\n                \"vars\": [\n                  [\"arg_name2\",\"==\",\"rose\"],\n                  [\"http_user-id2\",\"!\",\">\",\"33\"],\n                  [\"http_apisix-key2\",\"~~\",\"[a-z]+\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 3\n              },\n              {\n                \"weight\": 2\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\",\n      \"nodes\": {\n        \"mock.api7.ai:443\":1\n      }\n    }\n  }'\n```\n\n或者，您也可以使用 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) 中的 OR 运算符来实现这些条件。\n\n如果满足条件，则 60% 的流量应定向到 `httpbin.org`，其余 40% 应定向到 `mock.api7.ai`。如果不满足条件，则所有流量都应定向到 `mock.api7.ai`。\n\n发送 10 个满足第二组条件的连续请求以验证：\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name2=rose\" -H 'user-id:30' -H 'apisix-key2: helloapisix' -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\nhttpbin.org: 6, mock.api7.ai: 4\n```\n\n发送 10 个连续的不满足任何一组条件的请求来验证：\n\n```shell\nresp=$(seq 10 | xargs -I{} curl \"http://127.0.0.1:9080/headers?name=random\" -sL) && \\\n  count_httpbin=$(echo \"$resp\" | grep \"httpbin.org\" | wc -l) && \\\n  count_mockapi7=$(echo \"$resp\" | grep \"mock.api7.ai\" | wc -l) && \\\n  echo httpbin.org: $count_httpbin, mock.api7.ai: $count_mockapi7\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\nhttpbin.org: 0, mock.api7.ai: 10\n```\n\n### 为不同的上游配置不同的规则\n\n以下示例演示了如何在规则集和上游之间设置一对一映射。\n\n创建一个路由并使用以下匹配规则配置 `traffic-split` 插件，以便在请求包含标头 `x-api-id: 1` 或 `x-api-id: 2` 时将流量重定向到相应的上游服务：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${ADMIN_API_KEY}\" \\\n  -d '{\n    \"uri\": \"/headers\",\n    \"id\": \"traffic-split-route\",\n    \"plugins\": {\n      \"traffic-split\": {\n        \"rules\": [\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"http_x-api-id\",\"==\",\"1\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"httpbin.org:443\":1\n                  }\n                },\n                \"weight\": 1\n              }\n            ]\n          },\n          {\n            \"match\": [\n              {\n                \"vars\": [\n                  [\"http_x-api-id\",\"==\",\"2\"]\n                ]\n              }\n            ],\n            \"weighted_upstreams\": [\n              {\n                \"upstream\": {\n                  \"type\": \"roundrobin\",\n                  \"scheme\": \"https\",\n                  \"pass_host\": \"node\",\n                  \"nodes\": {\n                    \"mock.api7.ai:443\":1\n                  }\n                },\n                \"weight\": 1\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"postman-echo.com:443\": 1\n      },\n      \"scheme\": \"https\",\n      \"pass_host\": \"node\"\n    }\n  }'\n```\n\n发送带有标头 `x-api-id: 1` 的请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\" -H 'x-api-id: 1'\n```\n\n您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"httpbin.org\",\n    ...\n  }\n}\n```\n\n发送带有标头 `x-api-id: 2` 的请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\" -H 'x-api-id: 2'\n```\n\n您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"mock.api7.ai\",\n    ...\n  }\n}\n```\n\n发送不带任何附加标头的请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/headers\"\n```\n\n您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"headers\": {\n    \"accept\": \"*/*\",\n    \"host\": \"postman-echo.com\",\n    ...\n  }\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/ua-restriction.md",
    "content": "---\ntitle: ua-restriction\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - UA restriction\ndescription: ua-restriction 插件使用用户代理的允许列表或拒绝列表来限制对上游资源的访问，防止网络爬虫过载并增强 API 安全性。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/ua-restriction\" />\n</head>\n\n## 描述\n\n`ua-restriction` 插件支持通过配置用户代理的允许列表或拒绝列表来限制对上游资源的访问。一个常见的用例是防止网络爬虫使上游资源过载并导致服务降级。\n\n## 属性\n\n| 名称    | 类型          | 必选项 | 默认值 | 有效值 | 描述                             |\n| --------- | ------------- | ------ | ------ | ------ | -------------------------------- |\n| byp​​ass_missing |boolean| 否 | false | | 如果为 true，则在缺少 `User-Agent` 标头时绕过用户代理限制检查。|\n| allowlist | array[string] | 否 | | | 要允许的用户代理列表。支持正则表达式。应配置 `allowlist` 和 `denylist` 中至少一个，但不能同时配置。|\n| denylist | array[string] | 否 | | | 要拒绝的用户代理列表。支持正则表达式。应配置 `allowlist` 和 `denylist` 中至少一个，但不能同时配置。|\n| message | string | 否 | \"Not allowed\" | | 拒绝用户代理访问时返回的消息。|\n\n## 示例\n\n以下示例演示了如何针对不同场景配置 `ua-restriction`。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 拒绝网络爬虫并自定义错误消息\n\n以下示例演示了如何配置插件以抵御不需要的网络爬虫并自定义拒绝消息。\n\n创建路由并配置插件以使用自定义消息阻止特定爬虫访问资源：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ua-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ua-restriction\": {\n        \"bypass_missing\": false,\n        \"denylist\": [\n          \"(Baiduspider)/(\\\\d+)\\\\.(\\\\d+)\",\n          \"bad-bot-1\"\n        ],\n        \"message\": \"Access denied\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应。\n\n使用不允许的用户代理向路由发送另一个请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'User-Agent: Baiduspider/5.0'\n```\n\n您应该收到 `HTTP/1.1 403 Forbidden` 响应，其中包含以下消息：\n\n```text\n{\"message\":\"Access denied\"}\n```\n\n### 绕过 UA 限制检查\n\n以下示例说明如何配置插件以允许特定用户代理的请求绕过 UA 限制。\n\n创建如下路由：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"ua-restriction-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"ua-restriction\": {\n        \"bypass_missing\": true,\n        \"allowlist\": [\n          \"good-bot-1\"\n        ],\n        \"message\": \"Access denied\"\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org:80\": 1\n      }\n    }\n  }'\n```\n\n向路由发送一个请求而不修改用户代理：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\"\n```\n\n您应该收到一个 `HTTP/1.1 403 Forbidden` 响应，其中包含以下消息：\n\n```text\n{\"message\":\"Access denied\"}\n```\n\n向路由发送另一个请求，用户代理为空：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything\" -H 'User-Agent: '\n```\n\n您应该收到一个 `HTTP/1.1 200 OK` 响应。\n"
  },
  {
    "path": "docs/zh/latest/plugins/udp-logger.md",
    "content": "---\ntitle: udp-logger\nkeywords:\n  - APISIX\n  - API 网关\n  - Plugin\n  - UDP Logger\ndescription: 本文介绍了 API 网关 Apache APISIX 如何使用 udp-logger 插件将日志数据发送到 UDP 服务器。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`udp-logger` 插件可用于将日志数据发送到 UDP 服务器。\n\n该插件还实现了将日志数据以 JSON 格式发送到监控工具或其它 UDP 服务的能力。\n\n## 属性\n\n| 名称             | 类型    | 必选项  | 默认值       | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------ | ------- | ------------------------------------------------ |\n| host             | string  | 是     |              |         | UDP 服务的 IP 地址或主机名。                       |\n| port             | integer | 是     |              | [0,...] | 目标端口。                                         |\n| timeout          | integer | 否     | 1000         | [1,...] | 发送数据超时间。                                   |\n| log_format       | object  | 否   |          |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| name             | string  | 否     | \"udp logger\" |         | 标识 logger 的唯一标识符。如果您使用 Prometheus 监视 APISIX 指标，名称将以 `apisix_batch_process_entries` 导出。                                 |\n| include_req_body | boolean | 否     |              |  [false, true] | 当设置为 `true` 时，日志中将包含请求体。           |\n| include_req_body_expr | array | 否 |       |           | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时，才会记录请求体。有关更多信息，请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。    |\n| include_resp_body | boolean | 否     | false | [false, true]| 当设置为 `true` 时，日志中将包含响应体。                                                     |\n| include_resp_body_expr | array | 否 |       |           | 当 `include_resp_body` 属性设置为 `true` 时进行过滤响应体，并且只有当此处设置的表达式计算结果为 `true` 时，才会记录响应体。更多信息，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr)。 |\n\n该插件支持使用批处理器来聚合并批量处理条目（日志和数据）。这样可以避免插件频繁地提交数据，默认情况下批处理器每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据，如需了解批处理器相关参数设置，请参考 [Batch-Processor](../batch-processor.md#配置)。\n\n### 默认日志格式数据\n\n```json\n{\n  \"apisix_latency\": 99.999988555908,\n  \"service_id\": \"\",\n  \"server\": {\n    \"version\": \"3.7.0\",\n    \"hostname\": \"localhost\"\n  },\n  \"request\": {\n    \"method\": \"GET\",\n    \"headers\": {\n      \"connection\": \"close\",\n      \"host\": \"localhost\"\n    },\n    \"url\": \"http://localhost:1984/opentracing\",\n    \"size\": 65,\n    \"querystring\": {},\n    \"uri\": \"/opentracing\"\n  },\n  \"start_time\": 1704527399740,\n  \"client_ip\": \"127.0.0.1\",\n  \"response\": {\n    \"status\": 200,\n    \"size\": 136,\n    \"headers\": {\n      \"server\": \"APISIX/3.7.0\",\n      \"content-type\": \"text/plain\",\n      \"transfer-encoding\": \"chunked\",\n      \"connection\": \"close\"\n    }\n  },\n  \"upstream\": \"127.0.0.1:1982\",\n  \"route_id\": \"1\",\n  \"upstream_latency\": 12,\n  \"latency\": 111.99998855591\n}\n```\n\n## 插件元数据\n\n| 名称             | 类型    | 必选项 | 默认值        | 有效值  | 描述                                             |\n| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |\n| log_format       | object  | 否    |  |         | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象（最多五层，超出部分将被截断）。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 |\n| max_pending_entries | integer | 否 | | | 在批处理器中开始删除待处理条目之前可以购买的最大待处理条目数。|\n\n:::info 注意\n\n该设置全局生效。如果指定了 `log_format`，则所有绑定 `udp-logger` 的路由或服务都将使用该日志格式。\n\n:::\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/udp-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\",\n        \"request\": { \"method\": \"$request_method\", \"uri\": \"$request_uri\" },\n        \"response\": { \"status\": \"$status\" }\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```json\n{\"@timestamp\":\"2023-01-09T14:47:25+08:00\",\"route_id\":\"1\",\"host\":\"localhost\",\"client_ip\":\"127.0.0.1\",\"request\":{\"method\":\"GET\",\"uri\":\"/hello\"},\"response\":{\"status\":200}}\n```\n\n## 如何开启\n\n你可以通过如下命令在指定路由上启用 `udp-logger` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/5 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n      \"plugins\": {\n            \"udp-logger\": {\n                 \"host\": \"127.0.0.1\",\n                 \"port\": 3000,\n                 \"batch_max_size\": 1,\n                 \"name\": \"udp logger\"\n            }\n       },\n      \"upstream\": {\n           \"type\": \"roundrobin\",\n           \"nodes\": {\n               \"127.0.0.1:1980\": 1\n           }\n      },\n      \"uri\": \"/hello\"\n}'\n```\n\n## 测试插件\n\n现在你可以向 APISIX 发起请求：\n\n```shell\ncurl -i http://127.0.0.1:9080/hello\n```\n\n```\nHTTP/1.1 200 OK\n...\nhello, world\n```\n\n## 删除插件\n\n当你需要删除该插件时，可通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {},\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/uri-blocker.md",
    "content": "---\ntitle: uri-blocker\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - URI Blocker\ndescription: 本文介绍了 Apache APISIX uri-blocker 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`uri-blocker` 插件通过指定一系列 `block_rules` 来拦截用户请求。\n\n## 属性\n\n| 名称          | 类型          | 必选项 | 默认值 | 有效值     | 描述                                                                |\n| ------------- | ------------- | ------ | ------ | ---------- | ------------------------------------------------------------------- |\n| block_rules   | array[string] | 是   |        |            | 正则过滤数组。它们都是正则规则，如果当前请求 URI 命中其中任何一个，则将响应代码设置为 `rejected_code` 以退出当前用户请求。例如：`[\"root.exe\", \"root.m+\"]`。 |\n| rejected_code | integer       | 否   | 403    | [200, ...] | 当请求 URI 命中 `block_rules` 中的任何一个时，将返回的 HTTP 状态代码。 |\n| rejected_msg | string       | 否    |      | 非空 | 当请求 URI 命中 `block_rules` 中的任何一个时，将返回的 HTTP 响应体。 |\n| case_insensitive | boolean       | 否    | false     |  | 是否忽略大小写。当设置为 `true` 时，在匹配请求 URI 时将忽略大小写。 |\n\n## 启用插件\n\n以下示例展示了如何在指定的路由上启用 `uri-blocker` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"uri-blocker\": {\n            \"block_rules\": [\"root.exe\", \"root.m+\"]\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n## 测试插件\n\n启用并配置插件后，使用 `curl` 命令尝试访问 `block_rules` 中指定文件的 URI：\n\n```shell\ncurl -i http://127.0.0.1:9080/root.exe?a=a\n```\n\n如果发现返回了带有 `403` 状态码的 HTTP 响应头，则代表插件生效：\n\n```shell\nHTTP/1.1 403 Forbidden\nDate: Wed, 17 Jun 2020 13:55:41 GMT\nContent-Type: text/html; charset=utf-8\nContent-Length: 150\nConnection: keep-alive\nServer: APISIX web server\n...\n```\n\n通过设置属性 `rejected_msg` 的值为 `access is not allowed`，将会收到包含如下信息的响应体：\n\n```shell\n...\n{\"error_msg\":\"access is not allowed\"}\n...\n```\n\n## 删除插件\n\n当你需要禁用 `uri-blocker` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/wolf-rbac.md",
    "content": "---\ntitle: wolf-rbac\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - wolf RBAC\n  - wolf-rbac\ndescription: 本文介绍了关于 Apache APISIX `wolf-rbac` 插件的基本信息及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n`wolf-rbac` 插件为 [role-based access control](https://en.wikipedia.org/wiki/Role-based_access_control) 系统提供了添加 [wolf](https://github.com/iGeeky/wolf) 到 Route 或 Service 的功能。此插件需要与 [Consumer](../terminology/consumer.md) 一起使用。\n\n## 属性\n\n| 名称          | 类型   | 必选项  | 默认值                    | 描述                                                                                                                                               |\n| ------------- | ------ | ------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |\n| server        | string | 否     | \"http://127.0.0.1:12180\" |  `wolf-server` 的服务地址。                                                                                                                          |\n| appid         | string | 否     | \"unset\"                  | 在 `wolf-console` 中已经添加的应用 id。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源，将值保存在 Secret Manager 中。                                       |\n| header_prefix | string | 否     | \"X-\"                     | 自定义 HTTP 头的前缀。`wolf-rbac` 在鉴权成功后，会在请求头 (用于传给后端) 及响应头 (用于传给前端) 中添加 3 个 header：`X-UserId`, `X-Username`, `X-Nickname`。|\n\n## 接口\n\n该插件在启用时将会增加以下接口：\n\n* /apisix/plugin/wolf-rbac/login\n* /apisix/plugin/wolf-rbac/change_pwd\n* /apisix/plugin/wolf-rbac/user_info\n\n:::note\n\n以上接口需要通过 [public-api](../../../en/latest/plugins/public-api.md) 插件暴露。\n\n:::\n\n## 前提条件\n\n如果要使用这个插件，你必须要[安装 wolf](https://github.com/iGeeky/wolf/blob/master/quick-start-with-docker/README.md) 并启动它。\n\n完成后，你需要添加`application`、`admin`、`regular user`、`permission`、`resource` 等字段，并将用户授权到 [wolf-console](https://github.com/iGeeky/wolf/blob/master/docs/usage.md)。\n\n## 启用插件\n\n首先需要创建一个 Consumer 并配置该插件，如下所示：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"username\":\"wolf_rbac\",\n  \"plugins\":{\n    \"wolf-rbac\":{\n      \"server\":\"http://127.0.0.1:12180\",\n      \"appid\":\"restful\"\n    }\n  },\n  \"desc\":\"wolf-rbac\"\n}'\n```\n\n:::note\n\n示例中填写的 `appid`，必须是已经在 wolf 控制台中存在的。\n\n:::\n\n然后你需要添加 `wolf-rbac` 插件到 Route 或 Service 中。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/*\",\n    \"plugins\": {\n        \"wolf-rbac\": {}\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"www.baidu.com:80\": 1\n        }\n    }\n}'\n```\n\n你还可以通过 [APISIX Dashboard](https://github.com/apache/apisix-dashboard) 的 Web 界面完成上述操作。\n\n<!--\n![add a consumer](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/plugin/wolf-rbac-1.png)\n\n![enable wolf-rbac plugin](https://raw.githubusercontent.com/apache/apisix/master/docs/assets/images/plugin/wolf-rbac-2.png)\n-->\n\n## 测试插件\n\n你可以使用 [public-api](../../../en/latest/plugins/public-api.md) 插件来暴露 API.\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/wal \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/apisix/plugin/wolf-rbac/login\",\n    \"plugins\": {\n        \"public-api\": {}\n    }\n}'\n```\n\n同样，你需要参考上述命令为 `change_pwd` 和 `user_info` 两个 API 配置路由。\n\n现在你可以登录并获取 wolf `rbac_token`：\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \\\n-H \"Content-Type: application/json\" \\\n-d '{\"appid\": \"restful\", \"username\":\"test\", \"password\":\"user-password\", \"authType\":1}'\n```\n\n```\nHTTP/1.1 200 OK\nDate: Wed, 24 Jul 2019 10:33:31 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n{\"rbac_token\":\"V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts\",\"user_info\":{\"nickname\":\"test\",\"username\":\"test\",\"id\":\"749\"}}\n```\n\n:::note\n\n上述示例中，`appid`、`username` 和 `password` 必须为 wolf 系统中真实存在的。\n\n`authType` 为认证类型，`1` 为密码认证（默认），`2` 为 LDAP 认证。`wolf` 从 0.5.0 版本开始支持了 LDAP 认证。\n\n:::\n\n也可以使用 x-www-form-urlencoded 方式登陆：\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \\\n-H \"Content-Type: application/x-www-form-urlencoded\" \\\n-d 'appid=restful&username=test&password=user-password'\n```\n\n现在开始测试 Route：\n\n- 缺少 token\n\n```shell\ncurl http://127.0.0.1:9080/ -H\"Host: www.baidu.com\" -i\n```\n\n```shell\nHTTP/1.1 401 Unauthorized\n...\n{\"message\":\"Missing rbac token in request\"}\n```\n\n- token 放到请求头 (Authorization) 中：\n\n```shell\ncurl http://127.0.0.1:9080/ -H\"Host: www.baidu.com\" \\\n-H 'Authorization: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- token 放到请求头 (x-rbac-token) 中：\n\n```shell\ncurl http://127.0.0.1:9080/ -H\"Host: www.baidu.com\" \\\n-H 'x-rbac-token: V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- token 放到请求参数中：\n\n```shell\ncurl 'http://127.0.0.1:9080?rbac_token=V1%23restful%23eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts' -H\"Host: www.baidu.com\" -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- token 放到 `cookie` 中：\n\n```shell\ncurl http://127.0.0.1:9080 -H\"Host: www.baidu.com\" \\\n--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i\n```\n\n```shell\nHTTP/1.1 200 OK\n\n<!DOCTYPE html>\n```\n\n- 获取用户信息：\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/user_info \\\n--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i\n```\n\n```shell\nHTTP/1.1 200 OK\n{\n    \"user_info\":{\n        \"nickname\":\"test\",\n        \"lastLogin\":1582816780,\n        \"id\":749,\n        \"username\":\"test\",\n        \"appIDs\":[\"restful\"],\n        \"manager\":\"none\",\n        \"permissions\":{\"USER_LIST\":true},\n        \"profile\":null,\n        \"roles\":{},\n        \"createTime\":1578820506,\n        \"email\":\"\"\n    }\n}\n```\n\n- 更改用户的密码：\n\n```shell\ncurl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/change_pwd \\\n-H \"Content-Type: application/json\" \\\n--cookie x-rbac-token=V1#restful#eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NzQ5LCJ1c2VybmFtZSI6InRlc3QiLCJtYW5hZ2VyIjoiIiwiYXBwaWQiOiJyZXN0ZnVsIiwiaWF0IjoxNTc5NDQ5ODQxLCJleHAiOjE1ODAwNTQ2NDF9.n2-830zbhrEh6OAxn4K_yYtg5pqfmjpZAjoQXgtcuts -i \\\n-X PUT -d '{\"oldPassword\": \"old password\", \"newPassword\": \"new password\"}'\n```\n\n```shell\nHTTP/1.1 200 OK\n{\"message\":\"success to change password\"}\n```\n\n## 删除插件\n\n当你需要禁用 `wolf-rbac` 插件时，可以通过以下命令删除相应的 JSON 配置，APISIX 将会自动重新加载相关配置，无需重启服务：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/*\",\n    \"plugins\": {\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"www.baidu.com:80\": 1\n        }\n    }\n}'\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/workflow.md",
    "content": "---\ntitle: workflow\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - workflow\n  - 流量控制\ndescription: workflow 插件支持根据给定的一组规则有条件地执行对客户端流量的用户定义操作。这提供了一种实现复杂流量管理的细粒度方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/workflow\" />\n</head>\n\n## 描述\n\n`workflow` 插件支持根据给定的规则集有条件地执行对客户端流量的用户定义操作，这些规则集使用 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list) 定义。这为流量管理提供了一种细粒度的方法。\n\n## 属性\n\n| 名称          | 类型   | 必选项  | 默认值                    | 有效值                                                                                                                                            | 描述 |\n| ------------- | ------ | ------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |\n| rules | array[object] | 是 | | | 一对或多对匹配条件和要执行的操作组成的数组。 |\n| rules.case | array[array] | 否 | | | 一个或多个匹配条件的数组，形式为 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)，例如 `{\"arg_name\", \"==\", \"json\"}`。 |\n| rules.actions | array[object] | 是 | | | 条件匹配成功后要执行的操作的数组。目前数组只支持一个操作，必须是 `return` 或者 `limit-count`。当操作配置为 `return` 时，可以配置条件匹配成功时返回给客户端的 HTTP 状态码。当操作配置为 `limit-count` 时，可以配置 [`limit-count`](./limit-count.md) 插件除 `group` 之外的所有选项。当操作配置为 `limit-conn` 时，可以配置 [`limit-conn`](./limit-conn.md)。 |\n\n## 示例\n\n以下示例演示了如何在不同场景中使用 `workflow` 插件。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n### 有条件地返回响应 HTTP 状态代码\n\n以下示例演示了一个简单的规则，其中包含一个匹配条件和一个关联操作，用于有条件地返回 HTTP 状态代码。\n\n使用 `workflow` 插件创建一个路由，当请求的 URI 路径为 `/anything/rejected` 时返回 HTTP 状态代码 403：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"workflow-route\",\n    \"uri\": \"/anything/*\",\n    \"plugins\": {\n      \"workflow\":{\n        \"rules\":[\n          {\n            \"case\":[\n              [\"uri\", \"==\", \"/anything/rejected\"]\n            ],\n            \"actions\":[\n              [\n                \"return\",\n                {\"code\": 403}\n              ]\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n发送与任何规则都不匹配的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/anything\"\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应。\n\n发送与配置的规则匹配的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/rejected\"\n```\n\n您应该收到以下 `HTTP/1.1 403 Forbidden` 响应：\n\n```text\n{\"error_msg\":\"rejected by workflow\"}\n```\n\n### 通过 URI 和查询参数有条件地应用速率限制\n\n以下示例演示了一条具有两个匹配条件和一个关联操作的规则，用于有条件地限制请求速率。\n\n使用 `workflow` 插件创建路由，以在 URI 路径为 `/anything/rate-limit` 且查询参数 `env` 值为 `v1` 时应用速率限制：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"workflow-route\",\n    \"uri\": \"/anything/*\",\n    \"plugins\":{\n      \"workflow\":{\n        \"rules\":[\n          {\n            \"case\":[\n              [\"uri\", \"==\", \"/anything/rate-limit\"],\n              [\"arg_env\", \"==\", \"v1\"]\n            ],\n            \"actions\":[\n              [\n                \"limit-count\",\n                {\n                  \"count\":1,\n                  \"time_window\":60,\n                  \"rejected_code\":429\n                }\n              ]\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n生成两个符合第二条规则的连续请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/rate-limit?env=v1\"\n```\n\n您应该收到 `HTTP/1.1 200 OK` 响应和 `HTTP 429 Too Many Requests` 响应。\n\n生成不符合条件的请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/anything?env=v1\"\n```\n\n您应该收到所有请求的 `HTTP/1.1 200 OK` 响应，因为它们不受速率限制。\n\n### 消费者有条件地应用速率限制\n\n以下示例演示了如何配置插件以根据以下规范执行速率限制：\n\n* 消费者 `john` 在 30 秒内应有 5 个请求的配额\n* 消费者 `jane` 在 30 秒内应有 3 个请求的配额\n* 所有其他消费者在 30 秒内应有 2 个请求的配额\n\n虽然此示例将使用 [`key-auth`](./key-auth.md)，但您可以轻松地将其替换为其他身份验证插件。\n\n创建消费者 `john`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"john\"\n  }'\n```\n\nCreate `key-auth` credential for the consumer:\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/john/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-john-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"john-key\"\n      }\n    }\n  }'\n```\n\n创建第二个消费者 `jane`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jane\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jane-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jane-key\"\n      }\n    }\n  }'\n```\n\n创建第三个消费者 `jimmy`：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"username\": \"jimmy\"\n  }'\n```\n\n为消费者创建 `key-auth` 凭证：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/consumers/jimmy/credentials\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"cred-jimmy-key-auth\",\n    \"plugins\": {\n      \"key-auth\": {\n        \"key\": \"jimmy-key\"\n      }\n    }\n  }'\n```\n\n使用 `workflow` 和 `key-auth` 插件创建路由，并设置所需的速率限制规则：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"workflow-route\",\n    \"uri\": \"/anything\",\n    \"plugins\":{\n      \"key-auth\": {},\n      \"workflow\":{\n        \"rules\":[\n          {\n            \"actions\": [\n              [\n                \"limit-count\",\n                {\n                  \"count\": 5,\n                  \"key\": \"consumer_john\",\n                  \"key_type\": \"constant\",\n                  \"rejected_code\": 429,\n                  \"time_window\": 30\n                }\n              ]\n            ],\n            \"case\": [\n              [\n                \"consumer_name\",\n                \"==\",\n                \"john\"\n              ]\n            ]\n          },\n          {\n            \"actions\": [\n              [\n                \"limit-count\",\n                {\n                  \"count\": 3,\n                  \"key\": \"consumer_jane\",\n                  \"key_type\": \"constant\",\n                  \"rejected_code\": 429,\n                  \"time_window\": 30\n                }\n              ]\n            ],\n            \"case\": [\n              [\n                \"consumer_name\",\n                \"==\",\n                \"jane\"\n              ]\n            ]\n          },\n          {\n            \"actions\": [\n              [\n                \"limit-count\",\n                {\n                  \"count\": 2,\n                  \"key\": \"$consumer_name\",\n                  \"key_type\": \"var\",\n                  \"rejected_code\": 429,\n                  \"time_window\": 30\n                }\n              ]\n            ]\n          }\n        ]\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n为了验证，请使用 `john` 的密钥发送 6 个连续的请求：\n\n```shell\nresp=$(seq 6 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: john-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 6 个请求中，5 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200： 5，429： 1\n```\n\n使用 `jane` 的密钥连续发送 6 个请求：\n\n```shell\nresp=$(seq 6 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: jane-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 6 个请求中，3 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200： 3，429： 3\n```\n\n使用 `jimmy` 的密钥发送 3 个连续请求：\n\n```shell\nresp=$(seq 3 | xargs -I{} curl \"http://127.0.0.1:9080/anything\" -H 'apikey: jimmy-key' -o /dev/null -s -w \"%{http_code}\\n\") && \\\n  count_200=$(echo \"$resp\" | grep \"200\" | wc -l) && \\\n  count_429=$(echo \"$resp\" | grep \"429\" | wc -l) && \\\n  echo \"200\": $count_200, \"429\": $count_429\n```\n\n您应该看到以下响应，显示在 3 个请求中，2 个请求成功（状态代码 200），而其他请求被拒绝（状态代码 429）。\n\n```text\n200： 2，429： 1\n```\n"
  },
  {
    "path": "docs/zh/latest/plugins/zipkin.md",
    "content": "---\ntitle: zipkin\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - Plugin\n  - Zipkin\ndescription: Zipkin 是一个开源的分布式链路追踪系统。`zipkin` 插件为 APISIX 提供了追踪功能，并根据 Zipkin API 规范将追踪数据上报给 Zipkin。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements. See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<head>\n  <link rel=\"canonical\" href=\"https://docs.api7.ai/hub/zipkin\" />\n</head>\n\n## 描述\n\n[Zipkin](https://github.com/openzipkin/zipkin) 是一个开源的分布式链路追踪系统。`zipkin` 插件为 APISIX 提供了追踪功能，并根据 [Zipkin API 规范](https://zipkin.io/pages/instrumenting.html) 将追踪数据上报给 Zipkin。\n\n该插件还支持将追踪数据发送到其他兼容的收集器，例如 [Jaeger](https://www.jaegertracing.io/docs/1.51/getting-started/#migrating-from-zipkin) 和 [Apache SkyWalking](https://skywalking.apache.org/docs/main/latest/en/setup/backend/zipkin-trace/#zipkin-receiver)，这两者都支持 Zipkin [v1](https://zipkin.io/zipkin-api/zipkin-api.yaml) 和 [v2](https://zipkin.io/zipkin-api/zipkin2-api.yaml) API。\n\n## 静态配置\n\n默认情况下，`zipkin` 插件的 NGINX 变量配置在 [默认配置](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua) 中设置为 `false`：\n\n要修改此值，请将更新后的配置添加到 `config.yaml` 中。例如：\n\n```yaml\nplugin_attr:\n  zipkin:\n    set_ngx_var: true\n```\n\n重新加载 APISIX 以使更改生效。\n\n## 属性\n\n查看配置文件以获取所有插件可用的配置选项。\n\n| 名称          | 类型     | 是否必需    | 默认值        | 有效值      |      描述     |\n|--------------|---------|----------|----------------|-------------|------------------|\n| endpoint     | string   | 是       |                |            | 要 POST 的 Zipkin span 端点，例如 `http://127.0.0.1:9411/api/v2/spans`。 |\n|sample_ratio| number    | 是       |                | [0.00001, 1] | 请求采样频率。设置为 `1` 表示对每个请求进行采样。    |\n|service_name| string  | 否       | \"APISIX\"       |              | 在 Zipkin 中显示的服务名称。   |\n|server_addr | string  | 否       | `$server_addr` 的值 | IPv4 地址 | Zipkin 报告器的 IPv4 地址。例如，可以将其设置为你的外部 IP 地址。 |\n|span_version| integer    | 否       | `2`            | [1, 2]       | span 类型的版本。    |\n\n## 示例\n\n以下示例展示了使用 `zipkin` 插件的不同用例。\n\n### 将追踪数据发送到 Zipkin\n\n以下示例演示了如何追踪对路由的请求，并将追踪数据发送到使用 [Zipkin API v2](https://zipkin.io/zipkin-api/zipkin2-api.yaml) 的 Zipkin。还将介绍 span 版本 2 和 版本 1 之间的区别。\n\n在 Docker 中启动一个 Zipkin 实例：\n\n```shell\ndocker run -d --name zipkin -p 9411:9411 openzipkin/zipkin\n```\n\n创建一条路由，开启 `zipkin` 插件，并使用其默认的 `span_version`，即 `2`。同时请根据需要调整 Zipkin HTTP 端点的 IP 地址，将采样比率配置为 `1` 以追踪每个请求。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\"  -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"zipkin-tracing-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"zipkin\": {\n        \"endpoint\": \"http://127.0.0.1:9411/api/v2/spans\",\n        \"sample_ratio\": 1,\n        \"span_version\": 2\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\n你应该收到一个类似于以下的 `HTTP/1.1 200 OK` 响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"*/*\",\n    \"Host\": \"127.0.0.1\",\n    \"User-Agent\": \"curl/7.64.1\",\n    \"X-Amzn-Trace-Id\": \"Root=1-65af2926-497590027bcdb09e34752b78\",\n    \"X-B3-Parentspanid\": \"347dddedf73ec176\",\n    \"X-B3-Sampled\": \"1\",\n    \"X-B3-Spanid\": \"429afa01d0b0067c\",\n    \"X-B3-Traceid\": \"aea58f4b490766eccb08275acd52a13a\",\n    \"X-Forwarded-Host\": \"127.0.0.1\"\n  },\n  ...\n}\n```\n\n导航到 Zipkin Web UI [http://127.0.0.1:9411/zipkin](http://127.0.0.1:9411/zipkin) 并点击 __Run Query__，你应该看到一个与请求对应的 trace：\n\n![来自请求的追踪](https://static.api7.ai/uploads/2024/01/23/MaXhacYO_zipkin-run-query.png)\n\n点击 __Show__ 查看更多 trace 细节：\n\n![v2 trace span](https://static.api7.ai/uploads/2024/01/23/3SmfFq9f_trace-details.png)\n\n请注意，使用 span 版本 2 时，每个被 trace 的请求会创建以下 span：\n\n```text\nrequest\n├── proxy\n└── response\n```\n\n其中 `proxy` 表示从请求开始到 `header_filter` 开始的时间，而 `response` 表示从 `header_filter` 开始到 `log` 开始的时间。\n\n现在，更新路由上的插件以使用 span 版本 1：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/zipkin-tracing-route\"  -X PATCH \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"plugins\": {\n      \"zipkin\": {\n        \"span_version\": 1\n      }\n    }\n  }'\n```\n\n向路由发送另一个请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\n在 Zipkin Web UI 中，你应该看到一个具有以下细节的新 trace：\n\n![v1 trace span](https://static.api7.ai/uploads/2024/01/23/OPw2sTPa_v1-trace-spans.png)\n\n请注意，使用较旧的 span 版本 1 时，每个被追踪的请求会创建以下 span：\n\n```text\nrequest\n├── rewrite\n├── access\n└── proxy\n    └── body_filter\n```\n\n### 将追踪数据发送到 Jaeger\n\n以下示例演示了如何追踪对路由的请求并将追踪数据发送到 Jaeger。\n\n在 Docker 中启动一个 Jaeger 实例：\n\n```shell\ndocker run -d --name jaeger \\\n  -e COLLECTOR_ZIPKIN_HOST_PORT=9411 \\\n  -p 16686:16686 \\\n  -p 9411:9411 \\\n  jaegertracing/all-in-one\n```\n\n创建一条路由并开启 `zipkin` 插件。请根据需要调整 Zipkin HTTP 端点的 IP 地址，并将采样比率配置为 `1` 以追踪每个请求。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT \\\n  -H \"X-API-KEY: ${admin_key}\" \\\n  -d '{\n    \"id\": \"zipkin-tracing-route\",\n    \"uri\": \"/anything\",\n    \"plugins\": {\n      \"zipkin\": {\n        \"endpoint\": \"http://127.0.0.1:9411/api/v2/spans\",\n        \"sample_ratio\": 1\n      }\n    },\n    \"upstream\": {\n      \"type\": \"roundrobin\",\n      \"nodes\": {\n        \"httpbin.org\": 1\n      }\n    }\n  }'\n```\n\n向路由发送请求：\n\n```shell\ncurl \"http://127.0.0.1:9080/anything\"\n```\n\n你应该收到一个 `HTTP/1.1 200 OK` 响应。\n\n导航到 Jaeger Web UI [http://127.0.0.1:16686](http://127.0.0.1:16686)，选择 APISIX 作为服务，并点击 __Find Traces__，您应该看到一个与请求对应的 trace：\n\n![jaeger trace](https://static.api7.ai/uploads/2024/01/23/X6QdLN3l_jaeger.png)\n\n同样地，一旦点击进入一个追踪，你应该会找到更多 span 细节：\n\n![jaeger 细节](https://static.api7.ai/uploads/2024/01/23/iP9fXI2A_jaeger-details.png)\n\n### 在日志中使用追踪变量\n\n以下示例演示了如何配置 `zipkin` 插件以设置以下内置变量，这些变量可以在日志插件或访问日志中使用：\n\n- `zipkin_context_traceparent`: [W3C trace context](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format)\n- `zipkin_trace_id`: 当前 span 的 trace_id\n- `zipkin_span_id`: 当前 span 的 span_id\n\n按照以下方式更新配置文件。你可以自定义访问日志格式以使用 `zipkin` 插件变量，并在 `set_ngx_var` 字段中设置 `zipkin` 变量。\n\n```yaml title=\"conf/config.yaml\"\nnginx_config:\n  http:\n    enable_access_log: true\n    access_log_format: '{\"time\": \"$time_iso8601\",\"zipkin_context_traceparent\": \"$zipkin_context_traceparent\",\"zipkin_trace_id\": \"$zipkin_trace_id\",\"zipkin_span_id\": \"$zipkin_span_id\",\"remote_addr\": \"$remote_addr\"}'\n    access_log_format_escape: json\nplugin_attr:\n  zipkin:\n    set_ngx_var: true\n```\n\n重新加载 APISIX 以使配置更改生效。\n\n当生成请求时，你应该看到类似的访问日志：\n\n```text\n{\"time\": \"23/Jan/2024:06:28:00 +0000\",\"zipkin_context_traceparent\": \"00-61bce33055c56f5b9bec75227befd142-13ff3c7370b29925-01\",\"zipkin_trace_id\": \"61bce33055c56f5b9bec75227befd142\",\"zipkin_span_id\": \"13ff3c7370b29925\",\"remote_addr\": \"172.28.0.1\"}\n```\n"
  },
  {
    "path": "docs/zh/latest/profile.md",
    "content": "---\ntitle: 基于环境变量进行配置文件切换\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n配置之所以从代码中提取出来，就是为了更好适应变化。通常我们的应用都有开发环境、生产环境等不同运行环境，这些环境下应用的一些配置肯定会有不同，比如：配置中心的地址等。\n\n如果把所有环境的配置都放在同一个文件里，非常不好管理，我们接到新需求后，在开发环境进行开发时，需要将配置文件中的参数都改成开发环境的，提交代码时还要改回去，这样改来改去非常容易出错。\n\n上述问题的解决办法就是通过环境变量来区分当前运行环境，并通过环境变量来切换不同配置文件。APISIX 中对应的环境变量就是：`APISIX_PROFILE`。\n\n在没有设置 `APISIX_PROFILE` 时，默认使用以下三个配置文件：\n\n* conf/config.yaml\n* conf/apisix.yaml\n* conf/debug.yaml\n\n如果设置了 `APISIX_PROFILE` 的值为 `prod`，则使用以下三个配置文件：\n\n* conf/config-prod.yaml\n* conf/apisix-prod.yaml\n* conf/debug-prod.yaml\n\n通过这种方式虽然会增加配置文件的数量，但可以独立管理，再配置 git 等版本管理工具，还能更好实现版本管理。\n"
  },
  {
    "path": "docs/zh/latest/router-radixtree.md",
    "content": "---\ntitle: 路由 RadixTree\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### 什么是 libradixtree？\n\n[libradixtree](https://github.com/api7/lua-resty-radixtree), 是在 `Lua` 中为 `OpenResty` 实现的自适应\n[基数树](https://zh.wikipedia.org/wiki/%E5%9F%BA%E6%95%B0%E6%A0%91) 。\n\n`Apache APISIX` 使用 `libradixtree` 作为路由调度库。\n\n### 如何在 Apache APISIX 中使用 libradixtree？\n\n`libradixtree` 是基于 [rax](https://github.com/antirez/rax) 的 `lua-resty-*` 实现。\n\n我们通过下面的示例可以有一个直观的理解。\n\n#### 1. 完全匹配\n\n```text\n/blog/foo\n```\n\n此时只能匹配 `/blog/foo` 。\n\n#### 2. 前缀匹配\n\n```text\n/blog/bar*\n```\n\n它将匹配带有前缀 `/blog/bar` 的路径，\n例如： `/blog/bar/a` 、 `/blog/bar/b` 、 `/blog/bar/c/d/e` 、 `/blog/bar` 等。\n\n#### 3. 匹配优先级\n\n完全匹配 -> 深度前缀匹配\n\n以下是规则：\n\n```text\n/blog/foo/*\n/blog/foo/a/*\n/blog/foo/c/*\n/blog/foo/bar\n```\n\n| 路径            | 匹配结果        |\n| --------------- | --------------- |\n| /blog/foo/bar   | `/blog/foo/bar` |\n| /blog/foo/a/b/c | `/blog/foo/a/*` |\n| /blog/foo/c/d   | `/blog/foo/c/*` |\n| /blog/foo/gloo  | `/blog/foo/*` |\n| /blog/bar       | not match       |\n\n#### 4. 不同的路由具有相同 `uri`\n\n当不同的路由有相同的 `uri` 时，可以通过设置路由的 `priority` 字段来决定先匹配哪条路由，或者添加其他匹配规则来区分不同的路由。\n\n注意：在匹配规则中， `priority` 字段优先于除 `uri` 之外的其他规则。\n\n1、不同的路由有相同的 `uri` 并设置 `priority` 字段\n\n创建两条 `priority` 值不同的路由（值越大，优先级越高）。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"priority\": 3,\n    \"uri\": \"/hello\"\n}'\n```\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1981\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"priority\": 2,\n    \"uri\": \"/hello\"\n}'\n```\n\n测试：\n\n```shell\ncurl http://127.0.0.1:1980/hello\n1980\n```\n\n所有请求只到达端口 `1980` 的路由。\n\n2、不同的路由有相同的 `uri` 并设置不同的匹配条件\n\n以下是设置主机匹配规则的示例：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1980\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"hosts\": [\"localhost.com\"],\n    \"uri\": \"/hello\"\n}'\n```\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/2 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n       \"nodes\": {\n           \"127.0.0.1:1981\": 1\n       },\n       \"type\": \"roundrobin\"\n    },\n    \"hosts\": [\"test.com\"],\n    \"uri\": \"/hello\"\n}'\n```\n\n测试：\n\n```shell\n$ curl http://127.0.0.1:9080/hello -H 'host: localhost.com'\n1980\n```\n\n```shell\n$ curl http://127.0.0.1:9080/hello -H 'host: test.com'\n1981\n```\n\n```shell\n$ curl http://127.0.0.1:9080/hello\n{\"error_msg\":\"404 Route Not Found\"}\n```\n\n`host` 规则匹配，请求命中对应的上游，`host` 不匹配，请求返回 404 消息。\n\n#### 5. 参数匹配\n\n当使用 `radixtree_uri_with_parameter` 时，我们可以用参数匹配路由。\n\n例如，使用配置：\n\n```yaml\napisix:\n  router:\n    http: 'radixtree_uri_with_parameter'\n```\n\n示例：\n\n```bash\n/blog/:name\n```\n\n此时将匹配 `/blog/dog` 和 `/blog/cat`。\n\n更多使用方式请参考：[lua-resty-radixtree#parameters-in-path](https://github.com/api7/lua-resty-radixtree/#parameters-in-path)\n\n### 如何通过 Nginx 内置变量过滤路由\n\n具体参数及使用方式请查看 [radixtree#new](https://github.com/api7/lua-resty-radixtree#new) 文档，下面是一个简单的示例：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/index.html\",\n    \"vars\": [\n        [\"http_host\", \"==\", \"iresty.com\"],\n        [\"cookie_device_id\", \"==\", \"a66f0cdc4ba2df8c096f74c9110163a9\"],\n        [\"arg_name\", \"==\", \"json\"],\n        [\"arg_age\", \">\", \"18\"],\n        [\"arg_address\", \"~~\", \"China.*\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n这个路由需要请求头 `host` 等于 `iresty.com`，\n请求 cookie `_device_id` 等于 `a66f0cdc4ba2df8c096f74c9110163a9` 等。\n\n### 如何通过 POST 表单属性过滤路由\n\nAPISIX 支持通过 POST 表单属性过滤路由，其中需要您使用 `Content-Type` = `application/x-www-form-urlencoded` 的 POST 请求。\n\n我们可以定义这样的路由：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"methods\": [\"POST\"],\n    \"uri\": \"/_post\",\n    \"vars\": [\n        [\"post_arg_name\", \"==\", \"json\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n当 POST 表单中包含 `name=json` 的属性时，将匹配到路由。\n\n### 如何通过 GraphQL 属性过滤路由\n\n目前，APISIX 可以处理 HTTP GET 和 POST 方法。请求体正文可以是 GraphQL 查询字符串，也可以是 JSON 格式的内容。\n\nAPISIX 支持通过 GraphQL 的一些属性过滤路由。目前我们支持：\n\n* graphql_operation\n* graphql_name\n* graphql_root_fields\n\n例如，像这样的 GraphQL：\n\n```graphql\nquery getRepo {\n    owner {\n        name\n    }\n    repo {\n        created\n    }\n}\n```\n\n* `graphql_operation` 是 `query`\n* `graphql_name` 是 `getRepo`，\n* `graphql_root_fields` 是 `[\"owner\", \"repo\"]`\n\n我们可以用以下方法过滤掉这样的路由：\n\n```shell\n$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"methods\": [\"POST\", \"GET\"],\n    \"uri\": \"/graphql\",\n    \"vars\": [\n        [\"graphql_operation\", \"==\", \"query\"],\n        [\"graphql_name\", \"==\", \"getRepo\"],\n        [\"graphql_root_fields\", \"has\", \"owner\"]\n    ],\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n我们可以通过以下三种方式分别去验证 GraphQL 匹配：\n\n1. 使用 GraphQL 查询字符串\n\n```shell\n$ curl -H 'content-type: application/graphql' -X POST http://127.0.0.1:9080/graphql -d '\nquery getRepo {\n    owner {\n        name\n    }\n    repo {\n        created\n    }\n}'\n```\n\n2. 使用 JSON 格式\n\n```shell\n$ curl -H 'content-type: application/json' -X POST \\\nhttp://127.0.0.1:9080/graphql --data '{\"query\": \"query getRepo { owner {name } repo {created}}\"}'\n```\n\n3. 尝试 `GET` 请求\n\n```shell\n$ curl -H 'content-type: application/graphql' -X GET \\\n\"http://127.0.0.1:9080/graphql?query=query getRepo { owner {name } repo {created}}\" -g\n```\n\n为了防止花费太多时间读取无效的 `GraphQL` 请求正文，我们只读取前 `1 MiB`\n来自请求体的数据。此限制是通过以下方式配置的：\n\n```yaml\ngraphql:\n  max_size: 1048576\n```\n\n如果你需要传递一个大于限制的 GraphQL 查询语句，你可以增加 `conf/config.yaml` 中的值。\n"
  },
  {
    "path": "docs/zh/latest/ssl-protocol.md",
    "content": "---\ntitle: SSL 协议\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n`APISIX` 支持 TLS 协议，还支持动态的为每一个 SNI 指定不同的 TLS 协议版本。\n\n**为了安全考虑，APISIX 默认使用的加密套件不支持 TLSv1.1 以及更低的版本。**\n**如果你需要启用 TLSv1.1 协议，请在 config.yaml 的配置项 apisix.ssl.ssl_ciphers 增加 TLSv1.1 协议所支持的加密套件。**\n\n## ssl_protocols 配置\n\n### 静态配置\n\n静态配置中 config.yaml 的 ssl_protocols 参数会作用于 APISIX 全局，但是不能动态修改，仅当匹配的 SSL 资源未设置 `ssl_protocols`，静态配置才会生效。\n\n```yaml\napisix:\n  ssl:\n    ssl_protocols: TLSv1.2 TLSv1.3 # default TLSv1.2 TLSv1.3\n```\n\n### 动态配置\n\n使用 ssl 资源中 ssl_protocols 字段动态的为每一个 SNI 指定不同的 TLS 协议版本。\n\n指定 test.com 域名使用 TLSv1.2 TLSv1.3 协议版本：\n\n```bash\n{\n    \"cert\": \"$cert\",\n    \"key\": \"$key\",\n    \"snis\": [\"test.com\"],\n    \"ssl_protocols\": [\n        \"TLSv1.2\",\n        \"TLSv1.3\"\n    ]\n}\n```\n\n### 注意事项\n\n- 动态配置优先级比静态配置更高，当 ssl 资源配置项 ssl_protocols 不为空时 静态配置将会被覆盖。\n- 静态配置作用于全局需要重启 apisix 才能生效。\n- 动态配置可细粒度的控制每个 SNI 的 TLS 协议版本，并且能够动态修改，相比于静态配置更加灵活。\n\n## 使用示例\n\n### 如何指定 TLSv1.1 协议\n\n存在一些老旧的客户端，仍然采用较低级别的 TLSv1.1 协议版本，而新的产品则使用较高安全级别的 TLS 协议版本。如果让新产品支持 TLSv1.1 可能会带来一些安全隐患。为了保证 API 的安全性，我们需要在协议版本之间进行灵活转换。\n例如：test.com 是老旧客户端所使用的域名，需要将其配置为 TLSv1.1 而 test2.com 属于新产品，同时支持了 TLSv1.2，TLSv1.3 协议。\n\n1. config.yaml 配置。\n\n```yaml\napisix:\n  ssl:\n    ssl_protocols: TLSv1.3\n    # ssl_ciphers is for reference only\n    ssl_ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA\n```\n\n2. 为 test.com 域名指定 TLSv1.1 协议版本。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server.crt)\"'\",\n     \"key\": \"'\"$(cat server.key)\"'\",\n     \"snis\": [\"test.com\"],\n     \"ssl_protocols\": [\n         \"TLSv1.1\"\n     ]\n}'\n```\n\n3. 为 test.com 创建 SSL 对象，未指定 TLS 协议版本，将默认使用静态配置。\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server2.crt)\"'\",\n     \"key\": \"'\"$(cat server2.key)\"'\",\n     \"snis\": [\"test2.com\"]\n}'\n```\n\n4. 访问验证\n\n使用 TLSv1.3 访问 test.com 失败：\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n```\n\n使用 TLSv1.1 访问 test.com 成功：\n\n```shell\n$ curl --tls-max 1.1 --tlsv1.1  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.1 (OUT), TLS handshake, Client hello (1):\n* TLSv1.1 (IN), TLS handshake, Server hello (2):\n* TLSv1.1 (IN), TLS handshake, Certificate (11):\n* TLSv1.1 (IN), TLS handshake, Server key exchange (12):\n* TLSv1.1 (IN), TLS handshake, Server finished (14):\n* TLSv1.1 (OUT), TLS handshake, Client key exchange (16):\n* TLSv1.1 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.1 (OUT), TLS handshake, Finished (20):\n* TLSv1.1 (IN), TLS handshake, Finished (20):\n* SSL connection using TLSv1.1 / ECDHE-RSA-AES256-SHA\n```\n\n使用 TLSv1.3 访问 test2.com 成功：\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n```\n\n使用 TLSv1.1 访问 test2.com 失败：\n\n```shell\ncurl --tls-max 1.1 --tlsv1.1  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.1 (OUT), TLS handshake, Client hello (1):\n* TLSv1.1 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n```\n\n### 证书关联多个域名，但域名之间使用不同的 TLS 协议\n\n有时候，我们可能会遇到这样一种情况，即一个证书关联了多个域名，但是它们需要使用不同的 TLS 协议来保证安全性。例如 test.com 域名需要使用 TlSv1.2 协议，而 test2.com 域名则需要使用 TLSv1.3 协议。在这种情况下，我们不能简单地为所有的域名创建一个 SSL 对象，而是需要为每个域名单独创建一个 SSL 对象，并指定相应的协议版本。这样，我们就可以根据不同的域名和协议版本来进行正确的 SSL 握手和加密通信。示例如下：\n\n1. 使用证书为 test.com 创建 ssl 对象，并指定 TLSv1.2 协议。\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server.crt)\"'\",\n     \"key\": \"'\"$(cat server.key)\"'\",\n     \"snis\": [\"test.com\"],\n     \"ssl_protocols\": [\n         \"TLSv1.2\"\n     ]\n}'\n```\n\n2. 使用与 test.com 同一证书，为 test2.com 创建 ssl 对象，并指定 TLSv1.3 协议。\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/ssls/2 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat server.crt)\"'\",\n     \"key\": \"'\"$(cat server.key)\"'\",\n     \"snis\": [\"test2.com\"],\n     \"ssl_protocols\": [\n         \"TLSv1.3\"\n     ]\n}'\n```\n\n3. 访问验证\n\n使用 TLSv1.2 访问 test.com 成功：\n\n```shell\n$ curl --tls-max 1.2 --tlsv1.2  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.2 (OUT), TLS handshake, Client hello (1):\n* TLSv1.2 (IN), TLS handshake, Server hello (2):\n* TLSv1.2 (IN), TLS handshake, Certificate (11):\n* TLSv1.2 (IN), TLS handshake, Server key exchange (12):\n* TLSv1.2 (IN), TLS handshake, Server finished (14):\n* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):\n* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.2 (OUT), TLS handshake, Finished (20):\n* TLSv1.2 (IN), TLS handshake, Finished (20):\n* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test.com\n*  start date: Jul 20 15:50:08 2023 GMT\n*  expire date: Jul 17 15:50:08 2033 GMT\n*  issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test.com\n*  SSL certificate verify result: EE certificate key too weak (66), continuing anyway.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* Using Stream ID: 1 (easy handle 0x5608905ee2e0)\n> HEAD / HTTP/2\n> Host: test.com:9443\n> user-agent: curl/7.74.0\n> accept: */*\n\n```\n\n使用 TLSv1.3 协议访问 test.com 失败：\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n\n```\n\n使用 TLSv1.3 协议访问 test2.com 成功：\n\n```shell\n$ curl --tls-max 1.3 --tlsv1.3  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test2.com\n*  start date: Jul 20 16:05:47 2023 GMT\n*  expire date: Jul 17 16:05:47 2033 GMT\n*  issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test2.com\n*  SSL certificate verify result: EE certificate key too weak (66), continuing anyway.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* Using Stream ID: 1 (easy handle 0x55569cbe42e0)\n> HEAD / HTTP/2\n> Host: test2.com:9443\n> user-agent: curl/7.74.0\n> accept: */*\n>\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* old SSL session ID is stale, removing\n```\n\n使用 TLSv1.2 协议访问 test2.com 失败：\n\n```shell\n$ curl --tls-max 1.2 --tlsv1.2  https://test2.com:9443 -v -k -I\n*   Trying 127.0.0.1:9443...\n* Connected to test2.com (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*  CAfile: /etc/ssl/certs/ca-certificates.crt\n*  CApath: /etc/ssl/certs\n* TLSv1.2 (OUT), TLS handshake, Client hello (1):\n* TLSv1.2 (IN), TLS alert, protocol version (582):\n* error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n* Closing connection 0\ncurl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version\n```\n"
  },
  {
    "path": "docs/zh/latest/status-api.md",
    "content": "---\ntitle: Status API\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n在 Apache APISIX 中，Status API 用于：\n\n* 检查 APISIX 是否已成功启动并正确运行\n* 检查所有 workers 是否已收到配置并加载\n\n要更改 Status API 服务器的默认端点（`127.0.0.1:7085`），请更改配置文件（`conf/config.yaml`）中 `status` 部分中的 `ip` 和 `port`：\n\n```yaml\napisix:\n  status:\n    ip: \"127.0.0.1\"\n    port: 7085\n```\n\n此 API 可用于在 APISIX 开始接收用户请求之前对 APISIX 执行就绪探测。\n\n### GET /status\n\n返回报告 APISIX 工作人员状态的 JSON。如果 APISIX 未运行，建立 TCP 连接时请求将报错。否则，如果请求到达正在运行的 worker，此端点将始终返回 ok。\n\n```json\n{\n  \"status\": \"ok\"\n}\n```\n\n### GET /status/ready\n\n当所有 worker 都已加载配置时，返回 `ok`；否则，返回特定错误，错误代码为 `503`。以下是具体示例。\n\n当所有 worker 都已加载配置时：\n\n```json\n{\n  \"status\": \"ok\"\n}\n```\n\n当 1 个 workers 尚未初始化时：\n\n```json\n{\n  \"status\": \"error\",\n  \"error\": \"worker count: 16 but status report count: 15\"\n}\n```\n\n当特定 worker 尚未加载配置时：\n\n```json\n{\n  \"error\": \"worker id: 9 has not received configuration\",\n  \"status\": \"error\"\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/stream-proxy.md",
    "content": "---\ntitle: TCP/UDP 动态代理\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n众多的闻名的应用和服务，像 LDAP、MYSQL 和 RTMP，选择 TCP 作为通信协议。但是像 DNS、syslog 和 RADIUS 这类非事务性的应用，他们选择了 UDP 协议。\n\nAPISIX 可以对 TCP/UDP 协议进行代理并实现动态负载均衡。在 nginx 世界，称 TCP/UDP 代理为 stream 代理，在 APISIX 这里我们也遵循了这个声明。\n\n## 如何开启 Stream 代理\n\n要启用该选项，请将 `apisix.proxy_mode` 设置为 `stream` 或 `http&stream`，具体取决于您是只需要 stream 代理还是需要 http 和 stream。然后在 `conf/config.yaml` 中添加 `apisix.stream_proxy` 选项并指定 APISIX 应充当 stream 代理并侦听传入请求的地址列表。\n\n```yaml\napisix:\n  proxy_mode: http&stream  # enable both http and stream proxies\n  stream_proxy: # TCP/UDP proxy\n    tcp: # TCP proxy address list\n      - 9100\n      - \"127.0.0.1:9101\"\n    udp: # UDP proxy address list\n      - 9200\n      - \"127.0.0.1:9211\"\n```\n\n## 如何设置 route\n\n简例如下：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"remote_addr\": \"127.0.0.1\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n例子中 APISIX 对客户端 IP 为 `127.0.0.1` 的请求代理转发到上游主机 `127.0.0.1:1995`。\n更多用例，请参照 [test case](https://github.com/apache/apisix/blob/master/t/stream-node/sanity.t)。\n\n## 更多 route 匹配选项\n\n我们可以添加更多的选项来匹配 route。目前 Stream Route 配置支持 3 个字段进行过滤：\n\n- server_addr: 接受 Stream Route 连接的 APISIX 服务器的地址。\n- server_port: 接受 Stream Route 连接的 APISIX 服务器的端口。\n- remote_addr: 发出请求的客户端地址。\n\n例如\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"server_addr\": \"127.0.0.1\",\n    \"server_port\": 2000,\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n例子中 APISIX 会把服务器地址为 `127.0.0.1`, 端口为 `2000` 代理到上游地址 `127.0.0.1:1995`。\n\n让我们再举一个实际场景的例子：\n\n1. 将此配置放在 `config.yaml` 中\n\n   ```yaml\n   apisix:\n     proxy_mode: http&stream  # enable both http and stream proxies\n     stream_proxy: # TCP/UDP proxy\n       tcp: # TCP proxy address list\n         - 9100 # by default uses 0.0.0.0\n         - \"127.0.0.10:9101\"\n   ```\n\n2. 现在运行一个 mysql docker 容器并将端口 3306 暴露给主机\n\n   ```shell\n   $ docker run --name mysql -e MYSQL_ROOT_PASSWORD=toor -p 3306:3306 -d mysql mysqld --default-authentication-plugin=mysql_native_password\n   # check it using a mysql client that it works\n   $ mysql --host=127.0.0.1 --port=3306 -u root -p\n   Enter password:\n   Welcome to the MySQL monitor.  Commands end with ; or \\g.\n   Your MySQL connection id is 25\n   ...\n   mysql>\n   ```\n\n3. 现在我们将创建一个带有服务器过滤的 stream 路由：\n\n   ```shell\n   curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n   {\n       \"server_addr\": \"127.0.0.10\",\n       \"server_port\": 9101,\n       \"upstream\": {\n           \"nodes\": {\n               \"127.0.0.1:3306\": 1\n           },\n           \"type\": \"roundrobin\"\n       }\n   }'\n   ```\n\n   每当 APISIX 服务器 `127.0.0.10` 和端口 `9101` 收到连接时，它只会将请求转发到 mysql 上游。让我们测试一下：\n\n4. 向 `9100` 发出请求（在 config.yaml 中启用 stream 代理端口），过滤器匹配失败。\n\n   ```shell\n   $ mysql --host=127.0.0.1 --port=9100 -u root -p\n   Enter password:\n   ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 2\n   ```\n\n  下面的请求匹配到了 stream 路由，所以它可以正常代理到 mysql。\n\n   ```shell\n   mysql --host=127.0.0.10 --port=9101 -u root -p\n   Enter password:\n   Welcome to the MySQL monitor.  Commands end with ; or \\g.\n   Your MySQL connection id is 26\n   ...\n   mysql>\n   ```\n\n完整的匹配选项列表参见 [Admin API 的 Stream Route](./admin-api.md#stream-route)。\n\n## 接收基于 TCP 的 TLS 连接\n\nAPISIX 支持接收基于 TCP 的 TLS 连接。\n\n首先，我们需要给对应的 TCP 地址启用 TLS：\n\n```yaml\napisix:\n  proxy_mode: http&stream  # enable both http and stream proxies\n  stream_proxy: # TCP/UDP proxy\n    tcp: # TCP proxy address list\n      - addr: 9100\n        tls: true\n```\n\n接着，我们需要为给定的 SNI 配置证书。\n具体步骤参考 [Admin API 的 SSL](./admin-api.md#ssl)。\nmTLS 也是支持的，参考 [保护路由](./mtls.md#保护路由)。\n\n然后，我们需要配置一个 route，匹配连接并代理到上游：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n当连接为基于 TCP 的 TLS 时，我们可以通过 SNI 来匹配路由，比如：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"sni\": \"a.test.com\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:5991\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n在这里，握手时发送 SNI `a.test.com` 的连接会被代理到 `127.0.0.1:5991`。\n\n## 代理到基于 TCP 的 TLS 上游\n\nAPISIX 还支持代理到基于 TCP 的 TLS 上游。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"scheme\": \"tls\",\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n```\n\n通过设置 `scheme` 为 `tls`，APISIX 将与上游进行 TLS 握手。\n\n当客户端也使用基于 TCP 的 TLS 上游时，客户端发送的 SNI 将传递给上游。否则，将使用一个假的 SNI `apisix_backend`。\n"
  },
  {
    "path": "docs/zh/latest/support-fips-in-apisix.md",
    "content": "---\nid: support-fips-in-apisix\ntitle: 通过 OpenSSL 3.0 使 APISIX 支持 FIPS 模式\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 贡献代码\n  - 构建 APISIX\n  - OpenSSL 3.0 FIPS\ndescription: 本文将介绍如何在 Apache APISIX 中使用 OpenSSL 3.0 来编译 apisix-runtime，即可启用 FIPS 模式。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n目前，OpenSSL 3.0 [支持了](https://www.openssl.org/blog/blog/2022/08/24/FIPS-validation-certificate-issued/) [FIPS](https://en.wikipedia.org/wiki/FIPS_140-2) 模式。为了在 APISIX 中支持 FIPS 模式，你应该使用 OpenSSL 3.0 来编译 apisix-runtime。\n\n## 编译\n\n如果你需要使用 OpenSSL 3.0 来编译 apisix-runtime，请以 root 用户角色来执行以下命令：\n\n```bash\ncd $(mktemp -d)\nOPENSSL3_PREFIX=${OPENSSL3_PREFIX-/usr/local}\napt install -y build-essential\ngit clone https://github.com/openssl/openssl\ncd openssl\n./Configure --prefix=$OPENSSL3_PREFIX/openssl-3.0 enable-fips\nmake install\necho $OPENSSL3_PREFIX/openssl-3.0/lib64 > /etc/ld.so.conf.d/openssl3.conf\nldconfig\n$OPENSSL3_PREFIX/openssl-3.0/bin/openssl fipsinstall -out $OPENSSL3_PREFIX/openssl-3.0/ssl/fipsmodule.cnf -module $OPENSSL3_PREFIX/openssl-3.0/lib64/ossl-modules/fips.so\nsed -i 's@# .include fipsmodule.cnf@.include '\"$OPENSSL3_PREFIX\"'/openssl-3.0/ssl/fipsmodule.cnf@g; s/# \\(fips = fips_sect\\)/\\1\\nbase = base_sect\\n\\n[base_sect]\\nactivate=1\\n/g' $OPENSSL3_PREFIX/openssl-3.0/ssl/openssl.cnf\ncd ..\n\nexport cc_opt=\"-I$OPENSSL3_PREFIX/openssl-3.0/include\"\nexport ld_opt=\"-L$OPENSSL3_PREFIX/openssl-3.0/lib64 -Wl,-rpath,$OPENSSL3_PREFIX/openssl-3.0/lib64\"\n\nwget --no-check-certificate https://raw.githubusercontent.com/api7/apisix-build-tools/master/build-apisix-runtime.sh\nchmod +x build-apisix-runtime.sh\n./build-apisix-runtime.sh\n```\n\napisix-runtime 将安装在 `/usr/local/openresty`。\n"
  },
  {
    "path": "docs/zh/latest/terminology/api-gateway.md",
    "content": "---\ntitle: API Gateway\nkeywords:\n  - Apache APISIX\n  - API 网关\n  - 网关\ndescription: 本文主要介绍了 API 网关的作用以及为什么需要 API 网关。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nAPI 网关是位于客户端与后端服务集之间的 API 管理工具。API 网关相当于反向代理，用于接受所有 API 的调用、整合处理这些调用所需的各种服务，并返回相应的结果。API 网关通常会处理**跨 API 服务系统使用**的常见任务，并统一接入进行管理。通过 API 网关的统一拦截，可以实现对 API 接口的安全、日志等共性需求，如用户身份验证、速率限制和统计信息。\n\n## 为什么需要 API 网关？\n\n与传统的 API 微服务相比，API 网关有很多好处。比如：\n\n- 它是所有 API 请求的唯一入口。\n- 可用于将请求转发到不同的后端，或根据请求头将请求转发到不同的服务。\n- 可用于执行身份验证、授权和限速。\n- 它可用于支持分析，例如监控、日志记录和跟踪。\n- 可以保护 API 免受 SQL 注入、DDOS 攻击和 XSS 等恶意攻击媒介的攻击。\n- 它可以降低 API 和微服务的复杂性。\n"
  },
  {
    "path": "docs/zh/latest/terminology/consumer-group.md",
    "content": "---\ntitle: Consumer Groups\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Consumer Groups\ndescription: 本文介绍了 Apache APISIX Consumer Group 对象的概念及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n通过 Consumer Groups，你可以在同一个消费者组中启用任意数量的[插件](./plugin.md)，并在一个或者多个[消费者](./consumer.md)中引用该消费者组。\n\n## 配置示例\n\n以下示例展示了如何创建消费者组并将其绑定到消费者中。\n\n创建一个共享相同限流配额的消费者组：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumer_groups/company_a \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 200,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"group\": \"grp_company_a\"\n        }\n    }\n}'\n```\n\n在消费者组中创建消费者：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    },\n    \"group_id\": \"company_a\"\n}'\n```\n\n当 APISIX 无法找到 `group_id` 中定义的消费者组时，创建或者更新消费者的请求将会终止，并返回错误码 `404`。\n\n如果消费者已经配置了 `plugins` 字段，那么消费者组中配置的插件将与之合并。\n\n:::tip\n\n此处需要注意两点：\n\n1. 当在同一个插件分别配置在[消费者](./consumer.md)、[路由](./route.md)、[插件配置](./plugin-config.md)和[服务](./service.md)中时，只有一份配置是生效的，并且消费者的优先级最高。更多信息，请参考 [Plugin](./plugin.md)。\n2. 如果消费者和消费者组配置了相同的插件，则消费者中的插件配置优先级更高。对于第一点，因为消费者组需要配置在消费者中，因此你只需关心消费者中插件的优先级。\n\n:::\n\n如下示例，假如你配置了一个消费者组：\n\n```json title=\"Consumer Group\"\n{\n    \"id\": \"bar\",\n    \"plugins\": {\n        \"response-rewrite\": {\n            \"body\": \"hello\"\n        }\n    }\n}\n```\n\n并配置了消费者：\n\n```json title=\"Consumer\"\n{\n    \"username\": \"foo\",\n    \"group_id\": \"bar\",\n    \"plugins\": {\n        \"basic-auth\": {\n            \"username\": \"foo\",\n            \"password\": \"bar\"\n        },\n        \"response-rewrite\": {\n            \"body\": \"world\"\n        }\n    }\n}\n```\n\n那么 `response-rewrite` 中的 `body` 将保留 `world`。\n"
  },
  {
    "path": "docs/zh/latest/terminology/consumer.md",
    "content": "---\ntitle: Consumer\nkeywords:\n  - APISIX\n  - API 网关\n  - 消费者\n  - Consumer\ndescription: 本文介绍了 Apache APISIX Consumer 对象的作用以及如何使用 Consumer。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nConsumer 是某类服务的消费者，需要与用户认证配合才可以使用。当不同的消费者请求同一个 API 时，APISIX 会根据当前请求的用户信息，对应不同的 Plugin 或 Upstream 配置。如果 [Route](./route.md)、[Service](./service.md)、[Consumer](./consumer.md) 和 [Plugin Config](./plugin-config.md) 都绑定了相同的插件，只有消费者的插件配置会生效。插件配置的优先级由高到低的顺序是：Consumer > Route > Plugin Config > Service。\n\n对于 API 网关而言，一般情况可以通过请求域名、客户端 IP 地址等字段识别到某类请求方，然后进行插件过滤并转发请求到指定上游。但有时候该方式达不到用户需求，因此 APISIX 支持了 Consumer 对象。\n\n![Consumer](../../../assets/images/consumer-who.png)\n\n如上图所示，作为 API 网关，需要知道 API Consumer（消费方）具体是谁，这样就可以对不同 API Consumer 配置不同规则。\n\n## 配置选项\n\n定义 Consumer 的字段如下：\n\n| 名称     | 必选项 | 描述                                                                         |\n| -------- | ---- | ------------------------------------------------------------------------------|\n| username | 是   | Consumer 名称。                                                                |\n| plugins  | 否   | Consumer 对应的插件配置。详细信息，请参考 [Plugins](./plugin.md)。 |\n\n## 识别消费者\n\n在 APISIX 中，识别 Consumer 的过程如下图：\n\n![Consumer Internal](../../../assets/images/consumer-internal.png)\n\n1. 授权认证：比如有 [key-auth](../plugins/key-auth.md)、[JWT](../plugins/jwt-auth.md) 等；\n2. 获取 consumer_name：通过授权认证，即可自然获取到对应的 Consumer name，它是 Consumer 对象的唯一识别标识；\n3. 获取 Consumer 上绑定的 Plugin 或 Upstream 信息：完成对不同 Consumer 做不同配置的效果。\n\n当有不同的使用者请求相同的 API，并且需要根据使用者执行不同的插件和上游配置时，使用 Consumer 是非常合适的。需要与用户身份验证系统结合使用。\n\n目前，可以与 Consumer 配置的身份验证插件包括 `basic-auth` 、`hmac-auth`、`jwt-auth`、`key-auth`、`ldap-auth` 和 `wolf-rbac`。\n\n你可以参考 [key-auth](../plugins/key-auth.md) 认证授权插件的调用逻辑，进一步理解 Consumer 概念和使用。\n\n:::note 注意\n\n如需了解更多关于 Consumer 对象的信息，你可以参考 [Admin API Consumer](../admin-api.md#consumer) 资源介绍。\n\n:::\n\n## 使用示例\n\n以下示例介绍了如何对某个 Consumer 开启指定插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. 创建 Consumer，指定认证插件 `key-auth`，并开启特定插件 `limit-count`。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\",\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-one\"\n            },\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n2. 创建路由，设置路由规则和启用插件配置。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {}\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        },\n        \"uri\": \"/hello\"\n    }'\n    ```\n\n3. 测试插件。\n\n    连续发送三次测试请求，前两次返回正常，没达到限速阈值。\n\n    ```shell\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I\n    ```\n\n    第三次测试返回 `503`，请求被限制：\n\n    ```shell\n    HTTP/1.1 503 Service Temporarily Unavailable\n    ...\n    ```\n\n通过 [consumer-restriction](../plugins/consumer-restriction.md) 插件，限制用户 `jack` 对该 Route 的访问。\n\n1. 设置黑名单，禁止 jack 访问该 API。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {},\n            \"consumer-restriction\": {\n                \"blacklist\": [\n                    \"jack\"\n                ]\n            }\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        },\n        \"uri\": \"/hello\"\n    }'\n    ```\n\n2. 通过以下命令访问该路由，均返回 `403`，`jack` 被禁止访问。\n\n    ```shell\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I\n    ```\n\n    返回结果：\n\n    ```\n    HTTP/1.1 403\n    ...\n    ```\n"
  },
  {
    "path": "docs/zh/latest/terminology/credential.md",
    "content": "---\ntitle: Credential\nkeywords:\n  - APISIX\n  - API 网关\n  - 凭证\n  - Credential\ndescription: 本文介绍了 Apache APISIX Credential 对象的作用以及如何使用 Credential。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nCredential 是存放 [Consumer](./consumer.md) 凭证配置的对象。\n一个 Consumer 可以使用不同类型的多个凭证。\n当你需要为一个 Consumer 配置不同类型的多个凭证时，就会用到 Credential。\n\n目前，Credential 可以配置的身份认证插件包括 `basic-auth`、`hmac-auth`、`jwt-auth` 以及 `key-auth`。\n\n## 配置选项\n\n 定义 Credential 的字段如下：\n\n| 名称      | 必选项 | 描述                                                  |\n|---------|-----|-----------------------------------------------------|\n| desc    | 否   | Credential 描述。                                      |\n| labels  | 否   | Credential 标签。                                      |\n| plugins | 否   | Credential 对应的插件配置。详细信息，请参考 [Plugins](./plugin.md)。 |\n\n:::note\n\n如需了解更多关于 Credential 对象的信息，你可以参考 [Admin API Credential](../admin-api.md#credential) 资源介绍。\n\n:::\n\n## 使用示例\n\n[Consumer 使用示例](./consumer.md#使用示例) 介绍了如何对 Consumer 配置认证插件，并介绍了如何配合其他插件使用。\n在该示例中，该 Consumer 只有一个 key-auth 类型的凭证。\n现在假设用户需要为该 Consumer 配置多个凭证，你可以使用 Credential 来支持这一点。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. 创建 Consumer。不指定认证插件，而是稍后使用 Credential 来配置认证插件。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\"\n    }'\n    ```\n\n2. 为 Consumer 配置 2 个 启用 `key-auth` 的 Credential。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/key-auth-one \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-one\"\n            }\n        }\n    }'\n    ```\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials/key-auth-two \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {\n                \"key\": \"auth-two\"\n            }\n        }\n    }'\n    ```\n\n3. 创建路由，设置路由规则和启用插件配置。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {}\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            },\n            \"type\": \"roundrobin\"\n        },\n        \"uri\": \"/hello\"\n    }'\n    ```\n\n4. 测试插件\n\n    分别使用 `auth-one` 和 `auth-two` 两个 key 来测试请求，都响应正常。\n\n    ```shell\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-one' -I\n    curl http://127.0.0.1:9080/hello -H 'apikey: auth-two' -I\n    ```\n\n    为该 Consumer 启用 `limit-count` 插件。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\",\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n    分别使用这两个 key 连续 3 次以上请求该路由，测试返回 `503`，请求被限制。\n"
  },
  {
    "path": "docs/zh/latest/terminology/global-rule.md",
    "content": "---\ntitle: Global rules\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Global Rules\n  - 全局规则\ndescription: 本文介绍了全局规则的概念以及如何启用全局规则。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n[Plugin](plugin.md) 配置可直接绑定在 [Route](route.md) 上，也可以被绑定在 [Service](service.md) 或 [Consumer](consumer.md) 上。\n\n如果你需要一个能作用于所有请求的 Plugin，可以通过 Global Rules 启用一个全局的插件配置。\n\n全局规则相对于 Route、Service、Plugin Config、Consumer 中的插件配置，Global Rules 中的插件总是优先执行。\n\n## 使用示例\n\n以下示例展示了如何为所有请求启用 `limit-count` 插件：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/global_rules/1 -X PUT \\\n  -H 'Content-Type: application/json' \\\n  -H \"X-API-KEY: $admin_key\" \\\n  -d '{\n        \"plugins\": {\n            \"limit-count\": {\n                \"time_window\": 60,\n                \"policy\": \"local\",\n                \"count\": 2,\n                \"key\": \"remote_addr\",\n                \"rejected_code\": 503\n            }\n        }\n    }'\n```\n\n你也可以通过以下命令查看所有的全局规则：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/global_rules -H \"X-API-KEY: $admin_key\"\n```\n"
  },
  {
    "path": "docs/zh/latest/terminology/plugin-config.md",
    "content": "---\ntitle: Plugin Config\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 插件配置\n  - Plugin Config\ndescription: Plugin Config 对象，可以用于创建一组通用的插件配置，并在路由中使用这组配置。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n在很多情况下，我们在不同的路由中会使用相同的插件规则，此时就可以通过 Plugin Config 来设置这些规则。Plugin Config 属于一组通用插件配置的抽象。\n\n`plugins` 的配置可以通过 [Admin API](../admin-api.md#plugin-config) `/apisix/admin/plugin_configs` 进行单独配置，在路由中使用 `plugin_config_id` 与之进行关联。\n\n对于同一个插件的配置，只能有一个是有效的，优先级为 Consumer > Route > Plugin Config > Service。\n\n## 使用示例\n\n你可以参考如下步骤将 Plugin Config 绑定在路由上。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. 创建 Plugin config。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"desc\": \"enable limit-count plugin\",\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503\n            }\n        }\n    }'\n    ```\n\n2. 创建路由并绑定 `Plugin Config 1`。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uris\": [\"/index.html\"],\n        \"plugin_config_id\": 1,\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n如果找不到对应的 Plugin Config，该路由上的请求会报 `503` 错误。\n\n## 注意事项\n\n如果路由中已经配置了 `plugins`，那么 Plugin Config 里面的插件配置将会与 `plugins` 合并。\n\n相同的插件不会覆盖掉 `plugins` 原有的插件配置。详细信息，请参考 [Plugin](./plugin.md)。\n\n1. 假设你创建了一个 Plugin Config。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"desc\": \"enable ip-restruction and limit-count plugin\",\n        \"plugins\": {\n            \"ip-restriction\": {\n                \"whitelist\": [\n                    \"127.0.0.0/24\",\n                    \"113.74.26.106\"\n                ]\n            },\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503\n            }\n        }\n    }'\n    ```\n\n2. 并在路由中引入 Plugin Config。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uris\": [\"/index.html\"],\n        \"plugin_config_id\": 1,\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n        \"plugins\": {\n            \"proxy-rewrite\": {\n                \"uri\": \"/test/add\",\n                \"host\": \"apisix.iresty.com\"\n            },\n            \"limit-count\": {\n                \"count\": 20,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n\n3. 最后实现的效果如下。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n    {\n        \"uris\": [\"/index.html\"],\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n        \"plugins\": {\n            \"ip-restriction\": {\n                \"whitelist\": [\n                    \"127.0.0.0/24\",\n                    \"113.74.26.106\"\n                ]\n            },\n            \"proxy-rewrite\": {\n                \"uri\": \"/test/add\",\n                \"host\": \"apisix.iresty.com\"\n            },\n            \"limit-count\": {\n                \"count\": 20,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        }\n    }'\n    ```\n"
  },
  {
    "path": "docs/zh/latest/terminology/plugin-metadata.md",
    "content": "---\ntitle: Plugin Metadata\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 插件元数据配置\n  - Plugin Metadata\ndescription: APISIX 的插件元数据\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 摘要\n\n在本文档中，您将了解到 APISIX 中，插件元数据的基本概念和您可能使用到的场景。\n\n浏览文档末尾的相关资源，获取与此相关的更多信息。\n\n## 描述\n\n在 APISIX 中，配置通用的元数据属性，可以作用于包含该元数据插件的所有路由及服务中。例如为`rocketmq-logger`指定了 `log_format`，则所有绑定 rocketmq-logger 的路由或服务都将使用该日志格式。\n\n下图说明了插件元数据的概念，使用两个不同路由上的 [syslog](https://apisix.apache.org/zh/docs/apisix/plugins/syslog/)  插件的实例，以及为 [syslog](https://apisix.apache.org/zh/docs/apisix/plugins/syslog/)  插件设置全局`log_format`的插件元数据对象：\n\n![plugin_metadata](https://static.apiseven.com/uploads/2023/04/17/Z0OFRQhV_plugin%20metadata.svg)\n\n如果没有另外指定，插件元数据对象上的`log_format`应将相同的日志格式统一应用于两个`syslog`插件。但是，由于`/orders`路由上的`syslog`插件具有不同的`log_format`，因此访问该路由的请求将按照路由中插件指定的`log_format`生成日志。\n\n在插件级别设置的元数据属性更加精细，并且比`全局`元数据对象具有更高的优先级。\n\n插件元数据对象只能用于具有元数据属性的插件。有关哪些插件具有元数据属性的更多详细信息，请查看插件配置属性及相关信息。\n\n## 配置示例\n\n以下示例展示了如何通过 Admin API 配置插件元数据：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugin_metadata/http-logger \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"log_format\": {\n        \"host\": \"$host\",\n        \"@timestamp\": \"$time_iso8601\",\n        \"client_ip\": \"$remote_addr\"\n    }\n}'\n```\n\n配置完成后，你将在日志系统中看到如下类似日志：\n\n```json\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"route_id\":\"1\"}\n{\"host\":\"localhost\",\"@timestamp\":\"2020-09-23T19:05:05-04:00\",\"client_ip\":\"127.0.0.1\",\"route_id\":\"1\"}\n```\n\n## 相关资源\n\n核心概念 - [插件](https://apisix.apache.org/docs/apisix/terminology/plugin/)\n"
  },
  {
    "path": "docs/zh/latest/terminology/plugin.md",
    "content": "---\ntitle: Plugin\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 插件\n  - 插件优先级\ndescription: 本文介绍了 APISIX Plugin 对象的相关信息及其使用方法，并且介绍了如何自定义插件优先级、自定义错误响应、动态控制插件执行状态等。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nAPISIX 插件可以扩展 APISIX 的功能，以满足组织或用户特定的流量管理、可观测性、安全、请求/响应转换、无服务器计算等需求。\n\nAPISIX 提供了许多现有的插件，可以定制和编排以满足你的需求。这些插件可以全局启用，以在每个传入请求上触发，也可以局部绑定到其他对象，例如在 [Route](./route.md)、[Service](./service.md)、[Consumer](./consumer.md) 或 [Plugin Config](./plugin-config.md) 上。你可以参考 [Admin API](../admin-api.md#plugin) 了解如何使用该资源。\n\n如果现有的 APISIX 插件不满足需求，你还可以使用 Lua 或其他语言（如 Java、Python、Go 和 Wasm）编写自定义插件。\n\n## 插件安装\n\n默认情况下，大多数 APISIX 插件都已[安装](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua)：\n\n```lua title=\"apisix/cli/config.lua\"\nlocal _M = {\n  ...\n  plugins = {\n    \"real-ip\",\n    \"ai\",\n    \"client-control\",\n    \"proxy-control\",\n    \"request-id\",\n    \"zipkin\",\n    \"ext-plugin-pre-req\",\n    \"fault-injection\",\n    \"mocking\",\n    \"serverless-pre-function\",\n    ...\n  },\n  ...\n}\n```\n\n如果您想调整插件安装，请将自定义的 `plugins` 配置添加到 `config.yaml` 中。例如：\n\n```yaml\nplugins:\n  - real-ip                   # 安装\n  - ai\n  - client-control\n  - proxy-control\n  - request-id\n  - zipkin\n  - ext-plugin-pre-req\n  - fault-injection\n  # - mocking                 # 不安装\n  - serverless-pre-function\n  ...                         # 其它插件\n```\n\n完整配置参考请参见 [`config.yaml.example`](https://github.com/apache/apisix/blob/master/conf/config.yaml.example)。\n\n重新加载 APISIX 以使配置更改生效。\n\n## 插件执行生命周期\n\n安装的插件首先会被初始化。然后会检查插件的配置，以确保插件配置遵循定义的[JSON Schema](https://json-schema.org)。\n\n当一个请求通过 APISIX 时，插件的相应方法会在以下一个或多个阶段中执行： `rewrite`, `access`, `before_proxy`, `header_filter`, `body_filter`, and `log`。这些阶段在很大程度上受到[OpenResty 指令](https://openresty-reference.readthedocs.io/en/latest/Directives/)的影响。\n\n<br />\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.apiseven.com/uploads/2023/03/09/ZsH5C8Og_plugins-phases.png\" alt=\"Routes Diagram\" width=\"50%\"/>\n</div>\n<br />\n\n## 插件执行顺序\n\n通常情况下，插件按照以下顺序执行：\n\n1. [全局规则](./global-rule.md) 插件\n   1. rewrite 阶段的插件\n   2. access 阶段的插件\n\n2. 绑定到其他对象的插件\n   1. rewrite 阶段的插件\n   2. access 阶段的插件\n\n在每个阶段内，你可以在插件的 `_meta.priority` 字段中可选地定义一个新的优先级数，该优先级数优先于默认插件优先级在执行期间。具有更高优先级数的插件首先执行。\n\n例如，如果你想在请求到达路由时，让 `limit-count`（优先级 1002）先于 `ip-restriction`（优先级 3000）运行，可以通过将更高的优先级数传递给 `limit-count` 的 `_meta.priority` 字段来实现：\n\n```json\n{\n  ...,\n  \"plugins\": {\n    \"limit-count\": {\n      ...,\n      \"_meta\": {\n        \"priority\": 3010\n      }\n    }\n  }\n}\n```\n\n若要将此插件实例的优先级重置为默认值，只需从插件配置中删除`_meta.priority`字段即可。\n\n## 插件合并优先顺序\n\n当同一个插件在全局规则中和局部规则（例如路由）中同时配置时，两个插件将顺序执行。\n\n然而，如果相同的插件在多个对象上本地配置，例如在[`Route`](route.md), [`Service`](service.md), [`Consumer`](consumer.md) 或[`Plugin Config`](plugin-config.md) 上，每个非全局插件只会执行一次，因为在执行期间，针对特定的优先顺序，这些对象中配置的插件会被合并：\n\n`Consumer`  > `Consumer Group` > `Route` > `Plugin Config` > `Service`\n\n因此，如果相同的插件在不同的对象中具有不同的配置，则合并期间具有最高优先顺序的插件配置将被使用。\n\n## 通用配置\n\n通过 `_meta` 配置项可以将一些通用的配置应用于插件，你可以参考下文使用这些通用配置。通用配置如下：\n\n| 名称           | 类型           |     描述       |\n|--------------- |-------------- |----------------|\n| disable        | boolean       | 当设置为 `true` 时，则禁用该插件。可选值为 `true` 和 `false`。 |\n| error_response | string/object | 自定义错误响应。 |\n| priority       | integer       | 自定义插件优先级。 |\n| filter         | array         | 根据请求的参数，在运行时控制插件是否执行。此配置由一个或多个 {var, operator, val} 元素组成列表，类似：`{{var, operator, val}, {var, operator, val}, ...}}`。例如 `{\"arg_version\", \"==\", \"v2\"}`，表示当前请求参数 `version` 是 `v2`。这里的 `var` 与 NGINX 内部自身变量命名是保持一致。操作符的使用方法，请参考 [lua-resty-expr](https://github.com/api7/lua-resty-expr#operator-list)。|\n\n### 禁用指定插件\n\n通过 `disable` 参数，你可以将某个插件调整为“禁用状态”，即请求不会经过该插件。\n\n```json\n{\n    \"proxy-rewrite\": {\n        \"_meta\": {\n            \"disable\": true\n        }\n    }\n}\n```\n\n### 自定义错误响应\n\n通过 `error_response` 配置，可以将任意插件的错误响应配置成一个固定的值，避免因为插件内置的错误响应信息而带来不必要的麻烦。\n\n如下配置表示将 `jwt-auth` 插件的错误响应自定义为 `Missing credential in request`。\n\n```json\n{\n    \"jwt-auth\": {\n        \"_meta\": {\n            \"error_response\": {\n                \"message\": \"Missing credential in request\"\n            }\n        }\n    }\n}\n```\n\n### 自定义插件优先级\n\n所有插件都有默认优先级，但是你仍然可以通过 `priority` 配置项来自定义插件优先级，从而改变插件执行顺序。\n\n```json\n {\n    \"serverless-post-function\": {\n        \"_meta\": {\n            \"priority\": 10000\n        },\n        \"phase\": \"rewrite\",\n        \"functions\" : [\"return function(conf, ctx)\n                    ngx.say(\\\"serverless-post-function\\\");\n                    end\"]\n    },\n    \"serverless-pre-function\": {\n        \"_meta\": {\n            \"priority\": -2000\n        },\n        \"phase\": \"rewrite\",\n        \"functions\": [\"return function(conf, ctx)\n                    ngx.say(\\\"serverless-pre-function\\\");\n                    end\"]\n    }\n}\n```\n\n`serverless-pre-function` 的默认优先级是 `10000`，`serverless-post-function` 的默认优先级是 `-2000`。默认情况下会先执行 `serverless-pre-function` 插件，再执行 `serverless-post-function` 插件。\n\n上面的配置则将 `serverless-pre-function` 插件的优先级设置为 `-2000`，`serverless-post-function` 插件的优先级设置为 `10000`，因此 `serverless-post-function` 插件会优先执行。\n\n:::note 注意\n\n- 自定义插件优先级只会影响插件实例绑定的主体，不会影响该插件的所有实例。比如上面的插件配置属于路由 A，路由 B 上的插件 `serverless-post-function` 和 `serverless-post-function` 插件执行顺序不会受到影响，会使用默认优先级。\n- 自定义插件优先级不适用于 Consumer 上配置的插件的 `rewrite` 阶段。路由上配置的插件的 `rewrite` 阶段将会优先运行，然后才会运行 Consumer 上除 `auth` 类插件之外的其他插件的 `rewrite` 阶段。\n\n:::\n\n### 动态控制插件执行状态\n\n默认情况下，在路由中指定的插件都会被执行。但是你可以通过 `filter` 配置项为插件添加一个过滤器，通过过滤器的执行结果控制插件是否执行。\n\n1. 如下配置表示，只有当请求查询参数中 `version` 值为 `v2` 时，`proxy-rewrite` 插件才会执行。\n\n    ```json\n    {\n        \"proxy-rewrite\": {\n            \"_meta\": {\n                \"filter\": [\n                    [\"arg_version\", \"==\", \"v2\"]\n                ]\n            },\n            \"uri\": \"/anything\"\n        }\n    }\n    ```\n\n2. 使用下述配置创建一条完整的路由。\n\n    ```json\n    {\n        \"uri\": \"/get\",\n        \"plugins\": {\n            \"proxy-rewrite\": {\n                \"_meta\": {\n                    \"filter\": [\n                        [\"arg_version\", \"==\", \"v2\"]\n                    ]\n                },\n                \"uri\": \"/anything\"\n            }\n        },\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"httpbin.org:80\": 1\n            }\n        }\n    }\n    ```\n\n3. 当请求中不带任何参数时，`proxy-rewrite` 插件不会执行，请求将被转发到上游的 `/get`。\n\n    ```shell\n    curl -v /dev/null http://127.0.0.1:9080/get -H\"host:httpbin.org\"\n    ```\n\n    ```shell\n    < HTTP/1.1 200 OK\n    ......\n    < Server: APISIX/2.15.0\n    <\n    {\n    \"args\": {},\n    \"headers\": {\n        \"Accept\": \"*/*\",\n        \"Host\": \"httpbin.org\",\n        \"User-Agent\": \"curl/7.79.1\",\n        \"X-Amzn-Trace-Id\": \"Root=1-62eb6eec-46c97e8a5d95141e621e07fe\",\n        \"X-Forwarded-Host\": \"httpbin.org\"\n    },\n    \"origin\": \"127.0.0.1, 117.152.66.200\",\n    \"url\": \"http://httpbin.org/get\"\n    }\n    ```\n\n4. 当请求中携带参数 `version=v2` 时，`proxy-rewrite` 插件执行，请求将被转发到上游的 `/anything`:\n\n    ```shell\n    curl -v /dev/null http://127.0.0.1:9080/get?version=v2 -H\"host:httpbin.org\"\n    ```\n\n    ```shell\n    < HTTP/1.1 200 OK\n    ......\n    < Server: APISIX/2.15.0\n    <\n    {\n    \"args\": {\n        \"version\": \"v2\"\n    },\n    \"data\": \"\",\n    \"files\": {},\n    \"form\": {},\n    \"headers\": {\n        \"Accept\": \"*/*\",\n        \"Host\": \"httpbin.org\",\n        \"User-Agent\": \"curl/7.79.1\",\n        \"X-Amzn-Trace-Id\": \"Root=1-62eb6f02-24a613b57b6587a076ef18b4\",\n        \"X-Forwarded-Host\": \"httpbin.org\"\n    },\n    \"json\": null,\n    \"method\": \"GET\",\n    \"origin\": \"127.0.0.1, 117.152.66.200\",\n    \"url\": \"http://httpbin.org/anything?version=v2\"\n    }\n    ```\n\n## 热加载\n\nAPISIX 的插件是热加载的，不管你是新增、删除还是修改插件，都不需要重启服务。\n\n只需要通过 Admin API 发送一个 HTTP 请求即可：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n\n:::note 注意\n\n如果你已经在路由规则里配置了某个插件（比如在 Route 的 `plugins` 字段里面添加了它），然后在配置文件中禁用了该插件，在执行路由规则时则会跳过该插件。\n\n:::\n\n## Standalone 模式下的热加载\n\n关于 Stand Alone 模式下的热加载的信息，请参考 [stand alone 模式](../../../en/latest/deployment-modes.md#standalone)。\n"
  },
  {
    "path": "docs/zh/latest/terminology/route.md",
    "content": "---\ntitle: Route\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Route\n  - 路由\ndescription: 本文讲述了路由的概念以及使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nRoute（也称为路由）是 APISIX 中最基础和最核心的资源对象，APISIX 可以通过路由定义规则来匹配客户端请求，根据匹配结果加载并执行相应的插件，最后将请求转发给到指定的上游服务。\n\n## 配置简介\n\n路由中主要包含三部分内容：\n\n- 匹配规则：比如 `uri`、`host`、`remote_addr` 等等，你也可以自定义匹配规则，详细信息请参考 [Route body 请求参数](../admin-api.md#route-request-body-parameters)。\n- 插件配置：你可以根据业务需求，在路由中配置相应的插件来实现功能。详细信息请参考 [Plugin](./plugin.md) 和 [plugin-config](./plugin-config.md)。\n- 上游信息：路由会根据配置的负载均衡信息，将请求按照规则转发至相应的上游。详细信息请参考 [Upstream](./upstream.md)。\n\n下图示例是一些 Route 规则的实例，当某些属性值相同时，图中用相同颜色标识。\n\n![路由示例](../../../assets/images/routes-example.png)\n\n你可以在路由中完成所有参数的配置，该方式设置容易设置，每个路由的相对独立自由度比较高。示例如下：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key_type\": \"var\",\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n当你的路由中有比较多的重复配置（比如启用相同的插件配置或上游信息），你也可以通过配置 [Service](service.md) 和 [Upstream](upstream.md) 的 ID 或者其他对象的 ID 来完成路由配置。示例如下：\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"uri\": \"/index.html\",\n  \"plugin_config_id\": \"123456789apacheapisix\",\n  \"upstream_id\": \"1\"\n}'\n```\n\n:::tip 提示\n\nAPISIX 所有的资源对象的 ID，均使用字符串格式，如果使用的上游 ID、服务 ID 或其他资源对象的 ID 大于 14 个字符时，请务必使用字符串形式表示该资源对象。例如：\n\n```json\n  \"plugin_config_id\": \"1234a67891234apisix\",\n  \"service_id\": \"434199918991639234\",\n  \"upstream_id\": \"123456789123456789\"\n```\n\n:::\n\n## 配置示例\n\n以下示例创建的路由，是把 URI 为 `/index.html` 的请求代理到地址为 `127.0.0.1:1980` 的上游服务。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -i -d '\n{\n    \"uri\": \"/index.html\",\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n```shell\nHTTP/1.1 201 Created\nDate: Sat, 31 Aug 2019 01:17:15 GMT\nContent-Type: text/plain\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX web server\n\n{\"node\":{\"value\":{\"uri\":\"\\/index.html\",\"upstream\":{\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"}},\"createdIndex\":61925,\"key\":\"\\/apisix\\/routes\\/1\",\"modifiedIndex\":61925}}\n```\n\n当接收到成功应答后，表示该路由已成功创建。\n\n更多信息，请参考 [Admin API 的 Route 对象](../admin-api.md#route)。\n"
  },
  {
    "path": "docs/zh/latest/terminology/router.md",
    "content": "---\ntitle: Router\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Router\ndescription: 本文介绍了如何选择 Apache APISIX 的 Router。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nAPISIX 区别于其他 API 网关的一大特点是允许用户选择不同 Router 来更好匹配自由业务，在性能、自由之间做最适合选择。\n\n你可以通过配置 `conf/config.yaml` 文件，来设置符合自身业务需求的路由。\n\n## 配置简介\n\nRouter 具有以下配置：\n\n- `apisix.router.http`: HTTP 请求路由。\n\n  - `radixtree_uri`：只使用 `uri` 作为主索引。基于 `radixtree` 引擎，支持全量和深前缀匹配，更多信息请参考[如何使用 router-radixtree](../../../en/latest/router-radixtree.md)。\n    - `绝对匹配`：完整匹配给定的 `uri` ，比如 `/foo/bar`，`/foo/glo`。\n    - `前缀匹配`：末尾使用 `*` 代表给定的 `uri` 是前缀匹配。比如 `/foo*`，则允许匹配 `/foo/`、`/foo/a`和`/foo/b`等。\n    - `匹配优先级`：优先尝试绝对匹配，若无法命中绝对匹配，再尝试前缀匹配。\n    - `任意过滤属性`：允许指定任何 Nginx 内置变量作为过滤条件，比如 URL 请求参数、请求头、cookie 等。\n  - `radixtree_uri_with_parameter`：同 `radixtree_uri` 但额外有参数匹配的功能。\n  - `radixtree_host_uri`：（默认）使用 `host + uri` 作为主索引（基于 `radixtree` 引擎），对当前请求会同时匹配 `host` 和 `uri`，支持的匹配条件与 `radixtree_uri` 基本一致。\n\n::: 注意\n\n在 3.2 及之前版本，APISIX 使用 `radixtree_uri` 作为默认路由，`radixtree_uri` 比 `radixtree_host_uri` 拥有更好的性能，如果你对性能有更高的要求，并且能够接受 `radixtree_uri` 只使用 `uri` 作为主索引的特点，可以考虑继续使用 `radixtree_uri` 作为默认路由\n\n:::\n\n- `apisix.router.ssl`：SSL 加载匹配路由。\n  - `radixtree_sni`：（默认）使用 `SNI` (Server Name Indication) 作为主索引（基于 radixtree 引擎）。\n"
  },
  {
    "path": "docs/zh/latest/terminology/script.md",
    "content": "---\ntitle: Script\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Router\ndescription: 本文介绍了 Apache APISIX Script 的使用方法及注意事项。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nScript 表示将在 `HTTP` 请求/响应生命周期期间执行的脚本。\n\nScript 配置需要绑定在路由上。\n\nScript 与 Plugin 不兼容，并且 Script 优先执行 Script，这意味着配置 Script 后，Route 上配置的 Plugin 将**不被执行**。\n\n理论上，在 Script 中可以编写任意 Lua 代码，你也可以直接调用已有的插件以复用已有的代码。\n\nScript 也有执行阶段概念，支持 `access`、`header_filter`、`body_filter` 和 `log` 阶段。系统会在相应阶段中自动执行 `Script` 脚本中对应阶段的代码。\n\n```json\n{\n    ...\n    \"script\": \"local _M = {} \\n function _M.access(api_ctx) \\n ngx.log(ngx.INFO,\\\"hit access phase\\\") \\n end \\nreturn _M\"\n}\n```\n"
  },
  {
    "path": "docs/zh/latest/terminology/secret.md",
    "content": "---\ntitle: Secret\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n密钥是指 APISIX 运行过程中所需的任何敏感信息，它可能是核心配置的一部分（如 etcd 的密码），也可能是插件中的一些敏感信息。APISIX 中常见的密钥类型包括：\n\n- 一些组件（etcd、Redis、Kafka 等）的用户名、密码\n- 证书的私钥\n- API 密钥\n- 敏感的插件配置字段，通常用于身份验证、hash、签名或加密\n\nAPISIX Secret 允许用户在 APISIX 中通过一些密钥管理服务（Vault 等）来存储密钥，在使用的时候根据 key 进行读取，确保密钥在整个平台中不以明文的形式存在。\n\n其工作原理如图所示：\n![secret](../../../assets/images/secret.png)\n\nAPISIX 目前支持通过以下方式存储密钥：\n\n- [环境变量](#使用环境变量管理密钥)\n- [HashiCorp Vault](#使用-vault-管理密钥)\n- [AWS Secrets Manager](#使用-aws-secrets-manager-管理密钥)\n- [GCP Secrets Manager](#使用-gcp-secrets-manager-管理密钥)\n\n你可以在以下插件的 consumer 配置中通过指定格式的变量来使用 APISIX Secret 功能，比如 `key-auth` 插件。\n\n:::note\n\n如果某个配置项为：`key: \"$ENV://ABC\"`，当 APISIX Secret 中没有检索到 $ENV://ABC 对应的真实值，那么 key 的值将是 \"$ENV://ABC\" 而不是 `nil`。\n\n:::\n\n## 使用环境变量管理密钥\n\n使用环境变量来管理密钥意味着你可以将密钥信息保存在环境变量中，在配置插件时通过特定格式的变量来引用环境变量。APISIX 支持引用系统环境变量和通过 Nginx `env` 指令配置的环境变量。\n\n### 引用方式\n\n```\n$ENV://$env_name/$sub_key\n```\n\n- env_name: 环境变量名称\n- sub_key: 当环境变量的值是 JSON 字符串时，获取某个属性的值\n\n如果环境变量的值是字符串类型，如：\n\n```\nexport JACK_AUTH_KEY=abc\n```\n\n则可以通过如下方式引用：\n\n```\n$ENV://JACK_AUTH_KEY\n```\n\n如果环境变量的值是一个 JSON 字符串，例如：\n\n```\nexport JACK={\"auth-key\":\"abc\",\"openid-key\": \"def\"}\n```\n\n则可以通过如下方式引用：\n\n```\n# 获取环境变量 JACK 的 auth-key\n$ENV://JACK/auth-key\n\n# 获取环境变量 JACK 的 openid-key\n$ENV://JACK/openid-key\n```\n\n### 示例：在 key-auth 插件中使用\n\n第一步：APISIX 实例启动前创建环境变量\n\n```\nexport JACK_AUTH_KEY=abc\n```\n\n第二步：在 `key-auth` 插件中引用环境变量\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"$ENV://JACK_AUTH_KEY\"\n        }\n    }\n}'\n```\n\n通过以上步骤，可以将 `key-auth` 插件中的 key 配置保存在环境变量中，而不是在配置插件时明文显示。\n\n## 使用 Vault 管理密钥\n\n使用 Vault 来管理密钥意味着你可以将密钥信息保存在 Vault 服务中，在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 [Vault KV 引擎的 V1 版本](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1)。\n\n### 引用方式\n\n```\n$secret://$manager/$id/$secret_name/$key\n```\n\n- manager: 密钥管理服务，可以是 Vault、AWS、GCP 等\n- APISIX Secret 资源 ID，需要与添加 APISIX Secret 资源时指定的 ID 保持一致\n- secret_name: 密钥管理服务中的密钥名称\n- key：密钥管理服务中密钥对应的 key\n\n### 示例：在 key-auth 插件中使用\n\n第一步：在 Vault 中创建对应的密钥，可以使用如下命令：\n\n```shell\nvault kv put apisix/jack auth-key=value\n```\n\n第二步：通过 Admin API 添加 Secret 资源，配置 Vault 的地址等连接信息：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/secrets/vault/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"https://127.0.0.1:8200\"，\n    \"prefix\": \"apisix\",\n    \"token\": \"root\"\n}'\n```\n\n如果使用 APISIX Standalone 版本，则可以在 `apisix.yaml`  文件中添加如下配置：\n\n```yaml\nsecrets:\n  - id: vault/1\n    prefix: apisix\n    token: root\n    uri: 127.0.0.1:8200\n```\n\n:::tip\n\n它现在支持使用 [`namespace` 字段](../admin-api.md#secret-config-body-requset-parameters) 设置 [HashiCorp Vault Enterprise](https://developer.hashicorp.com/vault/docs/enterprise/namespaces#vault-api-and-namespaces) 和 HCP Vault 所支持的多租户命名空间概念。\n\n:::\n\n第三步：在 `key-auth` 插件中引用 APISIX Secret 资源，填充秘钥信息：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"$secret://vault/1/jack/auth-key\"\n        }\n    }\n}'\n```\n\n通过上面两步操作，当用户请求命中 `key-auth` 插件时，会通过 APISIX Secret 组件获取到 key 在 Vault 中的真实值。\n\n## 使用 AWS Secrets Manager 管理密钥\n\n使用 AWS Secrets Manager 管理密钥是一种安全且便捷的方式来存储和管理敏感信息。通过这种方式，你可以将密钥信息保存在 AWS Secret Manager 中，并在配置 APISIX 插件时通过特定的格式引用这些密钥。\n\nAPISIX 目前支持两种访问方式： [长期凭证的访问方式](https://docs.aws.amazon.com/zh_cn/sdkref/latest/guide/access-iam-users.html) 和 [短期凭证的访问方式](https://docs.aws.amazon.com/zh_cn/sdkref/latest/guide/access-temp-idc.html)。\n\n### 引用方式\n\n在 APISIX 中引用密钥时，可以使用以下格式：\n\n```\n$secret://$manager/$id/$secret_name/$key\n```\n\n- manager: 密钥管理服务，可以是 Vault、AWS 等\n- APISIX Secret 资源 ID，需要与添加 APISIX Secret 资源时指定的 ID 保持一致\n- secret_name: 密钥管理服务中的密钥名称\n- key：当密钥的值是 JSON 字符串时，获取某个属性的值\n\n### 相关参数\n\n| 名称 | 必选项 | 默认值 | 描述 |\n| --- | --- | --- | --- |\n| access_key_id | 是 |  | AWS 访问密钥 ID |\n| secret_access_key | 是 |  | AWS 访问密钥 |\n| session_token | 否 |  | 临时访问凭证信息 |\n| region | 否 | us-east-1 | AWS 区域 |\n| endpoint_url | 否 | https://secretsmanager.{region}.amazonaws.com | AWS Secret Manager 地址 |\n\n### 示例：在 key-auth 插件中使用\n\n这里以 key-auth 插件的使用为例，展示如何通过 AWS Secret Manager 管理密钥：\n\n第一步：在 AWS Secret Manager 中创建对应的密钥，这里使用 [localstack](https://www.localstack.cloud/) 模拟，可以使用如下命令：\n\n```shell\ndocker exec -i localstack sh -c \"awslocal secretsmanager create-secret --name jack --description 'APISIX Secret' --secret-string '{\\\"auth-key\\\":\\\"value\\\"}'\"\n```\n\n第二步：通过 Admin API 添加 Secret 资源，配置 AWS Secret Manager 的地址等连接信息：\n\n你可以在环境变量中存储关键密钥信息，保证配置信息是安全的，在使用到地方进行引用：\n\n```shell\nexport AWS_ACCESS_KEY_ID=<access_key_id>\nexport AWS_SECRET_ACCESS_KEY=<secrets_access_key>\nexport AWS_SESSION_TOKEN=<token>\n```\n\n当然，你也可以通过直接在配置中指定所有信息内容：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/secrets/aws/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"endpoint_url\": \"http://127.0.0.1:4566\",\n    \"region\": \"us-east-1\",\n    \"access_key_id\": \"access\",\n    \"secret_access_key\": \"secret\",\n    \"session_token\": \"token\"\n}'\n```\n\n如果使用 APISIX Standalone 版本，则可以在 `apisix.yaml`  文件中添加如下配置：\n\n```yaml\nsecrets:\n  - id: aws/1\n    endpoint_url: http://127.0.0.1:4566\n    region: us-east-1\n    access_key_id: access\n    secret_access_key: secret\n    session_token: token\n```\n\n第三步：在 `key-auth` 插件中引用 APISIX Secret 资源，填充秘钥信息：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"jack\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"$secret://aws/1/jack/auth-key\"\n        }\n    }\n}'\n```\n\n通过上面两步操作，当用户请求命中 `key-auth` 插件时，会通过 APISIX Secret 组件获取到 key 在 AWS Secret Manager 中的真实值。\n\n### 验证\n\n你可以通过如下指令进行验证：\n\n```shell\n# 示例：将下面的 your_route 替换为实际的路由路径\ncurl -i http://127.0.0.1:9080/your_route -H 'apikey: value'\n```\n\n这将验证 key-auth 插件是否正确地使用 AWS Secret Manager 中的密钥。\n\n## 使用 GCP Secrets Manager 管理密钥\n\n使用 GCP Secret Manager 来管理密钥意味着你可以将密钥信息保存在 GCP 服务中，在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 GCP Secret Manager, 所支持的验证方式是[OAuth 2.0](https://developers.google.com/identity/protocols/oauth2?hl=zh-cn)。\n\n### 引用方式\n\n```\n$secret://$manager/$id/$secret_name/$key\n```\n\n引用方式和之前保持一致：\n\n- manager: 密钥管理服务，可以是 Vault、AWS\\GCP 等\n- APISIX Secret 资源 ID，需要与添加 APISIX Secret 资源时指定的 ID 保持一致\n- secret_name: 密钥管理服务中的密钥名称\n- key：当密钥的值是 JSON 字符串时，获取某个属性的值\n\n### 必要参数\n\n| 名称                     | 必选项   | 默认值                                           | 描述                                                                                                                             |\n| ----------------------- | -------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------  |\n| auth_config             | 是       |                                                  | `auth_config` 和 `auth_file` 必须配置一个。                                                                                     |\n| auth_config.client_email | 是       |                                                  | 谷歌服务帐号的 email 参数。                                                                                                           |\n| auth_config.private_key | 是       |                                                  | 谷歌服务帐号的私钥参数。                                                                                                           |\n| auth_config.project_id  | 是       |                                                  | 谷歌服务帐号的项目 ID。                                                                                                            |\n| auth_config.token_uri   | 否       | https://oauth2.googleapis.com/token              | 请求谷歌服务帐户的令牌的 URI。                                                                                                        |\n| auth_config.entries_uri | 否       |   https://secretmanager.googleapis.com/v1     | 谷歌密钥服务访问端点 API。                                                                                                   |\n| auth_config.scope      | 否       |   https://www.googleapis.com/auth/cloud-platform                                               | 谷歌服务账号的访问范围，可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes)|\n| auth_file               | 是       |                                                  | `auth_config` 和 `auth_file` 必须配置一个。          |\n| ssl_verify              | 否       | true                                             | 当设置为 `true` 时，启用 `SSL` 验证。                 |\n\n你需要配置相应的认证参数，或者通过 auth_file 来指定认证文件，其中 auth_file 的内容为认证参数的 json 格式。\n\n### 示例\n\n以下一种正确的配置实例：\n\n```\ncurl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"auth_config\" : {\n        \"client_email\": \"email@apisix.iam.gserviceaccount.com\",\n        \"private_key\": \"private_key\",\n        \"project_id\": \"apisix-project\",\n        \"token_uri\": \"https://oauth2.googleapis.com/token\",\n        \"entries_uri\": \"https://secretmanager.googleapis.com/v1\",\n        \"scope\": [\"https://www.googleapis.com/auth/cloud-platform\"]\n    }\n}'\n\n```\n"
  },
  {
    "path": "docs/zh/latest/terminology/service.md",
    "content": "---\ntitle: Service\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Router\ndescription: 本文介绍了 Apache APISIX Service 对象的概念及其使用方法。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nService（也称之为服务）是某类 API 的抽象（也可以理解为一组 Route 的抽象）。它通常与上游服务抽象是一一对应的，但与路由之间，通常是 1:N 即一对多的关系。参看下图。\n\n![服务示例](../../../assets/images/service-example.png)\n\n不同路由规则同时绑定到一个服务上，这些路由将具有相同的上游和插件配置，减少冗余配置。当路由和服务都开启同一个插件时，路由中的插件优先级高于服务中的插件。关于插件优先级的更多信息，请参考 [Plugin](./plugin.md)。\n\n更多关于 Service 的信息，请参考 [Admin API 的 Service 对象](../admin-api.md#service)。\n\n## 配置示例\n\n以下示例创建了一个启用限流插件的服务，并且将该服务绑定到 ID 为 `100` 和 `101` 的路由上。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. 创建服务。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/services/200 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        },\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n    ```\n\n2. 创建 ID 为 `100` 的路由，并绑定 ID 为 `200` 的服务。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/100 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"methods\": [\"GET\"],\n        \"uri\": \"/index.html\",\n        \"service_id\": \"200\"\n    }'\n    ```\n\n3. 创建 ID 为 `101` 的路由，并绑定 ID 为 `200` 的服务。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/101 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"methods\": [\"GET\"],\n        \"uri\": \"/foo/index.html\",\n        \"service_id\": \"200\"\n    }'\n    ```\n\n当然你也可以为路由指定不同的插件配置或上游。比如在以下示例中，我们设置了不同的限流参数，其他部分（比如上游）则继续使用上述服务中的配置参数。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/102 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/bar/index.html\",\n    \"id\": \"102\",\n    \"service_id\": \"200\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2000,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    }\n}'\n```\n\n:::tip 提示\n\n当路由和服务都启用同一个插件时，路由中的插件配置会优先于服务。更多信息，请参考[Plugin](./plugin.md)。\n\n:::\n"
  },
  {
    "path": "docs/zh/latest/terminology/upstream.md",
    "content": "---\ntitle: Upstream\nkeywords:\n  - APISIX\n  - API 网关\n  - 上游\n  - Upstream\ndescription: 本文介绍了 Apache APISIX Upstream 对象的作用以及如何使用 Upstream。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\nUpstream（也称之为上游）是对虚拟主机抽象，即应用层服务或节点的抽象。你可以通过 Upstream 对象对多个服务节点按照配置规则进行负载均衡。\n\n上游的地址信息可以直接配置到[路由](./route.md)（或[服务](./service.md)）中。\n\n![Upstream 示例](../../../assets/images/upstream-example.png)\n\n如上图所示，当多个路由（或服务）使用该上游时，你可以单独创建上游对象，在路由中通过使用 `upstream_id` 的方式引用资源，减轻维护压力。\n\n你也可以将上游的信息直接配置在指定路由或服务中，不过路由中的配置优先级更高，优先级行为与[插件](./plugin.md) 非常相似。\n\n## 配置参数\n\nAPISIX 的 Upstream 对象除了基本的负载均衡算法外，还支持对上游做主被动健康检查、重试等逻辑。更多信息，请参考 [Admin API 中的 Upstream 资源](../admin-api.md#upstream)。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n1. 创建上游对象用例。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/upstreams/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"type\": \"chash\",\n        \"key\": \"remote_addr\",\n        \"nodes\": {\n            \"127.0.0.1:80\": 1,\n            \"httpbin.org:80\": 2\n        }\n    }'\n    ```\n\n    上游对象创建后，可以被路由或服务引用。\n\n2. 在路由中使用创建的上游对象。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/index.html\",\n        \"upstream_id\": 1\n    }'\n    ```\n\n3. 为方便使用，你也可以直接把上游信息直接配置在某个路由或服务。\n\n以下示例是将上游信息直接配置在路由中：\n\n```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/index.html\",\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        },\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }'\n```\n\n## 使用示例\n\n- 配置健康检查的示例。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/index.html\",\n        \"plugins\": {\n            \"limit-count\": {\n                \"count\": 2,\n                \"time_window\": 60,\n                \"rejected_code\": 503,\n                \"key\": \"remote_addr\"\n            }\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n            \"type\": \"roundrobin\",\n            \"retries\": 2,\n            \"checks\": {\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"interval\": 1,\n                        \"http_failures\": 2\n                    }\n                }\n            }\n        }\n    }'\n    ```\n\n    更多信息，请参考[健康检查的文档](../tutorials/health-check.md)。\n\n以下是使用不同 [`hash_on`](../admin-api.md#upstream-body-request-methods) 类型的配置示例：\n\n### Consumer\n\n1. 创建一个 Consumer 对象。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/consumers \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"username\": \"jack\",\n        \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-jack\"\n            }\n        }\n    }'\n    ```\n\n2. 创建路由，启用 `key-auth` 插件，配置 `upstream.hash_on` 的类型为 `consumer`。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"plugins\": {\n            \"key-auth\": {}\n        },\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1,\n                \"127.0.0.1:1981\": 1\n            },\n            \"type\": \"chash\",\n            \"hash_on\": \"consumer\"\n        },\n        \"uri\": \"/server_port\"\n    }'\n    ```\n\n3. 测试请求，认证通过后的 `consumer_name` 将作为负载均衡哈希算法的哈希值。\n\n    ```shell\n    curl http://127.0.0.1:9080/server_port -H \"apikey: auth-jack\"\n    ```\n\n### Cookie\n\n1. 创建路由并配置 `upstream.hash_on` 的类型为 `cookie`。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/hash_on_cookie\",\n        \"upstream\": {\n            \"key\": \"sid\",\n            \"type\": \"chash\",\n            \"hash_on\": \"cookie\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1,\n                \"127.0.0.1:1981\": 1\n            }\n        }\n    }'\n    ```\n\n2. 客户端请求携带 `Cookie`。\n\n    ```shell\n    curl http://127.0.0.1:9080/hash_on_cookie \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -H \"Cookie: sid=3c183a30cffcda1408daf1c61d47b274\"\n    ```\n\n### Header\n\n1. 创建路由并配置 `upstream.hash_on` 的类型为 `header`，`key` 为 `content-type`。\n\n    ```shell\n    curl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d '\n    {\n        \"uri\": \"/hash_on_header\",\n        \"upstream\": {\n            \"key\": \"content-type\",\n            \"type\": \"chash\",\n            \"hash_on\": \"header\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1,\n                \"127.0.0.1:1981\": 1\n            }\n        }\n    }'\n    ```\n\n2. 客户端请求携带 `content-type` 的 `header`。\n\n```shell\n curl http://127.0.0.1:9080/hash_on_header \\\n -H \"X-API-KEY: $admin_key\" \\\n -H \"Content-Type: application/json\"\n```\n"
  },
  {
    "path": "docs/zh/latest/tutorials/cache-api-responses.md",
    "content": "---\ntitle: API 响应缓存\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 缓存\n  - 性能\ndescription: This tutorial will focus primarily on handling caching at the API Gateway level by using Apache APISIX API Gateway and you will learn how to use proxy-caching plugin to improve response efficiency for your Web or Microservices API.\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n本教程将主要介绍如何在 **API 网关** 级别进行缓存处理，使用 **Apache APISIX API 网关**，你将学习如何使用 **proxy-cache** 插件来提升 Web 或微服务 API 的响应效率。\n\n**本次教程涵盖的内容概览：**\n\n* API 网关中的缓存\n* 关于 [Apache APISIX API 网关](https://apisix.apache.org/zh/docs/apisix/getting-started/)\n* 运行演示项目 [apisix-dotnet-docker](https://github.com/Boburmirzo/apisix-dotnet-docker)\n* 配置 [Proxy Cache](https://apisix.apache.org/zh/docs/apisix/plugins/proxy-cache/) 插件\n* 验证代理缓存功能\n\n## 通过缓存提升性能\n\n在构建 API 时，你希望保持其简单且高效。当并发请求需要访问相同数据量增加时，你可能会遇到一些问题，从而考虑引入 **缓存**：\n\n* 某些 API 请求存在延迟，明显影响用户体验。\n* 从数据库获取数据响应时间过长。\n* API 的高吞吐量可能威胁到其可用性。\n* 网络故障导致频繁访问的 API 信息获取失败。\n\n## API 网关中的缓存\n\n[缓存](https://zh.wikipedia.org/wiki/%E7%BC%93%E5%AD%98)能够存储并获取网络请求及其对应的响应。在 Web 应用中，缓存可以发生在不同层级：\n\n* 边缘缓存或 CDN\n* 数据库缓存\n* 服务器缓存（API 缓存）\n* 浏览器缓存\n\n**反向代理缓存（Reverse Proxy Caching）** 是另一种缓存机制，通常在 **API 网关** 内部实现。它可以减少对后端接口的调用次数，并通过缓存上游响应来提高 API 请求的延迟表现。如果 API 网关缓存中存在请求资源的最新副本，它会直接使用该副本响应请求，而无需访问后端服务。如果未命中缓存，请求将转发到目标上游服务（后端服务）。\n\n## Apache APISIX API 网关代理缓存\n\n借助 **Apache APISIX**，你可以使用 [proxy-cache](https://apisix.apache.org/zh/docs/apisix/plugins/proxy-cache/) 插件为 API 启用缓存，从而缓存 API 端点的响应并提升性能。该插件可以与其他插件组合使用，目前支持基于磁盘的缓存。\n\n要缓存的数据可以通过 **responseCodes**、**requestModes** 进行过滤，也可以使用 **noCache** 和 **cacheByPass** 属性进行更复杂的过滤。你还可以在插件配置中指定缓存的过期时间或内存容量。更多配置项请参考 `proxy-cache` 插件的 [属性说明](https://apisix.apache.org/zh/docs/apisix/plugins/proxy-cache/)。\n\n有了这些基础，我们接下来将通过一个例子演示如何使用 **Apache APISIX** 的 `proxy-cache` 插件，并将其应用于 **ASP.NET Core Web API** 的单个端点。\n\n## 运行演示项目\n\n到目前为止，我假设你已经启动并运行了演示项目 [apisix-dotnet-docker](https://github.com/Boburmirzo/apisix-dotnet-docker)。你可以在 **GitHub** 上查看完整源码，以及如何通过 **Docker CLI** 构建多容器 **APISIX** 的说明。\n\n在 **ASP.NET Core 项目** 中，有一个简单的 API，用于从服务层获取所有产品列表，位于 [ProductsController.cs](https://github.com/Boburmirzo/apisix-dotnet-docker/blob/main/ProductApi/Controllers/ProductsController.cs) 文件中。\n\n假设这个产品列表通常每天只更新一次，而该端点每天需要处理数十亿次请求来部分或全部获取产品列表。在这种场景下，使用 `proxy-cache` 插件进行 API 缓存将非常有用。为了演示的目的，我们仅为 `GET` 方法启用缓存。\n\n> 理想情况下，`GET` 请求应该默认是可缓存的——除非出现特殊条件。\n\n## 配置 Proxy Cache 插件\n\n现在，让我们开始在项目的 **Apache APISIX 声明式配置文件 `config.yaml`** 中添加 `proxy-cache` 插件。由于在当前项目中，我们还没有注册本次演示要使用的插件，因此需要将 `proxy-cache` 插件名称添加到插件列表末尾：\n\n```yaml\nplugins:\n - http-logger\n - ip-restriction\n …\n - proxy-cache\n```\n\n如果你需要指定缓存相关参数（如 **disk_size**、**memory_size**），也可以在同一个文件中添加缓存配置，例如：\n\n```yaml\nproxy_cache:\n cache_ttl: 10s # 如果上游未指定缓存时间，则使用默认缓存时间\n zones:\n - name: disk_cache_one # 缓存名称。管理员可以在 Admin API 中按名称指定使用哪个缓存\n memory_size: 50m # 用于存储缓存索引的共享内存大小\n disk_size: 1G # 用于存储缓存数据的磁盘大小\n disk_path: \"/tmp/disk_cache_one\" # 缓存数据存储路径\n cache_levels: \"1:2\" # 缓存的层级结构\n```\n\n接下来，我们可以直接运行 `apisix reload` 命令来重新加载最新的插件代码，而无需重启 Apache APISIX。重新加载新插件的命令如下：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/plugins/reload -H \"X-API-KEY: $admin_key\" -X PUT\n```\n\n然后，我们运行两个 curl 命令来为 `/api/products` 端点配置 **Upstream** 和 **Route**。首先，创建一个示例 Upstream（也就是我们的 API 服务器）：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/upstreams/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"type\": \"roundrobin\",\n  \"nodes\": {\n    \"productapi:80\": 1\n  }\n}'\n```\n\n接下来，我们为 `/api/products` 添加一个具备缓存能力的路由，通过在 `plugins` 属性中设置 `proxy-cache` 插件，并通过 **upstream_id** 引用上游服务，将请求转发到 API 服务器：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '{\n  \"name\": \"Route for API Caching\",\n  \"methods\": [\n    \"GET\"\n  ],\n  \"uri\": \"/api/products\",\n  \"plugins\": {\n    \"proxy-cache\": {\n      \"cache_key\": [\n        \"$uri\",\n        \"-cache-id\"\n      ],\n      \"cache_bypass\": [\n        \"$arg_bypass\"\n      ],\n      \"cache_method\": [\n        \"GET\"\n      ],\n      \"cache_http_status\": [\n        200\n      ],\n      \"hide_cache_headers\": true,\n      \"no_cache\": [\n        \"$arg_test\"\n      ]\n    }\n  },\n  \"upstream_id\": 1\n}'\n```\n\n如上配置所示，我们定义了一些插件属性，表示只缓存 **GET 方法的成功响应（HTTP 200）**。\n\n## 验证 Proxy Cache 功能\n\n最后，我们可以测试代理缓存是否按预期工作。\n\n我们将向 `/api/products` 路径发送多次请求，每次都应收到 `HTTP 200 OK` 响应。然而，响应头中的 `Apisix-Cache-Status` 会显示 **MISS**，表示当请求第一次访问路由时，该响应尚未缓存。此时，如果再次发送请求，你会看到响应已被缓存，`Apisix-Cache-Status` 显示 **HIT**。\n\n首先，发送初始请求：\n\n```shell\ncurl http://localhost:9080/api/products -i\n```\n\n响应示例：\n\n```shell\nHTTP/1.1 200 OK\n…\nApisix-Cache-Status: MISS\n```\n\n当你再次调用该服务时，由于上一次请求已缓存，路由会返回缓存的响应：\n\n```shell\nHTTP/1.1 200 OK\n…\nApisix-Cache-Status: HIT\n```\n\n如果在缓存的 **TTL（生存时间）** 结束后再次访问端点，你将得到：\n\n```shell\nHTTP/1.1 200 OK\n…\nApisix-Cache-Status: EXPIRED\n```\n\n太棒了！我们已经为 API 端点启用了缓存。\n\n### 额外测试案例\n\n你也可以在 **Product Controller** 代码中添加一些延迟，并测量有缓存和无缓存情况下的响应时间：\n\n```c#\n[HttpGet]\npublic IActionResult GetAll()\n{\n    Console.Write(\"The delay starts.\\n\");\n    System.Threading.Thread.Sleep(5000);\n    Console.Write(\"The delay ends.\");\n    return Ok(_productsService.GetAll());\n}\n```\n\n使用 `curl` 命令测量响应时间：\n\n```shell\ncurl -i 'http://localhost:9080/api/products' -s -o /dev/null -w \"Response time: %{time_starttransfer} seconds\\n\"\n```\n\n## 后续步骤\n\n如我们所学，在 **Apache APISIX** 的帮助下，为 **ASP.NET Core Web API** 配置 API 响应缓存既简单又快速。它可以显著减少对端点的调用次数，并改善 API 请求的延迟表现。Apache APISIX 还提供了众多内置插件，你可以在 [插件中心](https://apisix.apache.org/plugins) 查看并根据需要使用。\n\n## 推荐阅读\n\n* 你可以参考 [Expose API](./protect-api.md) 学习如何发布你的第一个 API。\n* 你可以参考 [Protect API](./protect-api.md) 学习如何保护你的 API。\n"
  },
  {
    "path": "docs/zh/latest/tutorials/client-to-apisix-mtls.md",
    "content": "---\ntitle: 配置客户端与 APISIX 之间的双向认证（mTLS）\nkeywords:\n  - mTLS\n  - API 网关\n  - APISIX\ndescription: 本文介绍了如何在客户端和 Apache APISIX 之间配置双向认证（mTLS）。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nmTLS 是一种双向身份认证的方式。如果在你的网络环境中，要求只有受信任的客户端才可以访问服务端，那么可以启用 mTLS 来验证客户端的身份，保证服务端 API 的安全。本文主要介绍了如何配置客户端与 Apache APISIX 之间的双向认证（mTLS）。\n\n## 配置\n\n本示例包含以下过程：\n\n1. 生成证书；\n2. 在 APISIX 中配置证书；\n3. 在 APISIX 中创建并配置路由；\n4. 测试验证。\n\n为了使测试结果更加清晰，本文提到的示例会向上游传递一些有关客户端证书的信息，其中包括：`serial`，`fingerprint` 和 `common name`。\n\n### 生成证书\n\n我们需要生成三个测试证书，分别是根证书、服务器证书、客户端证书。只需通过以下命令，就可以通过 `OpenSSL` 生成我们需要的测试证书。\n\n```shell\n# 根证书\nopenssl genrsa -out ca.key 2048\nopenssl req -new -sha256 -key ca.key -out ca.csr -subj \"/CN=ROOTCA\"\nopenssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.cer\n\n# 服务器证书\nopenssl genrsa -out server.key 2048\n# 注意：CN 值中的 `test.com` 为我们要测试的域名/主机名。\nopenssl req -new -sha256 -key server.key -out server.csr -subj \"/CN=test.com\"\nopenssl x509 -req -days 36500 -sha256 -extensions v3_req  -CA  ca.cer -CAkey ca.key  -CAserial ca.srl  -CAcreateserial -in server.csr -out server.cer\n\n# 客户端证书\nopenssl genrsa -out client.key 2048\nopenssl req -new -sha256 -key client.key  -out client.csr -subj \"/CN=CLIENT\"\nopenssl x509 -req -days 36500 -sha256 -extensions v3_req  -CA  ca.cer -CAkey ca.key  -CAserial ca.srl  -CAcreateserial -in client.csr -out client.cer\n\n# 将客户端证书转换为 pkcs12 供 Windows 使用（可选）\nopenssl pkcs12 -export -clcerts -in client.cer -inkey client.key -out client.p12\n```\n\n### 在 APISIX 中配置证书\n\n使用 `curl` 命令请求 APISIX Admin API 创建一个 SSL 资源并指定 SNI。\n\n:::note 注意\n\n证书中的换行需要替换为其转义字符 `\\n`\n\n:::\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/ssls/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"sni\": \"test.com\",\n    \"cert\": \"<服务器证书>\",\n    \"key\": \"<服务器证书私钥>\",\n    \"client\": {\n        \"ca\": \"<客户端证书公钥>\"\n    }\n}'\n```\n\n- `sni`：指定证书的域名（CN），当客户端尝试通过 TLS 与 APISIX 握手时，APISIX 会将 `ClientHello` 中的 SNI 数据与该字段进行匹配，找到对应的服务器证书进行握手。\n- `cert`：服务器证书。\n- `key`：服务器证书的私钥。\n- `client.ca`：用来验证客户端证书的 CA 文件。为了演示方便，这里使用了同一个 `CA`。\n\n### 配置测试路由\n\n使用 `curl` 命令请求 APISIX Admin API 创建一个路由。\n\n```shell\ncurl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \\\n--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"uri\": \"/anything\",\n    \"plugins\": {\n        \"proxy-rewrite\": {\n            \"headers\": {\n                \"X-Ssl-Client-Fingerprint\": \"$ssl_client_fingerprint\",\n                \"X-Ssl-Client-Serial\": \"$ssl_client_serial\",\n                \"X-Ssl-Client-S-DN\": \"$ssl_client_s_dn\"\n            }\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org\":1\n        },\n        \"type\":\"roundrobin\"\n    }\n}'\n```\n\nAPISIX 会根据 SNI 和上一步创建的 SSL 资源自动处理 TLS 握手，所以我们不需要在路由中指定主机名（但也可以显式地指定主机名）。\n\n另外，上面 `curl` 命令中，我们启用了 `proxy-rewrite` 插件，它将动态地更新请求头的信息，示例中变量值的来源是 `NGINX` 变量，你可以在这里找到它们：http://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables。\n\n### 测试验证\n\n由于我们使用域名 `test.com` 作为测试域名，在开始验证之前，我们必须先将测试域名添加到你的 DNS 或者本地的 `hosts` 文件中。\n\n1. 如果我们不使用 `hosts`，只是想测试一下结果，那么你可以使用下面的命令直接进行测试：\n\n```\ncurl --resolve \"test.com:9443:127.0.0.1\" https://test.com:9443/anything -k --cert ./client.cer --key ./client.key\n```\n\n2. 如果你需要修改 `hosts`，请阅读下面示例（以 Ubuntu 为例）：\n\n- 修改 /etc/hosts 文件\n\n  ```shell\n  # 127.0.0.1 localhost\n  127.0.0.1 test.com\n  ```\n\n- 验证测试域名是否生效\n\n  ```shell\n  ping test.com\n\n  PING test.com (127.0.0.1) 56(84) bytes of data.\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=64 time=0.028 ms\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=64 time=0.037 ms\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=3 ttl=64 time=0.036 ms\n  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=4 ttl=64 time=0.031 ms\n  ^C\n  --- test.com ping statistics ---\n  4 packets transmitted, 4 received, 0% packet loss, time 3080ms\n  rtt min/avg/max/mdev = 0.028/0.033/0.037/0.003 ms\n  ```\n\n- 测试\n\n  ```shell\n  curl https://test.com:9443/anything -k --cert ./client.cer --key ./client.key\n  ```\n\n  然后你将收到下面的响应体：\n\n  ```shell\n  {\n    \"args\": {},\n    \"data\": \"\",\n    \"files\": {},\n    \"form\": {},\n    \"headers\": {\n      \"Accept\": \"*/*\",\n      \"Host\": \"test.com\",\n      \"User-Agent\": \"curl/7.81.0\",\n      \"X-Amzn-Trace-Id\": \"Root=1-63256343-17e870ca1d8f72dc40b2c5a9\",\n      \"X-Forwarded-Host\": \"test.com\",\n      \"X-Ssl-Client-Fingerprint\": \"c1626ce3bca723f187d04e3757f1d000ca62d651\",\n      \"X-Ssl-Client-S-Dn\": \"CN=CLIENT\",\n      \"X-Ssl-Client-Serial\": \"5141CC6F5E2B4BA31746D7DBFE9BA81F069CF970\"\n    },\n    \"json\": null,\n    \"method\": \"GET\",\n    \"origin\": \"127.0.0.1\",\n    \"url\": \"http://test.com/anything\"\n  }\n  ```\n\n由于我们在示例中配置了 `proxy-rewrite` 插件，我们可以看到响应体中包含上游收到的请求体，包含了正确数据。\n\n## 基于对 URI 正则表达式匹配，绕过 MTLS\n\nAPISIX 允许配置 URI 白名单以便绕过 MTLS。如果请求的 URI 在白名单内，客户端证书将不被检查。注意，如果针对白名单外的 URI 发请求，而该请求缺乏客户端证书或者提供了非法客户端证书，会得到 HTTP 400 响应，而不是在 SSL 握手阶段被拒绝。\n\n### 时序图\n\n![skip mtls](../../../assets/images/skip-mtls.png)\n\n### 例子\n\n1. 配置路由和证书\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```bash\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/*\",\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org\": 1\n        }\n    }\n}'\n\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"cert\": \"'\"$(<t/certs/mtls_server.crt)\"'\",\n    \"key\": \"'\"$(<t/certs/mtls_server.key)\"'\",\n    \"snis\": [\n        \"*.apisix.dev\"\n    ],\n    \"client\": {\n        \"ca\": \"'\"$(<t/certs/mtls_ca.crt)\"'\",\n        \"depth\": 10,\n        \"skip_mtls_uri_regex\": [\n            \"/anything.*\"\n        ]\n    }\n}'\n```\n\n2. 如果没提供客户端证书，而 URI 又不在白名单内，会得到 HTTP 400 响应。\n\n```bash\ncurl https://admin.apisix.dev:9443/uuid -v \\\n--resolve 'admin.apisix.dev:9443:127.0.0.1' --cacert t/certs/mtls_ca.crt\n* Added admin.apisix.dev:9443:127.0.0.1 to DNS cache\n* Hostname admin.apisix.dev was found in DNS cache\n*   Trying 127.0.0.1:9443...\n* TCP_NODELAY set\n* Connected to admin.apisix.dev (127.0.0.1) port 9443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*   CAfile: t/certs/mtls_ca.crt\n  CApath: /etc/ssl/certs\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n*  subject: C=cn; ST=GuangDong; L=ZhuHai; CN=admin.apisix.dev; OU=ops\n*  start date: Dec  1 10:17:24 2022 GMT\n*  expire date: Aug 18 10:17:24 2042 GMT\n*  subjectAltName: host \"admin.apisix.dev\" matched cert's \"admin.apisix.dev\"\n*  issuer: C=cn; ST=GuangDong; L=ZhuHai; CN=ca.apisix.dev; OU=ops\n*  SSL certificate verify ok.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* Using Stream ID: 1 (easy handle 0x56246de24e30)\n> GET /uuid HTTP/2\n> Host: admin.apisix.dev:9443\n> user-agent: curl/7.68.0\n> accept: */*\n>\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n* old SSL session ID is stale, removing\n* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!\n< HTTP/2 400\n< date: Fri, 21 Apr 2023 07:53:23 GMT\n< content-type: text/html; charset=utf-8\n< content-length: 229\n< server: APISIX/3.2.0\n<\n<html>\n<head><title>400 Bad Request</title></head>\n<body>\n<center><h1>400 Bad Request</h1></center>\n<hr><center>openresty</center>\n<p><em>Powered by <a href=\"https://apisix.apache.org/\">APISIX</a>.</em></p></body>\n</html>\n* Connection #0 to host admin.apisix.dev left intact\n```\n\n3. 虽然没提供客户端证书，但是 URI 在白名单内，请求会被成功处理和响应。\n\n```bash\ncurl https://admin.apisix.dev:9443/anything/foobar -i \\\n--resolve 'admin.apisix.dev:9443:127.0.0.1' --cacert t/certs/mtls_ca.crt\nHTTP/2 200\ncontent-type: application/json\ncontent-length: 416\ndate: Fri, 21 Apr 2023 07:58:28 GMT\naccess-control-allow-origin: *\naccess-control-allow-credentials: true\nserver: APISIX/3.2.0\n...\n```\n\n## 总结\n\n想了解更多有关 Apache APISIX 的 mTLS 功能介绍，可以阅读：[TLS 双向认证](../mtls.md)。\n"
  },
  {
    "path": "docs/zh/latest/tutorials/expose-api.md",
    "content": "---\ntitle: 发布 API\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 发布路由\n  - 创建服务\ndescription: 本文介绍了如何通过 Apache APISIX 发布服务和路由。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n本文将引导你了解 APISIX 的上游、路由以及服务的概念，并介绍如何通过 APISIX 发布你的 API。\n\n## 概念介绍\n\n### 上游\n\n[Upstream](../terminology/upstream.md) 也称为上游，上游是对虚拟主机的抽象，即应用层服务或节点的抽象。\n\n上游的作用是按照配置规则对服务节点进行负载均衡，它的地址信息可以直接配置到路由或服务上。当多个路由或服务引用同一个上游时，可以通过创建上游对象，在路由或服务中使用上游 ID 的方式引用上游，减轻维护压力。\n\n### 路由\n\n[Route](../terminology/route.md) 也称为路由，是 APISIX 中最基础和最核心的资源对象。\n\nAPISIX 可以通过路由定义规则来匹配客户端请求，根据匹配结果加载并执行相应的[插件](../terminology/plugin.md)，最后把请求转发给到指定的上游服务。路由中主要包含三部分内容：匹配规则、插件配置和上游信息。\n\n### 服务\n\n[Service](../terminology/service.md) 也称为服务，是某类 API 的抽象（也可以理解为一组 Route 的抽象）。它通常与上游服务抽象是一一对应的，Route 与 Service 之间，通常是 N:1 的关系。\n\n## 前提条件\n\n在进行如下操作前，请确保你已经通过 Docker [启动 APISIX](../installation-guide.md)。\n\n## 公开你的服务\n\n1. 创建上游。\n\n创建一个包含 `httpbin.org` 的上游服务，你可以使用它进行测试。这是一个返回服务，它将返回我们在请求中传递的参数。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/upstreams/1\" \\\n-H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"type\": \"roundrobin\",\n  \"nodes\": {\n    \"httpbin.org:80\": 1\n  }\n}'\n```\n\n在该命令中，我们指定了 Apache APISIX 的 Admin API Key 为 `edd1c9f034335f136f87ad84b625c8f1`，并且使用 `roundrobin` 作为负载均衡机制，并设置了 `httpbin.org:80` 为上游服务。为了将该上游绑定到路由，此处需要把 `upstream_id` 设置为 `1`。此处你可以在 `nodes` 下指定多个上游，以达到负载均衡的效果。\n\n如需了解更多信息，请参考[上游](../terminology/upstream.md)。\n\n2. 创建路由。\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" \\\n-H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"host\": \"example.com\",\n  \"uri\": \"/anything/*\",\n  \"upstream_id\": \"1\"\n}'\n```\n\n:::note 注意\n\n创建上游非必须步骤，你可以通过在路由中，添加 `upstream` 对象，达到上述的效果。例如：\n\n```shell\ncurl \"http://127.0.0.1:9180/apisix/admin/routes/1\" \\\n-H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\" -X PUT -d '\n{\n  \"methods\": [\"GET\"],\n  \"host\": \"example.com\",\n  \"uri\": \"/anything/*\",\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"httpbin.org:80\": 1\n    }\n  }\n}'\n```\n\n:::\n\n3. 测试路由。\n\n在创建完成路由后，你可以通过以下命令测试路由是否正常：\n\n```\ncurl -i -X GET \"http://127.0.0.1:9080/anything/get?foo1=bar1&foo2=bar2\" -H \"Host: example.com\"\n```\n\n该请求将被 APISIX 转发到 `http://httpbin.org:80/anything/get?foo1=bar1&foo2=bar2`。\n\n## 更多教程\n\n你可以查看[保护 API](./protect-api.md) 来保护你的 API。\n\n接下来，你可以通过 APISIX 的一些[插件](../plugins/batch-requests.md)，实现更多功能。\n"
  },
  {
    "path": "docs/zh/latest/tutorials/health-check.md",
    "content": "---\ntitle: 健康检查\nkeywords:\n  - APISIX\n  - API 网关\n  - 健康检查\ndescription: 本文介绍了如何使用 API 网关 Apache APISIX 的健康检查功能来检查上游节点的健康状态。\n---\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n本文主要介绍了 Apache APISIX 的健康检查功能。健康检查功能可以在上游节点发生故障或者迁移时，将请求代理到健康的节点上，最大程度避免服务不可用的问题。APISIX 的健康检查功能使用 [lua-resty-healthcheck](https://github.com/api7/lua-resty-healthcheck) 实现，并分为主动检查和被动检查。\n\n## 主动健康检查\n\n主动健康检查主要是指 APISIX 通过预设的探针类型，主动探测上游节点的存活性。目前 APISIX 支持 `HTTP`、`HTTPS`、`TCP` 三种探针类型。\n\n当发向健康节点 A 的 N 个连续探针都失败时（取决于如何配置），则该节点将被标记为不健康，不健康的节点将会被 APISIX 的负载均衡器忽略，无法收到请求；若某个不健康的节点，连续 M 个探针都成功，则该节点将被重新标记为健康，进而可以被代理。\n\n## 被动健康检查\n\n被动健康检查是指，通过判断从 APISIX 转发到上游节点的请求响应状态，来判断对应的上游节点是否健康。相对于主动健康检查，被动健康检查的方式无需发起额外的探针，但是也无法提前感知节点状态，可能会有一定量的失败请求。\n\n若发向健康节点 A 的 N 个连续请求都被判定为失败（取决于如何配置），则该节点将被标记为不健康。\n\n:::note 注意\n\n由于不健康的节点无法收到请求，仅使用被动健康检查策略无法重新将节点标记为健康，因此通常需要结合主动健康检查策略。\n\n:::\n\n:::tip 提示\n\n- 只有在 `upstream` 被请求时才会开始健康检查，如果 `upstream` 被配置但没有被请求，不会触发启动健康检查。\n- 如果没有健康的节点，那么请求会继续发送给上游。\n\n:::\n\n## 属性\n\n| 名称                                            | 配置类型            |  类型   | 有效值               | 默认值                                                                                         | 描述                                                                    |\n| ----------------------------------------------- | ------------------ | ------- | -------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |\n| upstream.checks.active.type                     | 主动检查            | string  | `http` `https` `tcp` | http                                                                                           | 主动检查的类型。                                                  |\n| upstream.checks.active.timeout                  | 主动检查            | integer |                      | 1                                                                                              | 主动检查的超时时间（单位为秒）。                          |\n| upstream.checks.active.concurrency              | 主动检查            | integer |                      | 10                                                                                             | 主动检查时同时检查的目标数。                                |\n| upstream.checks.active.http_path                | 主动检查            | string  |                      | /                                                                                              | 主动检查的 HTTP 请求路径。                                      |\n| upstream.checks.active.host                     | 主动检查            | string  |                      | ${upstream.node.host}                                                                          | 主动检查的 HTTP 请求主机名。                                   |\n| upstream.checks.active.port                     | 主动检查            | integer | `1` 至 `65535`       | ${upstream.node.port}                                                                          | 主动检查的 HTTP 请求主机端口。                                |\n| upstream.checks.active.https_verify_certificate | 主动检查            | boolean |                      | true                                                                                           | 主动检查使用 HTTPS 类型检查时，是否检查远程主机的 SSL 证书。 |\n| upstream.checks.active.req_headers              | 主动检查            | array   |                      | []                                                                                             | 主动检查使用 HTTP 或 HTTPS 类型检查时，设置额外的请求头信息。 |\n| upstream.checks.active.healthy.interval         | 主动检查（健康节点）| integer | `>= 1`               | 1                                                                                              | 主动检查（健康节点）检查的间隔时间（单位为秒）|\n| upstream.checks.active.healthy.http_statuses    | 主动检查（健康节点）| array   | `200` 至 `599`       | [200, 302]                                                                                      | 主动检查（健康节点）HTTP 或 HTTPS 类型检查时，健康节点的 HTTP 状态码。 |\n| upstream.checks.active.healthy.successes        | 主动检查（健康节点）| integer | `1` 至 `254`         | 2                                                                                               | 主动检查（健康节点）确定节点健康的次数。              |\n| upstream.checks.active.unhealthy.interval       | 主动检查（非健康节点）| integer | `>= 1`               | 1                                                                                               | 主动检查（非健康节点）检查的间隔时间（单位为秒）|\n| upstream.checks.active.unhealthy.http_statuses  | 主动检查（非健康节点）| array   | `200` 至 `599`       | [429, 404, 500, 501, 502, 503, 504, 505]                                                        | 主动检查（非健康节点）HTTP 或 HTTPS 类型检查时，非健康节点的 HTTP 状态码。 |\n| upstream.checks.active.unhealthy.http_failures  | 主动检查（非健康节点）| integer | `1` 至 `254`         | 5                                                                                               | 主动检查（非健康节点）HTTP 或 HTTPS 类型检查时，确定节点非健康的次数。 |\n| upstream.checks.active.unhealthy.tcp_failures   | 主动检查（非健康节点）| integer | `1` 至 `254`         | 2                                                                                               | 主动检查（非健康节点）TCP 类型检查时，确定节点非健康的次数。 |\n| upstream.checks.active.unhealthy.timeouts       | 主动检查（非健康节点）| integer | `1` 至 `254`         | 3                                                                                               | 主动检查（非健康节点）确定节点非健康的超时次数。  |\n| upstream.checks.passive.type       | 被动检查 | string | `http` `https` `tcp`         | http                                                                                               | 被动检查的类型。  |\n| upstream.checks.passive.healthy.http_statuses   | 被动检查（健康节点）|  array   | `200` 至 `599`       | [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] | 被动检查（健康节点）HTTP 或 HTTPS 类型检查时，健康节点的 HTTP 状态码。 |\n| upstream.checks.passive.healthy.successes       | 被动检查（健康节点）|  integer | `0` 至 `254`         | 5                                                                                               | 被动检查（健康节点）确定节点健康的次数。              |\n| upstream.checks.passive.unhealthy.http_statuses | 被动检查（非健康节点）| array   | `200` 至 `599`       | [429, 500, 503]                                                                                 | 被动检查（非健康节点）HTTP 或 HTTPS 类型检查时，非健康节点的 HTTP 状态码。 |\n| upstream.checks.passive.unhealthy.tcp_failures  | 被动检查（非健康节点）| integer | `0` 至 `254`         | 2                                                                                               | 被动检查（非健康节点）TCP 类型检查时，确定节点非健康的次数。 |\n| upstream.checks.passive.unhealthy.timeouts      | 被动检查（非健康节点）| integer | `0` 至 `254`         | 7                                                                                               | 被动检查（非健康节点）确定节点非健康的超时次数。  |\n| upstream.checks.passive.unhealthy.http_failures | 被动检查（非健康节点）| integer | `0` 至 `254`         | 5                                                                                         | 被动检查（非健康节点）HTTP 或 HTTPS 类型检查时，确定节点非健康的次数。 |\n\n## 配置示例\n\n你可以通过 Admin API 在路由中启用健康检查功能：\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key\": \"remote_addr\"\n        }\n    },\n    \"upstream\": {\n         \"nodes\": {\n            \"127.0.0.1:1980\": 1,\n            \"127.0.0.1:1970\": 1\n        },\n        \"type\": \"roundrobin\",\n        \"retries\": 2,\n        \"checks\": {\n            \"active\": {\n                \"timeout\": 5,\n                \"http_path\": \"/status\",\n                \"host\": \"foo.com\",\n                \"healthy\": {\n                    \"interval\": 2,\n                    \"successes\": 1\n                },\n                \"unhealthy\": {\n                    \"interval\": 1,\n                    \"http_failures\": 2\n                },\n                \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n            },\n            \"passive\": {\n                \"healthy\": {\n                    \"http_statuses\": [200, 201],\n                    \"successes\": 3\n                },\n                \"unhealthy\": {\n                    \"http_statuses\": [500],\n                    \"http_failures\": 3,\n                    \"tcp_failures\": 3\n                }\n            }\n        }\n    }\n}'\n```\n\n启用成功后，如果 APISIX 探测到不健康的节点，将会在错误日志中输出如下日志：\n\n```shell\nenabled healthcheck passive while logging request\nfailed to receive status line from 'nil (127.0.0.1:1980)': closed\nunhealthy TCP increment (1/2) for '(127.0.0.1:1980)'\nfailed to receive status line from 'nil (127.0.0.1:1980)': closed\nunhealthy TCP increment (2/2) for '(127.0.0.1:1980'\n```\n\n:::tip 提示\n\n需要将错误日志的级别调整为 `info` 才可以观测到上述日志信息\n\n:::\n\n你可以通过[控制接口](../control-api.md) 中的 `GET /v1/healthcheck` 接口获取健康检查信息。如下所示：\n\n```shell\n\ncurl http://127.0.0.1:9090/v1/healthcheck/upstreams/healthycheck -s | jq .\n\n```\n\n## 健康检查信息\n\nAPISIX 提供了丰富的健康检查信息，其中  `status` 以及 `counter` 的返回对于健康检查是至关重要的。在 APISIX 中，节点有四个状态：`healthy`、`unhealthy`、`mostly_unhealthy`、`mostly_healthy`。`mostly_healthy` 状态表示当前节点状态是健康的，但在健康检查期间，节点健康检测并不是一直是成功的。`mostly_unhealthy` 状态表示当前节点状态是不健康的，但在健康检查期间，节点健康检测并不是一直是失败的。节点的状态转换取决于本次健康检查的成功或失败，以及 `counter` 中记录的 `tcp_failure`、`http_failure`、`success`、`timeout_failure` 四个数据。\n\n获取健康检查信息，通过以下 curl 命令可以获取健康检查信息：\n\n```shell\ncurl -i http://127.0.0.1:9090/v1/healthcheck\n```\n\n响应示例：\n\n```json\n[\n  {\n    \"nodes\": {},\n    \"name\": \"/apisix/routes/1\",\n    \"type\": \"http\"\n  },\n  {\n    \"nodes\": [\n      {\n        \"port\": 1970,\n        \"hostname\": \"127.0.0.1\",\n        \"status\": \"healthy\",\n        \"ip\": \"127.0.0.1\",\n        \"counter\": {\n          \"tcp_failure\": 0,\n          \"http_failure\": 0,\n          \"success\": 0,\n          \"timeout_failure\": 0\n        }\n      },\n      {\n        \"port\": 1980,\n        \"hostname\": \"127.0.0.1\",\n        \"status\": \"healthy\",\n        \"ip\": \"127.0.0.1\",\n        \"counter\": {\n          \"tcp_failure\": 0,\n          \"http_failure\": 0,\n          \"success\": 0,\n          \"timeout_failure\": 0\n        }\n      }\n    ],\n    \"name\": \"/apisix/routes/example-hc-route\",\n    \"type\": \"http\"\n  }\n]\n```\n\n### 状态转换图\n\n![image](../../../assets/images/health_check_node_state_diagram.png)\n\n请注意，所有节点在没有初始探测的情况下都以`healthy`状态启动，计数器仅在状态更改时重置和更新。因此，当节点处于`healthy`状态且所有后续检查都成功时，`success`计数器不会更新，保持为零。\n\n### counter 信息\n\n若健康检查失败，`counter` 中的 `success` 计数将被置零。若健康检查成功，则会将 `tcp_failure`、`http_failure`、`timeout_failure` 数据置零。\n\n| 名称            | 描述                    | 作用                                                                       |\n|----------------|------------------------|----------------------------------------------------------------------------|\n|success         | 健康检查成功的次数         |当 success 大于 healthy.successes 配置值时，节点会变为 healthy 状态               |\n|tcp_failure     | TCP 类型健康检查失败次数   |当 tcp_failure 大于 unhealthy.tcp_failures 配置值时，节点会变为 unhealthy 状态    |\n|http_failure    | HTTP 类型的健康检查失败次数 |当 http_failure 大于 unhealthy.http_failures 配置值时，节点会变为 unhealthy 状态 |\n|timeout_failure | 节点健康检查超时次数       |当 timeout_failure 大于 unhealthy.timeouts 配置值时，节点会变为 unhealthy 状态    |\n"
  },
  {
    "path": "docs/zh/latest/tutorials/keycloak-oidc.md",
    "content": "---\ntitle: Set Up SSO with Keycloak (OIDC)\nkeywords:\n  - APISIX\n  - API 网关\n  - OIDC\n  - Keycloak\ndescription: 本文介绍如何使用 openid-connect 插件，通过 authorization code grant、client credentials grant 和 password grant 将 APISIX 与 Keycloak 集成。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n[OpenID Connect (OIDC)](https://openid.net/connect/) 是 [OAuth 2.0 协议](https://www.rfc-editor.org/rfc/rfc6749) 之上的简单身份层。它允许客户端基于身份提供者执行的身份验证来验证最终用户的身份，以及以可互操作和类似 REST 的方式获取有关最终​​用户的基本个人资料信息。借助 APISIX 和 [Keycloak](https://www.keycloak.org/)，您可以实现基于 OIDC 的身份验证流程来保护您的 API 并启用单点登录 (SSO)。\n\n[Keycloak](https://www.keycloak.org/) 是适用于现代应用程序和服务的开源身份和访问管理解决方案。Keycloak 支持单点登录 (SSO)，这使得服务能够通过 OIDC 和 OAuth 2.0 等协议与 Keycloak 进行交互。此外，Keycloak 还支持将身份验证委托给第三方身份提供商，例如 Facebook 和 Google。\n\n本教程将向您展示如何使用 [`openid-connect`](/hub/openid-connect) 插件，通过 [authorization code grant](#implement-authorization-code-grant)、[client credentials grant](#implement-client-credentials-grant) 和 [password grant](#implement-password-grant) 将 APISIX 与 Keycloak 集成。\n\n## 配置 Keycloak\n\n在 Docker 中以 [开发模式](https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode) 启动一个名为 `apisix-quickstart-keycloak` 的 Keycloak 实例，管理员名称为 `quickstart-admin`，密码为 `quickstart-admin-pass`，暴露的端口映射到宿主机上的 `8080`：\n\n```shell\ndocker run -d --name \"apisix-quickstart-keycloak\" \\\n  -e 'KEYCLOAK_ADMIN=quickstart-admin' \\\n  -e 'KEYCLOAK_ADMIN_PASSWORD=quickstart-admin-pass' \\\n  -p 8080:8080 \\\n  quay.io/keycloak/keycloak:18.0.2 start-dev\n```\n\nKeycloak 提供了一个易于使用的 Web UI，帮助管理员管理所有资源，例如客户端、角色和用户。\n\n在浏览器中导航到 `http://localhost:8080` 以访问 Keycloak 网页，然后单击 __管理控制台__：\n\n![web-ui](https://static.api7.ai/uploads/2023/03/30/ItcwYPIx_web-ui.png)\n\n输入管理员用户名 `quickstart-admin` 和密码 `quickstart-admin-pass` 并登录：\n\n![admin-signin](https://static.api7.ai/uploads/2023/03/30/6W3pjzE1_admin-signin.png)\n\n您需要在以下步骤中保持登录状态来配置 Keycloak。\n\n### 创建 Realm\n\nKeycloak 中的 realm 是管理用户、凭证和角色等资源的工作区。不同领域中的资源彼此隔离。您需要为 APISIX 创建一个名为`quickstart-realm` 的 realm。\n\n在左侧菜单中，将鼠标悬停在 **Master** 上，然后在下拉菜单中选择 __Add realm__：\n\n![create-realm](https://static.api7.ai/uploads/2023/03/30/S1Xvqliv_create-realm.png)\n\n输入 realm 名称 `quickstart-realm`，然后单击 `__Create__` 进行创建：\n\n![add-realm](https://static.api7.ai/uploads/2023/03/30/jwb7QU8k_add-realm.png)\n\n### 创建 Client\n\nKeycloak 中的 client 是请求 Keycloak 对用户进行身份验证的实体。更多情况下，client 是希望使用 Keycloak 保护自身安全并提供单点登录解决方案的应用程序。APISIX 相当于负责向 Keycloak 发起身份验证请求的 client，因此您需要创建其对应的客户端，名为 `apisix-quickstart-client`。\n\n单击 __Clients__ > __Create__，打开 __Add Client__ 页面：\n\n![create-client](https://static.api7.ai/uploads/2023/03/30/qLom0axN_create-client.png)\n\n输入 __Client ID__ 为 `apisix-quickstart-client`，然后选择 __Client Protocol__ 为 `openid-connect` 并 __Save__:\n\n![add-client](https://static.api7.ai/uploads/2023/03/30/X5on2r7x_add-client.png)\n\nClient `apisix-quickstart-client` 已创建。重定向到详细信息页面后，选择 `confidential` 作为 __Access Type__:\n\n![config-client](https://static.api7.ai/uploads/2023/03/30/v70c8y9F_config-client.png)\n\n当用户在 SSO 期间登录成功时，Keycloak 会携带状态和代码将客户端重定向到 __Valid Redirect URIs__ 中的地址。为简化操作，输入通配符 `*` 以将任何 URI 视为有效：\n\n![client-redirect](https://static.api7.ai/uploads/2023/03/30/xLxcyVkn_client-redirect.png)\n\n如果您正在 [使用 PKCE authorization code grant](#implement-authorization-code-grant)，请在客户端的高级设置中配置 PKCE 质询方法：\n\n<div style={{textAlign: 'center'}}>\n<img src=\"https://static.api7.ai/uploads/2024/11/04/xvnCNb20_pkce-keycloak-revised.jpeg\" alt=\"PKCE keycloak configuration\" style={{width: '70%'}} />\n</div>\n\n如果您正在实施 [client credentials grant](#implement-client-credentials-grant)，请为 client 启用服务帐户：\n\n![enable-service-account](https://static.api7.ai/uploads/2023/12/29/h1uNtghd_sa.png)\n\n选择 __Save__ 以应用自定义配置。\n\n### 创建 User\n\nKeycloak 中的用户是能够登录系统的实体。他们可以拥有与自己相关的属性，例如用户名、电子邮件和地址。\n\n如果您只实施 [client credentials grant](#implement-client-credentials-grant)，则可以 [跳过此部分](#obtain-the-oidc-configuration)。\n\n点击 __Users__ > __Add user__ 打开 __Add user__ 页面：\n\n![create-user](https://static.api7.ai/uploads/2023/03/30/onQEp23L_create-user.png)\n\n点击 __Users__ > __Add user__ 打开 __Add user__ 页面：\n\n![add-user](https://static.api7.ai/uploads/2023/03/30/EKhuhgML_add-user.png)\n\n点击 __Credentials__，然后将 __Password__ 设置为 `quickstart-user-pass`。将 __Temporary__ 切换为 `OFF` 以关闭限制，这样您第一次登录时就无需更改密码：\n\n![user-pass](https://static.api7.ai/uploads/2023/03/30/rQKEAEnh_user-pass.png)\n\n## 获取 OIDC 配置\n\n在本节中，您将从 Keycloak 获取关键的 OIDC 配置并将其定义为 shell 变量。本节之后的步骤将使用这些变量通过 shell 命令配置 OIDC。\n\n:::info\n\n打开一个单独的终端按照步骤操作并定义相关的 shell 变量。然后本节之后的步骤可以直接使用定义的变量。\n\n:::\n\n### 获取发现端点\n\n单击 __Realm Settings__，然后右键单击 __OpenID Endpoints Configuration__ 并复制链接。\n\n![get-discovery](https://static.api7.ai/uploads/2023/03/30/526lbJbg_get-discovery.png)\n\n该链接应与以下内容相同：\n\n```text\nhttp://localhost:8080/realms/quickstart-realm/.well-known/openid-configuration\n```\n\n在 OIDC 身份验证期间需要使用此端点公开的配置值。使用您的主机 IP 更新地址并保存到环境变量：\n\n```shell\nexport KEYCLOAK_IP=192.168.42.145    # replace with your host IP\nexport OIDC_DISCOVERY=http://${KEYCLOAK_IP}:8080/realms/quickstart-realm/.well-known/openid-configuration\n```\n\n### 获取客户端 ID 和密钥\n\n单击 __Clients__ > `apisix-quickstart-client` > __Credentials__，并从 __Secret__ 复制客户端密钥：\n\n![client-ID](https://static.api7.ai/uploads/2023/03/30/MwYmU20v_client-id.png)\n\n![client-secret](https://static.api7.ai/uploads/2023/03/30/f9iOG8aN_client-secret.png)\n\n将 OIDC 客户端 ID 和密钥保存到环境变量：\n\n```shell\nexport OIDC_CLIENT_ID=apisix-quickstart-client\nexport OIDC_CLIENT_SECRET=bSaIN3MV1YynmtXvU8lKkfeY0iwpr9cH  # replace with your value\n```\n\n## 实现 Authorization Code Grant\n\nAuthorization Code Grant 由 Web 和移动应用程序使用。流程从授权服务器在浏览器中显示登录页面开始，用户可以在其中输入其凭据。在此过程中，将短期授权码交换为访问令牌，APISIX 将其存储在浏览器会话 cookie 中，并将随访问上游资源服务器的每次请求一起发送。\n\n要实现 Authorization Code Grant，请使用 `openid-connect` 插件创建一个路由，如下所示：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"bearer_only\": false,\n      \"session\": {\n        \"secret\": \"change_to_whatever_secret_you_want\"\n      },\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n或者，如果您想使用 PKCE 实现 authorization code grant，请使用 `openid-connect` 插件创建一个路由如下：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"bearer_only\": false,\n      \"session\": {\n        \"secret\": \"change_to_whatever_secret_you_want\"\n      },\n      \"use_pkce\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n### 使用有效凭证进行验证\n\n在浏览器中导航至 `http://127.0.0.1:9080/anything/test`。请求将重定向到登录页面：\n\n![test-sign-on](https://static.api7.ai/uploads/2023/03/30/i38u1x9a_validate-sign.png)\n\n使用正确的用户名 `quickstart-user` 和密码 `quickstart-user-pass` 登录。如果成功，请求将被转发到 `httpbin.org`，您应该会看到类似以下内容的响应：\n\n```json\n{\n  \"args\": {},\n  \"data\": \"\",\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"Accept\": \"text/html...\"\n    ...\n  },\n  \"json\": null,\n  \"method\": \"GET\",\n  \"origin\": \"127.0.0.1, 59.71.244.81\",\n  \"url\": \"http://127.0.0.1/anything/test\"\n}\n```\n\n### 使用无效凭证进行验证\n\n使用错误的凭证登录。您应该会看到身份验证失败：\n\n![test-sign-failed](https://static.api7.ai/uploads/2023/03/31/YOuSYX1r_validate-sign-failed.png)\n\n## 实现 Client Credential Grant\n\n在 client credential grant 中，客户端无需任何用户参与即可获得访问令牌。它通常用于机器对机器 (M2M) 通信。\n\n要实现 client credential grant，请使用 `openid-connect` 插件创建路由，以使用身份提供者的 JWKS 端点来验证令牌。端点将从发现文档中获取。\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"use_jwks\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n或者，如果您想使用自省端点来验证令牌，请按如下方式创建路由：\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"bearer_only\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n自省端点将从发现文档中获取。\n\n### 使用有效访问令牌进行验证\n\n在 [令牌端点](https://www.keycloak.org/docs/latest/securing_apps/#token-endpoint) 获取 Keycloak 服务器的访问令牌：\n\n```shell\ncurl -i \"http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token\" -X POST \\\n  -d 'grant_type=client_credentials' \\\n  -d 'client_id='$OIDC_CLIENT_ID'' \\\n  -d 'client_secret='$OIDC_CLIENT_SECRET''\n```\n\n预期响应类似于以下内容：\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoT3ludlBPY2d6Y3VWWnYtTU42bXZKMUczb0dOX2d6MFo3WFl6S2FSa1NBIn0.eyJleHAiOjE3MDM4MjU1NjQsImlhdCI6MTcwMzgyNTI2NCwianRpIjoiMWQ4NWE4N2UtZDFhMC00NThmLThiMTItNGZiYWM2ODA5YmYwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjE1OGUzOWFlLTk0YjAtNDI3Zi04ZGU3LTU3MTRhYWYwOGYzOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJjbGllbnRJZCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1hcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.TltzSXqrJuVID7aGrb35jn-oc07U_-jugSn-3jKz4A44LwtAsME_8b3qkmR4boMOIht_5pF6bnnp70MFAlg6JKu4_yIQDxF_GAHjnZXEO8OCKhtIKwXm2w-hnnJVIhIdGkIVkbPP0HfILuar_m0hpa53VpPBGYR-OS4pyh0KTUs8MB22xAEqyz9zjCm6SX9vXCqgeVkSpRW2E8NaGEbAdY25uY-ZC4dI_pON87Ey5e8GdD6HQLXQlGIOdCDi3N7k0HDoD9TZRv2bMRPfy4zVYm1ZlClIuF79A-ZBwr0c-XYuq7t6EY0gPGEXB-s0SaKlrIU5S9JBeVXRzYvqAih41g\",\"expires_in\":300,\"refresh_expires_in\":0,\"token_type\":\"Bearer\",\"not-before-policy\":0,\"scope\":\"email profile\"}\n```\n\n将访问令牌保存到环境变量：\n\n```shell\n# replace with your access token\nexport ACCESS_TOKEN=\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJoT3ludlBPY2d6Y3VWWnYtTU42bXZKMUczb0dOX2d6MFo3WFl6S2FSa1NBIn0.eyJleHAiOjE3MDM4MjU1NjQsImlhdCI6MTcwMzgyNTI2NCwianRpIjoiMWQ4NWE4N2UtZDFhMC00NThmLThiMTItNGZiYWM2ODA5YmYwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMS44Mzo4MDgwL3JlYWxtcy9xdWlja3N0YXJ0LXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjE1OGUzOWFlLTk0YjAtNDI3Zi04ZGU3LTU3MTRhYWYwOGYzOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJjbGllbnRJZCI6ImFwaXNpeC1xdWlja3N0YXJ0LWNsaWVudCIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1hcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSJ9.TltzSXqrJuVID7aGrb35jn-oc07U_-jugSn-3jKz4A44LwtAsME_8b3qkmR4boMOIht_5pF6bnnp70MFAlg6JKu4_yIQDxF_GAHjnZXEO8OCKhtIKwXm2w-hnnJVIhIdGkIVkbPP0HfILuar_m0hpa53VpPBGYR-OS4pyh0KTUs8MB22xAEqyz9zjCm6SX9vXCqgeVkSpRW2E8NaGEbAdY25uY-ZC4dI_pON87Ey5e8GdD6HQLXQlGIOdCDi3N7k0HDoD9TZRv2bMRPfy4zVYm1ZlClIuF79A-ZBwr0c-XYuq7t6EY0gPGEXB-s0SaKlrIU5S9JBeVXRzYvqAih41g\"\n```\n\n使用有效的访问令牌向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer $ACCESS_TOKEN\"\n```\n\n`HTTP/1.1 200 OK` 响应验证对上游资源的请求是否已获得授权。\n\n### 使用无效访问令牌进行验证\n\n使用无效访问令牌向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer invalid-access-token\"\n```\n\n`HTTP/1.1 401 Unauthorized` 响应验证 OIDC 插件是否拒绝了具有无效访问令牌的请求。\n\n### 验证无访问令牌\n\n向无访问令牌的路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\"\n```\n\n`HTTP/1.1 401 Unauthorized` 响应验证 OIDC 插件拒绝没有访问令牌的请求。\n\n## 实施 Password Grant\n\nPassword Grant 是一种将用户凭据交换为访问令牌的传统方法。\n\n要实施 Password Grant，请使用 `openid-connect` 插件创建路由，以使用身份提供者的 JWKS 端点来验证令牌。端点将从发现文档中获取。\n\n```shell\ncurl -i \"http://127.0.0.1:9180/apisix/admin/routes\" -X PUT -d '\n{\n  \"id\": \"auth-with-oidc\",\n  \"uri\":\"/anything/*\",\n  \"plugins\": {\n    \"openid-connect\": {\n      \"use_jwks\": true,\n      \"client_id\": \"'\"$OIDC_CLIENT_ID\"'\",\n      \"client_secret\": \"'\"$OIDC_CLIENT_SECRET\"'\",\n      \"discovery\": \"'\"$OIDC_DISCOVERY\"'\",\n      \"scope\": \"openid profile\",\n      \"redirect_uri\": \"http://localhost:9080/anything/callback\"\n    }\n  },\n  \"upstream\":{\n    \"type\":\"roundrobin\",\n    \"nodes\":{\n      \"httpbin.org:80\":1\n    }\n  }\n}'\n```\n\n### 使用有效访问令牌进行验证\n\n在 [令牌端点](https://www.keycloak.org/docs/latest/securing_apps/#token-endpoint) 获取 Keycloak 服务器的访问令牌：\n\n```shell\nOIDC_USER=quickstart-user\nOIDC_PASSWORD=quickstart-user-pass\ncurl -i \"http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token\" -X POST \\\n  -d 'grant_type=password' \\\n  -d 'client_id='$OIDC_CLIENT_ID'' \\\n  -d 'client_secret='$OIDC_CLIENT_SECRET'' \\\n  -d 'username='$OIDC_USER'' \\\n  -d 'password='$OIDC_PASSWORD''\n```\n\n预期响应类似于以下内容：\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ6U3FFaXN6VlpuYi1sRWMzZkp0UHNpU1ZZcGs4RGN3dXI1Mkx5V05aQTR3In0.eyJleHAiOjE2ODAxNjA5NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiMzQ5MTc4YjQtYmExZC00ZWZjLWFlYTUtZGY2MzJiMDJhNWY5IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTg4MTVjM2EtNmQwNy00YTY2LWJjZjItYWQ5NjdmMmIwMTFmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBpc2l4LXF1aWNrc3RhcnQtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoicXVpY2tzdGFydC11c2VyIn0.uD_7zfZv5182aLXu9-YBzBDK0nr2mE4FWb_4saTog2JTqFTPZZa99Gm8AIDJx2ZUcZ_ElkATqNUZ4OpWmL2Se5NecMw3slJReewjD6xgpZ3-WvQuTGpoHdW5wN9-Rjy8ungilrnAsnDA3tzctsxm2w6i9KISxvZrzn5Rbk-GN6fxH01VC5eekkPUQJcJgwuJiEiu70SjGnm21xDN4VGkNRC6jrURoclv3j6AeOqDDIV95kA_MTfBswDFMCr2PQlj5U0RTndZqgSoxwFklpjGV09Azp_jnU7L32_Sq-8coZd0nj5mSdbkJLJ8ZDQDV_PP3HjCP7EHdy4P6TyZ7oGvjw\",\"expires_in\":300,\"refresh_expires_in\":1800,\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0YjFiNTQ3Yi0zZmZjLTQ5YzQtYjE2Ni03YjdhNzIxMjk1ODcifQ.eyJleHAiOjE2ODAxNjI0NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiYzRjNjNlMTEtZTdlZS00ZmEzLWJlNGYtNDMyZWQ4ZmY5OTQwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJodHRwOi8vMTkyLjE2OC40Mi4xNDU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsInN1YiI6IjE4ODE1YzNhLTZkMDctNGE2Ni1iY2YyLWFkOTY3ZjJiMDExZiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2Iiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2In0.8xYP4bhDg1U9B5cTaEVD7B4oxNp8wwAYEynUne_Jm78\",\"token_type\":\"Bearer\",\"not-before-policy\":0,\"session_state\":\"b16b262e-1056-4515-a455-f25e077ccb76\",\"scope\":\"profile email\"}\n```\n\n将访问令牌和刷新令牌保存到环境变量中。刷新令牌将在刷新令牌步骤中使用。\n\n```shell\n# replace with your access token\nexport ACCESS_TOKEN=\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ6U3FFaXN6VlpuYi1sRWMzZkp0UHNpU1ZZcGs4RGN3dXI1Mkx5V05aQTR3In0.eyJleHAiOjE2ODAxNjA5NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiMzQ5MTc4YjQtYmExZC00ZWZjLWFlYTUtZGY2MzJiMDJhNWY5IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTg4MTVjM2EtNmQwNy00YTY2LWJjZjItYWQ5NjdmMmIwMTFmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBpc2l4LXF1aWNrc3RhcnQtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1xdWlja3N0YXJ0LXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6ImIxNmIyNjJlLTEwNTYtNDUxNS1hNDU1LWYyNWUwNzdjY2I3NiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoicXVpY2tzdGFydC11c2VyIn0.uD_7zfZv5182aLXu9-YBzBDK0nr2mE4FWb_4saTog2JTqFTPZZa99Gm8AIDJx2ZUcZ_ElkATqNUZ4OpWmL2Se5NecMw3slJReewjD6xgpZ3-WvQuTGpoHdW5wN9-Rjy8ungilrnAsnDA3tzctsxm2w6i9KISxvZrzn5Rbk-GN6fxH01VC5eekkPUQJcJgwuJiEiu70SjGnm21xDN4VGkNRC6jrURoclv3j6AeOqDDIV95kA_MTfBswDFMCr2PQlj5U0RTndZqgSoxwFklpjGV09Azp_jnU7L32_Sq-8coZd0nj5mSdbkJLJ8ZDQDV_PP3HjCP7EHdy4P6TyZ7oGvjw\"\nexport REFRESH_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0YjFiNTQ3Yi0zZmZjLTQ5YzQtYjE2Ni03YjdhNzIxMjk1ODcifQ.eyJleHAiOjE2ODAxNjI0NjgsImlhdCI6MTY4MDE2MDY2OCwianRpIjoiYzRjNjNlMTEtZTdlZS00ZmEzLWJlNGYtNDMyZWQ4ZmY5OTQwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguNDIuMTQ1OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJhdWQiOiJodHRwOi8vMTkyLjE2OC40Mi4xNDU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsInN1YiI6IjE4ODE1YzNhLTZkMDctNGE2Ni1iY2YyLWFkOTY3ZjJiMDExZiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2Iiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiYjE2YjI2MmUtMTA1Ni00NTE1LWE0NTUtZjI1ZTA3N2NjYjc2In0.8xYP4bhDg1U9B5cTaEVD7B4oxNp8wwAYEynUne_Jm78\"\n```\n\n使用有效的访问令牌向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer $ACCESS_TOKEN\"\n```\n\n`HTTP/1.1 200 OK` 响应验证对上游资源的请求是否已获得授权。\n\n### 使用无效访问令牌进行验证\n\n使用无效访问令牌向路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\" -H \"Authorization: Bearer invalid-access-token\"\n```\n\n`HTTP/1.1 401 Unauthorized` 响应验证 OIDC 插件是否拒绝了具有无效访问令牌的请求。\n\n### 验证无访问令牌\n\n向无访问令牌的路由发送请求：\n\n```shell\ncurl -i \"http://127.0.0.1:9080/anything/test\"\n```\n\n`HTTP/1.1 401 Unauthorized` 响应验证 OIDC 插件拒绝没有访问令牌的请求。\n\n### 刷新令牌\n\n要刷新访问令牌，请向 Keycloak 令牌端点发送请求，如下所示：\n\n```shell\ncurl -i \"http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token\" -X POST \\\n  -d 'grant_type=refresh_token' \\\n  -d 'client_id='$OIDC_CLIENT_ID'' \\\n  -d 'client_secret='$OIDC_CLIENT_SECRET'' \\\n  -d 'refresh_token='$REFRESH_TOKEN''\n```\n\n您应该看到类似以下的响应，其中包含新的访问令牌和刷新令牌，您可以将其用于后续请求和令牌刷新：\n\n```text\n{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJTdnVwLXlPMHhDdTJBVi1za2pCZ0h6SHZNaG1mcDVDQWc0NHpYb2QxVTlNIn0.eyJleHAiOjE3MzAyNzQ3NDUsImlhdCI6MTczMDI3NDQ0NSwianRpIjoiMjk2Mjk5MWUtM2ExOC00YWFiLWE0NzAtODgxNWEzNjZjZmM4IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMTUyLjU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2ZWI0ZTg0Yy00NmJmLTRkYzUtOTNkMC01YWM5YzE5MWU0OTciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcGlzaXgtcXVpY2tzdGFydC1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiNTU2ZTQyYjktMjE2Yi00NTEyLWE5ZjAtNzE3ZTAyYTQ4MjZhIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLXF1aWNrc3RhcnQtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiNTU2ZTQyYjktMjE2Yi00NTEyLWE5ZjAtNzE3ZTAyYTQ4MjZhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJxdWlja3N0YXJ0LXVzZXIifQ.KLqn1LQdazoPBqLLR856C35XpqbMO9I7WFt3KrDxZF1N8vwv4AvZYWI_2rsbdjCakh9JmPgyYRgEGufYLiDBsqy9CrMVejAIJPYsJIonIXBCp5Ysu92ODJuqtTKuuJ6K7dam7fisBFfCBbVvGspnZ3p0caedpOaF_kSd-F8ARHKVsmkuX3_ucDrP3UctjEXHezefTY4YHjNMB9wuMDPXX2vXt2BsOasnznsIHHHX-ZH8JY6eEfWPtfx0qAED6lVZICT6Rqj_j5-Cf9ogzFtLyy_XvtG9BbHME2B8AXYpxdzqxOxmVVbZdrB8elfmFjs1R3vUn2r3xA9hO_znZo_IoQ\",\"expires_in\":300,\"refresh_expires_in\":1800,\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwYWYwZTAwYy0xMThjLTRkNDktYmIwMS1iMDIwNDE3MmFjMzIifQ.eyJleHAiOjE3MzAyNzYyNDUsImlhdCI6MTczMDI3NDQ0NSwianRpIjoiZGQyZTJmYTktN2Y3Zi00MjM5LWEwODAtNWQyZDFiZTdjNzk4IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMTUyLjU6ODA4MC9yZWFsbXMvcXVpY2tzdGFydC1yZWFsbSIsImF1ZCI6Imh0dHA6Ly8xOTIuMTY4LjE1Mi41OjgwODAvcmVhbG1zL3F1aWNrc3RhcnQtcmVhbG0iLCJzdWIiOiI2ZWI0ZTg0Yy00NmJmLTRkYzUtOTNkMC01YWM5YzE5MWU0OTciLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiYXBpc2l4LXF1aWNrc3RhcnQtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjU1NmU0MmI5LTIxNmItNDUxMi1hOWYwLTcxN2UwMmE0ODI2YSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjU1NmU0MmI5LTIxNmItNDUxMi1hOWYwLTcxN2UwMmE0ODI2YSJ9.Uad4BVuojHfyxqedFT5BHliWjIqVDbjM-Xeme0G2AAg\",\"token_type\":\"Bearer\",\"not-before-policy\":0,\"session_state\":\"556e42b9-216b-4512-a9f0-717e02a4826a\",\"scope\":\"email profile\"}\n```\n"
  },
  {
    "path": "docs/zh/latest/tutorials/manage-api-consumers.md",
    "content": "---\ntitle: 管理 API 消费者\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - Rate Limit\n  - Consumer\n  - Consumer Group\ndescription: This tutorial explains how to manage your single or multiple API consumers with Apache APISIX.\n---\n\n<!--\n#\n\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n本教程介绍了如何使用 Apache APISIX 管理单个或多个 API 消费者（API Consumers）。\n\n如今，[API](https://en.wikipedia.org/wiki/API) 使得多个系统、内部服务以及第三方应用能够轻松且安全地互联。API 消费者（API consumers）对于 API 提供方来说是最重要的利益相关者之一，因为他们与 API 及开发者门户的交互最为频繁。本文将介绍如何使用开源 API 管理解决方案 [Apache APISIX](https://apisix.apache.org/) 来管理单个或多个 API 消费者。\n\n![Manage API Consumers](https://static.apiseven.com/2022/11/29/6385b565b4c11.png)\n\n## API 消费者（API Consumers）\n\nAPI 消费者是指使用某个 API 的用户，但他们并不会专门为该 API 开发应用。换句话说，API 消费者就是 API 的使用者。\n例如，市场部门可能会使用 [Facebook API](https://developers.facebook.com/docs/) 来分析社交媒体上对特定活动的反馈，他们会在需要时向提供的 API 发送独立且不定期的请求。\n\n一个 [API 管理](https://en.wikipedia.org/wiki/API_management) 解决方案需要能够识别谁是 API 的消费者，以便针对不同的消费者配置不同的规则。\n\n## Apache APISIX 中的消费者（Consumers）\n\n在 Apache APISIX 中，[Consumer 对象](https://apisix.apache.org/zh/docs/apisix/terminology/consumer/) 是 API 消费者访问通过 [API 网关（API Gateway）](https://apisix.apache.org/zh/docs/apisix/terminology/api-gateway/) 发布的 API 的主要方式。\n当不同的消费者请求同一个 API，而你需要针对不同消费者执行不同的 [插件（Plugin）](https://apisix.apache.org/zh/docs/apisix/terminology/plugin/) 或 [上游（Upstream）](https://apisix.apache.org/zh/docs/apisix/terminology/upstream/) 配置时，Consumer 概念会非常有用。\n\n通过 Apache APISIX API 网关发布 API 后，可以轻松使用消费者密钥（consumer key，也称订阅密钥 subscription key）来保护 API 访问。\n需要使用已发布 API 的开发者必须在调用这些 API 的 `HTTP` 请求中包含有效的订阅密钥。若订阅密钥无效，API 网关会立即拒绝请求，而不会将其转发到后端服务。\n\n消费者可以关联不同的作用范围：按插件、所有 API 或单个 API。\n在 API 网关中，结合插件使用消费者对象可以实现多种场景：\n\n1. 为不同消费者启用不同的认证方式。\n   当消费者尝试通过不同认证机制（如 [API key](https://apisix.apache.org/zh/docs/apisix/plugins/key-auth/)、[Basic](https://apisix.apache.org/zh/docs/apisix/plugins/basic-auth/)、或基于 [JWT](https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth/) 的认证）访问 API 时，这种机制非常有用。\n2. 限制特定消费者对 API 资源的访问。\n3. 根据消费者将请求路由到相应的后端服务。\n4. 定义数据消费的速率限制。\n5. 分析单个消费者或消费者子集的数据使用情况。\n\n## Apache APISIX Consumer 示例\n\n下面我们来看一个示例，演示如何结合 [key-auth](https://apisix.apache.org/zh/docs/apisix/plugins/key-auth/) 认证插件（API Key）与 [limit-count](https://apisix.apache.org/zh/docs/apisix/plugins/limit-count/) 插件，为单个消费者或一组消费者配置限流策略。\n\n在本示例中，我们将使用一个基于 [ASP.NET Core Web API](https://learn.microsoft.com/en-us/aspnet/core/?view=aspnetcore-7.0) 的 [示例项目](https://github.com/Boburmirzo/apisix-api-consumers-management)，该项目包含一个简单的 `GET` 接口，用于获取商品列表。\n项目的运行方式可在其 [README 文件](https://github.com/Boburmirzo/apisix-api-consumers-management#readme) 中找到详细说明。\n\n### 为单个消费者启用限流（Rate Limiting）\n\n假设此时示例项目已经启动运行。\n要将消费者对象与上述两个插件配合使用，我们需要执行以下步骤：\n\n1. 创建一个新的 **Consumer（消费者）**。\n2. 为该消费者配置认证插件 `key-auth` 和限流插件 `limit-count`。\n3. 创建新的 **Route（路由）**，并设置路由规则（如有需要）。\n4. 为该路由启用 `key-auth` 插件配置。\n\n以上步骤只需通过两条 [curl 命令](https://en.wikipedia.org/wiki/CURL) 调用 APISIX 的 [Admin API](https://apisix.apache.org/zh/docs/apisix/admin-api/) 即可完成。\n\n第一条命令创建一个启用了 API Key 认证的 **新消费者**，并配置限流规则：\n该消费者在 60 秒内最多只能调用产品 API 两次。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n   \"username\":\"consumer1\",\n   \"plugins\":{\n      \"key-auth\":{\n         \"key\":\"auth-one\"\n      },\n      \"limit-count\":{\n         \"count\":2,\n         \"time_window\":60,\n         \"rejected_code\":403,\n         \"rejected_msg\":\"Requests are too many, please try again later or upgrade your subscription plan.\",\n         \"key\":\"remote_addr\"\n      }\n   }\n}'\n```\n\n接下来，我们定义一个新的 **Route（路由）** 与 **Upstream（上游）**，\n使得所有到达网关端点 `/api/products` 的请求在通过认证后，都会被转发到示例项目的产品服务。\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"name\": \"Route for consumer request rate limiting\",\n  \"methods\": [\n    \"GET\"\n  ],\n  \"uri\": \"/api/products\",\n  \"plugins\": {\n      \"key-auth\": {}\n  },\n  \"upstream\": {\n    \"type\": \"roundrobin\",\n    \"nodes\": {\n      \"productapi:80\": 1\n    }\n  }\n}'\n```\n\n在此配置下，Apache APISIX 将正常处理前两次请求，\n但在相同的 60 秒时间窗口内的 **第三次请求** 将返回一个 `403` HTTP 状态码。\n\n```shell\ncurl http://127.0.0.1:9080/api/products -H 'apikey: auth-one' -i\n```\n\n如果在 60 秒内连续调用三次接口，示例输出如下：\n\n```shell\nHTTP/1.1 403 Forbidden\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\nServer: APISIX/2.13.1\n\n{\"error_msg\":\"Requests are too many, please try again later or upgrade your subscription plan.\"}\n```\n\n当请求次数达到阈值后，APISIX 将拒绝后续请求。\n\n### 为消费者组启用限流（Rate Limiting for Consumer Groups）\n\n在 Apache APISIX 中，[Consumer Group（消费者组）](https://apisix.apache.org/zh/docs/apisix/terminology/consumer-group/) 对象用于管理开发者对后端服务的可见性。\n后端服务首先对特定组可见，然后组内的开发者即可查看并订阅与该组关联的产品。\n\n借助消费者组，你可以为一组消费者定义多级限流策略，而无需逐个管理每个消费者。\n\n典型场景包括：\n\n* API 商业化中的不同定价策略，例如“Basic 套餐”的消费者每分钟允许调用 50 次 API；\n* 或根据用户角色（管理员、开发者、访客等）启用不同的 API 权限访问。\n\n你可以通过 Apache APISIX 的管理 REST API 中的 [Consumer Group 实体](https://apisix.apache.org/zh/docs/apisix/admin-api/#consumer-group) 来创建、更新、删除和管理消费者组。\n\n#### Consumer groups 示例\n\n为了演示，我们将分别为 **Basic（基础）** 和 **Premium（高级）** 两种套餐创建两个消费者组（Consumer Group）。\n我们可以为每个组添加一个或两个消费者，并通过 `rate-limiting` 插件来控制来自不同消费者组的流量。\n\n要在限流场景中使用消费者组，你需要执行以下步骤：\n\n* 创建一个或多个启用了 `limit-count` 插件的消费者组。\n* 创建消费者（Consumers），并将它们分配到对应的组中。\n\n下面的两条 `curl` 命令用于分别创建名为 `basic_plan` 和 `premium_plan` 的消费者组：\n\n**创建 Basic Plan（基础套餐）的消费者组**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumer_groups/basic_plan -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 403,\n            \"group\": \"basic_plan\"\n        }\n    }\n}'\n```\n\n**创建 Premium Plan（高级套餐）的消费者组**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumer_groups/premium_plan -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 200,\n            \"time_window\": 60,\n            \"rejected_code\": 403,\n            \"group\": \"premium_plan\"\n        }\n    }\n}'\n```\n\n在上述步骤中，我们为 **Basic Plan** 设置了限流规则：每 60 秒内仅允许 **2 次请求**；\n而 **Premium Plan** 则允许在相同时间窗口内执行 **200 次 API 请求**。\n\n**创建并将第一个消费者加入 Basic 组**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"consumer1\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-one\"\n        }\n    },\n    \"group_id\": \"basic_plan\"\n}'\n```\n\n**创建并将第二个消费者加入 Premium 组**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"consumer2\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-two\"\n        }\n    },\n    \"group_id\": \"premium_plan\"\n}'\n```\n\n**创建并将第三个消费者加入 Premium 组**\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/consumers -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"username\": \"consumer3\",\n    \"plugins\": {\n        \"key-auth\": {\n            \"key\": \"auth-three\"\n        }\n    },\n    \"group_id\": \"premium_plan\"\n}'\n```\n\n之后，我们可以验证限流效果：\n属于 **Basic Plan** 组的第一个消费者 `consumer1` 在 1 分钟内调用 API 超过 2 次后，将收到 **403 HTTP 状态码错误**；\n而属于 **Premium Plan** 组的其他两个消费者则可继续请求，直到达到各自的请求上限。\n\n你可以通过在请求头中更换认证密钥来执行以下命令进行测试：\n\n```shell\ncurl -i http://127.0.0.1:9080/api/products -H 'apikey: auth-one'\n```\n\n```shell\ncurl -i http://127.0.0.1:9080/api/products -H 'apikey: auth-two'\n```\n\n```shell\ncurl -i http://127.0.0.1:9080/api/products -H 'apikey: auth-three'\n```\n\n请注意，你还可以在任意时刻将消费者添加到或移出消费者组，并启用其他内置插件。\n\n## 更多教程\n\n阅读我们的其他 [教程](./expose-api.md)，以了解更多有关 **API 管理（API Management）** 的内容。\n"
  },
  {
    "path": "docs/zh/latest/tutorials/observe-your-api.md",
    "content": "---\ntitle: 监控 API\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 可观测性\n  - 监控\n  - 插件\ndescription: 本文介绍了 API 网关 Apache APISIX 可观察性插件并了解如何设置这些插件。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nAPISIX 中提供了很多具有丰富功能的可观测性插件。你可以通过使用和设置这些插件，来了解 API 行为，进而使整个业务流程更加清晰。\n\n## API 可观测性\n\n**API 可观测性**已经成为 API 开发的一部分，因为它解决了与 API 一致性、可靠性和快速迭代 API 功能的相关问题。可观测性可分为三个关键部分：日志、指标、链路追踪，接下来让我们逐个了解它们。\n\n![Observability of three key areas](https://static.apiseven.com/2022/09/14/6321cf14c555a.jpg)\n\n## 前提条件\n\n在进行该教程之前，请确保你已经[公开服务](./expose-api.md)。\n\n## 日志\n\n在 APISIX 中，**日志**可分为访问日志和错误日志。访问日志主要记录了每个请求的上下文信息，错误日志则是 APISIX 运行打印的日志信息，包括 NGINX 和插件相关的信息。APISIX 的日志存储在 `./apisix/logs/` 目录下。当然你可以通过一些 APISIX 的日志插件，将 APISIX 的日志发送到指定的日志服务中，APISIX 提供了以下插件：\n\n- [http-logger](../plugins/http-logger.md)\n- [skywalking-logger](../plugins/skywalking-logger.md)\n- [tcp-logger](../plugins/tcp-logger.md)\n- [kafka-logger](../plugins/kafka-logger.md)\n- [rocketmq-logger](../plugins/rocketmq-logger.md)\n- [udp-logger](../plugins/udp-logger.md)\n- [clickhouse-logger](../plugins/clickhouse-logger.md)\n- [error-logger](../plugins/error-log-logger.md)\n- [google-cloud-logging](../plugins/google-cloud-logging.md)\n\n你可以在 APISIX [插件中心](../plugins/http-logger.md) 查看 APISIX 支持的所有日志插件。接下来我们将使用 `http-logger` 插件为你演示如何将 APISIX 的日志数据发送到 HTTP/HTTPS 服务器中。\n\n:::note 注意\n\n你可以使用 [mockbin.com](https://mockbin.org/) 生成一个模拟的 HTTP 服务器来存储和查看日志。\n\n:::\n\n以下示例展示了在指定路由上启动 `http-logger` 的示例。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\n\ncurl http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"plugins\": {\n    \"http-logger\": {\n      \"uri\": \"http://mockbin.org/bin/5451b7cd-af27-41b8-8df1-282ffea13a61\"\n    }\n  },\n  \"upstream_id\": \"1\",\n  \"uri\": \"/get\"\n}'\n\n```\n\n:::note 注意\n\n你可以通过修改 `uri` 属性，将上述 `http-logger` 的服务器地址更换为你的服务器地址：\n\n```json\n{\n   \"uri\": \"http://mockbin.org/bin/5451b7cd-af27-41b8-8df1-282ffea13a61\"\n}\n```\n\n:::\n\n创建成功后，你可以通过以下命令向 `get` 端点发送请求以生成日志。\n\n```shell\ncurl -i http://127.0.0.1:9080/get\n```\n\n请求成功后，你可以单击[模拟服务器链接](http://mockbin.org/bin/5451b7cd-af27-41b8-8df1-282ffea13a61/log)查看访问日志。\n\n![http-logger-plugin-test-screenshot](https://static.apiseven.com/2022/09/14/6321d1d83eb7a.png)\n\n## 指标\n\n**指标**是在⼀段时间内测量的数值。与⽇志不同，指标在默认情况下是结构化的，这使得查询和优化存储变得更加容易。而 APISIX 也提供了 [Prometheus](../plugins/prometheus.md) 的插件来获取你的 API 指标，并在 Prometheus 中暴露它们。通过使用 APISIX 提供的 Grafana 仪表板元数据，并从 Prometheus 中获取指标，更加方便地监控你的 API。\n\n你可以通过以下命令启用 `prometheus` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"uri\": \"/get\",\n  \"plugins\": {\n    \"prometheus\": {}\n  },\n  \"upstream_id\": \"1\"\n}'\n```\n\n启用成功后，你可以通过 `/apisix/prometheus/metrics` 接口获取 APISIX 的指标。\n\n```shell\ncurl -i http://127.0.0.1:9091/apisix/prometheus/metrics\n```\n\n返回结果如下：\n\n```text\nHTTP/1.1 200 OK\nServer: openresty\nContent-Type: text/plain; charset=utf-8\nTransfer-Encoding: chunked\nConnection: keep-alive\n\n# HELP apisix_batch_process_entries batch process remaining entries\n# TYPE apisix_batch_process_entries gauge\napisix_batch_process_entries{name=\"http logger\",route_id=\"1\",server_addr=\"172.19.0.8\"} 0\n# HELP apisix_etcd_modify_indexes Etcd modify index for APISIX keys\n# TYPE apisix_etcd_modify_indexes gauge\napisix_etcd_modify_indexes{key=\"consumers\"} 17819\napisix_etcd_modify_indexes{key=\"global_rules\"} 17832\napisix_etcd_modify_indexes{key=\"max_modify_index\"} 20028\napisix_etcd_modify_indexes{key=\"prev_index\"} 18963\napisix_etcd_modify_indexes{key=\"protos\"} 0\napisix_etcd_modify_indexes{key=\"routes\"} 20028\n...\n```\n\n你还可以通过 `http://localhost:9090/targets` 在 Prometheus 仪表板上查看端点的状态。\n\n![plu​​gin-orchestration-configure-rule-screenshot](https://static.apiseven.com/2022/09/14/6321d30b32024.png)\n\n如上图，APISIX 公开的指标端点已启动并正在运行。\n\n现在，你可以查询 `apisix_http_status` 的指标，查看 APISIX 处理了哪些 HTTP 请求及其结果。\n\n![prometheus-plugin-dashboard-query-http-status-screenshot](https://static.apiseven.com/2022/09/14/6321d30aed3b2.png)\n\n除此之外，你还可以查看在本地实例中运行的 Grafana 仪表板。请访问 `http://localhost:3000/`。\n\n![prometheus-plugin-grafana-dashboard-screenshot](https://static.apiseven.com/2022/09/14/6321d30bba97c.png)\n\n目前，APISIX 还提供了其他两个关于指标的插件：\n\n- [Node status 插件](../plugins/node-status.md)(https://apisix.apache.org/docs/apisix/plugins/node-status/)\n- [Datadog 插件](../plugins/datadog.md)\n\n## 链路追踪\n\n**链路追踪**就是将一次请求还原成调用链路，并将该请求的调用情况使用拓扑的方式展现，比如展示各个微服务节点上的耗时，请求具体经过了哪些服务器以及每个服务节点的请求状态等内容。\n\n[Zipkin](https://zipkin.io/) 一个开源的分布式追踪系统。APISIX 的[zipkin 插件](../plugins/zipkin.md) 支持根据 [Zipkin API 规范](https://zipkin.io/pages/instrumenting.html) 收集链路信息并报告给 Zipkin Collector。\n\n:::tip 提示\n\n使用该插件前，请确保你已经有一个正在运行的 Zipkin 实例。你可以使用 Docker 快速启动一个 Zipkin 实例：\n\n```\ndocker run -d -p 9411:9411 openzipkin/zipkin\n```\n\n:::\n\n你可以通过如下示例，在指定路由中启用 `zipkin` 插件：\n\n```shell\ncurl http://127.0.0.1:9180/apisix/admin/routes/1  \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n  \"methods\": [\n    \"GET\"\n  ],\n  \"uri\": \"/get\",\n  \"plugins\": {\n    \"zipkin\": {\n      \"endpoint\": \"http://127.0.0.1:9411/api/v2/spans\",\n      \"sample_ratio\": 1\n    }\n  },\n  \"upstream_id\": \"1\"\n}'\n```\n\n你可以通过以下命令请求 APISIX：\n\n```shell\ncurl -i http://127.0.0.1:9080/get\n```\n\n如下所示，返回结果中的 `header` 部分附加了一些额外的跟踪标识符（TraceId、SpanId 和 ParentId）：\n\n```text\n\"X-B3-Parentspanid\": \"61bd3f4046a800e7\",\n\"X-B3-Sampled\": \"1\",\n\"X-B3-Spanid\": \"855cd5465957f414\",\n\"X-B3-Traceid\": \"e18985df47dab632d62083fd96626692\",\n```\n\n你可以通过访问 `http://127.0.0.1:9411/zipkin`，在 Zipkin 的 Web UI 上看到请求链路。\n\n![Zipkin plugin output 1](https://static.apiseven.com/2022/09/14/6321dc27f3d33.png)\n\n![Zipkin plugin output 2](https://static.apiseven.com/2022/09/14/6321dc284049c.png)\n\n你也可以通过另外两个插件进行链路追踪：\n\n- [Skywalking 插件](../plugins/skywalking.md)\n\n- [OpenTelemetry 插件](../plugins/opentelemetry.md)\n\n## 总结\n\nAPI 可观测性是一种用于在 API 世界中管理应用程序的框架，APISIX 的插件可以通过集成到多个可观测性平台来帮助你监控 API，让你更专注于开发核心业务功能，无需为集成多个可观测性应用花费更多时间。\n"
  },
  {
    "path": "docs/zh/latest/tutorials/protect-api.md",
    "content": "---\ntitle: 保护 API\nkeywords:\n  - API 网关\n  - Apache APISIX\n  - 发布路由\n  - 创建服务\ndescription: 本文介绍了如何通过 Apache APISIX 发布服务和路由。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## 描述\n\n本文将为你介绍使用限流限速和安全插件保护你的 API。\n\n## 概念介绍\n\n### 插件\n\n[Plugin](../terminology/plugin.md) 也称之为插件，它是扩展 APISIX 应用层能力的关键机制，也是在使用 APISIX 时最常用的资源对象。插件主要是在 HTTP 请求或响应生命周期期间执行的、针对请求的个性化策略。插件可以与路由、服务或消费者绑定。\n\n:::note 注意\n\n如果 [路由](../terminology/route.md)、[服务](../terminology/service.md)、[插件配置](../terminology/plugin-config.md) 或消费者都绑定了相同的插件，则只有一份插件配置会生效，插件配置的优先级由高到低顺序是：消费者 > 路由 > 插件配置 > 服务。同时在插件执行过程中也会涉及 6 个阶段，分别是 `rewrite`、`access`、`before_proxy`、`header_filter`、`body_filter` 和 `log`。\n\n:::\n\n## 前提条件\n\n在进行该教程前，请确保你已经[公开服务](./expose-api.md)。\n\n## 保护 API\n\n在很多时候，我们的 API 并不是处于一个非常安全的状态，它随时会收到不正常的访问，一旦访问流量突增，可能就会导致你的 API 发生故障，产生不必要的损失。因此你可以通过速率限制保护你的 API 服务，限制非正常的访问请求，保障 API 服务的稳定运行。对此，我们可以使用如下方式进行：\n\n1. 限制请求速率；\n2. 限制单位时间内的请求数；\n3. 延迟请求；\n4. 拒绝客户端请求；\n5. 限制响应数据的速率。\n\n为了实现上述功能，APISIX 提供了多个限流限速的插件，包括 [limit-conn](../plugins/limit-conn.md)、[limit-count](../plugins/limit-count.md) 和 [limit-req](../plugins/limit-req.md)。\n\n- `limit-conn` 插件主要用于限制客户端对服务的并发请求数。\n- `limit-req` 插件使用漏桶算法限制对用户服务的请求速率。\n- `limit-count` 插件主要用于在指定的时间范围内，限制每个客户端总请求个数。\n\n接下来，我们将以 `limit-count` 插件为例，为你介绍如何通过限流限速插件保护你的 API。\n\n1. 创建路由。\n\n:::note\n\n您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n```\n\n:::\n\n```shell\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"limit-count\": {\n            \"count\": 2,\n            \"time_window\": 60,\n            \"rejected_code\": 503,\n            \"key_type\": \"var\",\n            \"key\": \"remote_addr\"\n        }\n    },\n  \"upstream_id\": \"1\"\n}'\n\n```\n\n以上配置中，使用了[公开服务](./expose-api.md)中创建的上游创建了一个 ID 为 `1` 的路由， ，并且启用了 `limit-count` 插件。该插件仅允许客户端在 60 秒内，访问上游服务 2 次，超过两次，则会返回 `503` 错误码。\n\n2. 测试插件。\n\n```shell\n\ncurl http://127.0.0.1:9080/index.html\n\n```\n\n使用上述命令连续访问三次后，则会出现如下错误。\n\n```\n<html>\n<head><title>503 Service Temporarily Unavailable</title></head>\n<body>\n<center><h1>503 Service Temporarily Unavailable</h1></center>\n<hr><center>openresty</center>\n</body>\n</html>\n```\n\n返回上述结果，则表示 `limit-count` 插件已经配置成功。\n\n## 流量控制插件\n\nAPISIX 除了提供限流限速的插件外，还提供了很多其他的关于 **traffic** 插件来满足实际场景的需求：\n\n- [proxy-cache](../plugins/proxy-cache.md)：该插件提供缓存后端响应数据的能力，它可以和其他插件一起使用。该插件支持基于磁盘和内存的缓存。\n- [request-validation](../plugins/request-validation.md)：该插件用于提前验证向上游服务转发的请求。\n- [proxy-mirror](../plugins/proxy-mirror.md)：该插件提供了镜像客户端请求的能力。流量镜像是将线上真实流量拷贝到镜像服务中，以便在不影响线上服务的情况下，对线上流量或请求内容进行具体的分析。\n- [api-breaker](../plugins/api-breaker.md)：该插件实现了 API 熔断功能，从而帮助我们保护上游业务服务。\n- [traffic-split](../plugins/traffic-split.md)：该插件使用户可以逐步引导各个上游之间的流量百分比。，你可以使用该插件实现蓝绿发布，灰度发布。\n- [request-id](../plugins/request-id.md)：该插件通过 APISIX 为每一个请求代理添加 `unique` ID 用于追踪 API 请求。\n- [proxy-control](../plugins/proxy-control.md)：该插件能够动态地控制 NGINX 代理的相关行为。\n- [client-control](../plugins/client-control.md)：该插件能够通过设置客户端请求体大小的上限来动态地控制 NGINX 处理客户端的请求。\n\n## 更多操作\n\n你可以参考[监控 API](./observe-your-api.md) 文档，对 APISIX 进行监控，日志采集，链路追踪等。\n"
  },
  {
    "path": "docs/zh/latest/upgrade-guide-from-2.15.x-to-3.0.0.md",
    "content": "---\ntitle: 升级指南\nkeywords:\n  - APISIX\n  - APISIX 升级指南\n  - APISIX 版本升级\ndescription: 本文档将引导你了解如何升级 APISIX 版本。\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## APISIX 的版本升级方式\n\nAPISIX 的版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。\n\n升级到 APISIX 3.0.0 是一个重大的版本升级，我们建议您先升级到 2.15.x，然后再升级到 3.0.0。\n\n## 从 2.15.x 升级到 3.0.0\n\n### 升级注意事项和重大更新\n\n在升级之前，请查看 [3.0.0-beta](./CHANGELOG.md#300-beta) 和 [3.0.0](./CHANGELOG.md#300) 中的 Change 部分，以了解 3.0.0 版本的不兼容的修改与重大更新。\n\n#### 部署\n\n基于 alpine 的镜像已不再支持，如果你使用了 alpine 的镜像，那么你需要将镜像替换为基于 debian/centos 的镜像。\n\n目前，我们提供了：\n\n- 基于 debian/centos 的镜像，你可以在 [DockerHub](https://hub.docker.com/r/apache/apisix/tags?page=1&ordering=last_updated) 上找到它们\n- CentOS 7 和 CentOS 8 的 RPM 包，支持 AMD64 和 ARM64 架构，可参考文章[通过 RPM 仓库安装](./installation-guide.md#通过-rpm-仓库安装)\n- Debian 11(bullseye) 的 DEB 包，支持 AMD64 和 ARM64 架构，可参考文章[通过 DEB 仓库安装](./installation-guide.md#通过-deb-仓库安装)\n\n3.0.0 对部署模式进行了重大更新，具体如下：\n\n- 支持数据面与控制面分离的部署模式，具体可参考 [Decoupled](../../en/latest/deployment-modes.md#decoupled)\n- 如在使用中仍需沿用原来的部署模式，那么可以使用部署模式中的 `traditional` 模式，并且更新配置文件，具体可参考 [Traditional](../../en/latest/deployment-modes.md#traditional)\n- 支持 Standalone 模式，需要更新配置文件，具体可参考 [Standalone](../../en/latest/deployment-modes.md#standalone)\n\n#### 依赖项\n\n如果你使用提供的二进制包（Debian 和 RHEL）或者镜像，则它们已经捆绑了 APISIX 所有必要的依赖项，你可以跳过本节。\n\nAPISIX 的一些特性需要在 OpenResty 中引入额外的 NGINX 模块。如果要使用这些功能，你需要构建一个自定义的 OpenResty 发行版（APISIX-Runtime）。你可以参考 [api7/apisix-build-tools](https://github.com/api7/apisix-build-tools) 中的代码，构建自己的 APISIX-Runtime 环境。\n\n如果你希望 APISIX 运行在原生的 OpenResty 上，这种情况下将只支持运行在 OpenResty 1.19.3.2 及以上的版本。\n\n#### 迁移\n\n##### 静态配置迁移\n\nAPISIX 的配置方式是用自定义的 `conf/config.yaml` 中的内容覆盖默认的 `conf/config-default.yaml`，如果某个配置项在 `conf/config.yaml` 中不存在，那么就使用 `conf/config-default.yaml` 中的配置。在 3.0.0 中，我们调整了 `conf/config-default.yaml` 配置文件中的部分细节，具体内容如下。\n\n###### 移动配置项\n\n从 2.15.x 到 3.0.0 版本，在 `conf/config-default.yaml` 有一些配置项的位置被移动了。如果你使用了这些配置项，那么你需要将它们移动到新的位置。\n\n调整内容：\n\n  * `config_center` 功能改由 `deployment` 中的 `config_provider` 实现\n  * `etcd` 字段整体迁移到 `deployment` 中\n  * 以下的 Admin API 配置移动到 `deployment` 中的 `admin` 字段\n    - admin_key\n    - enable_admin_cors\n    - allow_admin\n    - admin_listen\n    - https_admin\n    - admin_api_mtls\n    - admin_api_version\n\n你可以在 `conf/config-default.yaml` 中找到这些配置的新的确切位置。\n\n###### 更新配置项\n\n某些配置在 3.0.0 中被移除了，并被新的配置项替代。如果你使用了这些配置项，那么你需要将它们更新为新的配置项。\n\n调整内容：\n\n  * 去除 `apisix.ssl.enable_http2` 和 `apisix.ssl.listen_port`，使用 `apisix.ssl.listen` 替代。\n\n  如果在 `conf/config.yaml` 中有这样的配置：\n\n  ```yaml\n    ssl:\n      enable_http2: true\n      listen_port: 9443\n  ```\n\n  则在 3.0.0 版本中需要转换成如下所示：\n\n  ```yaml\n    ssl:\n      listen:\n        - port: 9443\n          enable_http2: true\n  ```\n\n  * 去除 `nginx_config.http.lua_shared_dicts`，用 `nginx_config.http.custom_lua_shared_dict` 替代，这个配置用于声明自定义插件的共享内存。\n\n  如果在 `conf/config.yaml` 中有这样的配置：\n\n  ```yaml\n  nginx_config:\n    http:\n      lua_shared_dicts:\n        my_dict: 1m\n  ```\n\n  则在 3.0.0 版本中需要转换成如下所示：\n\n  ```yaml\n  nginx_config:\n    http:\n      custom_lua_shared_dict:\n        my_dict: 1m\n  ```\n\n  * 去除 `etcd.health_check_retry`，用 `deployment.etcd.startup_retry` 替代，这个配置用于在启动时，重试连接 etcd 的次数。\n\n  如果在 `conf/config.yaml` 中有这样的配置：\n\n  ```yaml\n  etcd:\n    health_check_retry: 2\n  ```\n\n  则在 3.0.0 版本中需要转换成如下所示：\n\n  ```yaml\n  deployment:\n    etcd:\n      startup_retry: 2\n  ```\n\n  * 去除 `apisix.port_admin`，用 `deployment.apisix.admin_listen` 替代。\n\n  如果在 `conf/config.yaml` 中有这样的配置：\n\n  ```yaml\n  apisix:\n    port_admin: 9180\n  ```\n\n  则在 3.0.0 中需要转换成如下所示：\n\n  ```yaml\n  deployment:\n    apisix:\n      admin_listen:\n        ip: 127.0.0.1 # 替换成实际暴露的 IP\n        port: 9180\n  ```\n\n  * 修改 `enable_cpu_affinity` 的默认值为 `false`。主要是因为越来越多的用户通过容器部署 APISIX，由于 Nginx 的 worker_cpu_affinity 不计入 cgroup，默认启用 worker_cpu_affinity 会影响 APISIX 的行为，例如多个实例会被绑定到一个 CPU 上。为了避免这个问题，我们在 `conf/config-default.yaml` 中默认禁用 `enable_cpu_affinity` 选项。\n  * 去除 `apisix.real_ip_header`，用 `nginx_config.http.real_ip_header` 替代\n\n##### 数据迁移\n\n如果你需要备份与恢复数据，可以利用 ETCD 的备份与恢复功能，参考 [etcdctl snapshot](https://etcd.io/docs/v3.5/op-guide/maintenance/#snapshot-backup)。\n\n#### 数据兼容\n\n在 3.0.0 中，我们调整了部分数据结构，这些调整影响到 APISIX 的路由、上游、插件等数据。3.0.0 版本与 2.15.x 版本之间数据不完全兼容。因此，你无法使用 3.0.0 版本的 APISIX 直接连接到 2.15.x 版本 APISIX 使用的 ETCD 集群。\n\n为了保持数据兼容，有两种方式，仅供参考：\n\n  1. 梳理 ETCD 中的数据，将不兼容的数据备份然后清除，将备份的数据结构转换成 3.0.0 版本的数据结构，通过 3.0.0 版本的 Admin API 来恢复数据\n  2. 梳理 ETCD 中的数据，编写脚本，将 2.15.x 版本的数据结构批量转换成 3.0.0 版本的数据结构\n\n数据层面调整内容如下。\n\n  * 将插件配置的元属性 `disable` 移动到 `_meta` 中。\n\n  `disable` 表示该插件的启用/禁用状态，如果在 ETCD 中存在这样的数据结构：\n\n  ```json\n  {\n      \"plugins\":{\n          \"limit-count\":{\n              ... // 插件配置\n              \"disable\":true\n          }\n      }\n  }\n  ```\n\n  则在 3.0.0 版本中，这个插件的数据结构应该变成如下所示：\n\n  ```json\n  {\n      \"plugins\":{\n          \"limit-count\":{\n              ... // 插件配置\n              \"_meta\":{\n                  \"disable\":true\n              }\n          }\n      }\n  }\n  ```\n\n  注意：`disable` 是插件的元配置，该调整对所有插件配置生效，不仅仅是 `limit-count` 插件。\n\n  * 去除路由的 `service_protocol` 字段，使用 `upstream.scheme` 替代。\n\n  如果在 ETCD 中存在这样的数据结构：\n\n  ```json\n  {\n      \"uri\":\"/hello\",\n      \"service_protocol\":\"grpc\",\n      \"upstream\":{\n          \"type\":\"roundrobin\",\n          \"nodes\":{\n              \"127.0.0.1:1980\":1\n          }\n      }\n  }\n  ```\n\n  则在 3.0.0 版本中，这个路由的数据结构应该变成如下所示：\n\n  ```json\n  {\n      \"uri\":\"/hello\",\n      \"upstream\":{\n          \"type\":\"roundrobin\",\n          \"scheme\":\"grpc\",\n          \"nodes\":{\n              \"127.0.0.1:1980\":1\n          }\n      }\n  }\n  ```\n\n  * 去除 `authz-keycloak` 插件中的 `audience` 字段，使用 `client_id` 替代。\n\n  如果在 ETCD 中 `authz-keycloak` 的插件配置存在这样的数据结构：\n\n  ```json\n  {\n      \"plugins\":{\n          \"authz-keycloak\":{\n              ... // 插件配置\n              \"audience\":\"Client ID\"\n          }\n      }\n  }\n  ```\n\n  则在 3.0.0 中，这个路由的数据结构应该变成如下所示：\n\n  ```json\n  {\n      \"plugins\":{\n          \"authz-keycloak\":{\n              ... // 插件配置\n              \"client_id\":\"Client ID\"\n          }\n      }\n  }\n  ```\n\n  * 去除 `mqtt-proxy` 插件中的 `upstream`，在插件外部配置 `upstream`，并在插件中引用。\n\n  如果在 ETCD 中 `mqtt-proxy` 的插件配置存在这样的数据结构：\n\n  ```json\n  {\n      \"remote_addr\":\"127.0.0.1\",\n      \"plugins\":{\n          \"mqtt-proxy\":{\n              \"protocol_name\":\"MQTT\",\n              \"protocol_level\":4,\n              \"upstream\":{\n                  \"ip\":\"127.0.0.1\",\n                  \"port\":1980\n              }\n          }\n      }\n  }\n  ```\n\n  则在 3.0.0 版本中，这个插件的数据结构应该变成如下所示：\n\n  ```json\n  {\n      \"remote_addr\":\"127.0.0.1\",\n      \"plugins\":{\n          \"mqtt-proxy\":{\n              \"protocol_name\":\"MQTT\",\n              \"protocol_level\":4\n          }\n      },\n      \"upstream\":{\n          \"type\":\"chash\",\n          \"key\":\"mqtt_client_id\",\n          \"nodes\":[\n              {\n                  \"host\":\"127.0.0.1\",\n                  \"port\":1980,\n                  \"weight\":1\n              }\n          ]\n      }\n  }\n  ```\n\n  * 去除 `syslog` 插件中的 `max_retry_times` 和 `retry_interval` 字段，使用 `max_retry_count` 和 `retry_delay` 替代。\n\n  如果在 ETCD 中 `syslog` 的插件配置存在这样的数据结构：\n\n  ```json\n  {\n      \"plugins\":{\n          \"syslog\":{\n              \"max_retry_times\":1,\n              \"retry_interval\":1,\n              ... // 其他配置\n          }\n      }\n  }\n  ```\n\n  则在 3.0.0 版本中，这个插件的数据结构应该变成如下所示：\n\n  ```json\n  {\n      \"plugins\":{\n          \"syslog\":{\n              \"max_retry_count\":1,\n              \"retry_delay\":1,\n              ... // 其他配置\n          }\n      }\n  }\n  ```\n\n  * 去除 `proxy-rewrite` 插件中的 `scheme` 字段，在配置上游时，用 `upstream.scheme` 替代。\n\n  如果在 ETCD 中 `proxy-rewrite` 的插件配置存在这样的数据结构：\n\n  ```json\n  {\n      \"plugins\":{\n          \"proxy-rewrite\":{\n              \"scheme\":\"https\",\n              ... // 其他配置\n          }\n      },\n      \"upstream\":{\n          \"nodes\":{\n              \"127.0.0.1:1983\":1\n          },\n          \"type\":\"roundrobin\"\n      },\n      \"uri\":\"/hello\"\n  }\n  ```\n\n  则在 3.0.0 版本中，这个插件的数据结构应该变成如下所示：\n\n  ```json\n  {\n    \"plugins\":{\n        \"proxy-rewrite\":{\n            ... // 其他配置\n        }\n    },\n    \"upstream\":{\n        \"scheme\":\"https\",\n        \"nodes\":{\n            \"127.0.0.1:1983\":1\n        },\n        \"type\":\"roundrobin\"\n    },\n    \"uri\":\"/hello\"\n  }\n  ```\n\n#### Admin API\n\n在 3.0.0 版本中，我们对 Admin API 也进行了一些调整。使得 Admin API 更加易用，更加符合 RESTful 的设计理念，具体调整内容如下。\n\n  * 操作资源时（包括查询单个资源和列表资源），删除了响应体中的 `count`、`action` 和 `node` 字段，并将 `node` 中的内容提升到响应体的根节点。\n\n  在 2.x 版本中，通过 Admin API 查询 `/apisix/admin/routes/1` 的响应格式是这样的：\n\n  ```json\n  {\n    \"count\":1,\n    \"action\":\"get\",\n    \"node\":{\n        \"key\":\"\\/apisix\\/routes\\/1\",\n        \"value\":{\n            ... // 配置内容\n        }\n    }\n  }\n  ```\n\n  在 3.0.0 版本中，通过 Admin API 查询 `/apisix/admin/routes/1` 资源的响应格式调整为如下所示：\n\n  ```json\n  {\n    \"key\":\"\\/apisix\\/routes\\/1\",\n    \"value\":{\n        ... // 配置内容\n    }\n  }\n  ```\n\n  * 查询列表资源时，删除 `dir` 字段，新增 `list` 字段，存放列表资源的数据；新增 `total` 字段，存放列表资源的总数。\n\n  在 2.x 版本中，通过 Admin API 查询 `/apisix/admin/routes` 的响应格式是这样的：\n\n  ```json\n  {\n    \"action\":\"get\",\n    \"count\":2,\n    \"node\":{\n        \"key\":\"\\/apisix\\/routes\",\n        \"nodes\":[\n            {\n                \"key\":\"\\/apisix\\/routes\\/1\",\n                \"value\":{\n                    ... // 配置内容\n                }\n            },\n            {\n                \"key\":\"\\/apisix\\/routes\\/2\",\n                \"value\":{\n                    ... // 配置内容\n                }\n            }\n        ],\n        \"dir\":true\n    }\n  }\n  ```\n\n  在 3.0.0 版本中，通过 Admin API 查询 `/apisix/admin/routes` 资源的响应格式调整为如下所示：\n\n  ```json\n  {\n    \"list\":[\n        {\n            \"key\":\"\\/apisix\\/routes\\/1\",\n            \"value\":{\n                ... // 配置内容\n            }\n\n        },\n        {\n            \"key\":\"\\/apisix\\/routes\\/2\",\n            \"value\":{\n                ... // 配置内容\n            }\n        }\n    ],\n    \"total\":2\n  }\n  ```\n\n  * 调整 ssl 资源的请求路径，从 `/apisix/admin/ssl/{id}` 调整为 `/apisix/admin/ssls/{id}`。\n\n  在 2.x 版本中，通过 Admin API 操作 ssl 资源是这样的：\n\n  ```shell\n  curl -i http://{apisix_listen_address}/apisix/admin/ssl/{id}\n  ```\n\n  在 3.0.0 版本中，通过 Admin API 操作 ssl 资源调整为如下所示：\n\n  ```shell\n  curl -i http://{apisix_listen_address}/apisix/admin/ssls/{id}\n  ```\n\n  * 调整 proto 资源的请求路径，从 `/apisix/admin/proto/{id}` 调整为 `/apisix/admin/protos/{id}`。\n\n  在 2.x 版本中，通过 Admin API 操作 proto 资源是这样的：\n\n  ```shell\n  curl -i http://{apisix_listen_address}/apisix/admin/proto/{id}\n  ```\n\n  在 3.0.0 版本中，通过 Admin API 操作 proto 资源调整为如下所示：\n\n  ```shell\n  curl -i http://{apisix_listen_address}/apisix/admin/protos/{id}\n  ```\n\n除以上内容外，我们也将 Admin API 的端口调整为 9180。\n\n## 总结\n\nApache APISIX 3.0.0 版本的发布，将产品的更多细节迭代了一大步。由于大版本的更新迭代会导致一些配置与数据也相应进行调整，为此我们为您整理了这份 APISIX 升级指南。希望对各位在使用 APISIX 的过程中，对于版本的更新操作也更得心应手。\n\n如果您有任何问题或意见，欢迎随时在社区进行交流。\n"
  },
  {
    "path": "docs/zh/latest/wasm.md",
    "content": "# Wasm\n\nAPISIX 支持使用 [Proxy Wasm SDK](https://github.com/proxy-wasm/spec#sdks) 编写的 Wasm 插件。\n\n目前，仅实现了少数 API。请关注 [wasm-nginx-module](https://github.com/api7/wasm-nginx-module) 以了解进展。\n\n## 编程模型\n\n所有插件都在同一个 Wasm VM 中运行，就像 Lua 插件在 Lua VM 中一样。\n\n每个插件都有自己的 VMContext（根 ctx）。\n\n每个配置的路由/全局规则都有自己的 PluginContext（插件 ctx）。例如，如果我们有一个配置了 Wasm 插件的服务，并且有两个路由继承自它，将会有两个插件 ctx。\n\n每个命中该配置的 HTTP 请求都有自己的 HttpContext（HTTP ctx）。例如，如果我们同时配置了全局规则和路由，HTTP 请求将有两个 HTTP ctx，一个用于来自全局规则的插件 ctx，另一个用于来自路由的插件 ctx。\n\n## 如何使用\n\n首先，我们需要在`config.yaml`中定义插件：\n\n```yaml\nwasm:\n  plugins:\n    - name: wasm_log # 插件的名称\n      priority: 7999 # 优先级\n      file: t/wasm/log/main.go.wasm # `.wasm` 文件的路径\n      http_request_phase: access # 默认是\"access\"，可以是[\"access\", \"rewrite\"]之一\n```\n\n就是这样。现在您可以像使用常规插件一样使用 Wasm 插件。\n\n例如，在指定路由上启用此插件：\n\n**注意**\n\n您可以从`config.yaml`中获取`<beginning of the code>admin_key<end of the code>`，并使用以下命令将其保存到环境变量中：\n\n```bash\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed's/\"//g')\n```\n\n然后执行以下命令：\n\n```bash\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/index.html\",\n    \"plugins\": {\n        \"wasm_log\": {\n            \"conf\": \"blahblah\"\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n```\n\n以下是插件中可以配置的属性：\n\n|名称 | 类型 | 要求 | 默认 | 有效 | 描述|\n|---|---|---|---|---|---|\n|conf|字符串或结构体 | 必填 | 无 | 不得为空 |插件 ctx 配置，可以通过 Proxy Wasm SDK 获取|\n\n这里是 Proxy Wasm 回调与 APISIX 阶段的映射：\n\n- `proxy_on_configure`：在新配置没有 PluginContext 时运行一次。例如，当第一个请求命中配置了 Wasm 插件的路由时。\n- `proxy_on_http_request_headers`：在 access/rewrite 阶段运行，具体取决于`http_request_phase`的配置。\n- `proxy_on_http_request_body`：在与`proxy_on_http_request_headers`相同的阶段运行。要运行此回调，我们需要在`proxy_on_http_request_headers`中将属性`wasm_process_req_body`设置为非空值。请参考`t/wasm/request-body/main.go`作为示例。\n- `proxy_on_http_response_headers`：在 header_filter 阶段运行。\n- `proxy_on_http_response_body`：在 body_filter 阶段运行。要运行此回调，我们需要在`proxy_on_http_response_headers`中将属性`wasm_process_resp_body`设置为非空值。请参考`t/wasm/response-rewrite/main.go`作为示例。\n\n## 示例\n\n我们在这个仓库的`t/wasm/`下重新实现了一些 Lua 插件：\n\n- fault - injection\n- forward - auth\n- response - rewrite\n- Slack\n- Twitter\n"
  },
  {
    "path": "example/apisix/plugins/3rd-party.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        body = {\n            description = \"body to replace response.\",\n            type = \"string\"\n        },\n    },\n    required = {\"body\"},\n}\n\nlocal plugin_name = \"3rd-party\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 12,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    return 200, conf.body\nend\n\n\nreturn _M\n"
  },
  {
    "path": "example/apisix/stream/plugins/3rd-party.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        body = {\n            description = \"body to replace response.\",\n            type = \"string\"\n        },\n    },\n    required = {\"body\"},\n}\n\nlocal plugin_name = \"3rd-party\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 12,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nfunction _M.access(conf, ctx)\n    return 200, conf.body\nend\n\n\nreturn _M\n"
  },
  {
    "path": "example/build-dev-image.dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM ubuntu:20.04\n\n# Install Test::Nginx\nRUN apt update\nRUN apt install -y cpanminus make\nRUN cpanm --notest Test::Nginx\n\n# Install development utils\nRUN apt install -y sudo git gawk curl nano vim inetutils-ping\n\nWORKDIR /apisix\n\nENV PERL5LIB=.:$PERL5LIB\n\nENTRYPOINT [\"tail\", \"-f\", \"/dev/null\"]\n"
  },
  {
    "path": "example/my_hook.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal apisix = require(\"apisix\")\n\nlocal old_http_init = apisix.http_init\napisix.http_init = function (...)\n    ngx.log(ngx.EMERG, \"my hook works in http\")\n    old_http_init(...)\nend\n\nlocal old_stream_init = apisix.stream_init\napisix.stream_init = function (...)\n    ngx.log(ngx.EMERG, \"my hook works in stream\")\n    old_stream_init(...)\nend\n"
  },
  {
    "path": "powered-by.md",
    "content": "---\ntitle: Powered by Apache APISIX\n---\n\n<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis page documents an alphabetical list of institutions that are using APISIX for research and production,\nor providing commercial products including APISIX.\n\nUsers are encouraged to add themselves to this page, [issue](https://github.com/apache/apisix/issues/487) and PR are welcomed.\n\n1. <a href=\"http://www.aimiaobi.com/\" rel=\"nofollow\">aimiaobi 妙笔 AI</a>\n1. <a href=\"http://www.augurit.com/\" rel=\"nofollow\">AUGUR 奥格科技股份有限公司</a>\n1. <a href=\"https://cloud.aispeech.com/\" rel=\"nofollow\">AISPEECH 思必驰信息科技股份有限公司</a>\n1. <a href=\"http://www.cunw.com.cn/\" rel=\"nofollow\">cunw 湖南新云网</a>\n1. <a href=\"https://www.chaolian360.com/\" rel=\"nofollow\">Chaolian 超链云商</a>\n1. <a href=\"https://www.ccbft.com/\" rel=\"nofollow\">CCB Fintech 建信金科</a>\n1. <a href=\"https://www.ctrl.cn\" rel=\"nofollow\">CTRL 开创云</a>\n1. <a href=\"http://51tiangou.com/\" rel=\"nofollow\">51tiangou 大商天狗</a>\n1. <a href=\"https://www.daocloud.io/\" rel=\"nofollow\">DaoCloud</a>\n1. <a href=\"https://www.dasouche.com/\" rel=\"nofollow\">dasouche 大搜车</a>\n1. <a href=\"https://www.dataoke.com/\" rel=\"nofollow\">dataoke 大淘客</a>\n1. <a href=\"https://www.didachuxing.com/\" rel=\"nofollow\">嘀嗒出行</a>\n1. <a href=\"http://dusto.cn/\" rel=\"nofollow\">dusto.cn 浙江大东鞋业有限公司</a>\n1. <a href=\"http://dian.so/\" rel=\"nofollow\">Dian 小电科技</a>\n1. <a href=\"https://www.efactory-project.eu/\" rel=\"nofollow\">eFactory</a>\n1. <a href=\"https://www.ehomepay.com.cn/\" rel=\"nofollow\">ehomepay 理房通</a>\n1. <a href=\"https://ezone.work/\" rel=\"nofollow\">eZone 简单一点科技</a>\n1. <a href=\"https://fansup.mobi/\" rel=\"nofollow\">fansup</a>\n1. <a href=\"https://game.qq.com/\" rel=\"nofollow\">Tencent Game 腾讯游戏</a>\n1. <a href=\"https://www.haier.com/haier-ecosystem/haier/\" rel=\"nofollow\">haieruplus 海尔优家</a>\n1. <a href=\"http://www.hellowin.cn/\" rel=\"nofollow\">hellowin 好洛维</a>\n1. <a href=\"https://www.hellotalk.com/\" rel=\"nofollow\">HelloTalk, Inc.</a>\n1. <a href=\"\" rel=\"nofollow\">航天网信</a>\n1. <a href=\"http://huawei.com/\" rel=\"nofollow\">Huawei 华为</a>\n1. <a href=\"https://www.huya.com/\" rel=\"nofollow\">虎牙</a>\n1. <a href=\"http://www.hys.cn/\" rel=\"nofollow\">好医生集团</a>\n1. <a href=\"https://www.ihomefnt.com/\" rel=\"nofollow\">ihomefnt 艾佳生活</a>\n1. <a href=\"https://www.intsig.com/\" rel=\"nofollow\">intsig 上海合合信息科技股份有限公司</a>\n1. <a href=\"https://www.jiandanxinli.com/\" rel=\"nofollow\">jiandanxinli 简单心理</a>\n1. <a href=\"https://jr.ly.com/\" rel=\"nofollow\">jr.ly 同程金服</a>\n1. <a href=\"https://www.kaishustory.com/\" rel=\"nofollow\">凯叔讲故事</a>\n1. <a href=\"https://www.ke.com/\" rel=\"nofollow\">ke.com 贝壳找房</a>\n1. <a href=\"https://www.meizu.com/\" rel=\"nofollow\">Meizu 魅族</a>\n1. <a href=\"https://www.mingyuanyun.com/\" rel=\"nofollow\">明源云客</a>\n1. <a href=\"https://www.meicai.cn/\" rel=\"nofollow\">美菜网</a>\n1. <a href=\"http://www.163.com\" rel=\"nofollow\">Netease 网易</a>\n1. <a href=\"https://www.jpl.nasa.gov\" rel=\"nofollow\">NASA JPL 美国国家航空航天局 喷气推进实验室</a>\n1. <a href=\"https://www.purcotton.com/\" rel=\"nofollow\">Purcotton 深圳全棉时代科技有限公司</a>\n1. <a href=\"https://www.360.cn/\" rel=\"nofollow\">360 奇虎</a>\n1. <a href=\"http://www.sinog2c.com/\" rel=\"nofollow\">sinog2c 湖南国科云通</a>\n1. <a href=\"https://www.sinovatech.com\" rel=\"nofollow\">sinovatech 炎黄新星</a>\n1. <a href=\"http://taikang.com/\" rel=\"nofollow\">Taikanglife 泰康云</a>\n1. <a href=\"http://www.tangdou.com/\" rel=\"nofollow\">tangdou 糖豆网</a>\n1. <a href=\"https://cloud.tencent.com/\" rel=\"nofollow\">Tencent Cloud 腾讯云</a>\n1. <a href=\"https://www.travelsky.com.cn/\" rel=\"nofollow\"> Travelsky 中国航信</a>\n1. <a href=\"https://www.unistamts.com/\" rel=\"nofollow\"> Unistam</a>\n1. <a href=\"https://vbill.cn/\" rel=\"nofollow\">vbill 随行付</a>\n1. <a href=\"https://www.vivo.com/hk/zh/\" rel=\"nofollow\">VIVO</a>\n1. <a href=\"https://www.teamones.cn/\" rel=\"nofollow\">万思</a>\n1. <a href=\"https://www.willclass.com/\" rel=\"nofollow\">willclass 会课</a>\n1. <a href=\"https://www.wps.cn/\" rel=\"nofollow\">金山办公</a>\n1. <a href=\"https://www.xin.com/\" rel=\"nofollow\">Xin 优信二手车</a>\n1. <a href=\"https://xueqiu.com/\" rel=\"nofollow\">雪球</a>\n1. <a href=\"https://open.youtu.qq.com/\" rel=\"nofollow\">Youtu 腾讯优图</a>\n1. <a href=\"http://www.ymm56.com/\" rel=\"nofollow\">YMM 满帮集团</a>\n1. <a href=\"https://hy.10086.cn/\" rel=\"nofollow\">中移杭研</a>\n1. <a href=\"https://www.zihao.biz/\" rel=\"nofollow\">紫豪网络</a>\n1. <a href=\"https://www.zuzuche.com/\" rel=\"nofollow\">zuzuche 租租车</a>\n1. <a href=\"https://www.zybang.com/\" rel=\"nofollow\">zybang 作业帮</a>\n1. <a href=\"\" rel=\"nofollow\">中食安泓（广东）健康产业有限公司</a>\n1. <a href=\"https://appadvice.com/app/e5-8c-bb-e6-82-a3-e5-ae-a2-e6-9c-8d/1502073770\" rel=\"nofollow\">上海泽怡信息科技</a>\n1. <a href=\"https://www.xinpianchang.com\" rel=\"nofollow\">北京新片场传媒股份有限公司</a>\n1. <a href=\"https://www.niimbot.com\" rel=\"nofollow\">武汉精臣智慧标识科技有限公司</a>\n1. <a href=\"https://www.aiit.org.cn/\" rel=\"nofollow\">北京大学信息技术高等研究院</a>\n1. <a href=\"https://www.hihonor.com/cn/\" rel=\"nofollow\">HONOR 荣耀</a>\n1. <a href=\"https://www.maiscrm.com/\" rel=\"nofollow\">群之脉信息科技</a>\n1. <a href=\"https://www.dafangya.com/\" rel=\"nofollow\">大房鸭</a>\n1. <a href=\"http://www.utyun.com/\" rel=\"nofollow\">优特云</a>\n1. <a href=\"https://www.unipus.cn/\" rel=\"nofollow\">外研在线</a>\n1. <a href=\"https://paramland.com/#/\" rel=\"nofollow\">数地科技</a>\n1. <a href=\"https://www.vhall.com/\" rel=\"nofollow\">微吼</a>\n1. <a href=\"https://www.xiaopeng.com/\" rel=\"nofollow\">小鹏汽车</a>\n1. <a href=\"\" rel=\"nofollow\">Ideacreep</a>\n\n<img src=\"https://user-images.githubusercontent.com/40708551/109484046-f7c4e280-7aa5-11eb-9d71-aab90830773a.png\" width=\"725\" height=\"1700\" />\n\n## User Cases\n\n## NASA JPL\n\nUsing Apache APISIX as an API gateway to deal with north-south and east-west traffic between microservices.\n\n## ke.com\n\nUsing Apache APISIX as traffic entry gateway\n\n## meizu\n\nUsing Apache APISIX as api gateway (limit, grpc transcode, abtest, dynamic configures ...)\n\n## zuzuche.com\n\nUsing Apache APISIX as a gateway, it uses the functions of current limiting, speed limiting, black-and-white list and so on. In the later stage, it also wants to add gRPC protocol, Serverless, custom plug-in, and other functions to meet business needs.\n\n## souche.com\n\nUsing Apache APISIX as a Web ACL gateway to deal with backend OA systems traffic.\n\n## HelloTalk, Inc.\n\nUsing Apache APISIX as an API gateway to manage all API and SSL certificates in test\\dev\\CMS environment.\n"
  },
  {
    "path": "t/APISIX.pm",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\npackage t::APISIX;\n\nuse lib 'lib';\nuse Cwd qw(cwd);\nuse Test::Nginx::Socket::Lua::Stream -Base;\n\nrepeat_each(1);\nlog_level('info');\nno_long_string();\nno_shuffle();\nno_root_location(); # avoid generated duplicate 'location /'\nworker_connections(128);\nmaster_on();\n\nmy $apisix_home = $ENV{APISIX_HOME} || cwd();\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n$ENV{TEST_NGINX_FAST_SHUTDOWN} ||= 1;\n\nsub read_file($) {\n    my $infile = shift;\n    open my $in, \"$apisix_home/$infile\"\n        or die \"cannot open $infile for reading: $!\";\n    my $data = do { local $/; <$in> };\n    close $in;\n    $data;\n}\n\nsub local_dns_resolver() {\n    open my $in, \"/etc/resolv.conf\" or die \"cannot open /etc/resolv.conf\";\n    my @lines =  <$in>;\n    my @dns_addrs = ();\n    foreach my $line (@lines){\n        $line =~ m/^nameserver\\s+(\\d+[.]\\d+[.]\\d+[.]\\d+)\\s*$/;\n        if ($1) {\n            push(@dns_addrs, $1);\n        }\n    }\n    close($in);\n    return @dns_addrs\n}\n\n\nmy $dns_addrs_str = \"\";\nmy $dns_addrs_tbl_str = \"\";\nmy $enable_local_dns = $ENV{\"ENABLE_LOCAL_DNS\"};\nif ($enable_local_dns) {\n    my @dns_addrs = local_dns_resolver();\n    $dns_addrs_tbl_str = \"{\";\n    foreach my $addr (@dns_addrs){\n        $dns_addrs_str = \"$dns_addrs_str $addr\";\n        $dns_addrs_tbl_str = \"$dns_addrs_tbl_str\\\"$addr\\\", \";\n    }\n    $dns_addrs_tbl_str = \"$dns_addrs_tbl_str}\";\n} else {\n    $dns_addrs_str = \"8.8.8.8 114.114.114.114\";\n    $dns_addrs_tbl_str = \"{\\\"8.8.8.8\\\", \\\"114.114.114.114\\\"}\";\n}\nmy $custom_dns_server = $ENV{\"CUSTOM_DNS_SERVER\"};\nif ($custom_dns_server) {\n    $dns_addrs_tbl_str = \"{\\\"$custom_dns_server\\\"}\";\n}\n\n\nmy $test_default_config = <<_EOC_;\n    -- read the default configuration, modify it, and the Lua package\n    -- cache will persist it for loading by other entrypoints\n    -- it is used to replace the test::nginx implementation\n    local default_config = require(\"apisix.cli.config\")\n    default_config.plugin_attr.prometheus.enable_export_server = false\n_EOC_\n\nmy $user_yaml_config = read_file(\"conf/config.yaml\");\nmy $ssl_crt = read_file(\"t/certs/apisix.crt\");\nmy $ssl_key = read_file(\"t/certs/apisix.key\");\nmy $ssl_ecc_crt = read_file(\"t/certs/apisix_ecc.crt\");\nmy $ssl_ecc_key = read_file(\"t/certs/apisix_ecc.key\");\nmy $test2_crt = read_file(\"t/certs/test2.crt\");\nmy $test2_key = read_file(\"t/certs/test2.key\");\nmy $etcd_pem = read_file(\"t/certs/etcd.pem\");\nmy $etcd_key = read_file(\"t/certs/etcd.key\");\n$user_yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - 9100\n  enable_resolv_search_opt: false\n  trusted_addresses:\n    - \"127.0.0.1\"\n_EOC_\n\nmy $etcd_enable_auth = $ENV{\"ETCD_ENABLE_AUTH\"} || \"false\";\n\nif ($etcd_enable_auth eq \"true\") {\n    $user_yaml_config .= <<_EOC_;\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    user: root\n    password: 5tHkHhYkjr6cQY\n_EOC_\n}\n\nmy $profile = $ENV{\"APISIX_PROFILE\"};\n\n\nmy $apisix_file;\nmy $apisix_file_json;\nmy $debug_file;\nmy $config_file;\nif ($profile) {\n    $apisix_file = \"apisix-$profile.yaml\";\n    $apisix_file_json = \"apisix-$profile.json\";\n    $debug_file = \"debug-$profile.yaml\";\n    $config_file = \"config-$profile.yaml\";\n} else {\n    $apisix_file = \"apisix.yaml\";\n    $apisix_file_json = \"apisix.json\";\n    $debug_file = \"debug.yaml\";\n    $config_file = \"config.yaml\";\n}\n\n\nmy $dubbo_upstream = \"\";\nmy $dubbo_location = \"\";\nmy $version = eval { `$nginx_binary -V 2>&1` };\nif ($version =~ m/\\/mod_dubbo/) {\n    $dubbo_upstream = <<_EOC_;\n    upstream apisix_dubbo_backend {\n        server 0.0.0.1;\n\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n\n        multi 1;\n        keepalive 320;\n    }\n\n_EOC_\n\n    $dubbo_location = <<_EOC_;\n        location \\@dubbo_pass {\n            access_by_lua_block {\n                apisix.dubbo_access_phase()\n            }\n\n            dubbo_pass_all_headers on;\n            dubbo_pass_body on;\n            dubbo_pass \\$dubbo_service_name \\$dubbo_service_version \\$dubbo_method apisix_dubbo_backend;\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n\n_EOC_\n}\n\nmy $grpc_location = <<_EOC_;\n        location \\@grpc_pass {\n            access_by_lua_block {\n                apisix.grpc_access_phase()\n            }\n\n_EOC_\n\nif ($version =~ m/\\/apisix-nginx-module/) {\n    $grpc_location .= <<_EOC_;\n            grpc_set_header   \":authority\" \\$upstream_host;\n_EOC_\n} else {\n    $grpc_location .= <<_EOC_;\n            grpc_set_header   \"Host\" \\$upstream_host;\n_EOC_\n}\n\n$grpc_location .= <<_EOC_;\n            grpc_set_header   Content-Type application/grpc;\n            grpc_set_header   TE trailers;\n            grpc_socket_keepalive on;\n            grpc_pass         \\$upstream_scheme://apisix_backend;\n            mirror              /proxy_mirror_grpc;\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n_EOC_\n\nmy $a6_ngx_directives = \"\";\nif ($version =~ m/\\/apisix-nginx-module/) {\n    $a6_ngx_directives = <<_EOC_;\n    apisix_delay_client_max_body_check on;\n    apisix_mirror_on_demand on;\n    wasm_vm wasmtime;\n_EOC_\n}\n\nmy $a6_ngx_vars = \"\";\nif ($version =~ m/\\/apisix-nginx-module/) {\n    $a6_ngx_vars = <<_EOC_;\n    set \\$wasm_process_req_body       '';\n    set \\$wasm_process_resp_body      '';\n_EOC_\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $wait_etcd_sync = $block->wait_etcd_sync // 0.1;\n\n    if ($block->apisix_yaml && (!defined $block->yaml_config)) {\n        $user_yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n    }\n\n    if ($block->apisix_json && (!defined $block->yaml_config)) {\n        $user_yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: json\n_EOC_\n    }\n\n    my $lua_deps_path = $block->lua_deps_path // <<_EOC_;\n    lua_package_path \"$apisix_home/?.lua;$apisix_home/?/init.lua;$apisix_home/deps/share/lua/5.1/?/init.lua;$apisix_home/deps/share/lua/5.1/?.lua;$apisix_home/apisix/?.lua;$apisix_home/t/?.lua;$apisix_home/t/xrpc/?.lua;$apisix_home/t/xrpc/?/init.lua;;\";\n    lua_package_cpath \"$apisix_home/?.so;$apisix_home/deps/lib/lua/5.1/?.so;$apisix_home/deps/lib64/lua/5.1/?.so;;\";\n_EOC_\n\n    my $main_config = $block->main_config // <<_EOC_;\nworker_rlimit_core  500M;\nenv ENABLE_ETCD_AUTH;\nenv APISIX_PROFILE;\nenv PATH; # for searching external plugin runner's binary\nenv TEST_NGINX_HTML_DIR;\nenv OPENSSL_BIN;\n_EOC_\n\n\n    if ($version =~ m/\\/apisix-nginx-module/) {\n        $main_config .= <<_EOC_;\nthread_pool grpc-client-nginx-module threads=1;\n\nlua {\n    lua_shared_dict prometheus-metrics 15m;\n    lua_shared_dict prometheus-cache 10m;\n    lua_shared_dict standalone-config 10m;\n    lua_shared_dict status-report 1m;\n    lua_shared_dict nacos 10m;\n    lua_shared_dict consul 10m;\n    lua_shared_dict upstream-healthcheck 10m;\n}\n_EOC_\n    }\n\n    # set default `timeout` to 5sec\n    my $timeout = $block->timeout // 5;\n    $block->set_value(\"timeout\", $timeout);\n\n    my $stream_tls_request = $block->stream_tls_request;\n    if ($stream_tls_request) {\n        # generate a springboard to send tls stream request\n        $block->set_value(\"stream_conf_enable\", 1);\n        # avoid conflict with stream_enable\n        $block->set_value(\"stream_enable\");\n        $block->set_value(\"request\", \"GET /stream_tls_request\");\n\n        my $sni = \"nil\";\n        if ($block->stream_sni) {\n            $sni = '\"' . $block->stream_sni . '\"';\n        }\n        chomp $stream_tls_request;\n\n        my $repeat = \"1\";\n        if (defined $block->stream_session_reuse) {\n            $repeat = \"2\";\n        }\n\n        my $config = <<_EOC_;\n            location /stream_tls_request {\n                content_by_lua_block {\n                    local sess\n                    for _ = 1, $repeat do\n                        local sock = ngx.socket.tcp()\n                        local ok, err = sock:connect(\"127.0.0.1\", 2005)\n                        if not ok then\n                            ngx.say(\"failed to connect: \", err)\n                            return\n                        end\n\n                        sess, err = sock:sslhandshake(sess, $sni, false)\n                        if not sess then\n                            ngx.say(\"failed to do SSL handshake: \", err)\n                            return\n                        end\n\n                        local bytes, err = sock:send(\"$stream_tls_request\")\n                        if not bytes then\n                            ngx.say(\"send stream request error: \", err)\n                            return\n                        end\n                        local data, err = sock:receive(\"*a\")\n                        if not data then\n                            sock:close()\n                            ngx.say(\"receive stream response error: \", err)\n                            return\n                        end\n                        ngx.print(data)\n                        sock:close()\n                    end\n                }\n            }\n_EOC_\n        $block->set_value(\"config\", $config)\n    }\n\n    # handling shell exec in test Nginx\n    my $exec_snippet = $block->exec;\n    if ($exec_snippet) {\n        # capture the stdin & max response size\n        my $stdin = \"nil\";\n        if ($block->stdin) {\n            $stdin = '\"' . $block->stdin . '\"';\n        }\n        chomp  $exec_snippet;\n        chomp $stdin;\n\n        my $max_size = $block->max_size // 8096;\n        $block->set_value(\"request\", \"GET /exec_request\");\n\n        my $config = $block->config // '';\n        $config .= <<_EOC_;\n            location /exec_request {\n                content_by_lua_block {\n                    local shell = require(\"resty.shell\")\n                    -- timeout one second before the actual timeout to allow shell.run to finish and collect the stdout/stderr\n                    local ok, stdout, stderr, reason, status = shell.run([[ $exec_snippet ]], $stdin, @{[($timeout-1)*1000]}, $max_size)\n                    if not ok then\n                        ngx.log(ngx.WARN, \"failed to execute the script with status: \" .. (status or \"nil \") .. \", reason: \" .. (reason or \"nil \") .. \", stdout: \" .. (stdout or \"nil \") .. \", stderr: \" .. (stderr or \"nil \"))\n                        ngx.print(\"stdout: \", stdout)\n                        ngx.print(\"stderr: \", stderr)\n                        return\n                    end\n                    ngx.print(stdout)\n                }\n            }\n_EOC_\n\n        $block->set_value(\"config\", $config)\n    }\n\n    my $stream_enable = $block->stream_enable;\n    if ($block->stream_request) {\n        # Like stream_tls_request, if stream_request is given, automatically enable stream\n        $stream_enable = 1;\n    }\n\n    my $stream_conf_enable = $block->stream_conf_enable;\n    my $extra_stream_config = $block->extra_stream_config // '';\n    my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_;\n            local sock = ngx.req.socket()\n            local data = sock:receive(\"1\")\n            ngx.say(\"hello world\")\n_EOC_\n\n    my $stream_config = $block->stream_config // <<_EOC_;\n    $lua_deps_path\n    lua_socket_log_errors off;\n\n    lua_shared_dict lrucache-lock-stream 10m;\n    lua_shared_dict plugin-limit-conn-stream 10m;\n    lua_shared_dict etcd-cluster-health-check-stream 10m;\n    lua_shared_dict worker-events-stream 10m;\n\n    lua_shared_dict kubernetes-stream 1m;\n    lua_shared_dict kubernetes-first-stream 1m;\n    lua_shared_dict kubernetes-second-stream 1m;\n    lua_shared_dict tars-stream 1m;\n\n    upstream apisix_backend {\n        server 127.0.0.1:1900;\n        balancer_by_lua_block {\n            apisix.stream_balancer_phase()\n        }\n    }\n_EOC_\n\n    my $stream_extra_init_by_lua_start = $block->stream_extra_init_by_lua_start // \"\";\n\n    my $stream_init_by_lua_block = $block->stream_init_by_lua_block // <<_EOC_;\n        if os.getenv(\"APISIX_ENABLE_LUACOV\") == \"1\" then\n            require(\"luacov.runner\")(\"t/apisix.luacov\")\n            jit.off()\n        end\n\n        require \"resty.core\"\n\n        $stream_extra_init_by_lua_start\n\n        apisix = require(\"apisix\")\n        local args = {\n            dns_resolver = $dns_addrs_tbl_str,\n        }\n        apisix.stream_init(args)\n_EOC_\n\n    my $stream_extra_init_by_lua = $block->stream_extra_init_by_lua // \"\";\n    my $stream_extra_init_worker_by_lua = $block->stream_extra_init_worker_by_lua // \"\";\n\n    $stream_config .= <<_EOC_;\n    init_by_lua_block {\n        $test_default_config\n        $stream_init_by_lua_block\n        $stream_extra_init_by_lua\n    }\n    init_worker_by_lua_block {\n        apisix.stream_init_worker()\n        $stream_extra_init_worker_by_lua\n    }\n\n    $extra_stream_config\n\n    server {\n        listen unix:$apisix_home/t/servroot/logs/stream_worker_events.sock;\n        access_log off;\n        content_by_lua_block {\n            require(\"resty.events.compat\").run()\n        }\n    }\n\n    # fake server, only for test\n    server {\n        listen 1995;\n\n        content_by_lua_block {\n            $stream_upstream_code\n        }\n    }\n_EOC_\n\n    if (defined $stream_enable) {\n        $block->set_value(\"stream_config\", $stream_config);\n    }\n\n    my $custom_trusted_cert = $block->custom_trusted_cert // 'cert/apisix.crt';\n\n    my $stream_server_config = $block->stream_server_config // <<_EOC_;\n    listen 2005 ssl;\n    ssl_certificate             cert/apisix.crt;\n    ssl_certificate_key         cert/apisix.key;\n    lua_ssl_trusted_certificate cert/apisix.crt;\n\n    ssl_session_cache    shared:STREAM_SSL:20m;\n    ssl_session_timeout 10m;\n    ssl_session_tickets off;\n\n    ssl_client_hello_by_lua_block {\n        apisix.ssl_client_hello_phase()\n    }\n\n    ssl_certificate_by_lua_block {\n        apisix.ssl_phase()\n    }\n\n    preread_by_lua_block {\n        -- wait for etcd sync\n        ngx.sleep($wait_etcd_sync)\n        apisix.stream_preread_phase()\n    }\n\n    proxy_pass apisix_backend;\n_EOC_\n\n    if ($version =~ m/\\/apisix-nginx-module/) {\n        $stream_server_config .= <<_EOC_;\n    proxy_ssl_server_name on;\n    proxy_ssl_name \\$upstream_sni;\n    set \\$upstream_sni \"apisix_backend\";\n_EOC_\n    }\n\n    $stream_server_config .= <<_EOC_;\n    log_by_lua_block {\n        apisix.stream_log_phase()\n    }\n_EOC_\n\n    if (defined $stream_enable) {\n        $block->set_value(\"stream_server_config\", $stream_server_config);\n    }\n\n    if (defined $stream_conf_enable) {\n        $main_config .= <<_EOC_;\nstream {\n$stream_config\n    server {\n        listen 1985;\n        $stream_server_config\n    }\n}\n_EOC_\n    }\n\n    $block->set_value(\"main_config\", $main_config);\n\n    # The new directive is introduced here to modify the schema\n    # before apisix validate in require(\"apisix\")\n    # Todo: merge extra_init_by_lua_start and extra_init_by_lua\n    my $extra_init_by_lua_start = $block->extra_init_by_lua_start // \"\";\n\n    my $extra_init_by_lua = $block->extra_init_by_lua // \"\";\n    my $init_by_lua_block = $block->init_by_lua_block // <<_EOC_;\n    if os.getenv(\"APISIX_ENABLE_LUACOV\") == \"1\" then\n        require(\"luacov.runner\")(\"t/apisix.luacov\")\n        jit.off()\n    end\n\n    require \"resty.core\"\n\n    $extra_init_by_lua_start\n\n    apisix = require(\"apisix\")\n    local args = {\n        dns_resolver = $dns_addrs_tbl_str,\n    }\n    apisix.http_init(args)\n\n    -- set apisix_lua_home into constants module\n    -- it may be used by plugins to determine the work path of apisix\n    local constants = require(\"apisix.constants\")\n    constants.apisix_lua_home = \"$apisix_home\"\n\n    $extra_init_by_lua\n_EOC_\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n\n    my $http_config = $block->http_config // '';\n    $http_config .= <<_EOC_;\n    $lua_deps_path\n\n    lua_shared_dict plugin-limit-req 10m;\n    lua_shared_dict plugin-limit-count 10m;\n    lua_shared_dict plugin-limit-count-reset-header 10m;\n    lua_shared_dict plugin-limit-conn 10m;\n    lua_shared_dict plugin-ai-rate-limiting 10m;\n    lua_shared_dict plugin-ai-rate-limiting-reset-header 10m;\n    lua_shared_dict internal-status 10m;\n    lua_shared_dict worker-events 10m;\n    lua_shared_dict lrucache-lock 10m;\n    lua_shared_dict balancer-ewma 1m;\n    lua_shared_dict balancer-ewma-locks 1m;\n    lua_shared_dict balancer-ewma-last-touched-at 1m;\n    lua_shared_dict plugin-limit-req-redis-cluster-slot-lock 1m;\n    lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m;\n    lua_shared_dict plugin-limit-conn-redis-cluster-slot-lock 1m;\n    lua_shared_dict tracing_buffer 10m;    # plugin skywalking\n    lua_shared_dict access-tokens 1m;    # plugin authz-keycloak\n    lua_shared_dict discovery 1m;    # plugin authz-keycloak\n    lua_shared_dict plugin-api-breaker 10m;\n    lua_capture_error_log 1m;    # plugin error-log-logger\n    lua_shared_dict etcd-cluster-health-check 10m; # etcd health check\n    lua_shared_dict ext-plugin 1m;\n    lua_shared_dict kubernetes 1m;\n    lua_shared_dict kubernetes-first 1m;\n    lua_shared_dict kubernetes-second 1m;\n    lua_shared_dict tars 1m;\n    lua_shared_dict ocsp-stapling 10m;\n    lua_shared_dict mcp-session 10m;\n    lua_shared_dict xds-config 1m;\n    lua_shared_dict xds-config-version 1m;\n    lua_shared_dict cas_sessions 10m;\n    lua_shared_dict test 5m;\n\n    proxy_ssl_name \\$upstream_host;\n    proxy_ssl_server_name on;\n\n    resolver $dns_addrs_str;\n    resolver_timeout 5;\n\n    underscores_in_headers on;\n    lua_socket_log_errors off;\n    client_body_buffer_size 8k;\n\n    variables_hash_bucket_size 128;\n\n    upstream apisix_backend {\n        server 0.0.0.1;\n_EOC_\n\n    if ($version =~ m/\\/apisix-nginx-module/) {\n    $http_config .= <<_EOC_;\n        keepalive 32;\n\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n    }\n_EOC_\n    } else {\n    $http_config .= <<_EOC_;\n        balancer_by_lua_block {\n            apisix.http_balancer_phase()\n        }\n\n        keepalive 32;\n    }\n\n    lua_shared_dict prometheus-metrics 10m;\n_EOC_\n    }\n\n    $http_config .= <<_EOC_;\n\n    $dubbo_upstream\n\n    init_by_lua_block {\n        $test_default_config\n        $init_by_lua_block\n    }\n\n    init_worker_by_lua_block {\n        require(\"apisix\").http_init_worker()\n        $extra_init_worker_by_lua\n    }\n\n    exit_worker_by_lua_block {\n        require(\"apisix\").http_exit_worker()\n    }\n\n    log_format main escape=default '\\$remote_addr - \\$remote_user [\\$time_local] \\$http_host \"\\$request\" \\$status \\$body_bytes_sent \\$request_time \"\\$http_referer\" \"\\$http_user_agent\" \\$upstream_addr \\$upstream_status \\$apisix_upstream_response_time \"\\$upstream_scheme://\\$upstream_host\\$upstream_uri\" \\$request_llm_model \\$llm_model \\$llm_time_to_first_token \\$llm_prompt_tokens \\$llm_completion_tokens';\n\n    # fake server, only for test\n    server {\n        listen 1980;\n        listen 1981;\n        listen 1982;\n        listen 5044;\n\n_EOC_\n\n    if (defined $block->upstream_server_config) {\n        $http_config .= $block->upstream_server_config;\n    }\n\n    my $ipv6_fake_server = \"\";\n    if (defined $block->listen_ipv6) {\n        $ipv6_fake_server = \"listen \\[::1\\]:1980;\";\n    }\n\n    $http_config .= <<_EOC_;\n        $ipv6_fake_server\n        server_tokens off;\n\n        access_log logs/fake-server-access.log main;\n\n        location / {\n            content_by_lua_block {\n                require(\"lib.server\").go()\n            }\n\n            more_clear_headers Date;\n        }\n    }\n\n    $a6_ngx_directives\n\n    server {\n        listen 1983 ssl;\n        ssl_certificate             cert/apisix.crt;\n        ssl_certificate_key         cert/apisix.key;\n        lua_ssl_trusted_certificate cert/apisix.crt;\n_EOC_\n\n    if (defined $block->upstream_server_config) {\n        $http_config .= $block->upstream_server_config;\n    }\n\n    $http_config .= <<_EOC_;\n        server_tokens off;\n\n        access_log logs/fake-server-access.log main;\n\n        ssl_certificate_by_lua_block {\n            local ngx_ssl = require \"ngx.ssl\"\n            ngx.log(ngx.WARN, \"Receive SNI: \", ngx_ssl.server_name())\n        }\n\n        location / {\n            content_by_lua_block {\n                require(\"lib.server\").go()\n            }\n\n            more_clear_headers Date;\n        }\n    }\n\n_EOC_\n\n    $http_config .= <<_EOC_;\n    server {\n        listen 7085;\n        location /status/ready {\n            content_by_lua_block {\n                apisix.status_ready()\n            }\n        }\n        location /status {\n            content_by_lua_block {\n                apisix.status()\n            }\n        }\n    }\n    server {\n        listen unix:$apisix_home/t/servroot/logs/worker_events.sock;\n        access_log off;\n        location / {\n            content_by_lua_block {\n                require(\"resty.events.compat\").run()\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    my $TEST_NGINX_HTML_DIR = $ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n    my $ipv6_listen_conf = '';\n    if (defined $block->listen_ipv6) {\n        $ipv6_listen_conf = \"listen \\[::1\\]:1984;\"\n    }\n\n    my $config = $block->config // '';\n    $config .= <<_EOC_;\n        $ipv6_listen_conf\n\n        listen 1994 quic reuseport;\n        listen 1994 ssl;\n        http2 on;\n        http3 on;\n        ssl_certificate             cert/apisix.crt;\n        ssl_certificate_key         cert/apisix.key;\n        lua_ssl_trusted_certificate $custom_trusted_cert;\n\n        ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;\n\n        ssl_session_cache    shared:SSL:20m;\n        ssl_session_timeout 10m;\n        ssl_session_tickets off;\n\n        ssl_client_hello_by_lua_block {\n            apisix.ssl_client_hello_phase()\n        }\n\n        ssl_certificate_by_lua_block {\n            apisix.ssl_phase()\n        }\n\n        access_log logs/access.log main;\n\n        set \\$dubbo_service_name          '';\n        set \\$dubbo_service_version       '';\n        set \\$dubbo_method                '';\n\n        location = /apisix/nginx_status {\n            allow 127.0.0.0/24;\n            access_log off;\n            stub_status;\n        }\n\n        location /apisix/admin {\n            set \\$upstream_scheme             'http';\n            set \\$upstream_host               \\$http_host;\n            set \\$upstream_uri                '';\n\n            content_by_lua_block {\n                apisix.http_admin()\n            }\n        }\n\n        location /v1/ {\n            content_by_lua_block {\n                apisix.http_control()\n            }\n        }\n\n        location / {\n            set \\$upstream_mirror_host        '';\n            set \\$upstream_mirror_uri         '';\n            set \\$upstream_upgrade            '';\n            set \\$upstream_connection         '';\n\n            set \\$upstream_scheme             'http';\n            set \\$upstream_host               \\$http_host;\n            set \\$upstream_uri                '';\n            set \\$ctx_ref                     '';\n\n            set \\$upstream_cache_zone            off;\n            set \\$upstream_cache_key             '';\n            set \\$upstream_cache_bypass          '';\n            set \\$upstream_no_cache              '';\n            $a6_ngx_vars\n\n            proxy_cache                         \\$upstream_cache_zone;\n            proxy_cache_valid                   any 10s;\n            proxy_cache_min_uses                1;\n            proxy_cache_methods                 GET HEAD POST;\n            proxy_cache_lock_timeout            5s;\n            proxy_cache_use_stale               off;\n            proxy_cache_key                     \\$upstream_cache_key;\n            proxy_no_cache                      \\$upstream_no_cache;\n            proxy_cache_bypass                  \\$upstream_cache_bypass;\n\n            set \\$llm_content_risk_level         '';\n            set \\$request_type               'traditional_http';\n\n            set \\$llm_time_to_first_token        '0';\n            set \\$request_llm_model              '';\n            set \\$llm_model                      '';\n            set \\$llm_prompt_tokens              '0';\n            set \\$llm_completion_tokens          '0';\n\n            set \\$apisix_upstream_response_time  \\$upstream_response_time;\n            access_log $apisix_home/t/servroot/logs/access.log main;\n\n            set \\$apisix_request_id \\$request_id;\n            lua_error_log_request_id \\$apisix_request_id;\n\n            access_by_lua_block {\n                -- wait for etcd sync\n                ngx.sleep($wait_etcd_sync)\n                apisix.http_access_phase()\n            }\n\n            proxy_http_version 1.1;\n            proxy_set_header   Host              \\$upstream_host;\n            proxy_set_header   Upgrade           \\$upstream_upgrade;\n            proxy_set_header   Connection        \\$upstream_connection;\n            proxy_set_header   X-Real-IP         \\$remote_addr;\n            proxy_pass_header  Date;\n\n            ### the following x-forwarded-* headers is to send to upstream server\n\n            set \\$var_x_forwarded_proto      \\$scheme;\n            set \\$var_x_forwarded_host       \\$host;\n            set \\$var_x_forwarded_port       \\$server_port;\n\n            proxy_set_header   X-Forwarded-For      \\$proxy_add_x_forwarded_for;\n            proxy_set_header   X-Forwarded-Proto    \\$var_x_forwarded_proto;\n            proxy_set_header   X-Forwarded-Host     \\$var_x_forwarded_host;\n            proxy_set_header   X-Forwarded-Port     \\$var_x_forwarded_port;\n\n            proxy_pass         \\$upstream_scheme://apisix_backend\\$upstream_uri;\n            mirror             /proxy_mirror;\n\n            header_filter_by_lua_block {\n                apisix.http_header_filter_phase()\n            }\n\n            body_filter_by_lua_block {\n                apisix.http_body_filter_phase()\n            }\n\n            log_by_lua_block {\n                apisix.http_log_phase()\n            }\n        }\n\n        $grpc_location\n        $dubbo_location\n\n        location = /proxy_mirror {\n            internal;\n_EOC_\n\n    if ($version !~ m/\\/apisix-nginx-module/) {\n        $config .= <<_EOC_;\n            if (\\$upstream_mirror_uri = \"\") {\n                return 200;\n            }\n_EOC_\n    }\n\n    $config .= <<_EOC_;\n            proxy_http_version 1.1;\n            proxy_set_header Host \\$upstream_host;\n            proxy_pass \\$upstream_mirror_uri;\n        }\n\n        location = /proxy_mirror_grpc {\n            internal;\n_EOC_\n\n    if ($version !~ m/\\/apisix-nginx-module/) {\n        $config .= <<_EOC_;\n            if (\\$upstream_mirror_uri = \"\") {\n                return 200;\n            }\n_EOC_\n    }\n\n    $config .= <<_EOC_;\n            grpc_pass \\$upstream_mirror_host;\n        }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $user_apisix_yaml = $block->apisix_yaml // \"\";\n    if ($user_apisix_yaml) {\n        $user_apisix_yaml = <<_EOC_;\n>>> ../conf/$apisix_file\n$user_apisix_yaml\n_EOC_\n    }\n\n    my $user_apisix_json = $block->apisix_json // \"\";\n    if ($user_apisix_json){\n        $user_apisix_json = <<_EOC_;\n>>> ../conf/$apisix_file_json\n$user_apisix_json\n_EOC_\n    }\n\n    my $yaml_config = $block->yaml_config // $user_yaml_config;\n\n    my $default_deployment = <<_EOC_;\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\n_EOC_\n\n    if ($yaml_config !~ m/deployment:/) {\n        $yaml_config = $default_deployment . $yaml_config;\n    }\n\n    if ($block->extra_yaml_config) {\n        $yaml_config .= $block->extra_yaml_config;\n    }\n\n    my $user_debug_config = $block->debug_config // \"\";\n\n    my $user_files = $block->user_files;\n    $user_files .= <<_EOC_;\n>>> ../conf/$debug_file\n$user_debug_config\n>>> ../conf/$config_file\n$yaml_config\n>>> ../conf/cert/apisix.crt\n$ssl_crt\n>>> ../conf/cert/apisix.key\n$ssl_key\n>>> ../conf/cert/apisix_ecc.crt\n$ssl_ecc_crt\n>>> ../conf/cert/apisix_ecc.key\n$ssl_ecc_key\n>>> ../conf/cert/test2.crt\n$test2_crt\n>>> ../conf/cert/test2.key\n$test2_key\n>>> ../conf/cert/etcd.pem\n$etcd_pem\n>>> ../conf/cert/etcd.key\n$etcd_key\n$user_apisix_yaml\n$user_apisix_json\n_EOC_\n\n    $block->set_value(\"user_files\", $user_files);\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)\n        && (!defined $block->grep_error_log)\n        && (!defined $block->ignore_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    $block;\n});\n\nsub run_or_exit ($) {\n    my ($cmd) = @_;\n    my $output = `$cmd`;\n    if ($?) {\n        warn \"$output\";\n        exit 1;\n    }\n}\n\nadd_cleanup_handler(sub {\n    if ($ENV{FLUSH_ETCD}) {\n        delete $ENV{APISIX_PROFILE};\n        run_or_exit \"etcdctl del --prefix /apisix\";\n        run_or_exit \"./bin/apisix init_etcd\";\n    }\n});\n\n1;\n"
  },
  {
    "path": "t/admin/api.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /apisix/admin/routes\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Server header for admin API\n--- response_headers_like\nServer: APISIX/(.*)\n\n\n\n=== TEST 2: Server header for admin API without token\n--- yaml_config\ndeployment:\n    admin:\n        admin_key:\n            - key: a\n              name: a\n              role: admin\napisix:\n  node_listen: 1984\n  enable_server_tokens: false\n--- error_code: 401\n--- response_headers\nServer: APISIX\n\n\n\n=== TEST 3: Version header for admin API (without apikey)\n--- yaml_config\ndeployment:\n    admin:\n        admin_key:\n            - key: a\n              name: a\n              role: admin\napisix:\n  admin_api_version: default\n--- error_code: 401\n--- response_headers\n! X-API-VERSION\n\n\n\n=== TEST 4: Version header for admin API (v2)\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: ~\n    admin_api_version: v2\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\n--- response_headers\nX-API-VERSION: v2\n\n\n\n=== TEST 5: Version header for admin API (v3)\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: ~\n    admin_api_version: v3\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\n--- response_headers\nX-API-VERSION: v3\n\n\n\n=== TEST 6: CORS header for admin API\n--- response_headers\nAccess-Control-Allow-Origin: *\n\n\n\n=== TEST 7: CORS header disabled for admin API\n--- yaml_config\ndeployment:\n    admin:\n        admin_key: ~\n        enable_admin_cors: false\n--- response_headers\nAccess-Control-Allow-Origin:\n\n\n\n=== TEST 8: Compatibility for admin API (v2)\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: ~\n    admin_api_version: default\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\n--- response_headers\nX-API-VERSION: v2\n--- response_body_like: \"/apisix/routes\"\n\n\n\n=== TEST 9: Head method support for admin API\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin',\n                ngx.HTTP_HEAD)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: Access with api key, and admin_key_required=true\n--- yaml_config\ndeployment:\n  admin:\n    admin_key_required: true\n    admin_key:\n    - name: admin\n      role: admin\n      key: rDAkLJbqvoBzBOoxuYAUDbbWaSilvIca\n--- more_headers\nX-API-KEY: rDAkLJbqvoBzBOoxuYAUDbbWaSilvIca\n--- request\nGET /apisix/admin/routes\n--- error_code: 200\n\n\n\n=== TEST 11: Access with wrong api key, and admin_key_required=true\n--- yaml_config\ndeployment:\n  admin:\n    admin_key_required: true\n--- more_headers\nX-API-KEY: wrong-key\n--- request\nGET /apisix/admin/routes\n--- error_code: 401\n\n\n\n=== TEST 12: Access without api key, and admin_key_required=true\n--- yaml_config\ndeployment:\n  admin:\n    admin_key_required: true\n--- request\nGET /apisix/admin/routes\n--- error_code: 401\n\n\n\n=== TEST 13: Access with api key, but admin_key_required=false\n--- yaml_config\ndeployment:\n  admin:\n    admin_key_required: false\n    admin_key:\n    - name: admin\n      role: admin\n      key: rDAkLJbqvoBzBOoxuYAUDbbWaSilvIca\n--- more_headers\nX-API-KEY: rDAkLJbqvoBzBOoxuYAUDbbWaSilvIca\n--- request\nGET /apisix/admin/routes\n--- error_code: 200\n--- error_log\nAdmin key is bypassed!\n\n\n\n=== TEST 14: Access with wrong api key, but admin_key_required=false\n--- yaml_config\ndeployment:\n  admin:\n    admin_key_required: false\n--- more_headers\nX-API-KEY: wrong-key\n--- request\nGET /apisix/admin/routes\n--- error_code: 200\n--- error_log\nAdmin key is bypassed!\n\n\n\n=== TEST 15: Access without api key, but admin_key_required=false\n--- yaml_config\ndeployment:\n  admin:\n    admin_key_required: false\n--- request\nGET /apisix/admin/routes\n--- error_code: 200\n--- error_log\nAdmin key is bypassed!\n"
  },
  {
    "path": "t/admin/balancer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $init_by_lua_block = <<_EOC_;\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    function test(route, ctx, count)\n        local balancer = require(\"apisix.balancer\")\n        local res = {}\n        for i = 1, count or 12 do\n            ctx.balancer_try_count = 0\n            local server, err = balancer.pick_server(route, ctx)\n            if err then\n                ngx.say(\"failed: \", err)\n            end\n\n            core.log.warn(\"host: \", server.host, \" port: \", server.port)\n            res[server.host] = (res[server.host] or 0) + 1\n        end\n\n        local keys = {}\n        for k,v in pairs(res) do\n            table.insert(keys, k)\n        end\n        table.sort(keys)\n\n        for _, key in ipairs(keys) do\n            ngx.say(\"host: \", key, \" count: \", res[key])\n        end\n\n        ctx.server_picker = nil\n    end\n_EOC_\n    $block->set_value(\"init_by_lua_block\", $init_by_lua_block);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: roundrobin with same weight\n--- config\n    location /t {\n        content_by_lua_block {\n            local up_conf = {\n                type = \"roundrobin\",\n                nodes = {\n                    {host = \"39.97.63.215\", port = 80, weight = 1, priority = 0},\n                    {host = \"39.97.63.216\", port = 81, weight = 1, priority = 0},\n                    {host = \"39.97.63.217\", port = 82, weight = 1, priority = 0},\n                }\n            }\n            local ctx = {conf_version = 1}\n            ctx.upstream_conf = up_conf\n            ctx.upstream_version = \"ver\"\n            ctx.upstream_key = up_conf.type .. \"#route_\" .. \"id\"\n\n            test(route, ctx)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhost: 39.97.63.215 count: 4\nhost: 39.97.63.216 count: 4\nhost: 39.97.63.217 count: 4\n\n\n\n=== TEST 2: roundrobin with different weight\n--- config\n    location /t {\n        content_by_lua_block {\n            local up_conf = {\n                type = \"roundrobin\",\n                nodes = {\n                    {host = \"39.97.63.215\", port = 80, weight = 1, priority = 0},\n                    {host = \"39.97.63.216\", port = 81, weight = 2, priority = 0},\n                    {host = \"39.97.63.217\", port = 82, weight = 3, priority = 0},\n                }\n            }\n            local ctx = {conf_version = 1}\n            ctx.upstream_conf = up_conf\n            ctx.upstream_version = \"ver\"\n            ctx.upstream_key = up_conf.type .. \"#route_\" .. \"id\"\n\n            test(route, ctx)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhost: 39.97.63.215 count: 2\nhost: 39.97.63.216 count: 4\nhost: 39.97.63.217 count: 6\n\n\n\n=== TEST 3: roundrobin, cached server picker by version\n--- config\n    location /t {\n        content_by_lua_block {\n            local up_conf = {\n                type = \"roundrobin\",\n                nodes = {\n                    {host = \"39.97.63.215\", port = 80, weight = 1, priority = 0},\n                    {host = \"39.97.63.216\", port = 81, weight = 1, priority = 0},\n                    {host = \"39.97.63.217\", port = 82, weight = 1, priority = 0},\n                }\n            }\n            local ctx = {}\n            ctx.upstream_conf = up_conf\n            ctx.upstream_version = 1\n            ctx.upstream_key = up_conf.type .. \"#route_\" .. \"id\"\n\n            test(route, ctx)\n\n            -- cached by version\n            up_conf.nodes = {\n                {host = \"39.97.63.218\", port = 80, weight = 1, priority = 0},\n                {host = \"39.97.63.219\", port = 80, weight = 0, priority = 0},\n            }\n            test(route, ctx)\n\n            -- update, version changed\n            ctx.upstream_version = 2\n            test(route, ctx)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhost: 39.97.63.215 count: 4\nhost: 39.97.63.216 count: 4\nhost: 39.97.63.217 count: 4\nhost: 39.97.63.215 count: 4\nhost: 39.97.63.216 count: 4\nhost: 39.97.63.217 count: 4\nhost: 39.97.63.218 count: 12\n\n\n\n=== TEST 4: chash\n--- config\n    location /t {\n        content_by_lua_block {\n            local up_conf = {\n                type = \"chash\",\n                key  = \"remote_addr\",\n                nodes = {\n                    {host = \"39.97.63.215\", port = 80, weight = 1, priority = 0},\n                    {host = \"39.97.63.216\", port = 81, weight = 1, priority = 0},\n                    {host = \"39.97.63.217\", port = 82, weight = 1, priority = 0},\n                }\n            }\n            local ctx = {\n                var = {remote_addr = \"127.0.0.1\"},\n            }\n            ctx.upstream_conf = up_conf\n            ctx.upstream_version = 1\n            ctx.upstream_key = up_conf.type .. \"#route_\" .. \"id\"\n\n            test(route, ctx)\n\n            -- cached by version\n            up_conf.nodes = {\n                {host = \"39.97.63.218\", port = 80, weight = 1, priority = 0},\n                {host = \"39.97.63.219\", port = 80, weight = 0, priority = 0},\n            }\n            test(route, ctx)\n\n            -- update, version changed\n            ctx.upstream_version = 2\n            test(route, ctx)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhost: 39.97.63.215 count: 12\nhost: 39.97.63.215 count: 12\nhost: 39.97.63.218 count: 12\n\n\n\n=== TEST 5: return item directly if only have one item in `nodes`\n--- config\n    location /t {\n        content_by_lua_block {\n            local up_conf = {\n                type = \"roundrobin\",\n                nodes = {\n                    {host = \"39.97.63.215\", port = 80, weight = 1, priority = 0},\n                    {host = \"39.97.63.216\", port = 81, weight = 1, priority = 0},\n                    {host = \"39.97.63.217\", port = 82, weight = 1, priority = 0},\n                }\n            }\n            local ctx = {}\n            ctx.upstream_conf = up_conf\n            ctx.upstream_version = 1\n            ctx.upstream_key = up_conf.type .. \"#route_\" .. \"id\"\n\n            test(route, ctx)\n\n            -- one item in nodes, return it directly\n            up_conf.nodes = {\n                {host = \"39.97.63.218\", port = 80, weight = 1, priority = 0},\n            }\n            test(route, ctx)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhost: 39.97.63.215 count: 4\nhost: 39.97.63.216 count: 4\nhost: 39.97.63.217 count: 4\nhost: 39.97.63.218 count: 12\n"
  },
  {
    "path": "t/admin/consumer-credentials.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Verify consumer plugin update takes effect immediately\n--- extra_yaml_config\nnginx_config:\n  worker_processes: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local http = require(\"resty.http\")\n\n            -- 1. Create route with key-auth plugin\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"query\": \"apikey\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"Route creation failed: \" .. body)\n                return\n            end\n\n            -- 2. Create consumer jack\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"Consumer creation failed: \" .. body)\n                return\n            end\n\n            -- 3. Create credentials for jack\n            code, body = t('/apisix/admin/consumers/jack/credentials/auth-one',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"Credential creation failed: \" .. body)\n                return\n            end\n            ngx.sleep(0.5)  -- wait for etcd to sync\n            -- 4. Verify valid request succeeds\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\n                \"http://127.0.0.1:\"..ngx.var.server_port..\"/anything?apikey=auth-one\",\n                { method = \"GET\" }\n            )\n            if not res then\n                ngx.say(\"Request failed: \", err)\n                return\n            end\n\n            if res.status ~= 200 then\n                ngx.say(\"Unexpected status: \", res.status)\n                ngx.say(res.body)\n                return\n            end\n\n            -- 5. Update consumer with fault-injection plugin\n            code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 400,\n                                \"body\": \"abort\"\n                            }\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"Consumer update failed: \" .. body)\n                return\n            end\n\n            -- 6. Verify all requests return 400\n            for i = 1, 5 do\n                local res, err = httpc:request_uri(\n                    \"http://127.0.0.1:\"..ngx.var.server_port..\"/anything?apikey=auth-one\",\n                    { method = \"GET\" }\n                )\n                if not res then\n                    ngx.say(i, \": Request failed: \", err)\n                    return\n                end\n\n                if res.status ~= 400 then\n                    ngx.say(i, \": Expected 400 but got \", res.status)\n                    return\n                end\n\n                if res.body ~= \"abort\" then\n                    ngx.say(i, \": Unexpected response body: \", res.body)\n                    return\n                end\n            end\n\n            ngx.say(\"All requests aborted as expected\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nAll requests aborted as expected\n"
  },
  {
    "path": "t/admin/consumer-group-force-delete.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set consumer_group(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 200,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"group\": \"consumer_group_1\"\n                        }\n                    }\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 201\n--- response_body\npassed\n\n\n\n=== TEST 2: add consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/consumers/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"1\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    },\n                    \"group_id\": \"1\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: delete consumer_group(wrong header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/consumer_groups/1?force=anyvalue',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this consumer group, consumer [1] is still using it now\"}\n\n\n\n=== TEST 4: delete consumer_group(without force delete header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/consumer_groups/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this consumer group, consumer [1] is still using it now\"}\n\n\n\n=== TEST 5: delete consumer_group(force delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/consumer_groups/1?force=true',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/consumers/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/consumer-group.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumer_groups/company_a\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumer_groups/company_a\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: GET all\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"total\": 1,\n                    \"list\": [\n                        {\n                            \"key\": \"/apisix/consumer_groups/company_a\",\n                            \"value\": {\n                                \"plugins\": {\n                                    \"limit-count\": {\n                                    \"time_window\": 60,\n                                    \"count\": 2,\n                                    \"key\": \"remote_addr\",\n                                    \"rejected_code\": 503\n                                    }\n                                }\n                            }\n                        }\n                    ]\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"plugins\": {\n                    \"limit-count\": {\n                        \"count\": 3,\n                        \"time_window\": 60,\n                        \"rejected_code\": 503,\n                        \"key\": \"remote_addr\"\n                    }\n                }}]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 3,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumer_groups/company_a\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: PATCH (sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/consumer_groups/company_a/plugins',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"limit-count\": {\n                        \"count\": 2,\n                        \"time_window\": 60,\n                        \"rejected_code\": 503,\n                        \"key\": \"remote_addr\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumer_groups/company_a\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: invalid plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"rejected_code\": 503,\n                            \"time_window\": 60,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none\"}\n--- error_code: 400\n\n\n\n=== TEST 7: PUT (with non-plugin fields)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"labels\": {\n                        \"你好\": \"世界\"\n                    },\n                    \"desc\": \"blah\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"labels\": {\n                            \"你好\": \"世界\"\n                        },\n                        \"desc\": \"blah\"\n                    },\n                    \"key\": \"/apisix/consumer_groups/company_a\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: GET (with non-plugin fields)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"labels\": {\n                            \"你好\": \"世界\"\n                        },\n                        \"desc\": \"blah\"\n                    },\n                    \"key\": \"/apisix/consumer_groups/company_a\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: invalid non-plugin fields\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"labels\": \"a\",\n                    \"plugins\": {\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: wrong type: expected object, got string\"}\n--- error_code: 400\n\n\n\n=== TEST 10: set consumer-group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/consumer_groups/company_a'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: add consumer with group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/foobar',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foobar\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-two\"\n                        }\n                    },\n                    \"group_id\": \"company_a\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: delete-consumer group failed\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                 ngx.HTTP_DELETE\n            )\n            ngx.print(body)\n        }\n    }\n--- response_body\n{\"error_msg\":\"can not delete this consumer group, consumer [foobar] is still using it now\"}\n\n\n\n=== TEST 13: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/foobar',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: delete consumer-group\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: add consumer with invalid group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/foobar',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foobar\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-two\"\n                        }\n                    },\n                    \"group_id\": \"invalid_group\"\n                }]]\n                )\n            assert(code >= 300)\n            ngx.say(body)\n        }\n    }\n--- response_body_like\n.*failed to fetch consumer group info by consumer group id.*\n"
  },
  {
    "path": "t/admin/consumers.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\",\n                     \"desc\": \"new consumer\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"jack\",\n                        \"desc\": \"new consumer\"\n                    },\n                    \"key\": \"/apisix/consumers/jack\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: update consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/jack'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"username\": \"jack\",\n                    \"desc\": \"new consumer\",\n                    \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"auth-one\"\n                            }\n                        }\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"jack\",\n                        \"desc\": \"new consumer\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"4y+JvURBE6ZwRbbgaryrhg==\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumers/jack\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/consumers/jack'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: get consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"username\": \"jack\",\n                        \"desc\": \"new consumer\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"auth-one\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumers/jack\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_DELETE\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: delete consumer(id: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/consumers/not_found',\n                 ngx.HTTP_DELETE,\n                 nil\n            )\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 6: missing username\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"id\":\"jack\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"id\": \"jack\"\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"username\\\" is required\"}\n\n\n\n=== TEST 7: consumer username allows '-' in it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"username\":\"Jack-and-Rose_123\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 201\n\n\n\n=== TEST 8: add consumer with labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\",\n                     \"desc\": \"new consumer\",\n                     \"labels\": {\n                         \"build\":\"16\",\n                         \"env\":\"production\",\n                         \"version\":\"v2\"\n                     }\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"jack\",\n                        \"desc\": \"new consumer\",\n                        \"labels\": {\n                            \"build\":\"16\",\n                            \"env\":\"production\",\n                            \"version\":\"v2\"\n                        }\n                    },\n                    \"key\": \"/apisix/consumers/jack\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: invalid format of label value: set consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"username\":\"jack\",\n                     \"desc\": \"new consumer\",\n                     \"labels\": {\n                        \"env\": [\"production\", \"release\"]\n                     }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: failed to validate env (matching \\\".*\\\"): wrong type: expected string, got table\"}\n\n\n\n=== TEST 10: post consumers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_POST,\n                 \"\"\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 405\n--- response_body\n{\"error_msg\":\"not supported `POST` method for consumer\"}\n\n\n\n=== TEST 11: add consumer with create_time and update_time(pony)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"pony\",\n                     \"desc\": \"new consumer\",\n                     \"create_time\": 1602883670,\n                     \"update_time\": 1602893670\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"pony\",\n                        \"desc\": \"new consumer\",\n                        \"create_time\": 1602883670,\n                        \"update_time\": 1602893670\n                    },\n                    \"key\": \"/apisix/consumers/pony\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"the property is forbidden:.*\"\\}/\n"
  },
  {
    "path": "t/admin/consumers2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/consumers/jack\",\"value\":{\"username\":\"jack\"}}\n\n\n\n=== TEST 2: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/consumers/jack\",\"value\":{\"username\":\"jack\"}}\n\n\n\n=== TEST 3: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/consumers/jack\"}\n\n\n\n=== TEST 4: list empty resources\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/consumers',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"list\":[],\"total\":0}\n\n\n\n=== TEST 5: mismatched username, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/consumers/jack1',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\"\n                }]]\n            )\n\n            ngx.print(message)\n        }\n    }\n--- response_body\n{\"error_msg\":\"wrong username\"}\n"
  },
  {
    "path": "t/admin/credentials.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create a credential for invalid consumer: consumer not found error\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_a',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\n                             \"key\": \"the-key\"\n                         }\n                     }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"consumer not found\"}\n\n\n\n=== TEST 2: add a consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\",\n                     \"desc\": \"new consumer\",\n                     \"plugins\": {\n                         \"basic-auth\": {\n                             \"username\": \"the-user\",\n                             \"password\": \"the-password\"\n                         }\n                     }\n                }]],\n                [[{\n                    \"key\": \"/apisix/consumers/jack\",\n                    \"value\":\n                    {\n                        \"username\":\"jack\",\n                        \"desc\": \"new consumer\",\n                        \"plugins\": {\n                            \"basic-auth\": {\n                                \"username\": \"the-user\",\n                                \"password\": \"WvF5kpaLvIzjuk4GNIMTJg==\"\n                            }\n                        }\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: add a credentials with basic-auth for the consumer jack, should success\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_a',\n                ngx.HTTP_PUT,\n                [[{\n                     \"desc\": \"basic-auth for jack\",\n                     \"plugins\": {\n                         \"basic-auth\": {\n                             \"username\": \"the-user\",\n                             \"password\": \"the-password\"\n                         }\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"desc\":\"basic-auth for jack\",\n                        \"id\":\"credential_a\",\n                        \"plugins\":{\"basic-auth\":{\"username\":\"the-user\",\"password\":\"WvF5kpaLvIzjuk4GNIMTJg==\"}}\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/credential_a\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: add a credential with key-auth for the consumer jack, should success\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b',\n                ngx.HTTP_PUT,\n                [[{\n                     \"desc\": \"key-auth for jack\",\n                     \"plugins\": {\n                         \"key-auth\": {\n                             \"key\": \"the-key\"\n                         }\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"desc\":\"key-auth for jack\",\n                        \"id\":\"credential_b\",\n                        \"plugins\":{\"key-auth\":{\"key\":\"JCX7x1qN5e9kHt0GuJfWpw==\"}}\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/credential_b\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: add a credential with a plugin which is not a auth plugin, should fail\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b',\n                ngx.HTTP_PUT,\n                [[{\n                     \"desc\": \"limit-conn for jack\",\n                     \"plugins\": {\n                         \"limit-conn\": {\n                            \"conn\": 1,\n                            \"burst\": 0,\n                            \"default_conn_delay\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key_type\": \"var\",\n                            \"key\": \"http_a\"\n                         }\n                     }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"only supports auth type plugins in consumer credential\"}\n\n\n\n=== TEST 6: list consumers: should not contain credential\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body, res = t('/apisix/admin/consumers', ngx.HTTP_GET)\n\n            ngx.status = code\n            res = json.decode(res)\n            assert(res.total == 1)\n            assert(res.list[1].key == \"/apisix/consumers/jack\")\n        }\n    }\n--- request\nGET /t\n--- response_body\n\n\n\n=== TEST 7: list credentials: should contain credential_a and credential_b\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body, res = t('/apisix/admin/consumers/jack/credentials', ngx.HTTP_GET)\n\n            ngx.status = code\n            res = json.decode(res)\n            assert(res.total == 2)\n            assert(res.list[1].key == \"/apisix/consumers/jack/credentials/credential_a\")\n            assert(res.list[2].key == \"/apisix/consumers/jack/credentials/credential_b\")\n        }\n    }\n--- request\nGET /t\n--- response_body\n\n\n\n=== TEST 8: get a credential\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"key\": \"/apisix/consumers/jack/credentials/credential_b\",\n                    \"value\": {\n                        \"desc\": \"key-auth for jack\",\n                         \"plugins\": {\"key-auth\": {\"key\": \"the-key\"}\n                     }}\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: update credential: should ok\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_b',\n                ngx.HTTP_PUT,\n                [[{\n                     \"desc\": \"new description\",\n                     \"plugins\": {\n                         \"key-auth\": {\n                             \"key\": \"new-key\"\n                         }\n                     }\n                }]],\n                [[{\n                    \"key\": \"/apisix/consumers/jack/credentials/credential_b\",\n                     \"value\": {\n                         \"desc\": \"new description\",\n                         \"plugins\": {\n                             \"key-auth\": {\n                                 \"key\": \"523EisB/dvqlIT9RzfF3ZQ==\"\n                             }\n                         }\n                     }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: delete credential\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/credential_a', ngx.HTTP_DELETE)\n\n            assert(code == 200)\n            ngx.status = code\n\n            code, body, res = t('/apisix/admin/consumers/jack/credentials', ngx.HTTP_GET)\n            res = json.decode(res)\n            assert(res.total == 1)\n            assert(res.list[1].key == \"/apisix/consumers/jack/credentials/credential_b\")\n        }\n    }\n--- request\nGET /t\n--- response_body\n\n\n\n=== TEST 11: create a credential has more than one plugin: should not ok\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/xxx-yyy-zzz',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\"key\": \"the-key\"},\n                         \"basic-auth\": {\"username\": \"the-user\", \"password\": \"the-password\"}\n                     }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"plugins\\\" validation failed: expect object to have at most 1 properties\"}\n\n\n\n=== TEST 12: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_DELETE\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: list credentials: should get 404 because the consumer is deleted\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials', ngx.HTTP_GET)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n--- response_body\n{\"message\":\"Key not found\"}\n\n\n\n=== TEST 14: add a consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\"\n                }]]\n            )\n\n            if ngx.status >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: add a credential with key-auth for the consumer jack (id in the payload but not in uri), should success\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials',\n                ngx.HTTP_PUT,\n                [[{\n                     \"id\": \"d79a5aa3\",\n                     \"desc\": \"key-auth for jack\",\n                     \"plugins\": {\n                         \"key-auth\": {\n                             \"key\": \"the-key\"\n                         }\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"desc\":\"key-auth for jack\",\n                        \"id\":\"d79a5aa3\",\n                        \"plugins\":{\"key-auth\":{\"key\":\"JCX7x1qN5e9kHt0GuJfWpw==\"}}\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/d79a5aa3\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: add a credential with key-auth for the consumer jack but missing id in uri and payload, should fail\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials',\n                ngx.HTTP_PUT,\n                [[{\n                     \"desc\": \"key-auth for jack\",\n                     \"plugins\": {\n                         \"key-auth\": {\n                             \"key\": \"the-key\"\n                         }\n                     }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing credential id\"}\n"
  },
  {
    "path": "t/admin/filter.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nworker_connections(1024);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: ~\n    admin_api_version: v3\napisix:\n    node_listen: 1984\n    proxy_mode: http&stream\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: bad page_size(page_size must be between 10 and 500)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            ngx.sleep(0.5)\n\n            local code, body = t('/apisix/admin/routes/?page=1&page_size=2',\n                ngx.HTTP_GET\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body\npage_size must be between 10 and 500\n\n\n\n=== TEST 2: ignore bad page and would use default value 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/routes/?page=-1&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 10)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: sort by createdIndex\n# the smaller the createdIndex, the higher the ranking\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/routes/?page=1&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n            for i = 1, #res.list - 1 do\n                assert(res.list[i].createdIndex < res.list[i + 1].createdIndex)\n            end\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: routes pagination\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/routes/?page=1&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 10)\n\n            code, body, res = t('/apisix/admin/routes/?page=2&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 1)\n\n            code, body, res = t('/apisix/admin/routes/?page=3&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 0)\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: services pagination\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/services/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/services/?page=1&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 10)\n\n            code, body, res = t('/apisix/admin/services/?page=2&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 1)\n\n            code, body, res = t('/apisix/admin/services/?page=3&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 0)\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: only search name or labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/services/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"]] .. i .. [[\",\n                        \"labels\": {\"]] .. i .. '\":\"' .. i .. [[\"}\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local matched = {1, 10, 11}\n\n            local code, body, res = t('/apisix/admin/services/?name=1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            -- match the name are 1, 10, 11\n            assert(#res.list == 3)\n\n            for _, node in ipairs(res.list) do\n                assert(core.table.array_find(matched, tonumber(node.value.name)))\n            end\n\n            code, body, res = t('/apisix/admin/services/?label=1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            -- match the label are 1, 10, 11\n            assert(#res.list == 1)\n            assert(res.list[1].value.id == \"1\")\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: services filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/services/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/services/?name=1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n            -- match the name and label are 1, 10, 11\n            assert(#res.list == 3)\n\n            local matched = {1, 10, 11}\n            for _, node in ipairs(res.list) do\n                assert(core.table.array_find(matched, tonumber(node.value.name)))\n            end\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: routes filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"]] .. i .. [[\",\n                        \"uri\": \"]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/services/?name=1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n            -- match the name and label are 1, 10, 11\n            assert(#res.list == 3)\n\n            local matched = {1, 10, 11}\n            for _, node in ipairs(res.list) do\n                assert(core.table.array_find(matched, tonumber(node.value.name)))\n            end\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: filter with pagination\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, res = t('/apisix/admin/services/?name=1&page=1&page_size=10',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n            -- match the name and label are 1, 10, 11\n            -- we do filtering first now, so it will first filter to 1, 10, 11, and then paginate\n            -- res will contain 1, 10, 11 instead of just 1, 10.\n            assert(#res.list == 3)\n\n            local matched = {1, 10, 11}\n            for _, node in ipairs(res.list) do\n                assert(core.table.array_find(matched, tonumber(node.value.name)))\n            end\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: routes filter with uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 11 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"]] .. i .. [[\",\n                        \"uri\": \"]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/routes/?uri=1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n            -- match the name and label are 1, 10, 11\n            assert(#res.list == 3)\n\n            local matched = {1, 10, 11}\n            for _, node in ipairs(res.list) do\n                assert(core.table.array_find(matched, tonumber(node.value.name)))\n            end\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: match labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"labels\": {\n                        \"env\": \"production\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello2\",\n                    \"labels\": {\n                        \"env2\": \"production\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.5)\n\n            -- only match labels' keys\n            local code, body, res = t('/apisix/admin/routes/?label=env',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 1)\n            assert(res.list[1].value.id == \"1\")\n\n            -- don't match labels' values\n            code, body, res = t('/apisix/admin/routes/?label=production',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 0)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: match uris\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\", \"/world\"]\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/foo\", \"/bar\"]\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/routes/?uri=world',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 1)\n            assert(res.list[1].value.id == \"1\")\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: match uris & labels\n# uris are same in different routes, filter by labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\", \"/world\"],\n                    \"labels\": {\n                        \"env\": \"production\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\", \"/world\"],\n                    \"labels\": {\n                        \"build\": \"16\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.5)\n\n            -- only match route 1\n            local code, body, res = t('/apisix/admin/routes/?uri=world&label=env',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 1)\n            assert(res.list[1].value.id == \"1\")\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: match uri & labels\n# uri is same in different routes, filter by labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"labels\": {\n                        \"env\": \"production\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"labels\": {\n                        \"env2\": \"production\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body, res = t('/apisix/admin/routes/?uri=hello&label=env',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 1)\n            assert(res.list[1].value.id == \"1\")\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: filtered data total\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, res = t('/apisix/admin/routes', ngx.HTTP_GET)\n            res = json.decode(res)\n            assert(res.total == 11)\n            assert(#res.list == 11)\n\n            local code, body, res = t('/apisix/admin/routes/?label=', ngx.HTTP_GET)\n            res = json.decode(res)\n            assert(res.total == 0)\n            assert(#res.list == 0)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: pagination data total\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, res = t('/apisix/admin/routes?page=1&page_size=10', ngx.HTTP_GET)\n            res = json.decode(res)\n            assert(res.total == 11)\n            assert(#res.list == 10)\n\n            local code, body, res = t('/apisix/admin/routes?page=10&page_size=10', ngx.HTTP_GET)\n            res = json.decode(res)\n            assert(res.total == 11)\n            assert(#res.list == 0)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST: 17: filter by route service_id/upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            -- create a service\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            -- create a upstream\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            for i = 1, 11 do\n                local route = { uri = \"/hello\" .. i }\n                if i % 2 == 0 then\n                    route.service_id = \"1\"\n                else\n                    route.upstream_id = \"1\"\n                end\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    json.encode(route)\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            -- check service_id\n            local code, body, res = t('/apisix/admin/routes?filter='\n                                        .. ngx.encode_args({ service_id = \"1\" }),\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 5, \"expected 5 routes with service_id 1, got \" .. #res.list)\n\n            for i = 1, #res.list do\n                assert(tonumber(res.list[i].value.id) % 2 == 0,\n                       \"expected route id to be even, got \" .. res.list[i].value.id)\n                assert(res.list[i].value.service_id == \"1\",\n                       \"expected service_id 1, got \" .. tostring(res.list[i].value.service_id))\n            end\n\n            -- check upstream_id\n            local code, body, res = t('/apisix/admin/routes?filter='\n                                        .. ngx.encode_args({ upstream_id = \"1\" }),\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 6, \"expected 6 routes with upstream_id 1, got \" .. #res.list)\n\n            for i = 1, #res.list do\n                assert(tonumber(res.list[i].value.id) % 2 == 1,\n                       \"expected route id to be odd, got \" .. res.list[i].value.id)\n                assert(res.list[i].value.upstream_id == \"1\",\n                       \"expected upstream_id 1, got \" .. tostring(res.list[i].value.upstream_id))\n            end\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST: 18: filter by stream route service_id/upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            -- create a service\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            -- create a upstream\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            for i = 1, 11 do\n                local route = { server_port = 5432 }\n                if i % 2 == 0 then\n                    route.service_id = \"1\"\n                else\n                    route.upstream_id = \"1\"\n                end\n                local code, body = t('/apisix/admin/stream_routes/' .. i,\n                    ngx.HTTP_PUT,\n                    json.encode(route)\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            -- check service_id\n            local code, body, res = t('/apisix/admin/stream_routes?filter='\n                                        .. ngx.encode_args({ service_id = \"1\" }),\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n            assert(#res.list == 5, \"expected 5 stream routes with service_id 1, got \" .. #res.list)\n\n            for i = 1, #res.list do\n                assert(tonumber(res.list[i].value.id) % 2 == 0,\n                       \"expected stream route id to be even, got \" .. res.list[i].value.id)\n                assert(res.list[i].value.service_id == \"1\",\n                       \"expected service_id 1, got \" .. tostring(res.list[i].value.service_id))\n            end\n\n            -- check upstream_id\n            local code, body, res = t('/apisix/admin/stream_routes?filter='\n                                        .. ngx.encode_args({ upstream_id = \"1\" }),\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 6, \"expected 6 stream routes with upstream_id 1, got \" .. #res.list)\n\n            for i = 1, #res.list do\n                assert(tonumber(res.list[i].value.id) % 2 == 1,\n                       \"expected stream route id to be odd, got \" .. res.list[i].value.id)\n                assert(res.list[i].value.upstream_id == \"1\",\n                       \"expected upstream_id 1, got \" .. tostring(res.list[i].value.upstream_id))\n            end\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST: 19: filter by route (both service_id/upstream_id)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            -- create a service\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            -- create a upstream\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            for i = 1, 11 do\n                local route = { uri = \"/hello\" .. i }\n                if i % 2 == 0 then\n                    route.service_id = \"1\"\n                else\n                    route.upstream_id = \"1\"\n                    route.service_id = \"1\"\n                end\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    json.encode(route)\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            -- check service_id\n            local code, body, res = t('/apisix/admin/routes?filter='\n                                        .. ngx.encode_args({ service_id = \"1\" }),\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 11, \"expected 11 routes with service_id 1, got \" .. #res.list)\n\n            for i = 1, #res.list do\n                assert(res.list[i].value.service_id == \"1\",\n                       \"expected service_id 1, got \" .. tostring(res.list[i].value.service_id))\n            end\n\n            -- check both service_id and upstream_id\n            local code, body, res = t('/apisix/admin/routes?'\n                                        .. ngx.encode_args({filter = ngx.encode_args({ service_id = \"1\", upstream_id = \"1\" })}),\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            assert(#res.list == 6, \"expected 6 routes with both service_id 1 and upstream_id 1, got \" .. #res.list)\n\n            for i = 1, #res.list do\n                assert(tonumber(res.list[i].value.id) % 2 == 1,\n                       \"expected route id to be odd, got \" .. res.list[i].value.id)\n                assert(res.list[i].value.service_id == \"1\",\n                       \"expected service_id 1, got \" .. tostring(res.list[i].value.service_id))\n                assert(res.list[i].value.upstream_id == \"1\",\n                       \"expected upstream_id 1, got \" .. tostring(res.list[i].value.upstream_id))\n            end\n        }\n    }\n--- error_code: 200\n"
  },
  {
    "path": "t/admin/global-rules-conflict.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create first global rule with limit-count plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_code: 201\n\n\n\n=== TEST 2: try to create second global rule with same plugin (should fail)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 5,\n                            \"time_window\": 120,\n                            \"rejected_code\": 429,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/plugin 'limit-count' already exists in global rule with id '1'/\n--- error_code: 400\n\n\n\n=== TEST 3: create second global rule with different plugin (should succeed)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"127.0.0.0/24\"]\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_code: 201\n\n\n\n=== TEST 4: try to create third global rule with plugin from first rule (should fail)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 10,\n                            \"time_window\": 30,\n                            \"rejected_code\": 429,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/plugin 'limit-count' already exists in global rule with id '1'/\n--- error_code: 400\n\n\n\n=== TEST 5: try to create global rule with multiple plugins where one conflicts (should fail)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 1,\n                            \"burst\": 0,\n                            \"key\": \"remote_addr\"\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"192.168.0.0/16\"]\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/plugin 'ip-restriction' already exists in global rule with id '2'/\n--- error_code: 400\n\n\n\n=== TEST 6: update existing global rule with its current plugin (should succeed)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 3,\n                            \"time_window\": 90,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_code: 200\n\n\n\n=== TEST 7: prepare data to test removal (during global rule execution) of global rules with re-occurring plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local http = require(\"resty.http\")\n\n            local plugin_conf = function(id_val, count_val, t_val)\n                return {\n                    id = id_val,\n                    create_time = 1764938795,\n                    update_time = 1764938811,\n                    plugins = {\n                        [\"limit-count\"] = {\n                            key_type = \"var\",\n                            show_limit_quota_header = true,\n                            rejected_code = 503,\n                            policy = \"local\",\n                            key = \"remote_addr\",\n                            allow_degradation = false,\n                            count = count_val,\n                            time_window = t_val\n                        }\n                    }\n                }\n            end\n\n            etcd.set(\"/global_rules/1\", plugin_conf(1, 2, 10))\n            etcd.set(\"/global_rules/2\", plugin_conf(2, 3, 60))\n            etcd.set(\"/global_rules/3\", plugin_conf(3, 5, 20))\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_code: 200\n\n\n\n=== TEST 8: re-occuring plugins in global rules should be removed and not executed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require(\"resty.http\")\n\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\")\n\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_code: 200\n--- no_error_log\nlimit key: global_rule\n--- error_log\nFound limit-count configured across different global rules. Removing it from execution list\n"
  },
  {
    "path": "t/admin/global-rules.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/global_rules/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/global_rules/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/global_rules/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: list global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"total\": 1,\n                    \"list\": [\n                        {\n                            \"key\": \"/apisix/global_rules/1\",\n                            \"value\": {\n                                \"plugins\": {\n                                    \"limit-count\": {\n                                    \"time_window\": 60,\n                                    \"count\": 2,\n                                    \"key\": \"remote_addr\",\n                                    \"rejected_code\": 503\n                                    }\n                                }\n                            }\n                        }\n                    ]\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: PATCH global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/global_rules/1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"plugins\": {\n                    \"limit-count\": {\n                        \"count\": 3,\n                        \"time_window\": 60,\n                        \"rejected_code\": 503,\n                        \"key\": \"remote_addr\"\n                    }\n                }}]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 3,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/global_rules/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/global_rules/1'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: PATCH global rules (sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/global_rules/1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/global_rules/1/plugins',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"limit-count\": {\n                        \"count\": 3,\n                        \"time_window\": 60,\n                        \"rejected_code\": 503,\n                        \"key\": \"remote_addr\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 3,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/global_rules/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/global_rules/1'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: delete global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 7: delete global rules(not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 8: set global rules(missing plugins)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{}]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"plugins\\\" is required\"}\n\n\n\n=== TEST 9: string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: string id(DELETE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/a-b-c-ABC_0123',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/global_rules/1\",\"value\":{\"id\":\"1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/\"}}}}\n--- request\nGET /t\n\n\n\n=== TEST 12: not unwanted data, PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PATCH,\n                [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/global_rules/1\",\"value\":{\"id\":\"1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/\"}}}}\n--- request\nGET /t\n\n\n\n=== TEST 13: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_GET\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/global_rules/1\",\"value\":{\"id\":\"1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/\"}}}}\n--- request\nGET /t\n\n\n\n=== TEST 14: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/global_rules/1\"}\n--- request\nGET /t\n\n\n\n=== TEST 15: not unwanted data, PUT with use_real_request_uri_unsafe\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/\",\n                            \"use_real_request_uri_unsafe\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/global_rules/1\",\"value\":{\"id\":\"1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/\",\"use_real_request_uri_unsafe\":true}}}}\n--- request\nGET /t\n"
  },
  {
    "path": "t/admin/global-rules2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: list empty resources\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/global_rules',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"list\":[],\"total\":0}\n\n\n\n=== TEST 2: set global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/global_rules/1\",\"value\":{\"id\":\"1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/\"}}}}\n\n\n\n=== TEST 3: list global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/global_rules',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.total == 1)\n            assert(#res.list == 1)\n            assert(res.list[1].createdIndex ~= nil)\n            assert(res.list[1].modifiedIndex ~= nil)\n            assert(res.list[1].key == \"/apisix/global_rules/1\")\n            assert(res.list[1].value ~= nil)\n\n            ngx.say(message)\n        }\n    }\n--- response_body_like\npassed\n\n\n\n=== TEST 4: delete global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/health-check.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $init_by_lua_block = <<_EOC_;\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    apisix.http_init()\n\n    json = require(\"toolkit.json\")\n    req_data = json.decode([[{\n        \"methods\": [\"GET\"],\n        \"upstream\": {\n            \"nodes\": {\n                \"127.0.0.1:8080\": 1\n            },\n            \"type\": \"roundrobin\",\n            \"checks\": {}\n        },\n        \"uri\": \"/index.html\"\n    }]])\n    exp_data = {\n        value = req_data,\n        key = \"/apisix/routes/1\",\n    }\n_EOC_\n\n    $block->set_value(\"init_by_lua_block\", $init_by_lua_block);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: active\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"interval\": 1,\n                        \"http_failures\": 2\n                    }\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: passive\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    }\n                },\n                \"passive\": {\n                    \"healthy\": {\n                        \"http_statuses\": [200, 201],\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"http_statuses\": [500],\n                        \"http_failures\": 2\n                    }\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: invalid route: active.healthy.successes counter exceed maximum value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"healthy\": {\n                        \"successes\": 255\n                    }\n                }\n            }]])\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, req_data)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"active\\\" validation failed: property \\\"healthy\\\" validation failed: property \\\"successes\\\" validation failed: expected 255 to be at most 254\"}\n\n\n\n=== TEST 4: invalid route: active.healthy.successes counter below the minimum value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"healthy\": {\n                        \"successes\": 0\n                    }\n                }\n            }]])\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, req_data)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"active\\\" validation failed: property \\\"healthy\\\" validation failed: property \\\"successes\\\" validation failed: expected 0 to be at least 1\"}\n\n\n\n=== TEST 5: invalid route: wrong passive.unhealthy.http_statuses\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"passive\": {\n                    \"unhealthy\": {\n                        \"http_statuses\": [500, 600]\n                    }\n                }\n            }]])\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, req_data)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"passive\\\" validation failed: property \\\"unhealthy\\\" validation failed: property \\\"http_statuses\\\" validation failed: failed to validate item 2: expected 600 to be at most 599\"}\n\n\n\n=== TEST 6: invalid route: wrong active.type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"type\": \"udp\"\n                }\n            }]])\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, req_data)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"active\\\" validation failed: property \\\"type\\\" validation failed: matches none of the enum values\"}\n\n\n\n=== TEST 7: invalid route: duplicate items in active.healthy.http_statuses\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"healthy\": {\n                        \"http_statuses\": [200, 200]\n                    }\n                }\n            }]])\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, req_data)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"active\\\" validation failed: property \\\"healthy\\\" validation failed: property \\\"http_statuses\\\" validation failed: expected unique items but items 1 and 2 are equal\"}\n\n\n\n=== TEST 8: invalid route: active.unhealthy.http_failure is a floating point value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"unhealthy\": {\n                        \"http_failures\": 3.1\n                    }\n                }\n            }]])\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, req_data)\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"active\\\" validation failed: property \\\"unhealthy\\\" validation failed: property \\\"http_failures\\\" validation failed: wrong type: expected integer, got number\"}\n\n\n\n=== TEST 9: valid req_headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: multiple request headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"req_headers\": [\"User-Agent: curl/7.29.0\", \"Accept: */*\"]\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: invalid req_headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"req_headers\": [\"User-Agent: curl/7.29.0\", 2233]\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: property \\\"active\\\" validation failed: property \\\"req_headers\\\" validation failed: failed to validate item 2: wrong type: expected string, got number\"}\n\n\n\n=== TEST 12: only passive\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"passive\": {\n                    \"healthy\": {\n                        \"http_statuses\": [200, 201],\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"http_statuses\": [500],\n                        \"http_failures\": 2\n                    }\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: object matches none of the required: [\\\"active\\\"] or [\\\"active\\\",\\\"passive\\\"]\"}\n\n\n\n=== TEST 13: only active\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"interval\": 1,\n                        \"http_failures\": 2\n                    }\n                }\n            }]])\n            exp_data.value.upstream.checks.active = req_data.upstream.checks.active\n            exp_data.value.upstream.checks.passive = {\n                type = \"http\",\n                healthy = {\n                    http_statuses = { 200, 201, 202, 203, 204, 205, 206, 207, 208, 226,\n                                      300, 301, 302, 303, 304, 305, 306, 307, 308 },\n                    successes = 0,\n                },\n                unhealthy = {\n                    http_statuses = { 429, 500, 503 },\n                    tcp_failures = 0,\n                    timeouts = 0,\n                    http_failures = 0,\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: number type timeout\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            req_data.upstream.checks = json.decode([[{\n                \"active\": {\n                    \"http_path\": \"/status\",\n                    \"host\": \"foo.com\",\n                    \"timeout\": 1.01,\n                    \"healthy\": {\n                        \"interval\": 2,\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"interval\": 1,\n                        \"http_failures\": 2\n                    }\n                }\n            }]])\n            exp_data.value.upstream.checks = req_data.upstream.checks\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                req_data,\n                exp_data\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/metadata.spec.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\n\nimport { request as requestAdminAPI } from '../ts/admin_api';\n\ndescribe('Resource Metadata', () => {\n  describe('Consumer', () => {\n    it('should ensure additionalProperties is false', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/consumers/jack',\n          'PUT',\n          {\n            username: 'jack',\n            invalid: true,\n          },\n          undefined,\n          { validateStatus: () => true },\n        ),\n      ).resolves.toMatchObject({ status: 400 }));\n\n    it('should accept desc field', () =>\n      expect(\n        requestAdminAPI('/apisix/admin/consumers/jack', 'PUT', {\n          username: 'jack',\n          desc: 'test_desc',\n        }),\n      ).resolves.not.toThrow());\n  });\n\n  describe('Consumer Credentials', () => {\n    it('should ensure additionalProperties is false', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/consumers/jack/credentials/cred1',\n          'PUT',\n          {\n            plugins: { 'key-auth': { key: 'test' } },\n            invalid: true,\n          },\n          undefined,\n          { validateStatus: () => true },\n        ),\n      ).resolves.toMatchObject({ status: 400 }));\n\n    it('should accept name field', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/consumers/jack/credentials/cred1',\n          'PUT',\n          {\n            name: 'test_name',\n            plugins: { 'key-auth': { key: 'test' } },\n          },\n        ),\n      ).resolves.not.toThrow());\n  });\n\n  describe('SSL', () => {\n    const path = resolve(__dirname, '../certs/');\n    let cert: string;\n    let key: string;\n\n    beforeAll(async () => {\n      cert = await readFile(resolve(path, 'apisix.crt'), 'utf-8');\n      key = await readFile(resolve(path, 'apisix.key'), 'utf-8');\n    });\n\n    it('should ensure additionalProperties is false', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/ssls/ssl1',\n          'PUT',\n          { sni: 'test.com', cert, key, invalid: true },\n          undefined,\n          { validateStatus: () => true },\n        ),\n      ).resolves.toMatchObject({ status: 400 }));\n\n    it('should accept desc field', () =>\n      expect(\n        requestAdminAPI('/apisix/admin/ssls/ssl1', 'PUT', {\n          desc: 'test_desc',\n          sni: 'test.com',\n          cert,\n          key,\n        }),\n      ).resolves.not.toThrow());\n  });\n\n  describe('Proto', () => {\n    it('should ensure additionalProperties is false', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/protos/proto1',\n          'PUT',\n          { content: 'syntax = \"proto3\";', invalid: true },\n          undefined,\n          { validateStatus: () => true },\n        ),\n      ).resolves.toMatchObject({ status: 400 }));\n\n    it('should accept name/labels field', () =>\n      expect(\n        requestAdminAPI('/apisix/admin/protos/proto1', 'PUT', {\n          name: 'test_name',\n          labels: { test: 'test' },\n          content: 'syntax = \"proto3\";',\n        }),\n      ).resolves.not.toThrow());\n  });\n\n  describe('Stream Route', () => {\n    it('should ensure additionalProperties is false', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/stream_routes/sr1',\n          'PUT',\n          { upstream: { nodes: { '127.0.0.1:5432': 1 } }, invalid: true },\n          undefined,\n          { validateStatus: () => true },\n        ),\n      ).resolves.toMatchObject({ status: 400 }));\n\n    it('should accept name field', () =>\n      expect(\n        requestAdminAPI('/apisix/admin/stream_routes/sr1', 'PUT', {\n          name: 'test_name',\n          upstream: { nodes: { '127.0.0.1:5432': 1 } },\n        }),\n      ).resolves.not.toThrow());\n  });\n\n  describe('Consumer Group', () => {\n    it('should ensure additionalProperties is false', () =>\n      expect(\n        requestAdminAPI(\n          '/apisix/admin/consumer_groups/cg1',\n          'PUT',\n          { plugins: {}, invalid: true },\n          undefined,\n          { validateStatus: () => true },\n        ),\n      ).resolves.toMatchObject({ status: 400 }));\n\n    it('should accept name field', () =>\n      expect(\n        requestAdminAPI('/apisix/admin/consumer_groups/cg1', 'PUT', {\n          name: 'test_name',\n          plugins: {},\n        }),\n      ).resolves.not.toThrow());\n  });\n});\n"
  },
  {
    "path": "t/admin/metadata.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nuse_hup();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: test\n--- timeout: 15\n--- max_size: 204800\n--- exec\ncd t && pnpm test admin/metadata.spec.ts 2>&1\n--- no_error_log\nfailed to execute the script with status\n--- response_body eval\nqr/PASS admin\\/metadata.spec.ts/\n"
  },
  {
    "path": "t/admin/plugin-configs-force-delete.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set plugin_configs(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503\n                        }\n                    }\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 201\n--- response_body\npassed\n\n\n\n=== TEST 2: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugin_config_id\": 1,\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: delete plugin_configs(wrong header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/plugin_configs/1?force=anyvalue',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this plugin config, route [1] is still using it now\"}\n\n\n\n=== TEST 4: delete plugin_configs(without force delete header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this plugin config, route [1] is still using it now\"}\n\n\n\n=== TEST 5: delete plugin_configs(force delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/plugin_configs/1?force=true',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/plugin-configs.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: GET all\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"total\": 1,\n                    \"list\": [\n                        {\n                            \"key\": \"/apisix/plugin_configs/1\",\n                            \"value\": {\n                                \"plugins\": {\n                                    \"limit-count\": {\n                                    \"time_window\": 60,\n                                    \"count\": 2,\n                                    \"key\": \"remote_addr\",\n                                    \"rejected_code\": 503\n                                    }\n                                }\n                            }\n                        }\n                    ]\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"plugins\": {\n                    \"limit-count\": {\n                        \"count\": 3,\n                        \"time_window\": 60,\n                        \"rejected_code\": 503,\n                        \"key\": \"remote_addr\"\n                    }\n                }}]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 3,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: PATCH (sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/plugin_configs/1/plugins',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"limit-count\": {\n                        \"count\": 2,\n                        \"time_window\": 60,\n                        \"rejected_code\": 503,\n                        \"key\": \"remote_addr\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: invalid plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"rejected_code\": 503,\n                            \"time_window\": 60,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none\"}\n--- error_code: 400\n\n\n\n=== TEST 7: PUT (with non-plugin fields)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"labels\": {\n                        \"你好\": \"世界\"\n                    },\n                    \"desc\": \"blah\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"labels\": {\n                            \"你好\": \"世界\"\n                        },\n                        \"desc\": \"blah\"\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: GET (with non-plugin fields)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"labels\": {\n                            \"你好\": \"世界\"\n                        },\n                        \"desc\": \"blah\"\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: invalid non-plugin fields\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"labels\": \"a\",\n                    \"plugins\": {\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: wrong type: expected object, got string\"}\n--- error_code: 400\n\n\n\n=== TEST 10: set plugin-configs(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugin_config_id\": 1,\n                    \"uri\": \"/index.html\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: delete-plugin configs failed(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.print(body)\n        }\n    }\n--- response_body\n{\"error_msg\":\"can not delete this plugin config, route [1] is still using it now\"}\n\n\n\n=== TEST 13: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: delete plugin-configs(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: PUT with limit-count policy=local\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"local\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"local\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/plugin_configs/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/plugin_configs/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: GET all with limit-count policy=local\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"total\": 1,\n                    \"list\": [\n                        {\n                            \"key\": \"/apisix/plugin_configs/1\",\n                            \"value\": {\n                                \"plugins\": {\n                                    \"limit-count\": {\n                                        \"time_window\": 60,\n                                        \"count\": 2,\n                                        \"key\": \"remote_addr\",\n                                        \"rejected_code\": 503,\n                                        \"policy\": \"local\"\n                                    }\n                                }\n                            }\n                        }\n                    ]\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/plugin-metadata.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skey\": \"val\",\n                    \"ikey\": 1\n                }]],\n                [[{\n                    \"value\": {\n                        \"skey\": \"val\",\n                        \"ikey\": 1\n                    },\n                    \"key\": \"/apisix/plugin_metadata/example-plugin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: update plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"skey\": \"val2\",\n                    \"ikey\": 2\n                 }]],\n                [[{\n                    \"value\": {\n                        \"skey\": \"val2\",\n                        \"ikey\": 2\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            -- hit again\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"skey\": \"val2\",\n                    \"ikey\": 2\n                 }]],\n                [[{\n                    \"value\": {\n                        \"skey\": \"val2\",\n                        \"ikey\": 2\n                    }\n                }]]\n            )\n\n            ngx.say(code)\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n200\npassed\n\n\n\n=== TEST 3: get plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"skey\": \"val2\",\n                        \"ikey\": 2\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: delete plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin', ngx.HTTP_DELETE)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: delete plugin metadata(key: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/plugin_metadata/not_found', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 6: missing plugin name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata',\n                ngx.HTTP_PUT,\n                [[{\"k\": \"v\"}]],\n                [[{\n                    \"value\": \"sdf\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing plugin name\"}\n\n\n\n=== TEST 7: invalid plugin name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/test',\n                ngx.HTTP_PUT,\n                [[{\"k\": \"v\"}]],\n                [[{\n                    \"value\": \"sdf\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugin name\"}\n\n\n\n=== TEST 8: verify metadata schema fail\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skey\": \"val\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"skey\": \"val\",\n                        \"ikey\": 1\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid configuration: property \\\\\"ikey\\\\\" is required\"\\}/\n\n\n\n=== TEST 9: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skey\": \"val\",\n                    \"ikey\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/plugin_metadata/example-plugin\",\"value\":{\"id\":\"example-plugin\",\"ikey\":1,\"skey\":\"val\"}}\n--- request\nGET /t\n\n\n\n=== TEST 10: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/plugin_metadata/example-plugin', ngx.HTTP_GET)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/plugin_metadata/example-plugin\",\"value\":{\"id\":\"example-plugin\",\"ikey\":1,\"skey\":\"val\"}}\n--- request\nGET /t\n\n\n\n=== TEST 11: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/plugin_metadata/example-plugin', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/plugin_metadata/example-plugin\"}\n--- request\nGET /t\n"
  },
  {
    "path": "t/admin/plugin-metadata2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: list empty resources\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/plugin_metadata', ngx.HTTP_GET)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"list\":[],\"total\":0}\n"
  },
  {
    "path": "t/admin/plugin-metadata3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    # setup default conf.yaml\n    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n        - abcdef1234567890\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: First get not exist plugin metadata when plugin.enable_data_encryption is nil\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugin\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_GET\n            )\n\n            local_conf, err = core.config.local_conf(true)\n            local enable_data_encryption =\n            core.table.try_read_attr(local_conf, \"apisix\", \"data_encryption\",\n                    \"enable_encrypt_fields\") and (core.config.type == \"etcd\")\n\n            ngx.status = code\n            ngx.say(enable_data_encryption)\n            ngx.say(plugin.enable_data_encryption) -- When no plugin configuration in the init phase. enable_data_encryption is not initialized\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n--- response_body_like\ntrue\nnil\n\\{\"message\":\"Key not found\"\\}\n\n\n\n=== TEST 2: add example-plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugin\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skey\": \"val\",\n                    \"ikey\": 1\n                }]],\n                [[{\n                    \"value\": {\n                        \"skey\": \"val\",\n                        \"ikey\": 1\n                    },\n                    \"key\": \"/apisix/plugin_metadata/example-plugin\"\n                }]]\n            )\n\n            ngx.status = 200\n            ngx.say(plugin.enable_data_encryption)  -- Trigger plugin.enable_data_encryption to synchronize the conf configuration\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\npassed\n\n\n\n=== TEST 3: Second get not exist plugin metadata when plugin.enable_data_encryption is true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n--- response_body_like\n{\"message\":\"Key not found\"}\n\n\n\n=== TEST 4: update example-plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skey\": \"val2\",\n                    \"ikey\": 2\n                }]],\n                [[{\n                    \"value\": {\n                        \"skey\": \"val2\",\n                        \"ikey\": 2\n                    },\n                    \"key\": \"/apisix/plugin_metadata/example-plugin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: get plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                 ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: delete plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_DELETE\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: get deleted example-plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugin\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.say(plugin.enable_data_encryption) -- When no plugin configuration in the init phase. enable_data_encryption is not initialized\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n--- response_body_like\nnil\n\\{\"message\":\"Key not found\"\\}\n"
  },
  {
    "path": "t/admin/plugins-reload.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\nworkers(2);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: reload plugins\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        -- now the plugin will be loaded twice,\n        -- one during startup and the other one by reload\n        local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n\n        ngx.status = code\n        ngx.say(org_body)\n        ngx.sleep(1)\n    }\n}\n--- request\nGET /t\n--- response_body\ndone\n--- grep_error_log eval\nqr/sync local conf to etcd/\n--- grep_error_log_out\nsync local conf to etcd\n--- error_log\nload plugin times: 2\nload plugin times: 2\nstart to hot reload plugins\nstart to hot reload plugins\n\n\n\n=== TEST 2: reload plugins triggers plugin list sync\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require \"apisix.core\"\n        local config_util   = require(\"apisix.core.config_util\")\n        ngx.sleep(1) -- make sure the sync happened when admin starts is already finished\n\n        local before_reload = true\n        local plugins_conf, err\n        plugins_conf, err = core.config.new(\"/plugins\", {\n            automatic = true,\n            single_item = true,\n            filter = function(item)\n                -- called once before reload for sync data from admin\n                ngx.log(ngx.WARN, \"reload plugins on node \",\n                        before_reload and \"before reload\" or \"after reload\")\n                ngx.log(ngx.WARN, require(\"toolkit.json\").encode(item.value))\n            end,\n        })\n        if not plugins_conf then\n            error(\"failed to create etcd instance for fetching /plugins : \"\n                .. err)\n        end\n        ngx.sleep(1)\n\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n    - jwt-auth\nstream_plugins:\n    - mqtt-proxy\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        before_reload = false\n        local t = require(\"lib.test_admin\").test\n        local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n\n        ngx.status = code\n        ngx.say(org_body)\n        ngx.sleep(1)\n    }\n}\n--- request\nGET /t\n--- response_body\ndone\n--- grep_error_log eval\nqr/reload plugins on node \\w+ reload/\n--- grep_error_log_out\nreload plugins on node before reload\nreload plugins on node after reload\n--- error_log\nfilter(): [{\"name\":\"jwt-auth\"},{\"name\":\"mqtt-proxy\",\"stream\":true}]\n\n\n\n=== TEST 3: reload plugins when attributes changed\n--- yaml_config\napisix:\n  node_listen: 1984\nplugins:\n    - example-plugin\nplugin_attr:\n    example-plugin:\n        val: 0\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require \"apisix.core\"\n        ngx.sleep(0.1)\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n    - example-plugin\nplugin_attr:\n    example-plugin:\n        val: 1\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        local t = require(\"lib.test_admin\").test\n        local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n\n        ngx.status = code\n        ngx.say(org_body)\n        ngx.sleep(0.1)\n\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n    - example-plugin\nplugin_attr:\n    example-plugin:\n        val: 1\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        local t = require(\"lib.test_admin\").test\n        local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n        ngx.say(org_body)\n        ngx.sleep(0.1)\n    }\n}\n--- request\nGET /t\n--- response_body\ndone\ndone\n--- grep_error_log eval\nqr/example-plugin get plugin attr val: \\d+/\n--- grep_error_log_out\nexample-plugin get plugin attr val: 0\nexample-plugin get plugin attr val: 0\nexample-plugin get plugin attr val: 0\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\n\n\n\n=== TEST 4: reload plugins to change prometheus' export uri\n--- yaml_config\napisix:\n  node_listen: 1984\nplugins:\n  - public-api\n  - prometheus\nplugin_attr:\n  prometheus:\n    export_uri: /metrics\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require \"apisix.core\"\n        ngx.sleep(0.1)\n        local t = require(\"lib.test_admin\").test\n\n        -- setup public API route\n        local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/metrics\"\n                 }]]\n                )\n        ngx.say(code)\n\n        local code, _, org_body = t('/apisix/metrics',\n                                    ngx.HTTP_GET)\n        ngx.say(code)\n\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n  - public-api\n  - prometheus\nplugin_attr:\n  prometheus:\n    export_uri: /apisix/metrics\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n\n        ngx.say(org_body)\n\n        ngx.sleep(0.1)\n        local code, _, org_body = t('/apisix/metrics',\n                                    ngx.HTTP_GET)\n        ngx.say(code)\n    }\n}\n--- request\nGET /t\n--- response_body\n201\n404\ndone\n200\n\n\n\n=== TEST 5: check disabling plugin via etcd\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"echo\": {\n                                \"body\":\"hello upstream\\n\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            ngx.sleep(0.1)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_admin: false\n--- request\nGET /hello\n--- response_body\nhello upstream\n\n\n\n=== TEST 7: hit after disabling echo\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_admin: false\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local etcd = require(\"apisix.core.etcd\")\n        assert(etcd.set(\"/plugins\", {{name = \"jwt-auth\"}}))\n\n        ngx.sleep(0.2)\n\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                    .. \"/hello\"\n        local res, err = httpc:request_uri(uri)\n        if not res then\n            ngx.say(err)\n            return\n        end\n        ngx.print(res.body)\n    }\n}\n--- request\nGET /t\n--- response_body\nhello world\n\n\n\n=== TEST 8: wrong method to reload plugins\n--- request\nGET /apisix/admin/plugins/reload\n--- error_code: 405\n--- response_body\n{\"error_msg\":\"please use PUT method to reload the plugins, GET method is not allowed.\"}\n"
  },
  {
    "path": "t/admin/plugins.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: get plugins' name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require('cjson')\n            local code, _, body = t(\"/apisix/admin/plugins/list\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local tab = json.decode(body)\n            for _, v in ipairs(tab) do\n                ngx.say(v)\n            end\n        }\n    }\n\n--- response_body\nreal-ip\nai\nclient-control\nproxy-control\nrequest-id\nzipkin\next-plugin-pre-req\nfault-injection\nmocking\nserverless-pre-function\ncors\nip-restriction\nua-restriction\nreferer-restriction\ncsrf\nuri-blocker\nrequest-validation\nchaitin-waf\nmulti-auth\nopenid-connect\ncas-auth\nauthz-casbin\nauthz-casdoor\nwolf-rbac\nldap-auth\nhmac-auth\nbasic-auth\njwt-auth\njwe-decrypt\nkey-auth\nconsumer-restriction\nattach-consumer-label\nforward-auth\nopa\nauthz-keycloak\nproxy-cache\nbody-transformer\nai-request-rewrite\nai-prompt-guard\nai-prompt-template\nai-prompt-decorator\nai-rag\nai-aws-content-moderation\nai-proxy-multi\nai-proxy\nai-rate-limiting\nai-aliyun-content-moderation\nproxy-mirror\nproxy-rewrite\nworkflow\napi-breaker\nlimit-conn\nlimit-count\nlimit-req\ngzip\ntraffic-split\nredirect\nresponse-rewrite\nmcp-bridge\ndegraphql\nkafka-proxy\ngrpc-transcode\ngrpc-web\nhttp-dubbo\npublic-api\nprometheus\ndatadog\nlago\nloki-logger\nelasticsearch-logger\necho\nloggly\nhttp-logger\nsplunk-hec-logging\nskywalking-logger\ngoogle-cloud-logging\nsls-logger\ntcp-logger\nkafka-logger\nrocketmq-logger\nsyslog\nudp-logger\nfile-logger\nclickhouse-logger\ntencent-cloud-cls\ninspect\nexample-plugin\naws-lambda\nazure-functions\nopenwhisk\nopenfunction\nserverless-post-function\next-plugin-post-req\next-plugin-post-resp\n\n\n\n=== TEST 2: invalid plugin\n--- request\nGET /apisix/admin/plugins/asdf\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"plugin not found in subsystem http\"}\n\n\n\n=== TEST 3: get plugin schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugins/limit-req',\n                ngx.HTTP_GET,\n                nil,\n                [[\n                {\"type\":\"object\",\"required\":[\"rate\",\"burst\",\"key\"],\"properties\":{\"rate\":{\"type\":\"number\",\"exclusiveMinimum\":0},\"key_type\":{\"type\":\"string\",\"enum\":[\"var\",\"var_combination\"],\"default\":\"var\"},\"burst\":{\"type\":\"number\",\"minimum\":0},\"nodelay\":{\"type\":\"boolean\",\"default\":false},\"key\":{\"type\":\"string\"},\"rejected_code\":{\"type\":\"integer\",\"minimum\":200,\"maximum\":599,\"default\":503},\"rejected_msg\":{\"type\":\"string\",\"minLength\":1},\"allow_degradation\":{\"type\":\"boolean\",\"default\":false}}}\n                ]]\n                )\n\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 4: get plugin node-status schema\n--- extra_yaml_config\nplugins:\n    - node-status\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugins/node-status',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"properties\":{},\"type\":\"object\"}\n                ]]\n                )\n\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 5: get plugin prometheus schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugins/prometheus',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"properties\":{},\"type\":\"object\"}\n                ]]\n                )\n\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 6: get plugin basic-auth schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugins/basic-auth',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"properties\":{},\"title\":\"work with route or service object\",\"type\":\"object\"}\n                ]]\n                )\n\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 7: get plugin basic-auth schema by schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugins/basic-auth?schema_type=consumer',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"title\":\"work with consumer object\",\"required\":[\"username\",\"password\"],\"properties\":{\"username\":{\"type\":\"string\"},\"password\":{\"type\":\"string\"}},\"type\":\"object\"}\n                ]]\n                )\n\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 8: confirm the name, priority, schema, type and version of plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/plugins?all=true',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            for k, v in pairs(res) do\n                if k == \"example-plugin\" then\n                    ngx.say(json.encode(v))\n                end\n            end\n        }\n    }\n--- response_body eval\nqr/\\{\"metadata_schema\":\\{\"properties\":\\{\"ikey\":\\{\"minimum\":0,\"type\":\"number\"\\},\"skey\":\\{\"type\":\"string\"\\}\\},\"required\":\\[\"ikey\",\"skey\"\\],\"type\":\"object\"\\},\"priority\":0,\"schema\":\\{\"\\$comment\":\"this is a mark for our injected plugin schema\",\"properties\":\\{\"_meta\":\\{\"additionalProperties\":false,\"properties\":\\{\"disable\":\\{\"type\":\"boolean\"\\},\"error_response\":\\{\"oneOf\":\\[\\{\"type\":\"string\"\\},\\{\"type\":\"object\"\\}\\]\\},\"filter\":\\{\"description\":\"filter determines whether the plugin needs to be executed at runtime\",\"type\":\"array\"\\},\"pre_function\":\\{\"description\":\"function to be executed in each phase before execution of plugins. The pre_function will have access to two arguments: `conf` and `ctx`.\",\"type\":\"string\"\\},\"priority\":\\{\"description\":\"priority of plugins by customized order\",\"type\":\"integer\"\\}\\},\"type\":\"object\"\\},\"i\":\\{\"minimum\":0,\"type\":\"number\"\\},\"ip\":\\{\"type\":\"string\"\\},\"port\":\\{\"type\":\"integer\"\\},\"s\":\\{\"type\":\"string\"\\},\"t\":\\{\"minItems\":1,\"type\":\"array\"\\}\\},\"required\":\\[\"i\"\\],\"type\":\"object\"\\},\"version\":0.1\\}/\n\n\n\n=== TEST 9: confirm the plugin of auth type\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/plugins?all=true',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            local auth_plugins = {}\n            for k, v in pairs(res) do\n                if v.type == \"auth\" then\n                    local plugin = {}\n                    plugin.name = k\n                    plugin.priority = v.priority\n                    table.insert(auth_plugins, plugin)\n                end\n            end\n\n            table.sort(auth_plugins, function(l, r)\n                return l.priority > r.priority\n            end)\n            ngx.say(json.encode(auth_plugins))\n        }\n    }\n--- response_body eval\nqr/\\[\\{\"name\":\"multi-auth\",\"priority\":2600\\},\\{\"name\":\"wolf-rbac\",\"priority\":2555\\},\\{\"name\":\"ldap-auth\",\"priority\":2540\\},\\{\"name\":\"hmac-auth\",\"priority\":2530\\},\\{\"name\":\"basic-auth\",\"priority\":2520\\},\\{\"name\":\"jwt-auth\",\"priority\":2510\\},\\{\"name\":\"jwe-decrypt\",\"priority\":2509\\},\\{\"name\":\"key-auth\",\"priority\":2500\\}\\]/\n\n\n\n=== TEST 10: confirm the consumer_schema of plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/plugins?all=true',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            local consumer_schema\n            for k, v in pairs(res) do\n                if k == \"basic-auth\" then\n                    consumer_schema = v.consumer_schema\n                end\n            end\n            ngx.say(json.encode(consumer_schema))\n        }\n    }\n--- response_body eval\nqr/\\{\"encrypt_fields\":\\[\"password\"\\],\"properties\":\\{\"password\":\\{\"type\":\"string\"\\},\"username\":\\{\"type\":\"string\"\\}\\},\"required\":\\[\"username\",\"password\"\\],\"title\":\"work with consumer object\",\"type\":\"object\"\\}/\n\n\n\n=== TEST 11: confirm the name, priority, schema, type and version of stream plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/plugins?all=true&subsystem=stream',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            for k, v in pairs(res) do\n                if k == \"limit-conn\" then\n                    ngx.say(json.encode(v))\n                end\n            end\n        }\n    }\n--- response_body\n{\"priority\":1003,\"schema\":{\"$comment\":\"this is a mark for our injected plugin schema\",\"properties\":{\"_meta\":{\"additionalProperties\":false,\"properties\":{\"disable\":{\"type\":\"boolean\"},\"error_response\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"object\"}]},\"filter\":{\"description\":\"filter determines whether the plugin needs to be executed at runtime\",\"type\":\"array\"},\"pre_function\":{\"description\":\"function to be executed in each phase before execution of plugins. The pre_function will have access to two arguments: `conf` and `ctx`.\",\"type\":\"string\"},\"priority\":{\"description\":\"priority of plugins by customized order\",\"type\":\"integer\"}},\"type\":\"object\"},\"burst\":{\"minimum\":0,\"type\":\"integer\"},\"conn\":{\"exclusiveMinimum\":0,\"type\":\"integer\"},\"default_conn_delay\":{\"exclusiveMinimum\":0,\"type\":\"number\"},\"key\":{\"type\":\"string\"},\"key_type\":{\"default\":\"var\",\"enum\":[\"var\",\"var_combination\"],\"type\":\"string\"},\"only_use_default_delay\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"conn\",\"burst\",\"default_conn_delay\",\"key\"],\"type\":\"object\"},\"version\":0.1}\n\n\n\n=== TEST 12: confirm the scope of plugin\n--- extra_yaml_config\nplugins:\n  - batch-requests\n  - error-log-logger\n  - server-info\n  - example-plugin\n  - node-status\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/plugins?all=true',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            local global_plugins = {}\n            for k, v in pairs(res) do\n                if v.scope == \"global\" then\n                    global_plugins[k] = v.scope\n                end\n            end\n            ngx.say(json.encode(global_plugins))\n        }\n    }\n--- response_body\n{\"batch-requests\":\"global\",\"error-log-logger\":\"global\",\"node-status\":\"global\",\"server-info\":\"global\"}\n\n\n\n=== TEST 13: check with wrong plugin subsystem\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local _, message, _ = t('/apisix/admin/plugins?subsystem=asdf',\n                ngx.HTTP_GET\n            )\n            ngx.say(message)\n        }\n    }\n--- response_body eval\nqr/\\{\"error_msg\":\"unsupported subsystem: asdf\"\\}/\n\n\n\n=== TEST 14: check with right plugin in wrong subsystem\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local _, message, _ = t('/apisix/admin/plugins/http-logger?subsystem=stream',\n                ngx.HTTP_GET\n            )\n            ngx.say(message)\n        }\n    }\n--- response_body eval\nqr/\\{\"error_msg\":\"plugin not found in subsystem stream\"\\}/\n\n\n\n=== TEST 15: check with right plugin in right subsystem\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local _, _ , message = t('/apisix/admin/plugins/http-logger?subsystem=http',\n                ngx.HTTP_GET\n            )\n            ngx.say(message)\n        }\n    }\n--- response_body eval\nqr/this is a mark for our injected plugin schema/\n"
  },
  {
    "path": "t/admin/protos-force-delete.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set proto(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                    package helloworld;\n                    service Greeter {\n                        rpc SayHello (HelloRequest) returns (HelloReply) {}\n                    }\n                    message HelloRequest {\n                        string name = 1;\n                    }\n                    message HelloReply {\n                        string message = 1;\n                    }\"\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 201\n--- response_body\npassed\n\n\n\n=== TEST 2: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                         \"proto_id\": \"1\",\n                         \"service\": \"helloworld.Greeter\",\n                         \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: delete proto(wrong header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/1?force=anyvalue',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this proto, route [1] is still using it now\"}\n\n\n\n=== TEST 4: delete proto(without force delete header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this proto, route [1] is still using it now\"}\n\n\n\n=== TEST 5: delete proto(force delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/1?force=true',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/protos.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: put proto (id:1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"content\": \"syntax = \\\"proto3\\\";\n                        package proto;\n                        message HelloRequest{\n                            string name = 1;\n                        }\n\n                        message HelloResponse{\n                            int32 code = 1;\n                            string msg = 2;\n                        }\n                        // The greeting service definition.\n                        service Hello {\n                            // Sends a greeting\n                            rpc SayHi (HelloRequest) returns (HelloResponse){}\n                        }\"\n                }]]\n            )\n\n            if code ~= 201 then\n                ngx.status = code\n                ngx.say(\"[put proto] code: \", code, \" message: \", message)\n                return\n            end\n\n            ngx.say(\"[put proto] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[put proto] code: 201 message: passed\n\n\n\n=== TEST 2: delete proto(id:1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/1',\n                 ngx.HTTP_DELETE\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(\"[delete proto] code: \", code, \" message: \", message)\n                return\n            end\n\n            ngx.say(\"[delete proto] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete proto] code: 200 message: passed\n\n\n\n=== TEST 3: put proto (id:2) + route refer proto(proto id 2) + delete proto(proto id 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\": \"syntax = \\\"proto3\\\";\n                    package proto;\n                    message HelloRequest{\n                        string name = 1;\n                    }\n\n                    message HelloResponse{\n                        int32 code = 1;\n                        string msg = 2;\n                    }\n                    // The greeting service definition.\n                    service Hello {\n                        // Sends a greeting\n                        rpc SayHi (HelloRequest) returns (HelloResponse){}\n                    }\"\n                }]]\n            )\n\n            if code ~= 201 then\n                ngx.status = code\n                ngx.say(\"[put proto] code: \", code, \" message: \", message)\n                return\n            end\n            ngx.say(\"[put proto] code: \", code, \" message: \", message)\n\n\n            code, message = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"grpc-transcode\": {\n                            \"_meta\": {\n                                \"disable\": false\n                            },\n                            \"method\": \"SayHi\",\n                            \"proto_id\": 2,\n                            \"service\": \"proto.Hello\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/grpc/sayhi\",\n                        \"name\": \"hi-grpc\"\n                }]]\n            )\n\n            if code ~= 201 then\n                ngx.status = code\n                ngx.say(\"[route refer proto] code: \", code, \" message: \", message)\n                return\n            end\n            ngx.say(\"[route refer proto] code: \", code, \" message: \", message)\n\n            ngx.sleep(0.1) -- ensure reference is synced from etcd\n\n            code, message = t('/apisix/admin/protos/2',\n                 ngx.HTTP_DELETE\n            )\n\n            ngx.say(\"[delete proto] code: \", code)\n        }\n    }\n--- response_body\n[put proto] code: 201 message: passed\n[route refer proto] code: 201 message: passed\n[delete proto] code: 400\n\n\n\n=== TEST 4: reject invalid proto\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"content\": \"syntax = \\\"proto3\\\";\n                        package proto;\n                        message HelloRequest{\n                            string name = 1;\n                        }\n\n                        message HelloResponse{\n                            int32 code = 1;\n                            string msg = 1;\n                        }\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n            end\n\n            ngx.say(message)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/invalid content:/\n"
  },
  {
    "path": "t/admin/resources.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: invalid resource type: 'routs'\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like\n{\"error_msg\":\"Unsupported resource type: routs\"}\n"
  },
  {
    "path": "t/admin/response_body_format.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: ~\n    admin_api_version: v3\napisix:\n    node_listen: 1984\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: use v3 admin api, no action in response body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: response body format only have total and list (total is 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes', ngx.HTTP_GET)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            res = json.decode(res)\n            assert(res.total == 1)\n            assert(res.total == #res.list)\n            assert(res.action == nil)\n            assert(res.node == nil)\n            assert(res.list.key == nil)\n            assert(res.list.dir == nil)\n            assert(res.list[1].createdIndex ~= nil)\n            assert(res.list[1].modifiedIndex ~= nil)\n            assert(res.list[1].key == \"/apisix/routes/1\")\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: response body format only have total and list (total is 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, message, res = t('/apisix/admin/routes',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.total == 2)\n            assert(res.total == #res.list)\n            assert(res.action == nil)\n            assert(res.node == nil)\n            assert(res.list.key == nil)\n            assert(res.list.dir == nil)\n            assert(res.list[1].createdIndex ~= nil)\n            assert(res.list[1].modifiedIndex ~= nil)\n            assert(res.list[1].key == \"/apisix/routes/1\")\n            assert(res.list[2].createdIndex ~= nil)\n            assert(res.list[2].modifiedIndex ~= nil)\n            assert(res.list[2].key == \"/apisix/routes/2\")\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: response body format (test services)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service 001\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/services/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service 002\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, message, res = t('/apisix/admin/services', ngx.HTTP_GET)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.total == 2)\n            assert(res.total == #res.list)\n            assert(res.action == nil)\n            assert(res.node == nil)\n            assert(res.list.key == nil)\n            assert(res.list.dir == nil)\n            assert(res.list[1].createdIndex ~= nil)\n            assert(res.list[1].modifiedIndex ~= nil)\n            assert(res.list[1].key == \"/apisix/services/1\")\n            assert(res.list[2].createdIndex ~= nil)\n            assert(res.list[2].modifiedIndex ~= nil)\n            assert(res.list[2].key == \"/apisix/services/2\")\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\npassed\npassed\n"
  },
  {
    "path": "t/admin/routes-array-nodes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8080,\n                            \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8080,\n                                \"weight\": 1\n                            }],\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8080,\n                                \"weight\": 1\n                            }],\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: PATCH route from array nodes to hash table nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/index.html\",\n                    \"upstream\": {\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8080,\n                            \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:9200\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8080,\n                            \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/routes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: delete route(id: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/routes/not_found',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 5: post route + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, message, res = t('/apisix/admin/routes',\n                 ngx.HTTP_POST,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n            local id = string.sub(res.key, #\"/apisix/routes/\" + 1)\n            local res = assert(etcd.get('/routes/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n            code, message = t('/apisix/admin/routes/' .. id,\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: uri + upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, message, res = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local res = assert(etcd.get('/routes/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 7: uri + plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/index.html\",\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 8: invalid route: duplicate method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\", \"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like\n\n\n\n=== TEST 9: invalid method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"invalid_method\"],\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"methods\\\" validation failed: failed to validate item 1: matches none of the enum values\"}\n\n\n\n=== TEST 10: invalid service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"service_id\": \"invalid_id$\",\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"service_id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 11: service id: not exist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"service_id\": \"99999999999999\",\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch service info by service id [99999999999999], response code: 404\"}\n\n\n\n=== TEST 12: invalid id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"id\": 3,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong route id\"}\n\n\n\n=== TEST 13: id in the rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"1\",\n                    \"plugins\":{},\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: integer id less than 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": -100,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 15: invalid upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream_id\": \"invalid$\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream_id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 16: not exist upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream_id\": \"99999999\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch upstream info by upstream id [99999999], response code: 404\"}\n\n\n\n=== TEST 17: wrong route id, do not need it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"id\": 1,\n                    \"plugins\":{},\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong route id, do not need it\"}\n\n\n\n=== TEST 18: wrong route id, do not need it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"plugins\":{},\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong route id, do not need it\"}\n\n\n\n=== TEST 19: limit-count with `disable` option\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"_meta\": {\n                                \"disable\": true\n                            }\n                        }\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 20: host: *.foo.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\": \"*.foo.com\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"host\": \"*.foo.com\",\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: invalid host: a.*.foo.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"host\": \"a.*.foo.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"invalid configuration: property \\\\\"host\\\\\" validation failed: failed to match pattern .*\n\n\n\n=== TEST 22: invalid host: *.a.*.foo.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"host\": \"*.a.*.foo.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"invalid configuration: property \\\\\"host\\\\\" validation failed: failed to match pattern .*\n\n\n\n=== TEST 23: removing the init_dir key from etcd can still list all routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local etcd = require(\"apisix.core.etcd\")\n\n            local code, body = t('/apisix/admin/routes/del_init_dir_1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n            assert(code == 200 or code == 201, \"failed to add route\")\n\n            local code, body = t('/apisix/admin/routes/del_init_dir_2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n            assert(code == 200 or code == 201, \"failed to add route\")\n\n            -- remove the init_dir key from etcd\n            assert(etcd.delete(\"/routes/\"))\n\n            -- list all routes and check them\n            local code, body, res = t('/apisix/admin/routes', ngx.HTTP_GET)\n            ngx.status = code\n            ngx.say(res)\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/del_init_dir_1.*del_init_dir_2/\n"
  },
  {
    "path": "t/admin/routes2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: invalid route: bad remote_addrs\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"remote_addrs\": [\"\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/property \\\\\"remote_addrs\\\\\" validation failed:/\n\n\n\n=== TEST 2: invalid route: bad remote_addrs cidr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"remote_addrs\": [\"/16\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/property \\\\\"remote_addrs\\\\\" validation failed:/\n\n\n\n=== TEST 3: valid route with remote_addrs\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"remote_addrs\": [\"::1/16\", \"::1\", \"::\", \"1.1.1.1\", \"1.1.1.1/32\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: invalid route: bad vars operator\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                    \"methods\": [\"GET\"],\n                    \"vars\": [[\"remote_addr\", \"=\", \"127.0.0.1\"]],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to validate the 'vars' expression: invalid operator '='\"}\n\n\n\n=== TEST 5: not unwanted data, POST\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes',\n                 ngx.HTTP_POST,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/not_unwanted_data_post\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.key = nil\n            res.value.create_time = nil\n            res.value.update_time = nil\n            assert(res.value.id ~= nil)\n            res.value.id = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"value\":{\"methods\":[\"GET\"],\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"},\"uri\":\"/not_unwanted_data_post\"}}\n\n\n\n=== TEST 6: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"id\": 1,\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"key\":\"/apisix/routes/1\",\"value\":{\"id\":1,\"methods\":[\"GET\"],\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"},\"uri\":\"/index.html\"}}\n\n\n\n=== TEST 7: not unwanted data, PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PATCH,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"key\":\"/apisix/routes/1\",\"value\":{\"id\":\"1\",\"methods\":[\"GET\"],\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"},\"uri\":\"/index\"}}\n\n\n\n=== TEST 8: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes/1',\n                 ngx.HTTP_GET\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"key\":\"/apisix/routes/1\",\"value\":{\"id\":\"1\",\"methods\":[\"GET\"],\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"},\"uri\":\"/index\"}}\n\n\n\n=== TEST 9: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes/1',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/routes/1\"}\n\n\n\n=== TEST 10: invalid route: empty remote_addrs\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"remote_addrs\": [],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/property \\\\\"remote_addrs\\\\\" validation failed:/\n\n\n\n=== TEST 11: invalid route: empty uris\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": []\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/property \\\\\"uris\\\\\" validation failed:/\n\n\n\n=== TEST 12: invalid route: empty hosts\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"hosts\": [],\n                    \"uri\": \"/\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/property \\\\\"hosts\\\\\" validation failed:/\n\n\n\n=== TEST 13: invalid route: uris & uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/\"],\n                    \"uri\": \"/\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/value should match only one schema/\n\n\n\n=== TEST 14: enable remote_addrs and remote_addr together\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/index.html\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"remote_addrs\": [\"127.0.0.1\"]\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"only one of remote_addr or remote_addrs is allowed\"}\n\n\n\n=== TEST 15: labels in Chinese\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"labels\": {\n                        \"您好\": \"世界\"\n                    },\n\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"labels\": {\n                            \"您好\": \"世界\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: labels value with whitespace\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"labels\": {\n                        \"您好\": \"世 界\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/invalid configuration: property \\\\\"labels\\\\\" validation failed/\n\n\n\n=== TEST 17: route with plugin_config_id (not found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"plugin_config_id\": \"not_found\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch plugin config info by plugin config id [not_found], response code: 404\"}\n\n\n\n=== TEST 18: valid route with timeout\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"timeout\": {\n                            \"connect\": 3,\n                            \"send\": 3,\n                            \"read\": 3\n                        },\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/routes3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: list empty resources\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/routes',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"list\":[],\"total\":0}\n\n\n\n=== TEST 2: remote_addr: 127.0.0.1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"remote_addr\": \"127.0.0.1\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: remote_addr: 127.0.0.1/24\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"remote_addr\": \"127.0.0.0/24\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"remote_addr\": \"127.0.0.0/24\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: remote_addr: 127.0.0.33333\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"remote_addr\": \"127.0.0.33333\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"remote_addr\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 5: all method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\",\n                                    \"HEAD\", \"OPTIONS\", \"CONNECT\", \"TRACE\", \"PURGE\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: patch route(new uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n\n            local id = 1\n            local res = assert(etcd.get('/routes/' .. id))\n            local prev_create_time = res.body.node.value.create_time\n            local prev_update_time = res.body.node.value.update_time\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"uri\": \"/patch_test\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/patch_test\"\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/routes/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: patch route(multi)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": null,\n                            \"127.0.0.2:8080\": 1\n                        }\n                    },\n                    \"desc\": \"new route\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/patch_test\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.2:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: patch route(new methods)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"methods\": [\"GET\", \"DELETE\", \"PATCH\", \"POST\", \"PUT\"]\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\"GET\", \"DELETE\", \"PATCH\", \"POST\", \"PUT\"]\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: patch route(minus methods)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"]\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\"GET\", \"POST\"]\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: patch route(new methods - sub path way)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/methods',\n                ngx.HTTP_PATCH,\n                '[\"POST\"]',\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"POST\"\n                        ]\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: patch route(new uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/uri',\n                ngx.HTTP_PATCH,\n                '\"/patch_uri_test\"',\n                [[{\n                    \"value\": {\n                        \"uri\": \"/patch_uri_test\"\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: patch route(whole)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"desc\": \"new route\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: multiple hosts\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/index.html\",\n                    \"hosts\": [\"foo.com\", \"*.bar.com\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"hosts\": [\"foo.com\", \"*.bar.com\"]\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: enable hosts and host together\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/index.html\",\n                    \"host\": \"xxx.com\",\n                    \"hosts\": [\"foo.com\", \"*.bar.com\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"only one of host or hosts is allowed\"}\n\n\n\n=== TEST 15: multiple remote_addrs\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/index.html\",\n                    \"remote_addrs\": [\"127.0.0.1\", \"192.0.0.1/8\", \"::1\", \"fe80::/32\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"remote_addrs\": [\"127.0.0.1\", \"192.0.0.1/8\", \"::1\", \"fe80::/32\"]\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: multiple vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/index.html\",\n                    \"vars\": [[\"arg_name\", \"==\", \"json\"], [\"arg_age\", \">\", 18]],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]=],\n                [=[{\n                    \"value\": {\n                        \"vars\": [[\"arg_name\", \"==\", \"json\"], [\"arg_age\", \">\", 18]]\n                    }\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: filter function\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/index.html\",\n                    \"filter_func\": \"function(vars) return vars.arg_name == 'json' end\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=],\n                [=[{\n                    \"value\": {\n                        \"filter_func\": \"function(vars) return vars.arg_name == 'json' end\"\n                    }\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: filter function (invalid)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/index.html\",\n                    \"filter_func\": \"function(vars) \",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to load 'filter_func' string: [string \\\"return function(vars) \\\"]:1: 'end' expected near '<eof>'\"}\n\n\n\n=== TEST 19: Support for multiple URIs\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uris\": [\"/index.html\",\"/index2.html\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: set route(id: 1, parameters with boolean values)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/index.html\",\n                    \"enable_websocket\": true,\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8080\":1\n                        }\n                    }\n                }]])\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: patch route(modify the boolean value of parameters to false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/enable_websocket',\n                ngx.HTTP_PATCH,\n                'false',\n                [[{\n                    \"value\": {\n                        \"enable_websocket\": false\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: patch route(modify the boolean value of parameters to true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/enable_websocket',\n                ngx.HTTP_PATCH,\n                'true',\n                [[{\n                    \"value\": {\n                        \"enable_websocket\": true\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/routes4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route with ttl\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local core = require(\"apisix.core\")\n        -- set\n        local code, body, res = t('/apisix/admin/routes/1?ttl=1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/index.html\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- get\n        code, body = t('/apisix/admin/routes/1?ttl=1',\n            ngx.HTTP_GET,\n            nil,\n            [[{\n                \"value\": {\n                    \"uri\": \"/index.html\"\n                },\n                \"key\": \"/apisix/routes/1\"\n            }]]\n        )\n\n        ngx.say(\"code: \", code)\n        ngx.say(body)\n\n        -- etcd v3 would still get the value at 2s, don't know why yet\n        ngx.sleep(2.5)\n\n        -- get again\n        code, body, res = t('/apisix/admin/routes/1', ngx.HTTP_GET)\n\n        ngx.say(\"code: \", code)\n        ngx.say(\"message: \", core.json.decode(body).message)\n    }\n}\n--- response_body\ncode: 200\npassed\ncode: 404\nmessage: Key not found\n--- timeout: 5\n\n\n\n=== TEST 2: post route with ttl\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local core = require(\"apisix.core\")\n\n        local code, body, res = t('/apisix/admin/routes?ttl=1',\n            ngx.HTTP_POST,\n            [[{\n                \"methods\": [\"GET\"],\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/index.html\"\n            }]],\n            [[{}]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(\"[push] succ: \", body)\n        ngx.sleep(2.5)\n\n        local id = string.sub(res.key, #\"/apisix/routes/\" + 1)\n        code, body = t('/apisix/admin/routes/' .. id, ngx.HTTP_GET)\n\n        ngx.say(\"code: \", code)\n        ngx.say(\"message: \", core.json.decode(body).message)\n    }\n}\n--- response_body\n[push] succ: passed\ncode: 404\nmessage: Key not found\n--- timeout: 5\n\n\n\n=== TEST 3: invalid argument: ttl\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body, res = t('/apisix/admin/routes?ttl=xxx',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/index.html\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.print(body)\n            return\n        end\n\n        ngx.say(\"[push] succ: \", body)\n    }\n}\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid argument ttl: should be a number\"}\n\n\n\n=== TEST 4: set route(id: 1, check priority)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\",\n                    \"priority\": 0\n                }]],\n                [[{\n                    \"value\": {\n                        \"priority\": 0\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: set route(id: 1 + priority: 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\",\n                    \"priority\": 1\n                }]],\n                [[{\n                    \"value\": {\n                        \"priority\": 1\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: set route(id: 1) and upstream(type:chash, default hash_on: vars, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]])\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 7: set route(id: 1) and upstream(type:chash, hash_on: header, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\":\"header\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]])\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 8: set route(id: 1) and upstream(type:chash, hash_on: cookie, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\":\"cookie\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]])\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 9: set route(id: 1) and upstream(type:chash, hash_on: consumer, missing key is ok)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\":\"consumer\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]])\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: set route(id: 1 + name: test name)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"name\": \"test name\",\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"name\": \"test name\"\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: string id(delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/a-b-c-ABC_0123',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: invalid string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/*invalid',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n\n\n\n=== TEST 14: Verify Response Content-Type=application/json\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            httpc:set_timeout(500)\n            httpc:connect(ngx.var.server_addr, ngx.var.server_port)\n            local res, err = httpc:request(\n                {\n                    path = '/apisix/admin/routes/1',\n                    query = { ttl = 1 },\n                    method = \"GET\",\n                }\n            )\n\n            ngx.header[\"Content-Type\"] = res.headers[\"Content-Type\"]\n            ngx.status = 200\n            ngx.say(\"passed\")\n        }\n    }\n--- response_headers\nContent-Type: application/json\n\n\n\n=== TEST 15: set route with size 36k (temporary file to store request body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local core = require(\"apisix.core\")\n            local s = string.rep(\"a\", 1024 * 35)\n            local req_body = [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"]] .. s .. [[\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/index.html\"\n            }]]\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT, req_body)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(\"req size: \", #req_body)\n            ngx.say(body)\n        }\n    }\n--- response_body\nreq size: 36066\npassed\n--- error_log\na client request body is buffered to a temporary file\n\n\n\n=== TEST 16: route size more than 1.5 MiB\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local s = string.rep( \"a\", 1024 * 1024 * 1.6 )\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"]] .. s .. [[\",\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid request body: request size 1678025 is greater than the maximum size 1572864 allowed\"}\n--- error_log\nfailed to read request body: request size 1678025 is greater than the maximum size 1572864 allowed\n\n\n\n=== TEST 17: uri + plugins + script  failed\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"script\": \"local _M = {} \\n function _M.access(api_ctx) \\n ngx.log(ngx.INFO,\\\"hit access phase\\\") \\n end \\nreturn _M\",\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n        }\n    }\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"invalid configuration: value wasn't supposed to match schema\"}\n\n\n\n=== TEST 18: invalid route: multi nodes with `node` mode to pass host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\", \"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"apisix.com:8080\": 1,\n                                \"test.com:8080\": 1\n                            },\n                            \"type\": \"roundrobin\",\n                            \"pass_host\": \"node\"\n                        },\n                        \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n\n\n\n=== TEST 19: set route(with labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"labels\": {\n                        \"build\": \"16\",\n                        \"env\": \"production\",\n                        \"version\": \"v2\"\n                    },\n\n                    \"uri\": \"/index.html\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"labels\": {\n                            \"build\": \"16\",\n                            \"env\": \"production\",\n                            \"version\": \"v2\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: patch route(change labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"labels\": {\n                        \"build\": \"17\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"methods\": [\n                            \"GET\"\n                        ],\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"labels\": {\n                            \"env\": \"production\",\n                            \"version\": \"v2\",\n                            \"build\": \"17\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: invalid format of label value: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"uri\": \"/index.html\",\n                        \"labels\": {\n                            \"env\": [\"production\", \"release\"]\n                        }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: failed to validate env (matching \\\".*\\\"): wrong type: expected string, got table\"}\n\n\n\n=== TEST 22: create route with create_time and update_time(id : 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\",\n                    \"create_time\": 1602883670,\n                    \"update_time\": 1602893670\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/index.html\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"create_time\": 1602883670,\n                        \"update_time\": 1602893670\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"the property is forbidden:.*\"\\}/\n"
  },
  {
    "path": "t/admin/routes_request_body.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route in request body vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [\n                        [\n                            [\"post_arg.model\",\"==\", \"deepseek\"]\n                        ]\n                    ],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [\n                        [\n                            [\"post_arg.model\",\"==\",\"openai\"]\n                        ]\n                    ],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request with model == deepseek\n--- request\nPOST /hello\n{ \"model\":\"deepseek\", \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }] }\n--- more_headers\nContent-Type: application/json\n--- error_code: 404\n\n\n\n=== TEST 3: send request with model == openai and content-type == application/json\n--- request\nPOST /hello\n{ \"model\":\"openai\", \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }] }\n--- more_headers\nContent-Type: application/json\n--- error_code: 200\n\n\n\n=== TEST 4: send request with model == openai and content-type == application/x-www-form-urlencoded\n--- request\nPOST /hello\nmodel=openai&messages[0][role]=system&messages[0][content]=You%20are%20a%20mathematician\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 200\n\n\n\n=== TEST 5: multipart/form-data with model=openai\n--- request\nPOST /hello\n--testboundary\nContent-Disposition: form-data; name=\"model\"\n\nopenai\n--testboundary--\n--- more_headers\nContent-Type: multipart/form-data; boundary=testboundary\n--- error_code: 200\n\n\n\n=== TEST 6: no match without content type\n--- request\nPOST /hello\n--testboundary\nContent-Disposition: form-data; name=\"model\"\n\nopenai\n--testboundary--\n--- error_code: 404\n--- error_log\nunsupported content-type in header:\n\n\n\n=== TEST 7: use array in request body vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [\n                        [\n                            [\"post_arg.messages[*].content[*].type\",\"has\",\"image_url\"]\n                        ]\n                    ],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: send request with type not image_url\n--- request\nPOST /hello\n{ \"model\":\"deepseek\", \"messages\": [ { \"role\": \"system\", \"content\": [{\"text\":\"You are a mathematician\",\"type\":\"text\"}] }] }\n--- more_headers\nContent-Type: application/json\n--- error_code: 404\n\n\n\n=== TEST 9: send request with type has image_url\n--- request\nPOST /hello\n{ \"model\":\"deepseek\", \"messages\": [ { \"role\": \"system\", \"content\": [{\"text\":\"You are a mathematician\",\"type\":\"text\"},{\"text\":\"You are a mathematician\",\"type\":\"image_url\"}] }] }\n--- more_headers\nContent-Type: application/json\n--- error_code: 200\n\n\n\n=== TEST 10: use invalid jsonpath input\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [\n                        [\n                            [\"post_arg.messages[.content[*].type\",\"has\",\"image_url\"]\n                        ]\n                    ],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body eval\nqr/.*failed to validate the 'vars' expression: invalid expression.*/\n--- error_code: 400\n\n\n\n=== TEST 11: use non array in request body vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [\n                        [\n                            [\"post_arg.model.name\",\"==\",\"deepseek\"]\n                        ]\n                    ],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: send request\n--- request\nPOST /hello\n{ \"model\":{\"name\": \"deepseek\"}, \"messages\": [ { \"role\": \"system\", \"content\": [{\"text\":\"You are a mathematician\",\"type\":\"text\"},{\"text\":\"You are a mathematician\",\"type\":\"image_url\"}] }] }\n--- more_headers\nContent-Type: application/json\n--- error_code: 200\n"
  },
  {
    "path": "t/admin/schema-validate.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"warn\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: validate ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"uri\": \"/httpbin/*\",\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 200\n\n\n\n=== TEST 2: validate failed, wrong uri type\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"uri\": 666,\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\": {\"property \\\"uri\\\" validation failed: wrong type: expected string, got number\"}}\n\n\n\n=== TEST 3: validate failed, length limit\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"uri\": \"\",\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"property \\\"uri\\\" validation failed: string too short, expected at least 1, got 0\"}\n\n\n\n=== TEST 4: validate failed, array type expected\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"uris\": \"foobar\",\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"property \\\"uris\\\" validation failed: wrong type: expected array, got string\"}\n\n\n\n=== TEST 5: validate failed, array size limit\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"uris\": [],\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"property \\\"uris\\\" validation failed: expect array to have at least 1 items\"}\n\n\n\n=== TEST 6: validate failed, array unique items\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"uris\": [\"/foo\", \"/foo\"],\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"property \\\"uris\\\" validation failed: expected unique items but items 1 and 2 are equal\"}\n\n\n\n=== TEST 7: validate failed, uri or uris is mandatory\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"allOf 1 failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 8: validate failed, enum check\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"status\": 3,\n                \"uri\": \"/foo\",\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"property \\\"status\\\" validation failed: matches none of the enum values\"}\n\n\n\n=== TEST 9: validate failed, wrong combination\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"script\": \"xxxxxxxxxxxxxxxxxxxxx\",\n                \"plugin_config_id\": \"foo\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"allOf 1 failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 10: validate failed, id_schema check\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/routes',\n            ngx.HTTP_POST,\n            [[{\n                \"plugin_config_id\": \"@@@@@@@@@@@@@@@@\",\n                \"uri\": \"/foo\",\n                \"upstream\": {\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"nghttp2.org\": 1\n                    }\n                }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"property \\\"plugin_config_id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 11: upstream ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/upstreams',\n            ngx.HTTP_POST,\n            [[{\n               \"nodes\":{\n                  \"nghttp2.org\":100\n               },\n               \"type\":\"roundrobin\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 200\n\n\n\n=== TEST 12: upstream failed, wrong nodes format\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/validate/upstreams',\n            ngx.HTTP_POST,\n            [[{\n               \"nodes\":[\n                   \"nghttp2.org\"\n               ],\n               \"type\":\"roundrobin\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response\n{\"error_msg\":\"allOf 1 failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 13: Check node_schema optional port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes',\n                ngx.HTTP_POST,\n                {\n                    uri = \"/hello\",\n                    upstream = {\n                        type = \"roundrobin\",\n                        nodes = {\n                            { host = \"127.0.0.1:1980\", weight = 1,}\n                        }\n                    },\n                    methods = {\"GET\"},\n                }\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: Test route upstream\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/admin/schema.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: get route schema\n--- request\nGET /apisix/admin/schema/route\n--- response_body eval\nqr/\"plugins\":\\{\"type\":\"object\"}/\n\n\n\n=== TEST 2: get service schema and check if it contains `anyOf`\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, _, res_body = t('/apisix/admin/schema/service', ngx.HTTP_GET)\n            local res_data = core.json.decode(res_body)\n            if res_data[\"anyOf\"] then\n                ngx.say(\"found `anyOf`\")\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: get not exist schema\n--- request\nGET /apisix/admin/schema/noexits\n--- error_code: 400\n\n\n\n=== TEST 4: wrong method\n--- request\nPUT /apisix/admin/schema/service\n--- error_code: 404\n\n\n\n=== TEST 5: wrong method\n--- request\nPOST /apisix/admin/schema/service\n--- error_code: 404\n\n\n\n=== TEST 6: ssl\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ssl = require(\"apisix.schema_def\").ssl\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/ssl',\n            ngx.HTTP_GET,\n            nil,\n            ssl\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: get plugin's schema\n--- request\nGET /apisix/admin/schema/plugins/limit-count\n--- response_body eval\nqr/\"required\":\\[\"count\",\"time_window\"\\]/\n\n\n\n=== TEST 8: get not exist plugin\n--- request\nGET /apisix/admin/schema/plugins/no-exist\n--- error_code: 404\n\n\n\n=== TEST 9: serverless-pre-function\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/plugins/serverless-pre-function',\n            ngx.HTTP_GET,\n            nil,\n            [[{\n                \"properties\": {\n                    \"phase\": {\n                        \"enum\": [\"rewrite\", \"access\", \"header_filter\", \"body_filter\", \"log\", \"before_proxy\"],\n                        \"type\": \"string\"\n                    },\n                    \"functions\": {\n                        \"minItems\": 1,\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                },\n                \"required\": [\"functions\"],\n                \"type\": \"object\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: serverless-post-function\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/schema/plugins/serverless-post-function',\n            ngx.HTTP_GET,\n            nil,\n            [[{\n                \"properties\": {\n                    \"phase\": {\n                        \"enum\": [\"rewrite\", \"access\", \"header_filter\", \"body_filter\", \"log\", \"before_proxy\"],\n                        \"type\": \"string\"\n                    },\n                    \"functions\": {\n                        \"minItems\": 1,\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                },\n                \"required\": [\"functions\"],\n                \"type\": \"object\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: get plugin udp-logger schema\n--- request\nGET /apisix/admin/schema/plugins/udp-logger\n--- response_body  eval\nqr/\"properties\":/\n\n\n\n=== TEST 12: get plugin grpc-transcode schema\n--- request\nGET /apisix/admin/schema/plugins/grpc-transcode\n--- response_body eval\nqr/(\"proto_id\".*additionalProperties|additionalProperties.*\"proto_id\")/\n\n\n\n=== TEST 13: get plugin prometheus schema\n--- request\nGET /apisix/admin/schema/plugins/prometheus\n--- response_body eval\nqr/\"disable\":\\{\"type\":\"boolean\"\\}/\n\n\n\n=== TEST 14: get plugin node-status schema\n--- extra_yaml_config\nplugins:\n    - node-status\n--- request\nGET /apisix/admin/schema/plugins/node-status\n--- response_body eval\nqr/\"disable\":\\{\"type\":\"boolean\"\\}/\n\n\n\n=== TEST 15: get global_rule schema to check if it contains `create_time` and `update_time`\n--- request\nGET /apisix/admin/schema/global_rule\n--- response_body eval\nqr/(\"update_time\":\\{\"type\":\"integer\"\\}.*\"create_time\":\\{\"type\":\"integer\"\\}|\"create_time\":\\{\"type\":\"integer\"\\}.*\"update_time\":\\{\"type\":\"integer\"\\})/\n\n\n\n=== TEST 16: get proto schema to check if it contains `create_time` and `update_time`\n--- request\nGET /apisix/admin/schema/proto\n--- response_body eval\nqr/(\"update_time\":\\{\"type\":\"integer\"\\}.*\"create_time\":\\{\"type\":\"integer\"\\}|\"create_time\":\\{\"type\":\"integer\"\\}.*\"update_time\":\\{\"type\":\"integer\"\\})/\n\n\n\n=== TEST 17: get stream_route schema to check if it contains `create_time` and `update_time`\n--- request\nGET /apisix/admin/schema/stream_route\n--- response_body eval\nqr/(\"update_time\":\\{\"type\":\"integer\"\\}.*\"create_time\":\\{\"type\":\"integer\"\\}|\"create_time\":\\{\"type\":\"integer\"\\}.*\"update_time\":\\{\"type\":\"integer\"\\})/\n"
  },
  {
    "path": "t/admin/secrets.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:12800/get\",\n                    \"prefix\" : \"apisix\",\n                    \"token\" : \"apisix\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"http://127.0.0.1:12800/get\",\n                        \"prefix\" : \"apisix\",\n                        \"token\" : \"apisix\"\n                    },\n                    \"key\": \"/apisix/secrets/vault/test1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/secrets/vault/test1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"uri\": \"http://127.0.0.1:12800/get\",\n                        \"prefix\" : \"apisix\",\n                        \"token\" : \"apisix\"\n                    },\n                    \"key\": \"/apisix/secrets/vault/test1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: GET all\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/secrets',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"total\": 1,\n                    \"list\": [\n                        {\n                            \"key\": \"/apisix/secrets/vault/test1\",\n                            \"value\": {\n                                \"uri\": \"http://127.0.0.1:12800/get\",\n                                \"prefix\" : \"apisix\",\n                                \"token\" : \"apisix\"\n                            }\n                        }\n                    ]\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: PATCH on path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/secrets/vault/test1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/secrets/vault/test1/token',\n                ngx.HTTP_PATCH,\n                [[\"unknown\"]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"http://127.0.0.1:12800/get\",\n                        \"prefix\" : \"apisix\",\n                        \"token\" : \"unknown\"\n                    },\n                    \"key\": \"/apisix/secrets/vault/test1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/secrets/vault/test1'))\n            assert(res.body.node.value.token == \"unknown\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/secrets/vault/test1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local prev_update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= nil, \"update_time is nil\")\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"uri\": \"http://127.0.0.1:12800/get\",\n                    \"prefix\" : \"apisix\",\n                    \"token\" : \"apisix\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"http://127.0.0.1:12800/get\",\n                        \"prefix\" : \"apisix\",\n                        \"token\" : \"apisix\"\n                    },\n                    \"key\": \"/apisix/secrets/vault/test1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/secrets/vault/test1'))\n            assert(res.body.node.value.token == \"apisix\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: PATCH without id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/secrets/vault',\n                ngx.HTTP_PATCH,\n                [[{}]],\n                [[{}]]\n                )\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"no secret id\"}\n\n\n\n=== TEST 7: DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: PUT with invalid format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/get\",\n                    \"prefix\" : \"apisix\",\n                    \"token\" : \"apisix\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"http://127.0.0.1:12800/get\",\n                        \"prefix\" : \"apisix\",\n                        \"token\" : \"apisix\"\n                    },\n                    \"key\": \"/apisix/secrets/vault/test1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/validation failed: failed to match pattern/\n"
  },
  {
    "path": "t/admin/services-array-nodes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8080,\n                            \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8080,\n                                \"weight\": 1\n                            }],\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8080,\n                                \"weight\": 1\n                            }],\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/services-force-delete.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 201\n--- response_body\npassed\n\n\n\n=== TEST 2: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"service_id\": 1,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: delete service(wrong header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/1?force=anyvalue',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this service directly, route [1] is still using it now\"}\n\n\n\n=== TEST 4: delete service(without force delete header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this service directly, route [1] is still using it now\"}\n\n\n\n=== TEST 5: delete service(force delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/1?force=true',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/services-string-id.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set service(id: 5eeb3dc90f747328b2930b0b)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get service(id: 5eeb3dc90f747328b2930b0b)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete service(id: 5eeb3dc90f747328b2930b0b)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: delete service(id: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/services/not_found', ngx.HTTP_DELETE)\n\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 5: post service + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services',\n                ngx.HTTP_POST,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local id = string.sub(res.key, #\"/apisix/services/\" + 1)\n            code, message = t('/apisix/admin/services/' .. id, ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: uri + upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 7: uri + plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 8: invalid service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/*invalid_id$',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.exit(code)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 9: invalid id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"3\",\n                    \"plugins\": {}\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong service id\"}\n\n\n\n=== TEST 10: id in the rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"5eeb3dc90f747328b2930b0b\",\n                    \"plugins\": {}\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {}\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: integer id less than 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": -100,\n                    \"plugins\": {}\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 12: invalid service id: contains symbols value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": \"*invalid_id$\",\n                    \"plugins\": {}\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 13: invalid upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"5eeb3dc90f747328b2930b0b\",\n                    \"upstream_id\": \"invalid$\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream_id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 14: not exist upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"5eeb3dc90f747328b2930b0b\",\n                    \"upstream_id\": \"9999999999\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch upstream info by upstream id [9999999999], response code: 404\"}\n\n\n\n=== TEST 15: wrong service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_POST,\n                [[{\n                    \"plugins\": {}\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong service id, do not need it\"}\n\n\n\n=== TEST 16: wrong service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_POST,\n                [[{\n                    \"id\": \"5eeb3dc90f747328b2930b0b\",\n                    \"plugins\": {}\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong service id, do not need it\"}\n\n\n\n=== TEST 17: patch service(whole)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new 20 service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new 20 service\"\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: patch service(new desc)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"desc\": \"new 19 service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new 19 service\"\n                    },\n                    \"key\": \"/apisix/services/5eeb3dc90f747328b2930b0b\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: patch service(new nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8081\": 3,\n                            \"127.0.0.1:8082\": 4\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1,\n                                \"127.0.0.1:8081\": 3,\n                                \"127.0.0.1:8082\": 4\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: set service(id: 5eeb3dc90f747328b2930b0b) and upstream(type:chash, default hash_on: vars, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\"\n                    },\n                    \"desc\": \"new service\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 21: set service(id: 5eeb3dc90f747328b2930b0b) and upstream(type:chash, hash_on: header, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"header\"\n                    },\n                    \"desc\": \"new service\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 22: set service(id: 5eeb3dc90f747328b2930b0b) and upstream(type:chash, hash_on: cookie, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"cookie\"\n                    },\n                    \"desc\": \"new service\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 23: set service(id: 5eeb3dc90f747328b2930b0b) and upstream(type:chash, hash_on: consumer, missing key is ok)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/5eeb3dc90f747328b2930b0b',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"consumer\"\n                    },\n                    \"desc\": \"new service\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(code .. \" \" .. body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200 passed\n"
  },
  {
    "path": "t/admin/services.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/services/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message,res = t('/apisix/admin/services/1', ngx.HTTP_DELETE)\n\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: delete service(id: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/services/not_found', ngx.HTTP_DELETE)\n\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 5: post service + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, message, res = t('/apisix/admin/services',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local id = string.sub(res.key, #\"/apisix/services/\" + 1)\n            local res = assert(etcd.get('/services/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n            code, message = t('/apisix/admin/services/' .. id, ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: uri + upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 7: uri + plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n\n\n\n=== TEST 8: invalid service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/invalid_id$',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.exit(code)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 9: invalid id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": 3,\n                    \"plugins\": {}\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong service id\"}\n\n\n\n=== TEST 10: id in the rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"1\",\n                    \"plugins\": {}\n                }]],\n                [[{\n                    \"value\": {\n                        \"plugins\": {}\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: integer id less than 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": -100,\n                    \"plugins\": {}\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 12: invalid service id: string value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"invalid_id$\",\n                    \"plugins\": {}\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 13: invalid upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": 1,\n                    \"upstream_id\": \"invalid$\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream_id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 14: not exist upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": 1,\n                    \"upstream_id\": \"9999999999\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch upstream info by upstream id [9999999999], response code: 404\"}\n\n\n\n=== TEST 15: wrong service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_POST,\n                [[{\n                    \"plugins\": {}\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong service id, do not need it\"}\n\n\n\n=== TEST 16: wrong service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                ngx.HTTP_POST,\n                [[{\n                    \"id\": 1,\n                    \"plugins\": {}\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong service id, do not need it\"}\n\n\n\n=== TEST 17: patch service(whole)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n\n            local id = 1\n            local res = assert(etcd.get('/services/' .. id))\n            local prev_create_time = res.body.node.value.create_time\n            local prev_update_time = res.body.node.value.update_time\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new 20 service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new 20 service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/services/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: patch service(new desc)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"desc\": \"new 19 service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new 19 service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: patch service(new nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8081\": 3,\n                            \"127.0.0.1:8082\": 4\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1,\n                                \"127.0.0.1:8081\": 3,\n                                \"127.0.0.1:8082\": 4\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: patch service(whole - sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1/',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new 22 service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new 22 service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: patch service(new desc - sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1/desc',\n                ngx.HTTP_PATCH,\n                '\"new 23 service\"',\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new 23 service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: patch service(new nodes - sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1/upstream',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.2:8081\": 3,\n                        \"127.0.0.3:8082\": 4\n                    },\n                    \"type\": \"roundrobin\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.2:8081\": 3,\n                                \"127.0.0.3:8082\": 4\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: set service(id: 1) and upstream(type:chash, default hash_on: vars, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\"\n                    },\n                    \"desc\": \"new service\"\n                }]])\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 24: set service(id: 1) and upstream(type:chash, hash_on: header, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"header\"\n                    },\n                    \"desc\": \"new service\"\n                }]])\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 25: set service(id: 1) and upstream(type:chash, hash_on: cookie, missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"cookie\"\n                    },\n                    \"desc\": \"new service\"\n                }]])\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 26: set service(id: 1) and upstream(type:chash, hash_on: consumer, missing key is ok)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"consumer\"\n                    },\n                    \"desc\": \"new service\"\n                }]])\n\n            ngx.status = code\n            ngx.say(code .. \" \" .. body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200 passed\n\n\n\n=== TEST 27: set service(id: 1 + test service name)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"name\": \"test service name\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"test service name\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: invalid string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/*invalid',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 29: set empty service. (id: 1)（allow empty `service` object）\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                '{}',\n                [[{\n                    \"value\": {\n                        \"id\":\"1\"\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: patch content to the empty service.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"desc\": \"empty service\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:80\": 1\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\":{\n                        \"desc\":\"empty service\",\n                        \"plugins\":{\n                            \"limit-count\":{\n                                \"time_window\":60,\n                                \"count\":2,\n                                \"rejected_code\":503,\n                                \"key\":\"remote_addr\"\n                            }\n                        },\n                        \"upstream\":{\n                            \"type\":\"roundrobin\",\n                            \"nodes\":{\n                                \"127.0.0.1:80\":1\n                            }\n                        },\n                        \"id\":\"1\"\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 31: set service(with labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"labels\": {\n                        \"build\":\"16\",\n                        \"env\":\"production\",\n                        \"version\":\"v2\"\n                    },\n                    \"desc\": \"new service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"labels\": {\n                            \"build\": \"16\",\n                            \"env\": \"production\",\n                            \"version\": \"v2\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 32: patch service(change labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"labels\": {\n                        \"build\": \"17\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"labels\": {\n                            \"build\": \"17\",\n                            \"env\": \"production\",\n                            \"version\": \"v2\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 33: invalid format of label value: set service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"labels\": {\n                        \"env\": [\"production\", \"release\"]\n                    },\n                    \"desc\": \"new service\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: failed to validate env (matching \\\".*\\\"): wrong type: expected string, got table\"}\n\n\n\n=== TEST 34: create service with create_time and update_time(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                  \"upstream\": {\n                    \"nodes\": {\n                      \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                  },\n                  \"create_time\": 1602883670,\n                  \"update_time\": 1602893670\n                }]])\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"the property is forbidden:.*\"\\}/\n\n\n\n=== TEST 35: create service and the built-in resource with create_time and update_time(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                  \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                      \"127.0.0.1:8080\": 1\n                    },\n                    \"create_time\": 1602883670,\n                    \"update_time\": 1602893670\n                  }\n                }]])\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"the property is forbidden:.*\"\\}/\n\n\n\n=== TEST 36: limit the length of service's name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1', ngx.HTTP_PUT,\n                require(\"toolkit.json\").encode({name = (\"1\"):rep(257)}))\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"name\\\" validation failed: string too long, expected at most 256, got 257\"}\n\n\n\n=== TEST 37: allow dot in the id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/a.b',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new service\"\n                    },\n                    \"key\": \"/apisix/services/a.b\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/services2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: not unwanted data, POST\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.key = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.id ~= nil)\n            res.value.id = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"value\":{\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n\n\n\n=== TEST 2: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/services/1\",\"value\":{\"id\":\"1\",\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n\n\n\n=== TEST 3: not unwanted data, PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/1',\n                 ngx.HTTP_PATCH,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            res.value.create_time = nil\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/services/1\",\"value\":{\"id\":\"1\",\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n\n\n\n=== TEST 4: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/1', ngx.HTTP_GET)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/services/1\",\"value\":{\"id\":\"1\",\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n\n\n\n=== TEST 5: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/services/1',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/services/1\"}\n\n\n\n=== TEST 6: set service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"service_id\": 1,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: delete service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/1', ngx.HTTP_DELETE)\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this service directly, route [1] is still using it now\"}\n\n\n\n=== TEST 9: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 10: delete service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/ssl.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set ssl(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local etcd = require(\"apisix.core.etcd\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/ssls/1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get ssl(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/ssls/1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"key\": null\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete ssl(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/ssls/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: delete ssl(id: 99999999999999)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/ssls/99999999999999', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 5: push ssl + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"foo.com\"}\n\n            local code, message, res = t.test('/apisix/admin/ssls',\n                ngx.HTTP_POST,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"foo.com\"\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local id = string.sub(res.key, #\"/apisix/ssls/\" + 1)\n            code, message = t.test('/apisix/admin/ssls/' .. id, ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: missing certificate information\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {sni = \"foo.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"foo.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: then clause did not match\"}\n\n\n\n=== TEST 7: wildcard host name\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"*.foo.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"*.foo.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: store sni in `snis`\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                cert = ssl_cert, key = ssl_key,\n                snis = {\"*.foo.com\", \"bar.com\"},\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"snis\": [\"*.foo.com\", \"bar.com\"]\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n            if code > 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: string id(delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/a-b-c-ABC_0123',\n                ngx.HTTP_DELETE\n            )\n            if code > 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: invalid id\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/*invalid',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n            if code > 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 12: set ssl with multicerts(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local ssl_ecc_cert = t.read_file(\"t/certs/apisix_ecc.crt\")\n            local ssl_ecc_key = t.read_file(\"t/certs/apisix_ecc.key\")\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                certs = {ssl_ecc_cert},\n                keys = {ssl_ecc_key}\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n              )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: mismatched certs and keys\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_ecc_cert = t.read_file(\"t/certs/apisix_ecc.crt\")\n\n            local data = {\n                sni = \"test.com\",\n                certs = { ssl_ecc_cert },\n                keys = {},\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: then clause did not match\"}\n\n\n\n=== TEST 14: set ssl(with labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\", labels = { version = \"v2\", build = \"16\", env = \"production\"}}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"labels\": {\n                            \"version\": \"v2\",\n                            \"build\": \"16\",\n                            \"env\": \"production\"\n                        }\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: invalid format of label value: set ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\", labels = { env = {\"production\", \"release\"}}}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"labels\": {\n                            \"env\": [\"production\", \"release\"]\n                        }\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: failed to validate env (matching \\\".*\\\"): wrong type: expected string, got table\"}\n\n\n\n=== TEST 16: create ssl with manage fields(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\"\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: delete test ssl(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/ssls/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 18: create/patch ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local etcd = require(\"apisix.core.etcd\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body, res = t.test('/apisix/admin/ssls',\n                ngx.HTTP_POST,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    }\n                }]]\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local id = string.sub(res.key, #\"/apisix/ssls/\" + 1)\n            local res = assert(etcd.get('/ssls/' .. id))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n            local code, body = t.test('/apisix/admin/ssls/' .. id,\n                ngx.HTTP_PATCH,\n                core.json.encode({create_time = 0, update_time = 1})\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local res = assert(etcd.get('/ssls/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(create_time == 0, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time == 1, \"update_time mismatched\")\n\n            -- clean up\n            local code, body = t.test('/apisix/admin/ssls/' .. id, ngx.HTTP_DELETE)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: missing sni information\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: then clause did not match\"}\n\n\n\n=== TEST 20: type client, missing sni information\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {type = \"client\", cert = ssl_cert, key = ssl_key}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- response_body chomp\npassed\n\n\n\n=== TEST 21: set ssl with sercret\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                sni = \"test.com\",\n                cert = \"$secret://vault/test/ssl/test.com.crt\",\n                key = \"$secret://vault/test/ssl/test.com.key\",\n                certs = {\"$secret://vault/test/ssl/test.com.2.crt\"},\n                keys = {\"$secret://vault/test/ssl/test.com.2.key\"}\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"cert\": \"$secret://vault/test/ssl/test.com.crt\",\n                        \"key\": \"$secret://vault/test/ssl/test.com.key\",\n                        \"certs\": [\"$secret://vault/test/ssl/test.com.2.crt\"],\n                        \"keys\": [\"$secret://vault/test/ssl/test.com.2.key\"]\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: set ssl with env, and prefix is all uppercase or lowercase\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                sni = \"test.com\",\n                cert = \"$ENV://APISIX_TEST_SSL_CERT\",\n                key = \"$env://APISIX_TEST_SSL_KEY\",\n                certs = {\"$env://APISIX_TEST_SSL_CERTS\"},\n                keys = {\"$ENV://APISIX_TEST_SSL_KEYS\"},\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"cert\": \"$ENV://APISIX_TEST_SSL_CERT\",\n                        \"key\": \"$env://APISIX_TEST_SSL_KEY\",\n                        \"certs\": [\"$env://APISIX_TEST_SSL_CERTS\"],\n                        \"keys\": [\"$ENV://APISIX_TEST_SSL_KEYS\"]\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: set ssl with invalid prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                sni = \"test.com\",\n                cert = \"$ENV://APISIX_TEST_SSL_CERT\",\n                key = \"$env://APISIX_TEST_SSL_KEY\",\n                certs = {\"https://APISIX_TEST_SSL_CERTS\"},\n                keys = {\"$ENV://APISIX_TEST_SSL_KEYS\"},\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"certs\\\" validation failed: failed to validate item 1: value should match only one schema, but matches none\"}\n"
  },
  {
    "path": "t/admin/ssl2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: not unwanted data, POST\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"not-unwanted-post.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls',\n                ngx.HTTP_POST,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.key ~= nil)\n            res.key = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.cert ~= nil)\n            res.value.cert = \"\"\n            assert(res.value.key ~= nil)\n            res.value.key = \"\"\n            assert(res.value.id ~= nil)\n            res.value.id = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"value\":{\"cert\":\"\",\"key\":\"\",\"sni\":\"not-unwanted-post.com\"}}\n\n\n\n=== TEST 2: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.cert ~= nil)\n            res.value.cert = \"\"\n            assert(res.value.key ~= nil)\n            res.value.key = \"\"\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/ssls/1\",\"value\":{\"cert\":\"\",\"id\":\"1\",\"key\":\"\",\"sni\":\"test.com\"}}\n\n\n\n=== TEST 3: not unwanted data, PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"t.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.cert ~= nil)\n            res.value.cert = \"\"\n            assert(res.value.key ~= nil)\n            res.value.key = \"\"\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/ssls/1\",\"value\":{\"cert\":\"\",\"id\":\"1\",\"key\":\"\",\"sni\":\"t.com\"}}\n\n\n\n=== TEST 4: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.cert ~= nil)\n            res.value.cert = \"\"\n            assert(res.value.key == nil)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/ssls/1\",\"value\":{\"cert\":\"\",\"id\":\"1\",\"sni\":\"t.com\"}}\n\n\n\n=== TEST 5: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/ssls/1\"}\n\n\n\n=== TEST 6: bad cert\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = [[-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n            ]], key = ssl_key, sni = \"test.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(res)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to parse cert: PEM_read_bio_X509_AUX() failed\"}\n\n\n\n=== TEST 7: bad key\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local data = {cert = ssl_cert, key = [[\n-----BEGIN RSA PRIVATE KEY-----\nMIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5\njhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo\nwzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==\n-----END RSA PRIVATE KEY-----]], sni = \"test.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(res)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to parse key: PEM_read_bio_PrivateKey() failed\"}\n\n\n\n=== TEST 8: bad certs\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"t.com\",\n                certs = {\n                    [[-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----]]\n                },\n                keys = {ssl_key}\n            }\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(res)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to handle cert-key pair[1]: failed to parse cert: PEM_read_bio_X509_AUX() failed\"}\n\n\n\n=== TEST 9: bad keys\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"t.com\",\n                certs = {ssl_cert},\n                keys = {[[-----BEGIN RSA PRIVATE KEY-----\nMIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5\njhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo\nwzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==\n-----END RSA PRIVATE KEY-----]]}\n            }\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(res)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to handle cert-key pair[1]: failed to parse key: PEM_read_bio_PrivateKey() failed\"}\n\n\n\n=== TEST 10: empty snis\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, snis = {}}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(res)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"snis\\\" validation failed: expect array to have at least 1 items\"}\n\n\n\n=== TEST 11: update snis, PATCH with sub path\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, snis = {\"test.com\"}}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n\n            local data = {\"update1.com\", \"update2.com\"}\n            local code, message, res = t.test('/apisix/admin/ssls/1/snis',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            ngx.say(res)\n        }\n    }\n--- response_body_like eval\nqr/\"snis\":\\[\"update1.com\",\"update2.com\"\\]/\n\n\n\n=== TEST 12: PATCH encrypt ssl key\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: \"qeddd145sfvddff3\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, certs = {ssl_cert}, keys = {ssl_key}}\n            local code, message, res = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.key == ssl_key)\n            ngx.say(res.value.keys[1] == ssl_key)\n        }\n    }\n--- response_body\nfalse\nfalse\n\n\n\n=== TEST 13: PATCH encrypt ssl key, sub_path\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: \"qeddd145sfvddff3\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local code, message, res = t.test('/apisix/admin/ssls/1/keys',\n                ngx.HTTP_PATCH,\n                json.encode({ssl_key})\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.keys[1] == ssl_key)\n        }\n    }\n--- response_body\nfalse\n"
  },
  {
    "path": "t/admin/ssl3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: list empty resources\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/ssls',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"list\":[],\"total\":0}\n"
  },
  {
    "path": "t/admin/ssl4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nno_root_location();\n\nadd_block_preprocessor( sub{\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $TEST_NGINX_HTML_DIR ||= html_dir();\n\n    my $config = <<_EOC_;\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test.com\", true)\n            if not sess then\n                sock = ngx.socket.tcp()\n\n                sock:settimeout(2000)\n\n                local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n                if not ok then\n                    ngx.say(\"failed to connect: \", err)\n                    return\n                end\n\n                ngx.say(\"connected: \", ok)\n\n                sess, err = sock:sslhandshake(nil, \"www.test.com\", true)\n                if not sess then\n                    ngx.say(\"failed to do SSL handshake: \", err)\n                    return\n                end\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n\n            local req = \"GET /hello HTTP/1.0\\\\r\\\\nHost: www.test.com\\\\r\\\\nConnection: close\\\\r\\\\n\\\\r\\\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n_EOC_\n\n   if (!$block->config) {\n       $block->set_value(\"config\", $config)\n   }\n}\n\n);\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set ssl(sni: www.test.com), encrypt with the first keyring\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - edd1c9f0985e76a1\n            - qeddd145sfvddff3\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: \"edd1c9f0985e76a1\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: client request with the old style keyring\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: \"edd1c9f0985e76a1\"\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 4: client request with the new style keyring\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - edd1c9f0985e76a1\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 5: client request failed with the wrong keyring\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - qeddd145sfvddff3\n--- error_log\ndecrypt ssl key failed\n\n\n\n=== TEST 6: client request successfully, use the two keyring to decrypt in turn\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - qeddd145sfvddff3\n            - edd1c9f0985e76a1\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- ignore_error_log\n\n\n\n=== TEST 7: remove test ssl certs\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - edd1c9f0985e76a1\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        t.test('/apisix/admin/ssls/1', ngx.HTTP_DELETE)\n    }\n}\n\n\n\n=== TEST 8: set ssl(sni: www.test.com), do not encrypt\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: null\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 9: client request without keyring\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: null\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 10: remove test ssl certs\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: null\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        t.test('/apisix/admin/ssls/1', ngx.HTTP_DELETE)\n    }\n}\n\n\n\n=== TEST 11: set ssl(sni: www.test.com) with long label\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: null\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\",\n                      labels = {secret = \"js-design-test-bigdata-data-app-service-router-my-secret-number-123456\"}}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\",\n                    \"labels\": {\n                        \"secret\": \"js-design-test-bigdata-data-app-service-router-my-secret-number-123456\"\n                    },\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 12: set ssl(sni: www.test.com), encrypt with the first keyring\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - edd1c9f0985e76a1\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 13: update encrypt keyring, and set ssl(sni: test2.com)\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - qeddd145sfvddff3\n            - edd1c9f0985e76a1\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"test2.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/2',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"test2.com\"\n                },\n                \"key\": \"/apisix/ssls/2\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 14: Successfully access test.com\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - qeddd145sfvddff3\n            - edd1c9f0985e76a1\n--- exec\ncurl -k -s --resolve \"test2.com:1994:127.0.0.1\" https://test2.com:1994/hello 2>&1 | cat\n--- response_body\nhello world\n\n\n\n=== TEST 15: Successfully access test2.com\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring:\n            - qeddd145sfvddff3\n            - edd1c9f0985e76a1\n--- exec\ncurl -k -s --resolve \"test2.com:1994:127.0.0.1\" https://test2.com:1994/hello 2>&1 | cat\n--- response_body\nhello world\n"
  },
  {
    "path": "t/admin/ssl5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Not supported set TLSv1.0 for ssl_protocols\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\", ssl_protocols = {\"TLSv1.0\", \"TLSv1.2\"}}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"ssl_protocols\\\" validation failed: failed to validate item 1: matches none of the enum values\"}\n\n\n\n=== TEST 2: The default value for the ssl_protocols is null\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"ssl_protocols\": null,\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/ssls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: test /apisix/admin/ssls/{id}\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local etcd = require(\"apisix.core.etcd\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/ssls/1'))\n            local prev_create_time = res.body.node.value.create_time\n            assert(prev_create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/standalone-healthcheck.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    # restarts cause the memory cache to be emptied, don't do this\n    $ENV{TEST_NGINX_FORCE_RESTART_ON_TEST} = 0;\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nuse_hup();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'EOF');\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: yaml\n    admin:\n        admin_key:\n            - name: admin\n              key: edd1c9f034335f136f87ad84b625c8f1\n              role: admin\nEOF\n    }\n\n    if (!defined $block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: send /healthcheck should fail because config is not loaded yet\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    local shared_dict  = ngx.shared[\"standalone-config\"]\n    shared_dict:delete(\"config\")\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local healthcheck_uri = \"http://127.0.0.1:7085\" .. \"/status/ready\"\n            local httpc = http.new()\n            local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n            ngx.status = res.status\n        }\n    }\n--- request\nGET /t\n--- error_code: 503\n\n\n\n=== TEST 2: configure route and send /healthcheck should pass\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1) -- wait for event broadcast started\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/configs',\n                 ngx.HTTP_PUT,\n                 [[{\"routes\":[{\"id\":\"r1\",\"uri\":\"/r1\",\"upstream\":{\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"},\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}]}]],\n                 nil,\n                 {\n                  [\"X-API-KEY\"] = \"edd1c9f034335f136f87ad84b625c8f1\",\n                  [\"X-Digest\"] = \"1\"\n                 }\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.sleep(1)\n            local http = require(\"resty.http\")\n            local healthcheck_uri = \"http://127.0.0.1:7085\" .. \"/status/ready\"\n            local httpc = http.new()\n            local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n            ngx.status = res.status\n        }\n    }\n--- request\nGET /t\n--- error_code: 200\n"
  },
  {
    "path": "t/admin/standalone-plugin-validation.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'_EOC_');\napisix:\n    admin_key:\n        - name: admin\n          key: edd1c9f034335f136f87ad84b625c8f1\n          role: admin\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n    }\n\n    $block->set_value(\"stream_enable\", 1);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: missing plugin on route blocks route matching\n--- extra_yaml_config\nplugins:\n  - redirect\n--- apisix_yaml\nroutes:\n  - id: 1\n    uri: /hello\n    plugins:\n      openid-connect:\n        client_id: x\n        client_secret: x\n        discovery: x\n        scope: openid email\n        bearer_only: false\n        realm: x\n    upstream:\n      type: roundrobin\n      nodes:\n        \"127.0.0.1:1980\": 1\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nunknown plugin [openid-connect]\n\n\n\n=== TEST 2: missing plugin on stream route blocks stream matching\n--- extra_yaml_config\nstream_plugins:\n  - ip-restriction\n--- apisix_yaml\nstream_routes:\n  - id: 1\n    server_port: 1985\n    plugins:\n      syslog:\n        host: 127.0.0.1\n        port: 514\n    upstream:\n      type: roundrobin\n      nodes:\n        \"127.0.0.1:1995\": 1\n#END\n--- config\nlocation /stream_request {\n    content_by_lua_block {\n        ngx.sleep(1)  -- wait for the stream route to take effect\n\n        local tcp_request = function(host, port)\n            local sock, err = ngx.socket.tcp()\n            assert(sock, err)\n\n            local ok, err = sock:connect(host, port)\n            if not ok then\n                ngx.say(\"connect to stream server error: \", err)\n                return\n            end\n            local bytes, err = sock:send(\"mmm\")\n            if not bytes then\n                ngx.say(\"send stream request error: \", err)\n                return\n            end\n\n            local data, err = sock:receive(\"*a\")\n            if not data then\n                sock:close()\n                ngx.say(\"receive stream response error: \", err)\n                return\n            end\n            sock:close()\n            ngx.print(data)\n        end\n\n        tcp_request(\"127.0.0.1\", 1985)\n    }\n}\n--- request\nGET /stream_request\n--- response_body\nreceive stream response error: connection reset by peer\n--- error_log\nunknown plugin [syslog]\n\n\n\n=== TEST 3: missing plugin on route blocks route matching (json)\n--- yaml_config\napisix:\n    admin_key:\n        - name: admin\n          key: edd1c9f034335f136f87ad84b625c8f1\n          role: admin\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: json\n--- extra_yaml_config\nplugins:\n  - redirect\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": \"1\",\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"openid-connect\": {\n          \"client_id\": \"x\",\n          \"client_secret\": \"x\",\n          \"discovery\": \"x\",\n          \"scope\": \"openid email\",\n          \"bearer_only\": false,\n          \"realm\": \"x\"\n        }\n      },\n      \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        }\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nunknown plugin [openid-connect]\n\n\n\n=== TEST 4: missing plugin on stream route blocks stream matching (json)\n--- yaml_config\napisix:\n    admin_key:\n        - name: admin\n          key: edd1c9f034335f136f87ad84b625c8f1\n          role: admin\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: json\n--- extra_yaml_config\nstream_plugins:\n  - ip-restriction\n--- apisix_json\n{\n  \"stream_routes\": [\n    {\n      \"id\": \"1\",\n      \"server_port\": 1985,\n      \"plugins\": {\n        \"syslog\": {\n          \"host\": \"127.0.0.1\",\n          \"port\": 514\n        }\n      },\n      \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n          \"127.0.0.1:1995\": 1\n        }\n      }\n    }\n  ]\n}\n--- config\nlocation /stream_request {\n    content_by_lua_block {\n        ngx.sleep(1)  -- wait for the stream route to take effect\n\n        local tcp_request = function(host, port)\n            local sock, err = ngx.socket.tcp()\n            assert(sock, err)\n\n            local ok, err = sock:connect(host, port)\n            if not ok then\n                ngx.say(\"connect to stream server error: \", err)\n                return\n            end\n            local bytes, err = sock:send(\"mmm\")\n            if not bytes then\n                ngx.say(\"send stream request error: \", err)\n                return\n            end\n\n            local data, err = sock:receive(\"*a\")\n            if not data then\n                sock:close()\n                ngx.say(\"receive stream response error: \", err)\n                return\n            end\n            sock:close()\n            ngx.print(data)\n        end\n\n        tcp_request(\"127.0.0.1\", 1985)\n    }\n}\n--- request\nGET /stream_request\n--- response_body\nreceive stream response error: connection reset by peer\n--- error_log\nunknown plugin [syslog]\n"
  },
  {
    "path": "t/admin/standalone.spec.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport axios from 'axios';\nimport YAML from 'yaml';\n\nconst ENDPOINT = '/apisix/admin/configs';\nconst VALIDATE_ENDPOINT = '/apisix/admin/configs/validate';\nconst HEADER_LAST_MODIFIED = 'x-last-modified';\nconst HEADER_DIGEST = 'x-digest';\nconst clientConfig = {\n  baseURL: 'http://localhost:1984',\n  headers: {\n    'X-API-KEY': 'edd1c9f034335f136f87ad84b625c8f1',\n  },\n};\nconst config1 = {\n  routes: [\n    {\n      id: 'r1',\n      uri: '/r1',\n      upstream: {\n        nodes: { '127.0.0.1:1980': 1 },\n        type: 'roundrobin',\n      },\n      plugins: { 'proxy-rewrite': { uri: '/hello' } },\n    },\n  ],\n};\nconst config2 = {\n  routes: [\n    {\n      id: 'r2',\n      uri: '/r2',\n      upstream: {\n        nodes: { '127.0.0.1:1980': 1 },\n        type: 'roundrobin',\n      },\n      plugins: { 'proxy-rewrite': { uri: '/hello' } },\n    },\n  ],\n};\nconst invalidConfVersionConfig1 = {\n  routes_conf_version: -1,\n};\nconst invalidConfVersionConfig2 = {\n  routes_conf_version: 'adc',\n};\nconst routeWithModifiedIndex = {\n  routes: [\n    {\n      id: 'r1',\n      uri: '/r1',\n      modifiedIndex: 1,\n      upstream: {\n        nodes: { '127.0.0.1:1980': 1 },\n        type: 'roundrobin',\n      },\n      plugins: { 'proxy-rewrite': { uri: '/hello' } },\n    },\n  ],\n};\nconst routeWithKeyAuth = {\n  routes: [\n    {\n      id: 'r1',\n      uri: '/r1',\n      upstream: {\n        nodes: { '127.0.0.1:1980': 1 },\n        type: 'roundrobin',\n      },\n      plugins: {\n        'proxy-rewrite': { uri: '/hello' },\n        'key-auth': {},\n      },\n    },\n  ],\n};\nconst consumerWithModifiedIndex = {\n  routes: routeWithKeyAuth.routes,\n  consumers: [\n    {\n      modifiedIndex: 10,\n      username: 'jack',\n      plugins: {\n        'key-auth': {\n          key: 'jack-key',\n        },\n      },\n    },\n  ],\n};\nconst credential1 = {\n  routes: routeWithKeyAuth.routes,\n  consumers: [\n    {\n      username: 'john_1',\n    },\n    {\n      id: 'john_1/credentials/john-a',\n      plugins: {\n        'key-auth': {\n          key: 'auth-a',\n        },\n      },\n    },\n    {\n      id: 'john_1/credentials/john-b',\n      plugins: {\n        'key-auth': {\n          key: 'auth-b',\n        },\n      },\n    },\n  ],\n};\n\nconst unknownPlugins = {\n  'invalid-plugin': {},\n};\n\nconst routeWithUnknownPlugins = {\n  routes: [\n    {\n      id: 'r1',\n      uri: '/r1',\n      plugins: unknownPlugins,\n    },\n  ],\n};\n\nconst servicesWithUnknownPlugins = {\n  services: [\n    {\n      id: 's1',\n      name: 's1',\n      plugins: unknownPlugins,\n    },\n  ],\n};\n\nconst invalidUpstream = {\n  nodes: { '127.0.0.1:1980': 1 },\n  type: 'chash',\n  hash_on: 'vars',\n  key: 'args_invalid',\n};\n\nconst routeWithInvalidUpstream = {\n  routes: [\n    {\n      id: 'r1',\n      uri: '/r1',\n      upstream: invalidUpstream,\n    },\n  ],\n};\n\nconst serviceWithInvalidUpstream = {\n  services: [\n    {\n      id: 's1',\n      name: 's1',\n      upstream: invalidUpstream,\n    },\n  ],\n};\n\nlet mockDigest = 1;\n\ndescribe('Admin - Standalone', () => {\n  const client = axios.create(clientConfig);\n  client.interceptors.response.use((response) => {\n    const contentType = response.headers['content-type'] || '';\n    if (\n      contentType.includes('application/yaml') &&\n      typeof response.data === 'string' &&\n      response.config.responseType !== 'text'\n    )\n      response.data = YAML.parse(response.data);\n    return response;\n  });\n\n  describe('Normal', () => {\n    it('dump empty config (default json format)', async () => {\n      const resp = await client.get(ENDPOINT);\n      expect(resp.status).toEqual(200);\n      expect(resp.data.routes_conf_version).toEqual(0);\n      expect(resp.data.ssls_conf_version).toEqual(0);\n      expect(resp.data.services_conf_version).toEqual(0);\n      expect(resp.data.upstreams_conf_version).toEqual(0);\n      expect(resp.data.consumers_conf_version).toEqual(0);\n      expect(resp.headers[HEADER_LAST_MODIFIED]).toBe(undefined);\n      expect(resp.headers[HEADER_DIGEST]).toBe(undefined);\n    });\n\n    it('dump empty config (yaml format)', async () => {\n      const resp = await client.get(ENDPOINT, {\n        headers: { Accept: 'application/yaml' },\n      });\n      expect(resp.status).toEqual(200);\n      expect(resp.headers['content-type']).toEqual('application/yaml');\n      expect(resp.data.routes_conf_version).toEqual(0);\n      expect(resp.data.ssls_conf_version).toEqual(0);\n      expect(resp.data.services_conf_version).toEqual(0);\n      expect(resp.data.upstreams_conf_version).toEqual(0);\n      expect(resp.data.consumers_conf_version).toEqual(0);\n      expect(resp.headers[HEADER_LAST_MODIFIED]).toBe(undefined);\n      expect(resp.headers[HEADER_DIGEST]).toBe(undefined);\n    });\n\n    it('update config (add routes, by json)', async () => {\n      const resp = await client.put(ENDPOINT, config1, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(202);\n      expect(parseInt(resp.headers[HEADER_LAST_MODIFIED])).toBeGreaterThan(0);\n      expect(resp.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n    });\n\n    it('update config (same digest, no update)', async () => {\n      const resp = await client.put(ENDPOINT, config1, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(204);\n    });\n\n    it('get metadata (by HTTP HEAD method)', async () => {\n      const resp = await client.head(ENDPOINT);\n      expect(resp.status).toEqual(200);\n      expect(parseInt(resp.headers[HEADER_LAST_MODIFIED])).toBeGreaterThan(0);\n      expect(resp.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n    });\n\n    it('dump config (json format)', async () => {\n      const resp = await client.get(ENDPOINT);\n      expect(resp.status).toEqual(200);\n      expect(resp.data.routes_conf_version).toEqual(1);\n      expect(resp.data.ssls_conf_version).toEqual(1);\n      expect(resp.data.services_conf_version).toEqual(1);\n      expect(resp.data.upstreams_conf_version).toEqual(1);\n      expect(resp.data.consumers_conf_version).toEqual(1);\n      expect(resp.data.routes).toEqual(config1.routes);\n      expect(parseInt(resp.headers[HEADER_LAST_MODIFIED])).toBeGreaterThan(0);\n      expect(resp.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n    });\n\n    it('dump config (yaml format)', async () => {\n      const resp = await client.get(ENDPOINT, {\n        headers: { Accept: 'application/yaml' },\n        responseType: 'text',\n      });\n      expect(resp.status).toEqual(200);\n      expect(resp.data).toContain('routes:');\n      expect(resp.data).toContain('id: r1');\n      expect(resp.data.startsWith('---')).toBe(false);\n      expect(resp.data.endsWith('...')).toBe(false);\n      expect(parseInt(resp.headers[HEADER_LAST_MODIFIED])).toBeGreaterThan(0);\n      expect(resp.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n    });\n\n    it('check route \"r1\"', async () => {\n      const resp = await client.get('/r1');\n      expect(resp.status).toEqual(200);\n      expect(resp.data).toEqual('hello world\\n');\n    });\n\n    it('update config (add routes, by yaml)', async () => {\n      mockDigest += 1;\n      const resp = await client.put(ENDPOINT, YAML.stringify(config2), {\n        headers: {\n          'Content-Type': 'application/yaml',\n          [HEADER_DIGEST]: mockDigest,\n        },\n      });\n      expect(resp.status).toEqual(202);\n    });\n\n    it('dump config (json format)', async () => {\n      const resp = await client.get(ENDPOINT);\n      expect(resp.status).toEqual(200);\n      expect(resp.data.routes_conf_version).toEqual(2);\n      expect(resp.data.ssls_conf_version).toEqual(2);\n      expect(resp.data.services_conf_version).toEqual(2);\n      expect(resp.data.upstreams_conf_version).toEqual(2);\n      expect(resp.data.consumers_conf_version).toEqual(2);\n      expect(parseInt(resp.headers[HEADER_LAST_MODIFIED])).toBeGreaterThan(0);\n      expect(resp.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n    });\n\n    it('check route \"r1\"', () =>\n      expect(client.get('/r1')).rejects.toThrow(\n        'Request failed with status code 404',\n      ));\n\n    it('check route \"r2\"', async () => {\n      const resp = await client.get('/r2');\n      expect(resp.status).toEqual(200);\n      expect(resp.data).toEqual('hello world\\n');\n    });\n\n    it('update config (delete routes)', async () => {\n      mockDigest += 1;\n      const resp = await client.put(\n        ENDPOINT,\n        {},\n        { headers: { [HEADER_DIGEST]: mockDigest } },\n      );\n      expect(resp.status).toEqual(202);\n    });\n\n    it('check route \"r2\"', () =>\n      expect(client.get('/r2')).rejects.toThrow(\n        'Request failed with status code 404',\n      ));\n\n    it('only set routes_conf_version', async () => {\n      mockDigest += 1;\n      const resp = await client.put(\n        ENDPOINT,\n        YAML.stringify({ routes_conf_version: 15 }),\n        {\n          headers: {\n            'Content-Type': 'application/yaml',\n            [HEADER_DIGEST]: mockDigest,\n          },\n        },\n      );\n      expect(resp.status).toEqual(202);\n\n      const resp_1 = await client.get(ENDPOINT);\n      expect(resp_1.status).toEqual(200);\n      expect(resp_1.data.routes_conf_version).toEqual(15);\n      expect(resp_1.data.ssls_conf_version).toEqual(4);\n      expect(resp_1.data.services_conf_version).toEqual(4);\n      expect(resp_1.data.upstreams_conf_version).toEqual(4);\n      expect(resp_1.data.consumers_conf_version).toEqual(4);\n      expect(resp_1.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n\n      mockDigest += 1;\n      const resp2 = await client.put(\n        ENDPOINT,\n        YAML.stringify({ routes_conf_version: 17 }),\n        {\n          headers: {\n            'Content-Type': 'application/yaml',\n            [HEADER_DIGEST]: mockDigest,\n          },\n        },\n      );\n      expect(resp2.status).toEqual(202);\n\n      const resp2_1 = await client.get(ENDPOINT);\n      expect(resp2_1.status).toEqual(200);\n      expect(resp2_1.data.routes_conf_version).toEqual(17);\n      expect(resp2_1.data.ssls_conf_version).toEqual(5);\n      expect(resp2_1.data.services_conf_version).toEqual(5);\n      expect(resp2_1.data.upstreams_conf_version).toEqual(5);\n      expect(resp2_1.data.consumers_conf_version).toEqual(5);\n      expect(resp2_1.headers[HEADER_DIGEST]).toEqual(`${mockDigest}`);\n    });\n\n    it('control resource changes using modifiedIndex', async () => {\n      const c1 = structuredClone(routeWithModifiedIndex);\n      c1.routes[0].modifiedIndex = 1;\n\n      const c2 = structuredClone(c1);\n      c2.routes[0].uri = '/r2';\n\n      const c3 = structuredClone(c2);\n      c3.routes[0].modifiedIndex = 2;\n\n      // Update with c1\n      mockDigest += 1;\n      const resp = await client.put(ENDPOINT, c1, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(202);\n\n      // Check route /r1 exists\n      const resp_1 = await client.get('/r1');\n      expect(resp_1.status).toEqual(200);\n\n      // Update with c2\n      mockDigest += 1;\n      const resp2 = await client.put(ENDPOINT, c2, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp2.status).toEqual(202);\n\n      // Check route /r1 exists\n      // But it is not applied because the modifiedIndex is the same as the old value\n      const resp2_2 = await client.get('/r1');\n      expect(resp2_2.status).toEqual(200);\n\n      // Check route /r2 not exists\n      const resp2_1 = await client.get('/r2').catch((err) => err.response);\n      expect(resp2_1.status).toEqual(404);\n\n      // Update with c3\n      mockDigest += 1;\n      const resp3 = await client.put(ENDPOINT, c3, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp3.status).toEqual(202);\n\n      // Check route /r1 not exists\n      const resp3_1 = await client.get('/r1').catch((err) => err.response);\n      expect(resp3_1.status).toEqual(404);\n\n      // Check route /r2 exists\n      const resp3_2 = await client.get('/r2');\n      expect(resp3_2.status).toEqual(200);\n    });\n\n    it('apply consumer with modifiedIndex', async () => {\n      mockDigest += 1;\n      const resp = await client.put(ENDPOINT, consumerWithModifiedIndex, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(202);\n\n      const resp_1 = await client\n        .get('/r1', { headers: { apikey: 'invalid-key' } })\n        .catch((err) => err.response);\n      expect(resp_1.status).toEqual(401);\n      const resp_2 = await client.get('/r1', {\n        headers: { apikey: 'jack-key' },\n      });\n      expect(resp_2.status).toEqual(200);\n\n      const updatedConsumer = structuredClone(consumerWithModifiedIndex);\n\n      // update key of key-auth plugin, but modifiedIndex is not changed\n      updatedConsumer.consumers[0].plugins['key-auth'] = {\n        key: 'jack-key-updated',\n      };\n      mockDigest += 1;\n      const resp2 = await client.put(ENDPOINT, updatedConsumer, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp2.status).toEqual(202);\n\n      const resp2_1 = await client\n        .get('/r1', { headers: { apikey: 'jack-key-updated' } })\n        .catch((err) => err.response);\n      expect(resp2_1.status).toEqual(401);\n      const resp2_2 = await client.get('/r1', {\n        headers: { apikey: 'jack-key' },\n      });\n      expect(resp2_2.status).toEqual(200);\n\n      // update key of key-auth plugin, and modifiedIndex is changed\n      updatedConsumer.consumers[0].modifiedIndex++;\n      mockDigest += 1;\n      const resp3 = await client.put(ENDPOINT, updatedConsumer, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      const resp3_1 = await client.get('/r1', {\n        headers: { apikey: 'jack-key-updated' },\n      });\n      expect(resp3_1.status).toEqual(200);\n      const resp3_2 = await client\n        .get('/r1', { headers: { apikey: 'jack-key' } })\n        .catch((err) => err.response);\n      expect(resp3_2.status).toEqual(401);\n    });\n\n    it('apply consumer with credentials', async () => {\n      mockDigest += 1;\n      const resp = await client.put(ENDPOINT, credential1, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(202);\n\n      const resp_1 = await client.get('/r1', { headers: { apikey: 'auth-a' } });\n      expect(resp_1.status).toEqual(200);\n      const resp_2 = await client.get('/r1', { headers: { apikey: 'auth-b' } });\n      expect(resp_2.status).toEqual(200);\n      const resp_3 = await client\n        .get('/r1', { headers: { apikey: 'invalid-key' } })\n        .catch((err) => err.response);\n      expect(resp_3.status).toEqual(401);\n    });\n  });\n\n  describe('Exceptions', () => {\n    const clientException = axios.create({\n      ...clientConfig,\n      validateStatus: () => true,\n    });\n\n    it('update config (without digest)', async () => {\n      const resp = await clientException.put(ENDPOINT, {\n        routes_conf_version: 100,\n      });\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({ error_msg: 'missing digest header' });\n    });\n\n    it('update config (lower conf_version)', async () => {\n      mockDigest += 1;\n      const resp = await clientException.put(\n        ENDPOINT,\n        { routes_conf_version: 100 },\n        { headers: { [HEADER_DIGEST]: mockDigest } },\n      );\n      expect(resp.status).toEqual(202);\n\n      mockDigest += 1;\n      const resp2 = await clientException.put(\n        ENDPOINT,\n        YAML.stringify(invalidConfVersionConfig1),\n        {\n          headers: {\n            'Content-Type': 'application/yaml',\n            [HEADER_DIGEST]: mockDigest,\n          },\n        },\n      );\n      expect(resp2.status).toEqual(400);\n      expect(resp2.data).toEqual({\n        error_msg: 'routes_conf_version must be greater than or equal to (100)',\n      });\n    });\n\n    it('update config (invalid conf_version)', async () => {\n      const resp = await clientException.put(\n        ENDPOINT,\n        YAML.stringify(invalidConfVersionConfig2),\n        {\n          headers: {\n            'Content-Type': 'application/yaml',\n            [HEADER_DIGEST]: mockDigest,\n          },\n        },\n      );\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'routes_conf_version must be a number',\n      });\n    });\n\n    it('update config (invalid json format)', async () => {\n      const resp = await clientException.put(ENDPOINT, '{abcd', {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg:\n          'invalid request body: Expected object key string but found invalid token at character 2',\n      });\n    });\n\n    it('update config (not compliant with jsonschema)', async () => {\n      const data = structuredClone(config1);\n      (data.routes[0].uri as unknown) = 123;\n      const resp = await clientException.put(ENDPOINT, data, {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toMatchObject({\n        error_msg:\n          'invalid routes at index 0, err: invalid configuration: property \"uri\" validation failed: wrong type: expected string, got number',\n      });\n    });\n\n    it('update config (empty request body)', async () => {\n      const resp = await clientException.put(ENDPOINT, '', {\n        headers: { [HEADER_DIGEST]: mockDigest },\n      });\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'invalid request body: empty request body',\n      });\n    });\n\n    it('update config (invalid plugin)', async () => {\n      const resp = await clientException.put(\n        ENDPOINT,\n        servicesWithUnknownPlugins,\n        {\n          headers: { [HEADER_DIGEST]: mockDigest },\n        },\n      );\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg:\n          'invalid services at index 0, err: unknown plugin [invalid-plugin]',\n      });\n      const resp2 = await clientException.put(\n        ENDPOINT,\n        routeWithUnknownPlugins,\n        {\n          headers: { [HEADER_DIGEST]: mockDigest },\n        },\n      );\n      expect(resp2.status).toEqual(400);\n      expect(resp2.data).toEqual({\n        error_msg:\n          'invalid routes at index 0, err: unknown plugin [invalid-plugin]',\n      });\n    });\n\n    it('update config (invalid upstream)', async () => {\n      const resp = await clientException.put(\n        ENDPOINT,\n        serviceWithInvalidUpstream,\n        {\n          headers: { [HEADER_DIGEST]: mockDigest },\n        },\n      );\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg:\n          'invalid services at index 0, err: invalid configuration: failed to match pattern \"^((uri|server_name|server_addr|request_uri|remote_port|remote_addr|query_string|host|hostname|mqtt_client_id)|arg_[0-9a-zA-z_-]+)$\" with \"args_invalid\"',\n      });\n\n      const resp2 = await clientException.put(\n        ENDPOINT,\n        routeWithInvalidUpstream,\n        {\n          headers: { [HEADER_DIGEST]: mockDigest },\n        },\n      );\n      expect(resp2.status).toEqual(400);\n      expect(resp2.data).toEqual({\n        error_msg:\n          'invalid routes at index 0, err: invalid configuration: failed to match pattern \"^((uri|server_name|server_addr|request_uri|remote_port|remote_addr|query_string|host|hostname|mqtt_client_id)|arg_[0-9a-zA-z_-]+)$\" with \"args_invalid\"',\n      });\n    });\n  });\n});\n\ndescribe('Validate API - Standalone', () => {\n  const client = axios.create(clientConfig);\n  client.interceptors.response.use((response) => {\n    const contentType = response.headers['content-type'] || '';\n    if (\n      contentType.includes('application/yaml') &&\n      typeof response.data === 'string' &&\n      response.config.responseType !== 'text'\n    )\n      response.data = YAML.parse(response.data);\n    return response;\n  });\n  describe('Normal', () => {\n    it('validate config (success case with json)', async () => {\n      const resp = await client.post(VALIDATE_ENDPOINT, config1);\n      expect(resp.status).toEqual(200);\n    });\n\n    it('validate config (success case with yaml)', async () => {\n      const resp = await client.post(VALIDATE_ENDPOINT, YAML.stringify(config1), {\n        headers: { 'Content-Type': 'application/yaml' },\n      });\n      expect(resp.status).toEqual(200);\n    });\n\n    it('validate config (success case with multiple resources)', async () => {\n      const multiResourceConfig = {\n        routes: [\n          {\n            id: 'r1',\n            uri: '/r1',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n            },\n          },\n          {\n            id: 'r2',\n            uri: '/r2',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n            },\n          },\n        ],\n        services: [\n          {\n            id: 's1',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n            },\n          },\n        ],\n        routes_conf_version: 1,\n        services_conf_version: 1,\n      };\n\n      const resp = await client.post(VALIDATE_ENDPOINT, multiResourceConfig);\n      expect(resp.status).toEqual(200);\n    });\n\n    it('validate config with consumer credentials', async () => {\n      const resp = await client.post(VALIDATE_ENDPOINT, credential1);\n      expect(resp.status).toEqual(200);\n    });\n\n    it('validate config does not persist changes', async () => {\n      // First validate a configuration\n      const validateResp = await client.post(VALIDATE_ENDPOINT, config1);\n      expect(validateResp.status).toEqual(200);\n\n      // Then check that the configuration was not persisted\n      const getResp = await client.get(ENDPOINT);\n      expect(getResp.data.routes).toBeUndefined();\n    });\n  });\n  describe('Exceptions', () => {\n    const clientException = axios.create({\n      ...clientConfig,\n      validateStatus: () => true,\n    });\n    it('validate config (duplicate route id)', async () => {\n      const duplicateConfig = {\n        routes: [\n          {\n            id: 'r1',\n            uri: '/r1',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n            },\n          },\n          {\n            id: 'r1', // Duplicate ID\n            uri: '/r2',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n            },\n          },],\n      };\n\n      const resp = await clientException.post(VALIDATE_ENDPOINT, duplicateConfig);\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'routes',\n            error: expect.stringContaining('found duplicate id r1 in routes'),\n          }),\n        ]),\n      });\n    });\n\n    it('validate config (invalid route configuration)', async () => {\n      const invalidConfig = {\n        routes: [\n          {\n            id: 'r1',\n            uri: '/r1',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n              // Add an invalid field that should definitely fail validation\n              invalid_field: 'this_should_fail'\n            },\n          },\n        ],\n      };\n\n      const resp = await clientException.post(VALIDATE_ENDPOINT, invalidConfig);\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'routes',\n            error: expect.stringContaining('invalid routes at index 0'),\n          }),\n        ]),\n      });\n    });\n\n    it('validate config (invalid version number)', async () => {\n      const invalidVersionConfig = {\n        routes: [\n          {\n            id: 'r1',\n            uri: '/r1',\n            upstream: {\n              nodes: { '127.0.0.1:1980': 1 },\n              type: 'roundrobin',\n            },\n          },\n        ],\n        routes_conf_version: 'not_a_number', // Invalid version type\n      };\n\n      const resp = await clientException.post(VALIDATE_ENDPOINT, invalidVersionConfig);\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'routes',\n            error: expect.stringContaining('routes_conf_version must be a number'),\n          }),\n        ]),\n      });\n    });\n\n    it('validate config (empty body)', async () => {\n      const resp = await clientException.post(VALIDATE_ENDPOINT, '');\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'invalid request body: empty request body',\n      });\n    });\n\n    it('validate config (invalid YAML)', async () => {\n      const resp = await clientException.post(VALIDATE_ENDPOINT, 'invalid: yaml: [', {\n        headers: { 'Content-Type': 'application/yaml' },\n      });\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: expect.stringContaining('invalid request body:'),\n      });\n    });\n\n    it('validate config (duplicate consumer username)', async () => {\n      const duplicateConsumerConfig = {\n        consumers: [\n          {\n            username: 'consumer1',\n            plugins: {\n              'key-auth': {\n                key: 'consumer1',\n              },\n            },\n          },\n          {\n            username: 'consumer1', // Duplicate username\n            plugins: {\n              'key-auth': {\n                key: 'consumer1',\n              },\n            },\n          },\n        ],\n      };\n\n      const resp = await clientException.post(VALIDATE_ENDPOINT, duplicateConsumerConfig);\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'consumers',\n            error: expect.stringContaining('found duplicate username consumer1 in consumers'),\n          }),\n        ]),\n      });\n    });\n\n    it('validate config (duplicate consumer credential id)', async () => {\n      const duplicateCredentialConfig = {\n        consumers: [\n          {\n            username: 'john_1',\n          },\n          {\n            id: 'john_1/credentials/john-a',\n            plugins: {\n              'key-auth': {\n                key: 'auth-a',\n              },\n            },\n          },\n          {\n            id: 'john_1/credentials/john-a', // Duplicate credential ID\n            plugins: {\n              'key-auth': {\n                key: 'auth-a',\n              },\n            },\n          },\n        ],\n      };\n\n      const resp = await clientException.post(VALIDATE_ENDPOINT, duplicateCredentialConfig);\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'consumers',\n            error: expect.stringContaining('found duplicate credential id john_1/credentials/john-a in consumers'),\n          }),\n        ]),\n      });\n    });\n\n    it('validate config (invalid plugin)', async () => {\n      const resp = await clientException.post(\n        VALIDATE_ENDPOINT,\n        routeWithUnknownPlugins,\n      );\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'routes',\n            error: expect.stringContaining('unknown plugin [invalid-plugin]'),\n          }),\n        ]),\n      });\n    });\n\n    it('validate config (invalid upstream)', async () => {\n      const resp = await clientException.post(\n        VALIDATE_ENDPOINT,\n        routeWithInvalidUpstream,\n      );\n      expect(resp.status).toEqual(400);\n      expect(resp.data).toEqual({\n        error_msg: 'Configuration validation failed',\n        errors: expect.arrayContaining([\n          expect.objectContaining({\n            resource_type: 'routes',\n            error: expect.stringContaining('failed to match pattern'),\n          }),\n        ]),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "t/admin/standalone.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    # restarts cause the memory cache to be emptied, don't do this\n    $ENV{TEST_NGINX_FORCE_RESTART_ON_TEST} = 0;\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nuse_hup();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'EOF');\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: yaml\n    admin:\n        admin_key:\n            - name: admin\n              key: edd1c9f034335f136f87ad84b625c8f1\n              role: admin\nEOF\n    }\n\n    $block->set_value(\"stream_enable\", 1);\n\n    if (!defined $block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: test\n--- timeout: 15\n--- max_size: 204800\n--- exec\ncd t && pnpm test admin/standalone.spec.ts 2>&1\n--- no_error_log\nfailed to execute the script with status\n--- response_body eval\nqr/PASS admin\\/standalone.spec.ts/\n\n\n\n=== TEST 2: init conf_version\n--- config\n    location /t {} # force the worker to restart by changing the configuration\n--- request\nPUT /apisix/admin/configs\n{\n    \"consumer_groups_conf_version\": 1000,\n    \"consumers_conf_version\": 1000,\n    \"global_rules_conf_version\": 1000,\n    \"plugin_configs_conf_version\": 1000,\n    \"plugin_metadata_conf_version\": 1000,\n    \"protos_conf_version\": 1000,\n    \"routes_conf_version\": 1000,\n    \"stream_routes_conf_version\": 1000,\n    \"secrets_conf_version\": 1000,\n    \"services_conf_version\": 1000,\n    \"ssls_conf_version\": 1000,\n    \"upstreams_conf_version\": 1000\n}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t2\n--- error_code: 202\n\n\n\n=== TEST 3: get config\n--- config\n    location /config {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body = t.test('/apisix/admin/configs',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"consumer_groups_conf_version\": 1000,\n                    \"consumers_conf_version\": 1000,\n                    \"global_rules_conf_version\": 1000,\n                    \"plugin_configs_conf_version\": 1000,\n                    \"plugin_metadata_conf_version\": 1000,\n                    \"protos_conf_version\": 1000,\n                    \"routes_conf_version\": 1000,\n                    \"stream_routes_conf_version\": 1000,\n                    \"secrets_conf_version\": 1000,\n                    \"services_conf_version\": 1000,\n                    \"ssls_conf_version\": 1000,\n                    \"upstreams_conf_version\": 1000\n                }]],\n                {\n                    [\"X-API-KEY\"] = \"edd1c9f034335f136f87ad84b625c8f1\"\n                }\n            )\n            ngx.say(body)\n        }\n    }\n--- request\nGET /config\n--- response_body\npassed\n\n\n\n=== TEST 4: configure route\n--- config\n    location /t {} # force the worker to restart by changing the configuration\n--- request\nPUT /apisix/admin/configs\n{\"routes\":[{\"id\":\"r1\",\"uri\":\"/r1\",\"upstream\":{\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"},\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}]}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t4\n--- error_code: 202\n\n\n\n=== TEST 5: test route\n--- config\n    location /t1 {}\n--- request\nGET /r1\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 6: remove route\n--- config\n    location /t2 {}\n--- request\nPUT /apisix/admin/configs\n{}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t6\n--- error_code: 202\n\n\n\n=== TEST 7: test non-exist route\n--- config\n    location /t3 {}\n--- request\nGET /r1\n--- error_code: 404\n\n\n\n=== TEST 8: route references upstream, but only updates the route\n--- config\n    location /t6 {}\n--- pipelined_requests eval\n[\n    \"PUT /apisix/admin/configs\\n\" . \"{\\\"routes_conf_version\\\":1060,\\\"upstreams_conf_version\\\":1060,\\\"routes\\\":[{\\\"id\\\":\\\"r1\\\",\\\"uri\\\":\\\"/r1\\\",\\\"upstream_id\\\":\\\"u1\\\",\\\"plugins\\\":{\\\"proxy-rewrite\\\":{\\\"uri\\\":\\\"/hello\\\"}}}],\\\"upstreams\\\":[{\\\"id\\\":\\\"u1\\\",\\\"nodes\\\":{\\\"127.0.0.1:1980\\\":1},\\\"type\\\":\\\"roundrobin\\\"}]}\",\n    \"PUT /apisix/admin/configs\\n\" . \"{\\\"routes_conf_version\\\":1062,\\\"upstreams_conf_version\\\":1060,\\\"routes\\\":[{\\\"id\\\":\\\"r1\\\",\\\"uri\\\":\\\"/r2\\\",\\\"upstream_id\\\":\\\"u1\\\",\\\"plugins\\\":{\\\"proxy-rewrite\\\":{\\\"uri\\\":\\\"/hello\\\"}}}],\\\"upstreams\\\":[{\\\"id\\\":\\\"u1\\\",\\\"nodes\\\":{\\\"127.0.0.1:1980\\\":1},\\\"type\\\":\\\"roundrobin\\\"}]}\"\n]\n--- more_headers eval\n[\n    \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\\n\" . \"X-Digest: t8-1\",\n    \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\\n\" . \"x-apisix-conf-version-routes: 100\\n\" . \"X-Digest: t8-2\",\n]\n--- error_code eval\n[202, 202]\n\n\n\n=== TEST 9: hit r2\n--- config\n    location /t3 {}\n--- pipelined_requests eval\n[\"GET /r1\", \"GET /r2\"]\n--- error_code eval\n[404, 200]\n\n\n\n=== TEST 10: routes_conf_version < 1062 is not allowed\n--- config\n    location /t {}\n--- request\nPUT /apisix/admin/configs\n{\"routes_conf_version\":1,\"routes\":[{\"id\":\"r1\",\"uri\":\"/r2\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}]}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nx-apisix-conf-version-routes: 100\nX-Digest: t10\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"routes_conf_version must be greater than or equal to (1062)\"}\n\n\n\n=== TEST 11: duplicate route id found\n--- config\n    location /t11 {}\n--- request\nPUT /apisix/admin/configs\n{\"routes_conf_version\":1063,\"routes\":[{\"id\":\"r1\",\"uri\":\"/r2\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}},\n{\"id\":\"r1\",\"uri\":\"/r2\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}]}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t11\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"found duplicate id r1 in routes\"}\n\n\n\n=== TEST 12: duplicate consumer username found\n--- config\n    location /t12 {}\n--- request\nPUT /apisix/admin/configs\n{\"consumers_conf_version\":1064,\"consumers\":[{\"username\":\"consumer1\",\"plugins\":{\"key-auth\":{\"key\":\"consumer1\"}}},\n{\"username\":\"consumer1\",\"plugins\":{\"key-auth\":{\"key\":\"consumer1\"}}}]}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t12\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"found duplicate username consumer1 in consumers\"}\n\n\n\n=== TEST 13: duplicate consumer credential id found\n--- config\n    location /t13 {}\n--- request\nPUT /apisix/admin/configs\n{\"consumers_conf_version\":1065,\"consumers\":[\n    {\"username\": \"john_1\"},\n    {\"id\":\"john_1/credentials/john-a\",\"plugins\":{\"key-auth\":{\"key\":\"auth-a\"}}},\n    {\"id\":\"john_1/credentials/john-a\",\"plugins\":{\"key-auth\":{\"key\":\"auth-a\"}}}\n]}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t13\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"found duplicate credential id john_1/credentials/john-a in consumers\"}\n\n\n\n=== TEST 14: configure stream route\n--- request\nPUT /apisix/admin/configs\n{\"stream_routes\":[{\"modifiedIndex\": 1, \"server_addr\":\"127.0.0.1\",\"server_port\":1985,\"id\":1,\"upstream\":{\"nodes\":{\"127.0.0.1:1995\":1},\"type\":\"roundrobin\"}}]}\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\nX-Digest: t14\n--- error_code: 202\n\n\n\n=== TEST 15: hit stream route\n--- config\nlocation /stream_request {\n    content_by_lua_block {\n        ngx.sleep(1)  -- wait for the stream route to take effect\n\n        local tcp_request = function(host, port)\n            local sock, err = ngx.socket.tcp()\n            assert(sock, err)\n\n            local ok, err = sock:connect(host, port)\n            if not ok then\n                ngx.say(\"connect to stream server error: \", err)\n                return\n            end\n            local bytes, err = sock:send(\"mmm\")\n            if not bytes then\n                ngx.say(\"send stream request error: \", err)\n                return\n            end\n\n            local data, err = sock:receive(\"*a\")\n            if not data then\n                sock:close()\n                ngx.say(\"receive stream response error: \", err)\n                return\n            end\n            sock:close()\n            ngx.print(data)\n        end\n\n        tcp_request(\"127.0.0.1\", 1985)\n\n        -- update the stream route in runtime to confirm the new stream route takes effect\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/configs',\n            ngx.HTTP_PUT,\n            [[{\n              \"stream_routes\": [\n                {\n                  \"modifiedIndex\": 2,\n                  \"server_addr\": \"127.0.0.2\",\n                  \"server_port\": 1985,\n                  \"id\": 1,\n                  \"upstream\": {\n                    \"nodes\": {\n                      \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                  }\n                }\n              ]\n            }]], nil, { [\"X-API-KEY\"] = \"edd1c9f034335f136f87ad84b625c8f1\", [\"X-Digest\"] = \"t15\"}\n        )\n        if code ~= 202 then\n            ngx.print(\"failed to update stream route, code: \", code, \", body: \", body)\n            return\n        end\n\n        ngx.sleep(1)\n\n        tcp_request(\"127.0.0.1\", 1985)\n        tcp_request(\"127.0.0.2\", 1985)\n    }\n}\n--- request\nGET /stream_request\n--- response_body\nhello world\nreceive stream response error: connection reset by peer\nhello world\n"
  },
  {
    "path": "t/admin/stream-routes-disable.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\nuse Cwd qw(cwd);\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n_EOC_\n\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(disabled stream model)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n"
  },
  {
    "path": "t/admin/stream-routes-subordinate.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: redis\n    - name: dubbo\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create superior route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"protocol\": {\"name\": \"redis\"},\n                    \"upstream\": {\n                        \"nodes\": {\"127.0.0.1:6379\": 1},\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create subordinate route with valid superior_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"protocol\": {\n                        \"name\": \"redis\",\n                        \"superior_id\": \"1\"\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\"127.0.0.1:6380\": 1},\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: superior_id not exist (should fail)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/stream_routes/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"protocol\": {\"name\": \"redis\", \"superior_id\": \"999\"},\n                    \"upstream\": {\n                        \"nodes\": {\"127.0.0.1:6381\": 1},\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code ~= 400 then\n                ngx.say(\"failed: expected 400, got \", code)\n                return\n            end\n            local data = json.decode(body)\n            if not data or not data.error_msg then\n                ngx.say(\"failed: unexpected body: \", body)\n                return\n            end\n            if not string.find(data.error_msg, \"failed to fetch stream routes[999]\", 1, true) then\n                ngx.say(\"failed: unexpected body: \", body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: protocol mismatch (should fail)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code = t('/apisix/admin/stream_routes/4',\n                ngx.HTTP_PUT,\n                [[{\n                    \"protocol\": {\"name\": \"dubbo\"},\n                    \"upstream\": {\n                        \"nodes\": {\"127.0.0.1:20880\": 1},\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            local code, body = t('/apisix/admin/stream_routes/5',\n                ngx.HTTP_PUT,\n                [[{\n                    \"protocol\": {\"name\": \"redis\", \"superior_id\": \"4\"},\n                    \"upstream\": {\n                        \"nodes\": {\"127.0.0.1:6382\": 1},\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code ~= 400 then\n                ngx.say(\"failed: expected 400, got \", code)\n                return\n            end\n            local data = json.decode(body)\n            if not data or not data.error_msg then\n                ngx.say(\"failed: unexpected body: \", body)\n                return\n            end\n            if not string.find(data.error_msg, \"protocol mismatch\", 1, true) then\n                ngx.say(\"failed: unexpected body: \", body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: delete superior route being referenced (should fail)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_DELETE\n            )\n            if code ~= 400 then\n                ngx.say(\"failed: expected 400, got \", code)\n                return\n            end\n            local data = json.decode(body)\n            if not data or not data.error_msg then\n                ngx.say(\"failed: unexpected body: \", body)\n                return\n            end\n            if not string.find(data.error_msg, \"can not delete this stream route\", 1, true) then\n                ngx.say(\"failed: unexpected body: \", body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: delete subordinate route first\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/2',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: now delete superior route should succeed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/stream-routes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"desc\": \"test-desc\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"remote_addr\": \"127.0.0.1\",\n                        \"desc\": \"test-desc\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new route\"\n                    },\n                    \"key\": \"/apisix/stream_routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/stream_routes/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"remote_addr\": \"127.0.0.1\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new route\"\n                    },\n                    \"key\": \"/apisix/stream_routes/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/stream_routes/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: post route + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, message, res = t('/apisix/admin/stream_routes',\n                ngx.HTTP_POST,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"remote_addr\": \"127.0.0.1\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:8080\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"new route\"\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local id = string.sub(res.key, #\"/apisix/stream_routes/\" + 1)\n\n            local ret = assert(etcd.get('/stream_routes/' .. id))\n            local create_time = ret.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = ret.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n            id = ret.body.node.value.id\n            assert(id ~= nil, \"id is nil\")\n\n            code, message = t('/apisix/admin/stream_routes/' .. id, ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 5: set route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1980,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: set route with server_addr and server_port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_addr\": \"127.0.0.1\",\n                    \"server_port\": 1982,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1980,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/stream_routes/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 8: string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: string id(delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/a-b-c-ABC_0123', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: invalid string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/*invalid',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 11: not unwanted data, POST\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/stream_routes',\n                 ngx.HTTP_POST,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            -- clean data\n            local id = string.sub(res.key, #\"/apisix/stream_routes/\" + 1)\n            local code, message = t('/apisix/admin/stream_routes/' .. id,\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            assert(res.key ~= nil)\n            res.key = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.id ~= nil)\n            res.value.id = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"value\":{\"remote_addr\":\"127.0.0.1\",\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n--- request\nGET /t\n\n\n\n=== TEST 12: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/stream_routes/1\",\"value\":{\"id\":\"1\",\"remote_addr\":\"127.0.0.1\",\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n--- request\nGET /t\n\n\n\n=== TEST 13: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_GET\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/stream_routes/1\",\"value\":{\"id\":\"1\",\"remote_addr\":\"127.0.0.1\",\"upstream\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}}\n--- request\nGET /t\n\n\n\n=== TEST 14: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/stream_routes/1\"}\n--- request\nGET /t\n\n\n\n=== TEST 15: set route with unknown plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"plugins\": {\n                        \"mqttt-proxy\": {\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"unknown plugin [mqttt-proxy]\"}\n\n\n\n=== TEST 16: validate protocol\n--- extra_yaml_config\nxrpc:\n  protocols:\n    - name: pingpong\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {input = {\n                    name = \"xxx\",\n                }},\n                {input = {\n                    name = \"pingpong\",\n                }},\n                {input = {\n                    name = \"pingpong\",\n                    conf = {\n                        faults = \"a\",\n                    }\n                }},\n            }) do\n                local code, body = t('/apisix/admin/stream_routes/1',\n                    ngx.HTTP_PUT,\n                    {\n                        protocol = case.input,\n                        upstream = {\n                            nodes = {\n                                [\"127.0.0.1:8080\"] = 1\n                            },\n                            type = \"roundrobin\"\n                        }\n                    }\n                )\n                if code > 300 then\n                    ngx.print(body)\n                else\n                    ngx.say(body)\n                end\n            end\n\n            -- Clean up the stream route to avoid interfering with subsequent tests\n            local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_DELETE)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"error_msg\":\"unknown protocol [xxx]\"}\npassed\n{\"error_msg\":\"property \\\"faults\\\" validation failed: wrong type: expected array, got string\"}\n\n\n\n=== TEST 17: set route with remote_addr and server_addr in IPV6\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"::1\",\n                    \"server_addr\": \"::1\",\n                    \"server_port\": 1982,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1980,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/token.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\nuse Cwd qw(cwd);\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\ndeployment:\n    admin:\n        admin_key:\n            - name: admin\n              role: admin\n              key: edd1c9f034335f136f87ad84b625c8f1\n\napisix:\n  node_listen: 1984\n_EOC_\n\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route without token\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").req_self_with_http\n            local res, err = t('/apisix/admin/routes/1',\n                \"PUT\",\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            ngx.status = res.status\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 401\n\n\n\n=== TEST 2: set route with wrong token\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").req_self_with_http\n            local res, err = t(\n                '/apisix/admin/routes/1',\n                \"PUT\",\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]],\n                {apikey = \"wrong_key\"}\n                )\n\n            ngx.status = res.status\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 401\n\n\n\n=== TEST 3: set route with correct token\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").req_self_with_http\n            local res, err = t(\n                '/apisix/admin/routes/1',\n                \"PUT\",\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]],\n                {x_api_key = \"edd1c9f034335f136f87ad84b625c8f1\"}\n                )\n\n            if res.status > 299 then\n                ngx.status = res.status\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 4: get plugins name\n--- request\nGET /apisix/admin/plugins/list\n--- error_code: 401\n\n\n\n=== TEST 5: reload plugins\n--- request\nPUT /apisix/admin/plugins/reload\n--- error_code: 401\n\n\n\n=== TEST 6: reload plugins with api key(arguments)\n--- request\nPUT /apisix/admin/plugins/reload?api_key=edd1c9f034335f136f87ad84b625c8f1\n--- error_code: 200\n\n\n\n=== TEST 7: reload plugins with api key(cookie)\n--- request\nPUT /apisix/admin/plugins/reload\n--- more_headers\nX-API-KEY: edd1c9f034335f136f87ad84b625c8f1\n--- error_code: 200\n\n\n\n=== TEST 8: reload plugins with api key(viewer role)\n--- request\nPUT /apisix/admin/plugins/reload?api_key=4054f7cf07e344346cd3f287985e76a2\n--- error_code: 401\n\n\n\n=== TEST 9: fetch with api key(viewer role)\n--- request\nGET /apisix/admin/routes??api_key=4054f7cf07e344346cd3f287985e76a2\n--- error_code: 401\n"
  },
  {
    "path": "t/admin/upstream-array-nodes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": [{\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 8080,\n                        \"weight\": 1\n                    }],\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8080,\n                                \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8080,\n                                \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: delete upstream(id: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/upstreams/not_found', ngx.HTTP_DELETE)\n\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 5: push upstream + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"nodes\": [{\n                         \"host\": \"127.0.0.1\",\n                         \"port\": 8080,\n                         \"weight\": 1\n                    }],\n                    \"type\": \"roundrobin\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8080,\n                            \"weight\": 1\n                        }],\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local id = string.sub(res.key, #\"/apisix/upstreams/\" + 1)\n            code, message = t('/apisix/admin/upstreams/' .. id, ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: empty nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [],\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: refer to empty nodes upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream_id\": \"1\",\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit empty nodes upstream\n--- request\nGET /index.html\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 9: additional properties is invalid\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": [{\n                          \"host\": \"127.0.0.1\",\n                          \"port\": 8080,\n                          \"weight\": 1\n                    }],\n                    \"type\": \"roundrobin\",\n                    \"_service_name\": \"xyz\",\n                    \"_discovery_type\": \"nacos\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid configuration: additional properties forbidden, found .*\"\\}/\n\n\n\n=== TEST 10: invalid weight of node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": [{\n                          \"host\": \"127.0.0.1\",\n                          \"port\": 8080,\n                          \"weight\": \"1\"\n                    }],\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 11: invalid weight of node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": [{\n                          \"host\": \"127.0.0.1\",\n                          \"port\": 8080,\n                          \"weight\": -100\n                    }],\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 12: invalid port of node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": [{\n                          \"host\": \"127.0.0.1\",\n                          \"port\": 0,\n                          \"weight\": 1\n                    }],\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 13: invalid host of node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": [{\n                          \"host\": \"127.#.%.1\",\n                          \"port\": 8080,\n                          \"weight\": 1\n                    }],\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 14: nodes host include ipv6 addr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": [\n                            {\n                                \"host\":\"[::1]\",\n                                \"port\":8082,\n                                \"weight\":1\n                            }\n                        ],\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/admin/upstream-force-delete.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 201\n--- response_body\npassed\n\n\n\n=== TEST 2: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream_id\": 1,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: delete upstream(wrong header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1?force=anyvalue',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n\n\n\n=== TEST 4: delete upstream(without force delete header)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n\n\n\n=== TEST 5: delete upstream(force delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1?force=true',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body chomp\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set upstream (use an id can't be referred by other route\nso that we can delete it later)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/upstreams/admin_up',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/admin_up\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/upstreams/admin_up'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: get upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/admin_up',\n                 ngx.HTTP_GET,\n                 nil,\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/admin_up\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/admin_up', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 4: delete upstream(id: not_found)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/upstreams/not_found', ngx.HTTP_DELETE)\n\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 5: push upstream + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, message, res = t('/apisix/admin/upstreams',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"[push] code: \", code, \" message: \", message)\n\n            local id = string.sub(res.key, #\"/apisix/upstreams/\" + 1)\n            local res = assert(etcd.get('/upstreams/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n            code, message = t('/apisix/admin/upstreams/' .. id, ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[push] code: 200 message: passed\n[delete] code: 200 message: passed\n\n\n\n=== TEST 6: invalid upstream id in uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/invalid_id$',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.exit(code)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 7: different id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 3,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong upstream id\"}\n\n\n\n=== TEST 8: id in the rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": \"1\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: integer id less than 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": -100,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 10: invalid upstream id: string value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": \"invalid_id$\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 11: additional properties is invalid\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"_service_name\": \"xyz\",\n                    \"_discovery_type\": \"nacos\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid configuration: additional properties forbidden, found .*\"\\}/\n\n\n\n=== TEST 12: set upstream(type: chash)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"key\": \"remote_addr\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"key\": \"remote_addr\",\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"chash\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: unknown type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"unknown\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- response_body chomp\npassed\n\n\n\n=== TEST 14: invalid weight of node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": 1,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": \"1\"\n                    },\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 15: invalid weight of node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": 1,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": -100\n                    },\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 16: set upstream (missing key)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"missing key\"}\n\n\n\n=== TEST 17: wrong upstream id, do not need it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_POST,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong upstream id, do not need it\"}\n\n\n\n=== TEST 18: wrong upstream id, do not need it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams',\n                ngx.HTTP_POST,\n                [[{\n                    \"id\": 1,\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"wrong upstream id, do not need it\"}\n\n\n\n=== TEST 19: client_cert/client_key and client_cert_id cannot appear at the same time\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert_id = 1,\n                    client_cert = ssl_cert,\n                    client_key = ssl_key\n                }\n            }\n            local code, body = t.test('/apisix/admin/upstreams',\n                ngx.HTTP_POST,\n                core.json.encode(data)\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/{\"error_msg\":\"invalid configuration: property \\\\\\\"tls\\\\\\\" validation failed: failed to validate dependent schema for \\\\\\\"client_cert|client_key\\\\\\\": value wasn't supposed to match schema\"}/\n\n\n\n=== TEST 20: tls.client_cert_id does not exist\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert_id = 9999999\n                }\n            }\n            local code, body = t.test('/apisix/admin/upstreams',\n                ngx.HTTP_POST,\n                core.json.encode(data)\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch ssl info by ssl id [9999999], response code: 404\"}\n\n\n\n=== TEST 21: tls.client_cert_id exist with wrong ssl type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                sni = \"test.com\",\n                cert = ssl_cert,\n                key = ssl_key\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1\n                    },\n                    tls = {\n                        client_cert_id = 1\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to fetch ssl info by ssl id [1], wrong ssl type\"}\n\n\n\n=== TEST 22: type with default vale\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/upstreams/admin_up',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"desc\": \"new upstream\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"desc\": \"new upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/admin_up\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: verify wrong Ipv6\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                nodes = {\n                    {host = \"::1\", port = 80, weight = 1},\n                },\n                type = \"roundrobin\"\n            }\n            local code, body = t.test('/apisix/admin/upstreams',\n                ngx.HTTP_POST,\n                core.json.encode(data)\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: IPv6 address must be enclosed with '[' and ']'\"}\n"
  },
  {
    "path": "t/admin/upstream2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: not unwanted data, POST\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams',\n                 ngx.HTTP_POST,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.key ~= nil)\n            res.key = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            assert(res.value.id ~= nil)\n            res.value.id = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"value\":{\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}\n\n\n\n=== TEST 2: not unwanted data, PUT\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams/unwanted',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/upstreams/unwanted\",\"value\":{\"id\":\"unwanted\",\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}\n\n\n\n=== TEST 3: not unwanted data, PATCH\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams/unwanted',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/upstreams/unwanted\",\"value\":{\"id\":\"unwanted\",\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}\n\n\n\n=== TEST 4: not unwanted data, GET\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams/unwanted', ngx.HTTP_GET)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            assert(res.createdIndex ~= nil)\n            res.createdIndex = nil\n            assert(res.modifiedIndex ~= nil)\n            res.modifiedIndex = nil\n            assert(res.value.create_time ~= nil)\n            res.value.create_time = nil\n            assert(res.value.update_time ~= nil)\n            res.value.update_time = nil\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"key\":\"/apisix/upstreams/unwanted\",\"value\":{\"id\":\"unwanted\",\"nodes\":{\"127.0.0.1:8080\":1},\"type\":\"roundrobin\"}}\n\n\n\n=== TEST 5: not unwanted data, DELETE\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, message, res = t('/apisix/admin/upstreams/unwanted', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"deleted\":\"1\",\"key\":\"/apisix/upstreams/unwanted\"}\n\n\n\n=== TEST 6: empty nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {},\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: refer to empty nodes upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream_id\": \"1\",\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit empty nodes upstream\n--- request\nGET /index.html\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 9: upstream timeouts equal to zero\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"timeout\": {\n                        \"connect\": 0,\n                        \"send\": 0,\n                        \"read\": 0\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body_like eval\nqr/{\"error_msg\":\"invalid configuration: property \\\\\\\"timeout\\\\\\\" validation failed: property \\\\\\\"(connect|send|read)\\\\\\\" validation failed: expected 0 to be greater than 0\"}/\n"
  },
  {
    "path": "t/admin/upstream3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: list empty resources\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/apisix/admin/upstreams',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(json.encode(res))\n        }\n    }\n--- response_body\n{\"list\":[],\"total\":0}\n\n\n\n=== TEST 2: retry_timeout is -1 (INVALID)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1,\n                        \"127.0.0.1:8090\": 1\n                    },\n                    \"retry_timeout\": -1,\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"retry_timeout\\\" validation failed: expected -1 to be at least 0\"}\n\n\n\n=== TEST 3: provide upstream for patch\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1,\n                        \"127.0.0.1:8090\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n\n\n\n=== TEST 4: patch upstream(whole)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n\n            local id = 1\n            local res = assert(etcd.get('/upstreams/' .. id))\n            local prev_create_time = res.body.node.value.create_time\n            local prev_update_time = res.body.node.value.update_time\n            ngx.sleep(1)\n\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            local res = assert(etcd.get('/upstreams/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: patch upstream(new desc)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"desc\": \"new 21 upstream\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new 21 upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: patch upstream(new nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8081\": 3,\n                        \"127.0.0.1:8082\": 4\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1,\n                            \"127.0.0.1:8081\": 3,\n                            \"127.0.0.1:8082\": 4\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new 21 upstream\"\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: patch upstream(weight is 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8081\": 3,\n                        \"127.0.0.1:8082\": 0\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8081\": 3,\n                            \"127.0.0.1:8082\": 0\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new 21 upstream\"\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: patch upstream(whole - sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1/',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream 24\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream 24\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: patch upstream(new desc - sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1/desc',\n                ngx.HTTP_PATCH,\n                '\"new 25 upstream\"',\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new 25 upstream\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: patch upstream(new nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1/nodes',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"127.0.0.6:8081\": 3,\n                    \"127.0.0.7:8082\": 4\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.6:8081\": 3,\n                            \"127.0.0.7:8082\": 4\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new 25 upstream\"\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: patch upstream(weight is 0 - sub path)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1/nodes',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"127.0.0.7:8081\": 0,\n                    \"127.0.0.8:8082\": 4\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.7:8081\": 0,\n                            \"127.0.0.8:8082\": 4\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new 25 upstream\"\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: set upstream(type: chash)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"server_name\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13:  wrong upstream key, hash_on default vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1,\n                        \"127.0.0.1:8081\": 2\n                    },\n                    \"type\": \"chash\",\n                    \"key\": \"not_support\",\n                    \"desc\": \"new upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: failed to match pattern \\\"^((uri|server_name|server_addr|request_uri|remote_port|remote_addr|query_string|host|hostname|mqtt_client_id)|arg_[0-9a-zA-z_-]+)$\\\" with \\\"not_support\\\"\"}\n\n\n\n=== TEST 14: set upstream with args(type: chash)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"arg_device_id\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: set upstream(type: chash)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"server_name\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16:  wrong upstream key, hash_on default vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1,\n                        \"127.0.0.1:8081\": 2\n                    },\n                    \"type\": \"chash\",\n                    \"key\": \"not_support\",\n                    \"desc\": \"new upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: failed to match pattern \\\"^((uri|server_name|server_addr|request_uri|remote_port|remote_addr|query_string|host|hostname|mqtt_client_id)|arg_[0-9a-zA-z_-]+)$\\\" with \\\"not_support\\\"\"}\n\n\n\n=== TEST 17: set upstream with args(type: chash)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"key\": \"arg_device_id\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: type chash, hash_on: vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"arg_device_id\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"vars\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: type chash, hash_on: header, header name with '_', underscores_in_headers on\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"custom_header\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"header\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: type chash, hash_on: header, header name with invalid character\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"$#^@\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"header\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: failed to match pattern \\\"^[a-zA-Z0-9-_]+$\\\" with \\\"$#^@\\\"\"}\n\n\n\n=== TEST 21: type chash, hash_on: cookie\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"custom_cookie\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"cookie\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: type chash, hash_on: cookie, cookie name with invalid character\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"$#^@abc\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"cookie\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: failed to match pattern \\\"^[a-zA-Z0-9-_]+$\\\" with \\\"$#^@abc\\\"\"}\n\n\n\n=== TEST 23: type chash, hash_on: consumer, do not need upstream key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"consumer\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 24: type chash, hash_on: consumer, set key but invalid\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"consumer\",\n                    \"key\": \"invalid-key\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: type chash, invalid hash_on type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"key\": \"dsadas\",\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"chash\",\n                    \"hash_on\": \"aabbcc\",\n                    \"desc\": \"new chash upstream\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"hash_on\\\" validation failed: matches none of the enum values\"}\n"
  },
  {
    "path": "t/admin/upstream4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1 + name: test name)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"name\": \"test upstream name\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"name\": \"test upstream name\"\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: string id(delete)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/a-b-c-ABC_0123', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: invalid string id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/*invalid',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"id\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 5: retries is 0\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1,\n                        \"127.0.0.1:8090\": 1\n                    },\n                    \"retries\": 0,\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: retries is -1 (INVALID)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/a-b-c-ABC_0123',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1,\n                        \"127.0.0.1:8090\": 1\n                    },\n                    \"retries\": -1,\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"retries\\\" validation failed: expected -1 to be at least 0\"}\n\n\n\n=== TEST 7: invalid route: empty `upstream_host` when `pass_host` is `rewrite`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"apisix.com:8080\": 1,\n                        \"test.com:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"pass_host\": \"rewrite\",\n                    \"upstream_host\": \"\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n\n\n\n=== TEST 8: set upstream(with labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"labels\": {\n                        \"build\":\"16\",\n                        \"env\":\"production\",\n                        \"version\":\"v2\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"labels\": {\n                            \"build\":\"16\",\n                            \"env\":\"production\",\n                            \"version\":\"v2\"\n                        }\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: get upstream(with labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"labels\": {\n                            \"version\":\"v2\",\n                            \"build\":\"16\",\n                            \"env\":\"production\"\n                        }\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: patch upstream(only labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"labels\": {\n                        \"build\": \"17\"\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"labels\": {\n                            \"version\":\"v2\",\n                            \"build\":\"17\",\n                            \"env\":\"production\"\n                        }\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: invalid format of label value: set upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"labels\": {\n                        \"env\": [\"production\", \"release\"]\n                    }\n                }]]\n            )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"labels\\\" validation failed: failed to validate env (matching \\\".*\\\"): wrong type: expected string, got table\"}\n\n\n\n=== TEST 12: patch upstream(whole, create_time)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\",\n                    \"create_time\": 1705252779\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\",\n                        \"create_time\": 1705252779\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            if code >= 300 then\n                return\n            end\n\n            local res = assert(etcd.get('/upstreams/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time == 1705252779, \"create_time mismatched\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: patch upstream(whole, update_time)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\",\n                    \"update_time\": 1705252779\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\",\n                        \"create_time\": 1705252779\n                    },\n                    \"key\": \"/apisix/upstreams/1\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n\n            if code >= 300 then\n                return\n            end\n\n            local res = assert(etcd.get('/upstreams/1'))\n            local update_time = res.body.node.value.update_time\n            assert(update_time == 1705252779, \"update_time mismatched\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: create upstream with create_time and update_time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/up_create_update_time',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"create_time\": 1602883670,\n                    \"update_time\": 1602893670\n                }]],\n                [[{\n                    \"value\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"create_time\": 1602883670,\n                        \"update_time\": 1602893670\n                    },\n                    \"key\": \"/apisix/upstreams/up_create_update_time\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"the property is forbidden:.*\"\\}/\n\n\n\n=== TEST 15: patch upstream with sub_path, the data is number\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, message = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {},\n                    \"type\": \"roundrobin\"\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            local id = 1\n            local res = assert(etcd.get('/upstreams/' .. id))\n            local prev_create_time = res.body.node.value.create_time\n            local prev_update_time = res.body.node.value.update_time\n            ngx.sleep(1)\n\n            local code, message = t('/apisix/admin/upstreams/1/retries',\n                ngx.HTTP_PATCH,\n                json.encode(1)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(message)\n            local res = assert(etcd.get('/upstreams/' .. id))\n            local create_time = res.body.node.value.create_time\n            assert(prev_create_time == create_time, \"create_time mismatched\")\n            local update_time = res.body.node.value.update_time\n            assert(prev_update_time ~= update_time, \"update_time should be changed\")\n        }\n    }\n\n\n\n=== TEST 16: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:8080\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: set service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream_id\": 1,\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: delete upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1', ngx.HTTP_DELETE)\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n\n\n\n=== TEST 20: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 21: delete service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/services/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 22: delete upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1', ngx.HTTP_DELETE)\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n"
  },
  {
    "path": "t/admin/upstream5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set upstream(kafka scheme)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local code, body = t.test(\"/apisix/admin/upstreams/kafka\", ngx.HTTP_PUT, [[{\n                \"nodes\": {\n                    \"127.0.0.1:9092\": 1\n                },\n                \"type\": \"none\",\n                \"scheme\": \"kafka\"\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: set upstream(empty tls)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local code, body = t.test(\"/apisix/admin/upstreams/kafka\", ngx.HTTP_PUT, [[{\n                \"nodes\": {\n                    \"127.0.0.1:9092\": 1\n                },\n                \"type\": \"none\",\n                \"scheme\": \"kafka\",\n                \"tls\": {}\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: set upstream(tls without verify)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local code, body = t.test(\"/apisix/admin/upstreams/kafka\", ngx.HTTP_PUT, [[{\n                \"nodes\": {\n                    \"127.0.0.1:9092\": 1\n                },\n                \"type\": \"none\",\n                \"scheme\": \"kafka\",\n                \"tls\": {\n                    \"verify\": false\n                }\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: prepare upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_PUT, [[{\n                \"nodes\": {\n                    \"127.0.0.1:1980\": 1\n                },\n                \"type\": \"roundrobin\"\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: prepare route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/routes/1\", ngx.HTTP_PUT, [[{\n                \"plugins\": {\n                    \"traffic-split\": {\n                        \"rules\": [\n                            {\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream_id\": 1,\n                                        \"weight\": 1\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: delete upstream when plugin in route still refer it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, plugin in route [1] is still using it now\"}\n\n\n\n=== TEST 7: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/routes/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: prepare service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/services/1\", ngx.HTTP_PUT, [[{\n                \"plugins\": {\n                    \"traffic-split\": {\n                        \"rules\": [\n                            {\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream_id\": 1,\n                                        \"weight\": 1\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: delete upstream when plugin in service still refer it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, plugin in service [1] is still using it now\"}\n\n\n\n=== TEST 10: delete service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/services/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: prepare global_rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t(\"/apisix/admin/global_rules/1\", ngx.HTTP_PUT, [[{\n                \"plugins\": {\n                    \"traffic-split\": {\n                        \"rules\": [\n                            {\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream_id\": 1,\n                                        \"weight\": 1\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: delete upstream when plugin in global_rule still refer it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, plugin in global_rules [1] is still using it now\"}\n\n\n\n=== TEST 13: delete global_rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/global_rules/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: prepare plugin_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t(\"/apisix/admin/plugin_configs/1\", ngx.HTTP_PUT, [[{\n                \"plugins\": {\n                    \"traffic-split\": {\n                        \"rules\": [\n                            {\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream_id\": 1,\n                                        \"weight\": 1\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: delete upstream when plugin in plugin_config still refer it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, plugin in plugin_config [1] is still using it now\"}\n\n\n\n=== TEST 16: delete plugin_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/plugin_configs/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: prepare consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t(\"/apisix/admin/consumers\", ngx.HTTP_PUT, [[{\n                \"username\": \"test\",\n                \"plugins\": {\n                    \"key-auth\": {\n                        \"key\": \"auth-one\"\n                    },\n                    \"traffic-split\": {\n                        \"rules\": [\n                            {\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream_id\": 1,\n                                        \"weight\": 1\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: delete upstream when plugin in consumer still refer it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, plugin in consumer [test] is still using it now\"}\n\n\n\n=== TEST 19: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/consumers/test\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: prepare consumer_group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t(\"/apisix/admin/consumer_groups/1\", ngx.HTTP_PUT, [[{\n                \"plugins\": {\n                    \"key-auth\": {\n                        \"key\": \"auth-one\"\n                    },\n                    \"traffic-split\": {\n                        \"rules\": [\n                            {\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream_id\": 1,\n                                        \"weight\": 1\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: delete upstream when plugin in consumer_group still refer it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, plugin in consumer_group [1] is still using it now\"}\n\n\n\n=== TEST 22: delete consumer_group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/consumer_groups/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: delete upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/upstreams/1\", ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/apisix.luacov",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nreturn {\n    modules = {\n        [\"lua.*\"] = \"lua\",\n        [\"apisix/*\"] = \"apisix\",\n        [\"apisix/admin/*\"] = \"admin\",\n        [\"apisix/core/*\"] = \"core\",\n        [\"apisix/http/*\"] = \"http\",\n        [\"apisix/http/router/*\"] = \"http/router\",\n        [\"apisix/plugins/*\"] = \"plugins\",\n        [\"apisix/plugins/grpc-transcode/*\"] = \"plugins/grpc-transcode\",\n        [\"apisix/plugins/limit-count/*\"] = \"plugins/limit-count\",\n        [\"apisix/plugins/prometheus/*\"] = \"plugins/prometheus\",\n        [\"apisix/plugins/zipkin/*\"] = \"plugins/zipkin\",\n        [\"apisix/utils/*\"] = \"utils\",\n        [\"apisix/discovery/*\"] = \"discovery\",\n\n        -- can not enable both at http and stream, will fix it later.\n        --  [\"apisix/stream/*\"] = \"stream\",\n        --  [\"apisix/stream/plugins/*\"] = \"stream/plugins\",\n        --  [\"apisix/stream/router/*\"] = \"stream/router\",\n   },\n}\n"
  },
  {
    "path": "t/assets/ai-proxy-response.json",
    "content": "{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": { \"content\": \"1 + 1 = 2.\", \"role\": \"assistant\" }\n    }\n  ],\n  \"created\": 1723780938,\n  \"id\": \"chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P\",\n  \"model\": \"gpt-4o-2024-05-13\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": { \"completion_tokens\": 8, \"prompt_tokens\": 23, \"total_tokens\": 31 }\n}\n"
  },
  {
    "path": "t/assets/content-moderation-responses.json",
    "content": "{\n  \"good_request\": {\n    \"ResultList\": [\n      {\n        \"Toxicity\": 0.02150000333786,\n        \"Labels\": [\n          {\n            \"Name\": \"PROFANITY\",\n            \"Score\": 0.00589999556541\n          },\n          {\n            \"Name\": \"HATE_SPEECH\",\n            \"Score\": 0.01729999780655\n          },\n          {\n            \"Name\": \"INSULT\",\n            \"Score\": 0.00519999861717\n          },\n          {\n            \"Name\": \"GRAPHIC\",\n            \"Score\": 0.00520000338554\n          },\n          {\n            \"Name\": \"HARASSMENT_OR_ABUSE\",\n            \"Score\": 0.00090001106262\n          },\n          {\n            \"Name\": \"SEXUAL\",\n            \"Score\": 0.00810000061989\n          },\n          {\n            \"Name\": \"VIOLENCE_OR_THREAT\",\n            \"Score\": 0.00570000290871\n          }\n        ]\n      }\n    ]\n  },\n  \"profane\": {\n    \"ResultList\": [\n      {\n        \"Toxicity\": 0.62150000333786,\n        \"Labels\": [\n          {\n            \"Name\": \"PROFANITY\",\n            \"Score\": 0.55589999556541\n          },\n          {\n            \"Name\": \"HATE_SPEECH\",\n            \"Score\": 0.21729999780655\n          },\n          {\n            \"Name\": \"INSULT\",\n            \"Score\": 0.25519999861717\n          },\n          {\n            \"Name\": \"GRAPHIC\",\n            \"Score\": 0.12520000338554\n          },\n          {\n            \"Name\": \"HARASSMENT_OR_ABUSE\",\n            \"Score\": 0.27090001106262\n          },\n          {\n            \"Name\": \"SEXUAL\",\n            \"Score\": 0.44810000061989\n          },\n          {\n            \"Name\": \"VIOLENCE_OR_THREAT\",\n            \"Score\": 0.27570000290871\n          }\n        ]\n      }\n    ]\n  },\n  \"profane_but_not_toxic\": {\n    \"ResultList\": [\n      {\n        \"Toxicity\": 0.12150000333786,\n        \"Labels\": [\n          {\n            \"Name\": \"PROFANITY\",\n            \"Score\": 0.55589999556541\n          },\n          {\n            \"Name\": \"HATE_SPEECH\",\n            \"Score\": 0.21729999780655\n          },\n          {\n            \"Name\": \"INSULT\",\n            \"Score\": 0.25519999861717\n          },\n          {\n            \"Name\": \"GRAPHIC\",\n            \"Score\": 0.12520000338554\n          },\n          {\n            \"Name\": \"HARASSMENT_OR_ABUSE\",\n            \"Score\": 0.27090001106262\n          },\n          {\n            \"Name\": \"SEXUAL\",\n            \"Score\": 0.44810000061989\n          },\n          {\n            \"Name\": \"VIOLENCE_OR_THREAT\",\n            \"Score\": 0.27570000290871\n          }\n        ]\n      }\n    ]\n  },\n  \"very_profane\": {\n    \"ResultList\": [\n      {\n        \"Toxicity\": 0.72150000333786,\n        \"Labels\": [\n          {\n            \"Name\": \"PROFANITY\",\n            \"Score\": 0.85589999556541\n          },\n          {\n            \"Name\": \"HATE_SPEECH\",\n            \"Score\": 0.21729999780655\n          },\n          {\n            \"Name\": \"INSULT\",\n            \"Score\": 0.25519999861717\n          },\n          {\n            \"Name\": \"GRAPHIC\",\n            \"Score\": 0.12520000338554\n          },\n          {\n            \"Name\": \"HARASSMENT_OR_ABUSE\",\n            \"Score\": 0.27090001106262\n          },\n          {\n            \"Name\": \"SEXUAL\",\n            \"Score\": 0.94810000061989\n          },\n          {\n            \"Name\": \"VIOLENCE_OR_THREAT\",\n            \"Score\": 0.27570000290871\n          }\n        ]\n      }\n    ]\n  },\n  \"toxic\": {\n    \"ResultList\": [\n      {\n        \"Toxicity\": 0.72150000333786,\n        \"Labels\": [\n          {\n            \"Name\": \"PROFANITY\",\n            \"Score\": 0.25589999556541\n          },\n          {\n            \"Name\": \"HATE_SPEECH\",\n            \"Score\": 0.21729999780655\n          },\n          {\n            \"Name\": \"INSULT\",\n            \"Score\": 0.75519999861717\n          },\n          {\n            \"Name\": \"GRAPHIC\",\n            \"Score\": 0.12520000338554\n          },\n          {\n            \"Name\": \"HARASSMENT_OR_ABUSE\",\n            \"Score\": 0.27090001106262\n          },\n          {\n            \"Name\": \"SEXUAL\",\n            \"Score\": 0.64810000061989\n          },\n          {\n            \"Name\": \"VIOLENCE_OR_THREAT\",\n            \"Score\": 0.27570000290871\n          }\n        ]\n      }\n    ]\n  },\n  \"very_toxic\": {\n    \"ResultList\": [\n      {\n        \"Toxicity\": 0.92150000333786,\n        \"Labels\": [\n          {\n            \"Name\": \"PROFANITY\",\n            \"Score\": 0.25589999556541\n          },\n          {\n            \"Name\": \"HATE_SPEECH\",\n            \"Score\": 0.21729999780655\n          },\n          {\n            \"Name\": \"INSULT\",\n            \"Score\": 0.25519999861717\n          },\n          {\n            \"Name\": \"GRAPHIC\",\n            \"Score\": 0.12520000338554\n          },\n          {\n            \"Name\": \"HARASSMENT_OR_ABUSE\",\n            \"Score\": 0.27090001106262\n          },\n          {\n            \"Name\": \"SEXUAL\",\n            \"Score\": 0.44810000061989\n          },\n          {\n            \"Name\": \"VIOLENCE_OR_THREAT\",\n            \"Score\": 0.27570000290871\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "t/assets/embeddings.json",
    "content": "{\n    \"object\": \"list\",\n    \"data\": [\n      {\n        \"object\": \"embedding\",\n        \"index\": 0,\n        \"embedding\": [\n          123456789,\n          0.01902593,\n          0.008967914,\n          -0.013226582,\n          -0.026961878,\n          -0.017892223,\n          -0.0007785152,\n          -0.011031842,\n          0.0068531134\n        ]\n      }\n    ],\n    \"model\": \"text-embedding-3-small\",\n    \"usage\": {\n      \"prompt_tokens\": 4,\n      \"total_tokens\": 4\n    }\n  }\n"
  },
  {
    "path": "t/assets/openai-compatible-api-response.json",
    "content": "{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": { \"content\": \"1 + 1 = 2.\", \"role\": \"assistant\" }\n    }\n  ],\n  \"created\": 1723780938,\n  \"id\": \"chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P\",\n  \"model\": \"gpt-4o-2024-05-13\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": { \"completion_tokens\": 8, \"prompt_tokens\": 23, \"total_tokens\": 31 }\n}\n"
  },
  {
    "path": "t/certs/apisix.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/apisix.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5\njhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo\neLj0efMiOepOSZflj9Ob4yKR2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5s\nmPtW1Oc/BV5terhscJdOgmRrabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt\n6iMWEGeQU6mwPENgvj1olji2WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiy\nVt1TmtMWn1ztk6FfLRqwJWR/Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1o\nnpRVeXhrBajbCRDRBMwaNw/1/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2\nfzaqpIfyUbPST4GdqNG9NyIh/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI\n1cGrGwyXbrieNp63AgMBAAECggGBAJM8g0duoHmIYoAJzbmKe4ew0C5fZtFUQNmu\nO2xJITUiLT3ga4LCkRYsdBnY+nkK8PCnViAb10KtIT+bKipoLsNWI9Xcq4Cg4G3t\n11XQMgPPgxYXA6m8t+73ldhxrcKqgvI6xVZmWlKDPn+CY/Wqj5PA476B5wEmYbNC\nGIcd1FLl3E9Qm4g4b/sVXOHARF6iSvTR+6ol4nfWKlaXSlx2gNkHuG8RVpyDsp9c\nz9zUqAdZ3QyFQhKcWWEcL6u9DLBpB/gUjyB3qWhDMe7jcCBZR1ALyRyEjmDwZzv2\njlv8qlLFfn9R29UI0pbuL1eRAz97scFOFme1s9oSU9a12YHfEd2wJOM9bqiKju8y\nDZzePhEYuTZ8qxwiPJGy7XvRYTGHAs8+iDlG4vVpA0qD++1FTpv06cg/fOdnwshE\nOJlEC0ozMvnM2rZ2oYejdG3aAnUHmSNa5tkJwXnmj/EMw1TEXf+H6+xknAkw05nh\nzsxXrbuFUe7VRfgB5ElMA/V4NsScgQKBwQDmMRtnS32UZjw4A8DsHOKFzugfWzJ8\nGc+3sTgs+4dNIAvo0sjibQ3xl01h0BB2Pr1KtkgBYB8LJW/FuYdCRS/KlXH7PHgX\n84gYWImhNhcNOL3coO8NXvd6+m+a/Z7xghbQtaraui6cDWPiCNd/sdLMZQ/7LopM\nRbM32nrgBKMOJpMok1Z6zsPzT83SjkcSxjVzgULNYEp03uf1PWmHuvjO1yELwX9/\ngoACViF+jst12RUEiEQIYwr4y637GQBy+9cCgcEA3pN9W5OjSPDVsTcVERig8++O\nBFURiUa7nXRHzKp2wT6jlMVcu8Pb2fjclxRyaMGYKZBRuXDlc/RNO3uTytGYNdC2\nIptU5N4M7iZHXj190xtDxRnYQWWo/PR6EcJj3f/tc3Itm1rX0JfuI3JzJQgDb9Z2\ns/9/ub8RRvmQV9LM/utgyOwNdf5dyVoPcTY2739X4ZzXNH+CybfNa+LWpiJIVEs2\ntxXbgZrhmlaWzwA525nZ0UlKdfktdcXeqke9eBghAoHARVTHFy6CjV7ZhlmDEtqE\nU58FBOS36O7xRDdpXwsHLnCXhbFu9du41mom0W4UdzjgVI9gUqG71+SXrKr7lTc3\ndMHcSbplxXkBJawND/Q1rzLG5JvIRHO1AGJLmRgIdl8jNgtxgV2QSkoyKlNVbM2H\nWy6ZSKM03lIj74+rcKuU3N87dX4jDuwV0sPXjzJxL7NpR/fHwgndgyPcI14y2cGz\nzMC44EyQdTw+B/YfMnoZx83xaaMNMqV6GYNnTHi0TO2TAoHBAKmdrh9WkE2qsr59\nIoHHygh7Wzez+Ewr6hfgoEK4+QzlBlX+XV/9rxIaE0jS3Sk1txadk5oFDebimuSk\nlQkv1pXUOqh+xSAwk5v88dBAfh2dnnSa8HFN3oz+ZfQYtnBcc4DR1y2X+fVNgr3i\nnxruU2gsAIPFRnmvwKPc1YIH9A6kIzqaoNt1f9VM243D6fNzkO4uztWEApBkkJgR\n4s/yOjp6ovS9JG1NMXWjXQPcwTq3sQVLnAHxZRJmOvx69UmK4QKBwFYXXjeXiU3d\nbcrPfe6qNGjfzK+BkhWznuFUMbuxyZWDYQD5yb6ukUosrj7pmZv3BxKcKCvmONU+\nCHgIXB+hG+R9S2mCcH1qBQoP/RSm+TUzS/Bl2UeuhnFZh2jSZQy3OwryUi6nhF0u\nLDzMI/6aO1ggsI23Ri0Y9ZtqVKczTkxzdQKR9xvoNBUufjimRlS80sJCEB3Qm20S\nwzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/apisix_admin_ssl.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFsTCCA5mgAwIBAgIUODyT8W4gAxf8uwMNmtj5M1ANoUwwDQYJKoZIhvcNAQEL\nBQAwVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\nWmh1SGFpMQ0wCwYDVQQKDARhcGk3MRMwEQYDVQQDDAphcGlzaXguZGV2MCAXDTIw\nMDYwNDAzMzc1MFoYDzIxMjAwNTExMDMzNzUwWjBWMQswCQYDVQQGEwJDTjESMBAG\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDTALBgNVBAoMBGFwaTcx\nEzARBgNVBAMMCmFwaXNpeC5kZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQDQveSdplH49Lr+LsLWpGJbNRhf2En0V4SuFKpzGFP7mXaI7rMnpdH3BUVY\nS3juMgPOdNh6ho4BeSbGZGfU3lG1NwIOXiPNA1mrTWGNGV97crJDVZeWTuDpqNHJ\n4ATrnF6RnRbg0en8rjVtce6LBMrDJVyGbi9VAqBUPrCmzT/l0V1jPL6KNSN8mQog\nladrJuzUanfhWM9K9xyM+/SUt1MNUYFLNsVHasPzsi5/YDRBiwuzTtiT56O6yge2\nlvrdPFvULrCxlGteyvhtrFJwqjN//YtnQFooNR0CXBfXs0a7WGgMjawupuP1JKiY\nt9KEcGHWGZDeLfsGGKgQ9G+PaP4y+gHjLr5xQvwt68otpoafGy+BpOoHZZFoLBpx\nTtJKA3qnwyZg9zr7lrtqr8CISO/SEyh6xkAOUzb7yc2nHu9UpruzVIR7xI7pjc7f\n2T6WyCVy6gFYQwzFLwkN/3O+ZJkioxXsnwaYWDj61k3d9ozVDkVkTuxmNJjXV8Ta\nhtGRAHo0/uHmpFTcaQfDf5o+iWi4z9B5kgfA/A1XWFQlCH1kl3mHKg7JNCN9qGF8\nrG+YzdiLQfo5OqJSvzGHRXbdGI2JQe/zyJHsMO7d0AhwXuPOWGTTAODOPlaBCxNB\nAgjuUgt+3saqCrK4eaOo8sPt055AYJhZlaTH4EeD4sv7rJGm7wIDAQABo3UwczAd\nBgNVHQ4EFgQUPS1LXZMqgQvH/zQHHzgTzrd7PIIwHwYDVR0jBBgwFoAUPS1LXZMq\ngQvH/zQHHzgTzrd7PIIwDAYDVR0TBAUwAwEB/zAjBgNVHREEHDAaggphcGlzaXgu\nZGV2ggwqLmFwaXNpeC5kZXYwDQYJKoZIhvcNAQELBQADggIBAMlwNS8uo3JkkshI\nrpYobdjCZfr74PBl+LhoihvzHs25/in3+CxETRA8cYo5pRotqdA63po3wiCCPs6a\nmZiELQxyGHhFcqoYxnoURR4nyogRZLA6jjLGkbG4H+CA4ApmZmvGnP3X5uQW4v5q\nIdqIXL3BvoUBln8GMEC7Rz5SGUjWG03JPkl6MdeziFyHkwdBCOrtK5m7icRncvq+\niL8CMUx024LLI6A5hTBPwfVfgbWJTSv7tEu85q54ZZoYQhiD8dde4D7g5/noPvXM\nZyA9C3Sl981+pUhhazad9j9k8DCcqf9e8yH9lPY26tjiEcShv4YnwbErWzJU1F9s\nZI5Z6nj5PU66upnBWAWV7fWCOrlouB4GjNaznSNrmpn4Bb2+FinDK3t4AfWDPS5s\nljQBGQNXOd30DC7BdNAF5dQAUhVfz1EgQGqYa+frMQLiv8rNMs7h6gKQEqU+jC/1\njbGe4/iwc0UeTtSgTPHMofqjqc99/R/ZqtJ3qFPJmoWpyu0NlNINw2KWRQaMoGLo\nWgDCS0YA5/hNXVFcWnZ73jY62yrVSoj+sFbkUpGWhEFnO+uSmBv8uwY3UeCOQDih\nX7Yazs3TZRqEPU+25QATf0kbxyzlWbGkwvyRD8x+n3ZHs5Ilhrc6jWHqM/S3ir7i\nm9GcWiwg++EbusQsqs3w3uKAHAdT\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/apisix_admin_ssl.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA0L3knaZR+PS6/i7C1qRiWzUYX9hJ9FeErhSqcxhT+5l2iO6z\nJ6XR9wVFWEt47jIDznTYeoaOAXkmxmRn1N5RtTcCDl4jzQNZq01hjRlfe3KyQ1WX\nlk7g6ajRyeAE65xekZ0W4NHp/K41bXHuiwTKwyVchm4vVQKgVD6wps0/5dFdYzy+\nijUjfJkKIJWnaybs1Gp34VjPSvccjPv0lLdTDVGBSzbFR2rD87Iuf2A0QYsLs07Y\nk+ejusoHtpb63Txb1C6wsZRrXsr4baxScKozf/2LZ0BaKDUdAlwX17NGu1hoDI2s\nLqbj9SSomLfShHBh1hmQ3i37BhioEPRvj2j+MvoB4y6+cUL8LevKLaaGnxsvgaTq\nB2WRaCwacU7SSgN6p8MmYPc6+5a7aq/AiEjv0hMoesZADlM2+8nNpx7vVKa7s1SE\ne8SO6Y3O39k+lsglcuoBWEMMxS8JDf9zvmSZIqMV7J8GmFg4+tZN3faM1Q5FZE7s\nZjSY11fE2obRkQB6NP7h5qRU3GkHw3+aPolouM/QeZIHwPwNV1hUJQh9ZJd5hyoO\nyTQjfahhfKxvmM3Yi0H6OTqiUr8xh0V23RiNiUHv88iR7DDu3dAIcF7jzlhk0wDg\nzj5WgQsTQQII7lILft7GqgqyuHmjqPLD7dOeQGCYWZWkx+BHg+LL+6yRpu8CAwEA\nAQKCAgBNsbBLAWHXYPfMrgj1LUAypIOLAQ0dtgl7ZdO/fRmdNxSIiRgDtNN+tuaF\no6nCNrl1+cWtbTGj2L0W8L442/rbkTrhsCZxI0MX4HhjtUL1xs4VA+GlH3zVW3Gi\nSxBpxczpM+gVC+ykkQ7vyo04DzONCPX0T0Ssxop4cND9dL3Iw3GYAz8EYBzyPmAn\nmqwy1M0nju1J4e1eALYOv6TcSZPPDDwsi5lIKLQAm5x06pDoqGFVfw5blsc5OgM+\n8dkzyUiApFQ99Hk2UiO/ZnlU1/TNOcjOSISGHKbMfwycy2yTRKeNrJmez51fXCKo\nnRrtEotHzkI+gCzDqx+7F9ACN9kM4f4JO5ca0/My6tCY+mH8TA/nVzMnUpL7329w\nNobuNTpyA6x5nmB3QqElrzQCRtTj7Nw5ytMdRbByJhXww9C5tajUysdq8oGoZdz5\n94kXr6qCC5Qm3CkgyF2RjqZyg9tHUEEdaFKouHgziiqG9P2Nk1SHk7Jd7bF4rleI\ni93u/f0fdVK7aMksofgUbOmfhnS+o1NxerVcbdX+E/iv6yfkrYDb46y3//4dcpwk\nTeUEMCjc7ShwvYPq350q3jmzgwxeTK8ZdXwJymdJ7MaGcnMXPqd9A43evYM6nG6f\ni3l2tYhH4cp6misGChnGORR68qsRkY8ssvSFNFzjcFHhnPyoCQKCAQEA8isIC1IJ\nIq9kB4mDVh0QdiuoBneNOEHy/8fASeZsqedu0OZPyoXU96iOhXuqf8sQ33ydvPef\niRwasLLkgw8sDeWILUjS36ZzwGP2QNxWfrapCFS8VfKl7hTPMVp0Wzxh8qqpGLSh\nO0W7EEAJCgzzULagfupaO0Chmb3LZqXRp8m5oubnmE+9z0b5GrCIT1S8Yay2mEw9\njxqZJGBhV7QnupyC2DIxLXlGmQk7Qs1+1mCCFwyfugHXclWYa+fet/79SkkADK0/\nysxfy+FdZgGT/Ba5odsEpt1zH+tw4WXioJsX9mU3zAHbpPqtcfuVU+2xyKfQYrRG\nNSm9MMNmart0wwKCAQEA3Koaj/0gNxLLslLIES50KmmagzU8CkEmCa/WLoVy02xr\nqp42hvj+PzBTf3rIno3KEpRhMmnAtswozbV3P4l/VSZdfY+pwWsx7/5+Cf1R9nAP\nvp6YCjGcLcbASazYNOWf0FRInt3pxdgT9DWjJDi99FGKA+UbI2yxHwzE+cE8r9Od\nIy42uhzCjJBqdg+an+q63k6yrOwv18KP69LlU/4vknhw4g3WxF4yTwVmXU8WKmux\naOrJv2ED8pfA7k+zwv0rPyN+F2nOySxoChaFfeu6ntBCX7zK/nV0DsMQImOycfzO\nyN8WB9lRZTJVzU2r6PaGAI359uLHEmURy0069g+yZQKCAQAbECwJ99UFh0xKe1eu\nG/lm+2H/twSVMOmTJCOdHp8uLar4tYRdQa+XLcMfr75SIcN09lw6bgHqNLXW4Wcg\nLmXh97DMPsMyM0vkSEeQ4A7agldJkw6pHEDm5nRxM4alW44mrGPRWv5ZvWU2X7Gi\n6eeXMZGmHVKQJJzqrYc5pXZUpfqU9fET2HWB4JCeJvRUyUd0MvUE+CA5CePraMn4\nHy4BcNQ+jP1p84+sMpfo00ZFduuS39pJ00LciCxMgtElBt4PmzDiOcpTQ5vBESJ6\n79o15eRA7lUKwNzIyGsJBXXaNPrskks2BU8ilNElV9RMWNfxcK+dGEBwWIXIGU4s\nx145AoIBAQCst9R8udNaaDLaTGNe126DuA8B/kwVdrLwSBqsZTXgeO+5J4dklEZl\nbU0d7hxTxoXRjySZEh+OtTSG9y/0oonxO0tYOXfU9jOrNxaueQKLk2EvgfFdoUEu\nr2/Y+xpsJQO3TBFfkDEn856Cuu0MMAG214/gxpY8XxowRI11NCRtN4S6gbTCbjp1\nTaCW8lXEMDW+Rfki0ugLyLVgD74CxWW1DuLEfbKKF3TnV0GtbXbbE1pU1dm+G5C8\ndL3FissYp5MPI5fRebcqzcBNjR1F15pGLpqVVy/IhmSmHVZmpISLJicxITScRiSo\nwgJY5R/XBAcVLgvmi9Dn/AY2jCfHa7flAoIBAQCbnZ6ivZg81g6/X9qdo9J61hX0\nY7Fn7bLvcs1L0ARGTsfXMvegA806XyZThqjpY47nHpQtoz4z62kiTTsdpAZUeA3z\n9HUWr0b3YEpsvZpgyMNHgwq1vRDPjw4AWz0pBoDWMxx8Ck5nP1A//c1zyu9pgYEU\nR+OutDeCJ+0VAc6JSH9WMA08utGPGs3t02Zhtyt2sszE9vzz4hTi5340/AYG72p7\nYGlikUxvbyylYh9wR4YUYa/klikvKLHEML1P0BCr8Vex+wLSGS1h1F5tW1Xr2CZQ\ndVxFmfGmPDmwWbCQR6Rvt6FHRwNMpMrLr011h2RBcHBpdQl7XpUENDoopIh0\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/apisix_ecc.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB8jCCAZmgAwIBAgIJALwxr+GMOgSKMAoGCCqGSM49BAMCMFYxCzAJBgNVBAYT\nAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0GA1UE\nCgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAeFw0yMDA4MTgwODI0MzdaFw0y\nMTA4MTgwODI0MzdaMFYxCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0Rvbmcx\nDzANBgNVBAcMBlpodUhhaTEPMA0GA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0\nLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEJACTxb5qYd4v9VaNKlv2fe\nXlZSTDYe+0fZwT4l9sifmPzmpwjiVTB2wLiCYYzy+BPrb29r5ubgtXIflsWKRBKj\nUDBOMB0GA1UdDgQWBBQPXxOYAHfboUjsoo1xm6/GJ1qHijAfBgNVHSMEGDAWgBQP\nXxOYAHfboUjsoo1xm6/GJ1qHijAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0cA\nMEQCICQ70LiJ+Z2lv9ZF+FQL+VEdVQ938rz6RGXBmnl2oEvkAiBY2eeTl//JanNX\nGsSV104WrpHjcBjcY24jb11Y1H3R9g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/apisix_ecc.key",
    "content": "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBlQqVD3bK6CQT5puOOngrb50+3K66MKJdhtpWQoUw2poAoGCCqGSM49\nAwEHoUQDQgAEQkAJPFvmph3i/1Vo0qW/Z95eVlJMNh77R9nBPiX2yJ+Y/OanCOJV\nMHbAuIJhjPL4E+tvb2vm5uC1ch+WxYpEEg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/client_enc.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB2TCCAX6gAwIBAgIBBTAKBggqgRzPVQGDdTBFMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEPMA0GA1UEAwwGc3Vi\nIGNhMB4XDTIyMTEwMjAzMTkzNloXDTMyMTAzMDAzMTkzNlowSTELMAkGA1UEBhMC\nQUExCzAJBgNVBAgMAkJCMQswCQYDVQQKDAJDQzELMAkGA1UECwwCREQxEzARBgNV\nBAMMCmNsaWVudCBlbmMwWjAUBggqgRzPVQGCLQYIKoEcz1UBgi0DQgAEYYuPPz5e\n0QMSGPeBfVbK02GwYhSieSCuc12WsNw+ZQEiaN3NJ2Mh0EAH95eWVutKAeMwKwQZ\nq7QgnSoo3io8hKNaMFgwCQYDVR0TBAIwADALBgNVHQ8EBAMCAzgwHQYDVR0OBBYE\nFEL0AwvahirH+kdK5Poq+e0yhii1MB8GA1UdIwQYMBaAFCTrpmbUig3JfveqAIGJ\n6n+vAk2AMAoGCCqBHM9VAYN1A0kAMEYCIQDx+KxdaJ7YX5gR492EgiGn7//HsjOU\nB7+jyTVvkNzN2AIhAIbDKNJQ2i5Edcw/nDIWJQLec7NZui3QfC/gr9AuCfHN\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/client_enc.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgq7+Y1ql10Uvv2vTf\nAHR26o4B3RJTJO5XTh0BNLdtB7OhRANCAARhi48/Pl7RAxIY94F9VsrTYbBiFKJ5\nIK5zXZaw3D5lASJo3c0nYyHQQAf3l5ZW60oB4zArBBmrtCCdKijeKjyE\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/client_sign.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB2TCCAX+gAwIBAgIBBDAKBggqgRzPVQGDdTBFMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEPMA0GA1UEAwwGc3Vi\nIGNhMB4XDTIyMTEwMjAzMTkzNloXDTMyMTAzMDAzMTkzNlowSjELMAkGA1UEBhMC\nQUExCzAJBgNVBAgMAkJCMQswCQYDVQQKDAJDQzELMAkGA1UECwwCREQxFDASBgNV\nBAMMC2NsaWVudCBzaWduMFowFAYIKoEcz1UBgi0GCCqBHM9VAYItA0IABFZcc94m\nhTZRKis639AnlAbS0cKQv73GP5RzBdNlLpAaUwi4hqAh0ZUIcTH/5ZbOTal9MvHA\ngOLjVxv197o+fNejWjBYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgbAMB0GA1UdDgQW\nBBTkdzyphRCxqD6m6j/AUhMSEBASRDAfBgNVHSMEGDAWgBQk66Zm1IoNyX73qgCB\niep/rwJNgDAKBggqgRzPVQGDdQNIADBFAiB2rcm1UI84yPYT5q6vjucBNPw01cHM\n3/Hc9fhiuYPyHwIhAIiPPPj4XI2l98C+DBaqoZtSiwDK2IA6Q8lf9SmHdFPN\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/client_sign.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgIsAr+s4TL7K4AQWO\nPbXrJfHoO5yE2V7oYQxUBsieOQOhRANCAARWXHPeJoU2USorOt/QJ5QG0tHCkL+9\nxj+UcwXTZS6QGlMIuIagIdGVCHEx/+WWzk2pfTLxwIDi41cb9fe6PnzX\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/etcd.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\n5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\nBctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\n0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\nSXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\ngf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\nSF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\nyLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\n2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\ng0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\nQS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\nL/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\nLR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\n7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\nt1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\nbe7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\nV3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\nzAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\nIeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\nr8yiEiskqRmy7P7MY9hDmEbG\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/etcd.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\ns9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\ntdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\nD44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\nNFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\nquDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\nbnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\nMB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\nDQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\nqvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\nrAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\nHCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\ngeAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\n2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/gm_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB3zCCAYWgAwIBAgIBADAKBggqgRzPVQGDdTBGMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEQMA4GA1UEAwwHcm9v\ndCBjYTAeFw0yMjExMDIwMzE5MzZaFw0zMjEwMzAwMzE5MzZaMEYxCzAJBgNVBAYT\nAkFBMQswCQYDVQQIDAJCQjELMAkGA1UECgwCQ0MxCzAJBgNVBAsMAkREMRAwDgYD\nVQQDDAdyb290IGNhMFowFAYIKoEcz1UBgi0GCCqBHM9VAYItA0IABB+V1+bwQsP4\nIMZEVCu3LSekz9SIhxWVVtlqdQYZG55S46PmAqICzrO3KFJ/IPtMx9wKn3L6V5M8\nhAc/UwOAnKCjYzBhMB0GA1UdDgQWBBRV7bTJ6vT1fliZR42/+E+fEBfTSjAfBgNV\nHSMEGDAWgBRV7bTJ6vT1fliZR42/+E+fEBfTSjAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBhjAKBggqgRzPVQGDdQNIADBFAiEA1SGACV1wj158Spgh+HOW\noOr7rTO2fR4cK9Zx7eUvmAECIHbKbsw5szaC/EH7CdsHdFgrj2tWaXiQUnx/rxM/\nupAQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB4TCCAYegAwIBAgIBATAKBggqgRzPVQGDdTBGMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEQMA4GA1UEAwwHcm9v\ndCBjYTAeFw0yMjExMDIwMzE5MzZaFw0zMjEwMzAwMzE5MzZaMEUxCzAJBgNVBAYT\nAkFBMQswCQYDVQQIDAJCQjELMAkGA1UECgwCQ0MxCzAJBgNVBAsMAkREMQ8wDQYD\nVQQDDAZzdWIgY2EwWjAUBggqgRzPVQGCLQYIKoEcz1UBgi0DQgAElA5ey1dYWNkT\nzfvwcKEhX1vHL+Kjil+egM6QssbNrts2S0M07L77XDe1q2zPpHjo0MR05x862/tZ\nj87OgmEE0KNmMGQwHQYDVR0OBBYEFCTrpmbUig3JfveqAIGJ6n+vAk2AMB8GA1Ud\nIwQYMBaAFFXttMnq9PV+WJlHjb/4T58QF9NKMBIGA1UdEwEB/wQIMAYBAf8CAQAw\nDgYDVR0PAQH/BAQDAgGGMAoGCCqBHM9VAYN1A0gAMEUCIArKNHWYLmd3thQmJv89\no0wr6O2q26WJuy6y7Eu14rdFAiEA7XNZ0JGMXPKiG5Hl7nmL8ooTrVhrmRrvs9No\nY8rH88Y=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/incorrect.crt",
    "content": "test not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\ntest not base64 encoded crt\n"
  },
  {
    "path": "t/certs/incorrect.key",
    "content": "test not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\ntest not base64 encoded key\n"
  },
  {
    "path": "t/certs/localhost_slapd_cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDcjCCAdoCFCS4ndwl6lusO7yj4zxbngp17nNUMA0GCSqGSIb3DQEBCwUAMFYx\nCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhh\naTEPMA0GA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0yMzA4MDMw\nNjM3NDhaGA8yMTA1MDkyMjA2Mzc0OFowEzERMA8GA1UEAwwIdGVzdC5jb20wggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4mQik/vxL1bdjXcXWNT6DBVGL\nA87CeVYleNE5Fx5KROU5Y388h80VgSTmV3ytu9ZuNEB8hcZqmAttZXcipMyNm9PI\nvTXaDFaQVclYNJ27hy7rpaJ29WkeLKlUx/pRUpOCg3lbafSmURf5C234LZL1qe5O\n0CIvY3uAzkGWMUE8ABYnef+ucULZPLa2+Y9wIx76oP5tfmcM/pQhDXt+GK/bZyat\n1sUmEHCVC2gjvHoZO8T7n4ccpi5v06Klj8BKVxRlGVkO2w4hlDbNyh6FWKK31nF8\nBLu/TKF70xSOzX4OFNT6/GJ8R9AyeK52f6OzNmlNUY3UsMEeX8Y2qL4hNrFhAgMB\nAAEwDQYJKoZIhvcNAQELBQADggGBAL6g7NZfVTEVklJPmTFSqwQfuu0YXmIvUQIF\njvbNOmwC+nHSq6yuJFC+83R/on/IPWrkP489bEVwqaBQXnZnIMOTjIk8k9elBm/N\n1BBpzWUiKNp+HqRjPCanvRCIPUZ9hWpmJy6uzG6VodEtxZgJ7lsArj0AlOYlkHHa\nCtmnl5g6H4m8sNACZaAixesb9wM8Slvn1zhpAeIYZsvIaBZOZnWuwHD1R7seh4ob\nBDhDaUfXOYb0VJaKNWnJ5ItPxh4/YMSuS7mG5o2ELnzWN6OeDEQrqKFW17qWLXko\nDXEfyrQnODDI+fXvasJhQ62hH33rQF/Q4yJQOEEr7gQUxtMYCxtGCumx2/5MFTuB\nE8sf8FykV5jGjwdwMHhPGAmhpMJwM6i65P9GwZguqVmeFv2l4eSTmMinURlkwaAw\ncx+LrigAxSKOCcnnnC6Uza1VShyDAuj+XKPglwwJd99UJlk1VG/9TXp3WZTOvSt+\nKttglpiMHyqzCYcMDTGbjPm/UsjFTw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/localhost_slapd_key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4mQik/vxL1bdj\nXcXWNT6DBVGLA87CeVYleNE5Fx5KROU5Y388h80VgSTmV3ytu9ZuNEB8hcZqmAtt\nZXcipMyNm9PIvTXaDFaQVclYNJ27hy7rpaJ29WkeLKlUx/pRUpOCg3lbafSmURf5\nC234LZL1qe5O0CIvY3uAzkGWMUE8ABYnef+ucULZPLa2+Y9wIx76oP5tfmcM/pQh\nDXt+GK/bZyat1sUmEHCVC2gjvHoZO8T7n4ccpi5v06Klj8BKVxRlGVkO2w4hlDbN\nyh6FWKK31nF8BLu/TKF70xSOzX4OFNT6/GJ8R9AyeK52f6OzNmlNUY3UsMEeX8Y2\nqL4hNrFhAgMBAAECggEAKO9euGAHCLYS3ckcpvzVqO5T/9TPU9beYJ7jHprez69p\neYlz3LNsqhkiWqYJ8ujVi0ixCCwOLPMcjZzTh24uIjTtCPXUbE8SHx228YVxePVo\nVT88wM55CgTzY+aYvtHl/iozji733q3a+BItx7wre6i8POPwwLt51r1mU+0GP0yQ\n+spwGR3POjRZeXiWYKSwhfu/STBpQXLANHCpQOmYFCbjTVpCzJ03msQUfYJNmETd\nDqyLGbE4aBoPmekrk8GQa/gn04SIsOi8WZeNhsUT9WXyeLFV0DEwnx0sv6IwOk2o\nFqymr71fKNMIvTpCt8wB0Q/rmvPzrprC+hHIZyX5AQKBgQDol5SYzTijZQasV+2d\nDqO5IxE8xl8z10bLgsExKcHC6xyhk+el0XFO1fWs0SJ9ptNuKH32C8IlfEr6M3EE\nXovQcRfT4crtnWhLmGPAFYKo91f5fiKy1YWggEtsnY+OODo34RCtORtKD6+iC+XE\nLFbLMNQA6sHXVONthiEQ6fhIkQKBgQDLLPHgFFL8kSj+Tt/7ctRnwA7qPTUtAwWG\nb2QzDMEpifu4O9DJWoE3sfMYkQhyz2g6S+O5XXm56CDtkV+Miazt29z0dM2Jz7uV\nNLtymba/s6wBiWFUggHA4Dro1vYa4MJ94ampqi+XPJaJP/j6WoYIu/JhKQDIBtlP\nARaG0O3D0QKBgQCYMyB8wMXQPfqY6kzFml/OdUEBWPnRTYy4/U34Is/1Aa7RmJxb\n6GrR4LaLqKp+OJ1gF0UdrWIU73mMsf7BkjDBbE/gSX9l77vgw856UlkWwgwiacTA\n63IureUtJQlcUjTefftQru7JjuwqCMkIjs8Y1VHVa8j+ZEESWVPn4oKi0QKBgQCT\n4YnHlGN2q91PhG9ooILTZSo1+gj7UyixWeBve8gYiPMrfHYSKIrG1AHhjqa8khQF\n4njE0bGoy7kz0UzfiNHSauYfE+kKdqXNCw2ocxNd4tO+ZpTuIpZOIacfFF8a3x8Q\n6rBH6rQq+xGCooqBBmRqdQoNCAAmlz2SUHNp+yYkEQKBgQDNBbH7WPjiggahF0TI\noweS86hQ9tDlUFMg/On2eMOY0juSU1yPsPA/JsIglSTDWGRasjFDhd29FVrnGGY5\n5GHy/Gh/6ZZUdrJVsypGw/Dy9amLgmkKTJU4SWDYOb6s1ocGvNPFSYgw0yFe56nx\nTU+2zHJo/t2FXssGfnbFWrQAxA==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/mtls_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDNzCCAh8CFHIjXWyzoKYAEb8cJgTxRYdhZDu0MA0GCSqGSIb3DQEBDQUAMFgx\nCzAJBgNVBAYTAmNuMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhh\naTEWMBQGA1UEAwwNY2EuYXBpc2l4LmRldjEMMAoGA1UECwwDb3BzMB4XDTIyMTIw\nMTEwMTY0OFoXDTQyMTIwMzEwMTY0OFowWDELMAkGA1UEBhMCY24xEjAQBgNVBAgM\nCUd1YW5nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMRYwFAYDVQQDDA1jYS5hcGlzaXgu\nZGV2MQwwCgYDVQQLDANvcHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQC6QRtbK1qZg1gUuaVWn0M8hw73H360hH6EvHtEil8meni3W000HpCsi/JdDSr3\ntD6Z88O7x0IToPCT0NsAWA0B2LK1oshFkiyIXiq4CWgfwM9qd5GIwA71WUzJ6Jkq\nkz0r3M3/ogo0+Z7RKoKilvBinqVjTZhRpd63Dg5cLrgPBKFGUBMfRPmKSgwPSWfV\nV85SuHlzpcgcK09NHgSLu2DlFGK+lXGWTLLsrb4F0GAAwL/lk4kplcHK0IZCxfJJ\npuXynmoOmgWKcZcHipgv4+LY6+8K+8Lh9FF6ZXOuW7RLwTY1woLMKK2u40fG4C0I\nWcyh+vzTivCrJ72pSC3rYGX5AgMBAAEwDQYJKoZIhvcNAQENBQADggEBAG6RnAvo\nAMQPog3TIAtIrXi1JPfJ5kuI26KOn/yUlVIoUjbqxkeELwmoUF/K4eg5sT6RX/8U\n0gFVuBi5FZXfPZUt/JSN+fVRSoMP9d1K8ImVpu5gxdp5UZSINlCLvessMx9vafjQ\nEwDcACseel511LIe0rOlVvwHeM2P9pNIMfoexGP0U2U5ubZIO8Ye4ZbNieHYgNCN\nUgJpadvBOC8I3eML2hx79di5y4R1niRXhAd1IYnL9eK4xUoHwyMtl/1kXtq/nrXB\n0njzpqb0GQg0badsF+7v+QM/zrbSSwDTzriCCTWSrd9ze4HYoRqCV6Dc3DjqmHq2\nj4wg2QntJBQWmSc=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/mtls_ca.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAukEbWytamYNYFLmlVp9DPIcO9x9+tIR+hLx7RIpfJnp4t1tN\nNB6QrIvyXQ0q97Q+mfPDu8dCE6Dwk9DbAFgNAdiytaLIRZIsiF4quAloH8DPaneR\niMAO9VlMyeiZKpM9K9zN/6IKNPme0SqCopbwYp6lY02YUaXetw4OXC64DwShRlAT\nH0T5ikoMD0ln1VfOUrh5c6XIHCtPTR4Ei7tg5RRivpVxlkyy7K2+BdBgAMC/5ZOJ\nKZXBytCGQsXySabl8p5qDpoFinGXB4qYL+Pi2OvvCvvC4fRRemVzrlu0S8E2NcKC\nzCitruNHxuAtCFnMofr804rwqye9qUgt62Bl+QIDAQABAoIBABfHXiW6mDuHIESt\nGuW/OYdNuuRj+foz/C8YHSi3/cPc2PKXznh7+n5883lbyAON2HwxOekMXGxDHNPS\nU1Ns6mQ09UPpP2ZabiMO2qdaVBfRtulh0IvD8WTzfLE+Z+eemq2x5/7eAi2XPOZ5\nZeo6GQCOPpE6A9tQsOlv+vdb45XPCsDdyC2WukeUfB2wsFBFEgMmcW7HLuzMYp9W\nJUuQ6OcOf52ePkGnmEuXIK8Wc3RXitivHlkPLO49Y1LG8t4f61HfG9b2pvgNbHn2\nYdkzemCkBMSjzV4oAoIuG56IWBwuqXETRj2xduVcoxd0t2vLK9qlPADBMCa0hcnW\nuCBqGM0CgYEA3ZjZQuQqer7XrA0qVbXdOdNXal/efUpqfxdxlwMug5sFqt3rSwVF\nS9ZbbtGFeQZp4UAG41o2bWvXuWg59QnKOkKleVQ8zlQdPPjHSOz/7YwXcf/gNYqN\nWHSaT8MqaDYY2m0bo3t9SwSMIBxas05guYuBenisOh3jjcaRPYJELucCgYEA1yuY\nvlIVksPGN+vx6RBaOyY86B8MuXyvi0jVf96UvnL7soUIZDDWojGZvlq3r7ol8Ljj\nLF4Wqdg3MxoOfmUDNWCnSZPjsFo1AUFP/2MJiyCDq75yO0CoVj54QsVYkDTnTosp\nat3CKWTueIeOOtXUAKTObMjrWi9A7cF4rS+siB8CgYBOO2UQcX7xwKhhjHBSvBbz\nEEK/QkNJFlmMrtkiSDRGsBcLILetz5mMUYwMDppBhNsic7k60KGAdd8+DKbRdHhZ\noyfKMswYx6de3DF29HzR/3BThdNA87486UWFPVCeY+LYUka8q58rOdrCh2AaB2Ss\nfKzkcO/UwLKSXfTusyuhJwKBgEIEslzSuqPJRawqzJKB3e2AEff2buUKiKHnuvn8\nxQ6aIPfpMWXsRi6FoXJySyGzr6hoUetvAu0h1e3r9L57J7zc5vcAVT/qrZCxBWaK\ncIcrdrrfOBVOBVhQ2n1CJ6Y3VTEYKaEMYWJqAXEhxlXu/Zkk9+EQ1IVbMkTAs9IP\napRpAoGBALd3ZhaP9WvAUVVFs489CKFEP14dZi+SRM6pCsqaQ+zxkW7Cc3Vt72rt\nd+CwIiUMJlac/Efk2Za8MKn/ctMngvRrym8SxGjxJq9jhGo/jSOkHnTy9W//hE/X\nSqlmnJHek5EXl+w0HjgFdukyIo8athDj0jS2ch3OPG4tgavRpKAI\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/mtls_client.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDUzCCAjugAwIBAgIURw+Rc5FSNUQWdJD+quORtr9KaE8wDQYJKoZIhvcNAQEN\nBQAwWDELMAkGA1UEBhMCY24xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\nWmh1SGFpMRYwFAYDVQQDDA1jYS5hcGlzaXguZGV2MQwwCgYDVQQLDANvcHMwHhcN\nMjIxMjAxMTAxOTU3WhcNNDIwODE4MTAxOTU3WjBOMQswCQYDVQQGEwJjbjESMBAG\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxGjAYBgNVBAMMEWNsaWVu\ndC5hcGlzaXguZGV2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzypq\nkrsJ8MaqpS0kr2SboE9aRKOJzd6mY3AZLq3tFpio5cK5oIHkQLfeaaLcd4ycFcZw\nFTpxc+Eth6I0X9on+j4tEibc5IpDnRSAQlzHZzlrOG6WxcOza4VmfcrKqj27oodr\noqXv05r/5yIoRrEN9ZXfA8n2OnjhkP+C3Q68L6dBtPpv+e6HaAuw8MvcsEo+MQwu\ncTZyWqWT2UzKVzToW29dHRW+yZGuYNWRh15X09VSvx+E0s+uYKzN0Cyef2C6VtBJ\nKmJ3NtypAiPqw7Ebfov2Ym/zzU9pyWPi3P1mYPMKQqUT/FpZSXm4iSy0a5qTYhkF\nrFdV1YuYYZL5YGl9aQIDAQABox8wHTAbBgNVHREEFDASghBhZG1pbi5hcGlzaXgu\nZGV2MA0GCSqGSIb3DQEBDQUAA4IBAQBepRpwWdckZ6QdL5EuufYwU7p5SIqkVL/+\nN4/l5YSjPoAZf/M6XkZu/PsLI9/kPZN/PX4oxjZSDH14dU9ON3JjxtSrebizcT8V\naQ13TeW9KSv/i5oT6qBmj+V+RF2YCUhyzXdYokOfsSVtSlA1qMdm+cv0vkjYcImV\nl3L9nVHRPq15dY9sbmWEtFBWvOzqNSuQYax+iYG+XEuL9SPaYlwKRC6eS/dbXa1T\nPPWDQad2X/WmhxPzEHvjSl2bsZF1u0GEdKyhXWMOLCLiYIJo15G7bMz8cTUvkDN3\n6WaWBd6bd2g13Ho/OOceARpkR/ND8PU78Y8cq+zHoOSqH+1aly5H\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/mtls_client.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAzypqkrsJ8MaqpS0kr2SboE9aRKOJzd6mY3AZLq3tFpio5cK5\noIHkQLfeaaLcd4ycFcZwFTpxc+Eth6I0X9on+j4tEibc5IpDnRSAQlzHZzlrOG6W\nxcOza4VmfcrKqj27oodroqXv05r/5yIoRrEN9ZXfA8n2OnjhkP+C3Q68L6dBtPpv\n+e6HaAuw8MvcsEo+MQwucTZyWqWT2UzKVzToW29dHRW+yZGuYNWRh15X09VSvx+E\n0s+uYKzN0Cyef2C6VtBJKmJ3NtypAiPqw7Ebfov2Ym/zzU9pyWPi3P1mYPMKQqUT\n/FpZSXm4iSy0a5qTYhkFrFdV1YuYYZL5YGl9aQIDAQABAoIBAD7tUG//lnZnsj/4\nJXONaORaFj5ROrOpFPuRemS+egzqFCuuaXpC2lV6RHnr+XHq6SKII1WfagTb+lt/\nvs760jfmGQSxf1mAUidtqcP+sKc/Pr1mgi/SUTawz8AYEFWD6PHmlqBSLTYml+La\nckd+0pGtk49wEnYSb9n+cv640hra9AYpm9LXUFaypiFEu+xJhtyKKWkmiVGrt/X9\n3aG6MuYeZplW8Xq1L6jcHsieTOB3T+UBfG3O0bELBgTVexOQYI9O4Ejl9/n5/8WP\nAbIw7PaAYc7fBkwOGh7/qYUdHnrm5o9MiRT6dPxrVSf0PZVACmA+JoNjCPv0Typf\n3MMkHoECgYEA9+3LYzdP8j9iv1fP5hn5K6XZAobCD1mnzv3my0KmoSMC26XuS71f\nvyBhjL7zMxGEComvVTF9SaNMfMYTU4CwOJQxLAuT69PEzW6oVEeBoscE5hwhjj6o\n/lr5jMbt807J9HnldSpwllfj7JeiTuqRcCu/cwqKQQ1aB3YBZ7h5pZkCgYEA1ejo\nKrR1hN2FMhp4pj0nZ5+Ry2lyIVbN4kIcoteaPhyQ0AQ0zNoi27EBRnleRwVDYECi\nXAFrgJU+laKsg1iPjvinHibrB9G2p1uv3BEh6lPl9wPFlENTOjPkqjR6eVVZGP8e\nVzxYxIo2x/QLDUeOpxySdG4pdhEHGfvmdGmr2FECgYBeknedzhCR4HnjcTSdmlTA\nwI+p9gt6XYG0ZIewCymSl89UR9RBUeh++HQdgw0z8r+CYYjfH3SiLUdU5R2kIZeW\nzXiAS55OO8Z7cnWFSI17sRz+RcbLAr3l4IAGoi9MO0awGftcGSc/QiFwM1s3bSSz\nPAzYbjHUpKot5Gae0PCeKQKBgQCHfkfRBQ2LY2WDHxFc+0+Ca6jF17zbMUioEIhi\n/X5N6XowyPlI6MM7tRrBsQ7unX7X8Rjmfl/ByschsTDk4avNO+NfTfeBtGymBYWX\nN6Lr8sivdkwoZZzKOSSWSzdos48ELlThnO/9Ti706Lg3aSQK5iY+aakJiC+fXdfT\n1TtsgQKBgQDRYvtK/Cpaq0W6wO3I4R75lHGa7zjEr4HA0Kk/FlwS0YveuTh5xqBj\nwQz2YyuQQfJfJs7kbWOITBT3vuBJ8F+pktL2Xq5p7/ooIXOGS8Ib4/JAS1C/wb+t\nuJHGva12bZ4uizxdL2Q0/n9ziYTiMc/MMh/56o4Je8RMdOMT5lTsRQ==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/mtls_server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDYDCCAkigAwIBAgIURw+Rc5FSNUQWdJD+quORtr9KaE4wDQYJKoZIhvcNAQEN\nBQAwWDELMAkGA1UEBhMCY24xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\nWmh1SGFpMRYwFAYDVQQDDA1jYS5hcGlzaXguZGV2MQwwCgYDVQQLDANvcHMwHhcN\nMjIxMjAxMTAxNzI0WhcNNDIwODE4MTAxNzI0WjBbMQswCQYDVQQGEwJjbjESMBAG\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxGTAXBgNVBAMMEGFkbWlu\nLmFwaXNpeC5kZXYxDDAKBgNVBAsMA29wczCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAONlOihn9AtXWay72aPDFbwm2zJOe+5ngV1D3B4f2q+KlkAnjAPx\n1GWO1buRDEALL8g5NfbZF3gU9v+wjsEsFK/Sn8ejtziVwJUFVmvRr+PCm/2DEARN\nRe1cp0cwRVQJAKLVFpy19ALlSSTQRoCAjLVXC8tEsIxrvN3DVVet9g6AxnPPd4oR\nLosDGQ+p+qbriQdx20gg5MHmjZX+/ByZq4BIQkshmQW2LnwxAS3xOpqPmFHmdn56\nRXw8JlyvYS3KRiGU3z59uph4wnIic4r/11Puj1LoGd+YtFJash6ZRU/rM6JSdPS7\nb53m8HdRcjGdBG+EnsqN67qZUWbGBntmu2cCAwEAAaMfMB0wGwYDVR0RBBQwEoIQ\nYWRtaW4uYXBpc2l4LmRldjANBgkqhkiG9w0BAQ0FAAOCAQEAMAxCZmKwWEDHuAzW\nPHJUyrdK1eos2UExmeq9TY8c7IluIMClTxS8ui3+7+oc6ZqffyrZL4jx7oe8Ua6V\nbat75H+OF05f9ziGJjJY5PM3wxOP1QvR7HePSkwBkNPhGOQf3bi7rQEhBuqhpRfr\nGfPmzKejaUm9m8IiHnFKxjTfQ7pm3hR8/+P9LKDO21i5Ua3ec+rKv0Y1jsCuv3t/\nAPMN7MTDsFqxudqbOG3dufOSe1E7qs16/ygTRvYpIe+kz4rldGWmo0joOrrti43T\nOi1BAGaC3znJe3aaihr08c37NZ/A6WHiX+h5wBEdboOJc4Htytkicd8jBvU2Svjq\ndZS3wQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/mtls_server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA42U6KGf0C1dZrLvZo8MVvCbbMk577meBXUPcHh/ar4qWQCeM\nA/HUZY7Vu5EMQAsvyDk19tkXeBT2/7COwSwUr9Kfx6O3OJXAlQVWa9Gv48Kb/YMQ\nBE1F7VynRzBFVAkAotUWnLX0AuVJJNBGgICMtVcLy0SwjGu83cNVV632DoDGc893\nihEuiwMZD6n6puuJB3HbSCDkweaNlf78HJmrgEhCSyGZBbYufDEBLfE6mo+YUeZ2\nfnpFfDwmXK9hLcpGIZTfPn26mHjCciJziv/XU+6PUugZ35i0UlqyHplFT+szolJ0\n9Ltvnebwd1FyMZ0Eb4Seyo3ruplRZsYGe2a7ZwIDAQABAoIBAEgQ8sePenaFrnPh\n7O3Li/3fSqS83uYFg6gtM3uQmNv9TfTzE5rEb43oILCbHYjGgtQv3Xxn/Nofus/6\nAqQR9lRqqhy5M/4I58nSsTrmb5n9OTa07MSQQNMjBBi5oZ8qYzs30TzFJZotVGsI\nXu+mzfFCrwgysskt8+NMXqW1CkA50pvipVLtjULZ0p8XQqggV8kQpDGUr4eQ36OH\nekImj4K54GbO4z9IkuiBS/b+J687/hGMPYj5XPS18OU+hQaZnjzWPviAcnsGy8l7\n1dDL9bgUFjGvyVLtK+g4meRYRshymq93e4CdSwssTt/Gnbmc6UxsOhTAW5vzn/e2\nGDShxJECgYEA+1pKXkMdqaj0aIYmGpiaX8FgZvnxruwDXwDIFE4p9zIdrqDAtAk6\nxBBUM6f1+IvvhyqeADOGr78AHFGz8YG0Vcp2+2cw8sRM9ibceIBRwJ/QwW/P1R7q\nN0phykACW+fcGYhb/gyu6HWT9B8NSPBBGggL0LA745E3rSnsIeXGvJUCgYEA55mK\nmwuBzU1yusfb24cYJQqBmb9YleWouEDerrHbFoYCHFi3/E3OyGbuBy2n2wYTS0k5\nPTX8tOWmqM4TsH2JawwMdtNJ+6B8qvAYHSacwLIr4GcZzyJd/ikF3ujXm0da0NA0\nf7rz20kRj8GhcksQTMtWtOXCXJyonNNxZgrQ3QsCgYBjaBcnZoXRtpdKy1tAg3/y\nROlacJlr472FkiqPFUa1k+V3Te5IhanvJsIWV+QIs1c87tbkH3yx/ukNSibPacun\nblZWIT6TlJ0XcNEa+yzZ8JrAFfdtQzfAPDOmqGAGdxFuK6auN9fo6a9lCe7YHOSy\nZeI+W6Sj4KfTXVQdJ+HMbQKBgE8DlEU3VM6NSMIuo3SvD267ueGRZZCmbLyH7TEe\nnsd9asTvA75BcXXvn++1BNp1pSl/TtbyT0gMPaLDw/XnrnVmA+6aQVhmtYHALgnr\n/XjEkLGbmzOO3xByQH1/ZOemHXa2QeL+DmpW8HXiMsmCkIoSqX9ID9p23BO9E6gj\nsoRnAoGBAPkmGcoz96/pb52QyMKcEo1pBK0sqRsfZW1Cpz2/hg2hA3BLLaZIBCEj\ngtXcknib9CLwh4DxBiew3/41pMq1fq1aGTWwf0c9PjolOB4E+Z2NQsqEydTso7jP\nB6M4+3xrWaHkrHFOhOT4hoBgyUJPzQ1fOiSn/0mXHxjfJ2pRF3Xk\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/ocsp/ecc_good.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAWKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjES\nMBAGA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDzANBgNVBAoMBmly\nZXN0eTERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjQwMTEyMTQ1OTUwWhcNMzQwMTA5\nMTQ1OTUwWjA9MQswCQYDVQQGEwJDTjEWMBQGA1UECwwNQXBhY2hlIEFQSVNJWDEW\nMBQGA1UEAwwNb2NzcC50ZXN0LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBIuZCmq/vmX5LqFrpa9ot5SboEhNyS/r5UGT7akjIOAXBVwZkn1vm/EsQp9VMF8y\nrWZkGKFmElo0ZAXAyhjn9D6jNzA1MDMGCCsGAQUFBwEBBCcwJTAjBggrBgEFBQcw\nAYYXaHR0cDovLzEyNy4wLjAuMToxMTQ1MS8wDQYJKoZIhvcNAQELBQADggGBAIEm\nLKKS+eGBazPpSRvq2agnqmjM+PHVWRB/O/+LNOO69Lji3wRtq6T2zNHPZQXw1OMA\n3C9HcIwaawTyb+hm+vX8yBr5mgS1UOtmDYzbnlpERjJBjxmPXTZLDbzogHshbabp\n227p/IAjWm/2F2VPXjiX+aV1pYrhCcO7zUtBEu9KaoG3Amxg8T2WVamTV+J6r0SL\nfkvYItZwbawSfwQlZ+22H4Mttu/bd2USTusT4zLAflv9UFh20bA1PizvcKK1brWS\nIH2rxxSLCvu2wmrGsrLVn+9yD6xNsn4m6DyCWx9S/Tas7KLub8BjnCzP8YEvrVpV\nfotefEMY5h0waj9Zc32l+6gk8Ntyp2ozWi+iu4eo0Y5SUqHlPjuGUXOivp5o/6b0\ngF5M9jtkXvbH2ffrOiz9YUo4fVwk6ws5OQTr9WsildEHZH4ADOW6HqPYkOnuxhdM\np6JP0LmnO/S60/k/ZH8nMTcSUfE+qcDg3LlH5ay2fv6IKz5BaVkyHPNreRi9qg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/ocsp/ecc_good.key",
    "content": "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIHMwGqSAcIFnsy8Sa6NxlSmGuOXV13SbZbZVIobN+3xboAoGCCqGSM49\nAwEHoUQDQgAEi5kKar++ZfkuoWulr2i3lJugSE3JL+vlQZPtqSMg4BcFXBmSfW+b\n8SxCn1UwXzKtZmQYoWYSWjRkBcDKGOf0Pg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/ocsp/index.txt",
    "content": "V\t340109124821Z\t\t1\tunknown\t/C=CN/OU=Apache APISIX/CN=ocsp.test.com1\nV\t340109125024Z\t\t2\tunknown\t/C=CN/OU=Apache APISIX/CN=ocsp.test.com2\nR\t340109125151Z\t240109125151Z\t3\tunknown\t/C=CN/OU=Apache APISIX/CN=ocsp-revoked.test.com\nV\t340109125746Z\t\t5\tunknown\t/C=CN/OU=Apache APISIX/CN=ocsp test CA signer\n"
  },
  {
    "path": "t/certs/ocsp/rsa_good.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDxTCCAi2gAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjES\nMBAGA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDzANBgNVBAoMBmly\nZXN0eTERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjQwMTEyMTQ1OTA4WhcNMzQwMTA5\nMTQ1OTA4WjA9MQswCQYDVQQGEwJDTjEWMBQGA1UECwwNQXBhY2hlIEFQSVNJWDEW\nMBQGA1UEAwwNb2NzcC50ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAPPXWs5Bb+OuZv8D1dxReaZYiNN3gTCi6QuC7ABKq9ScufBFhdvT5K3I\n6QbzphGJSetio9AS46ztX4tp2vXA20Wyw/R97reVt1lYRNCSaDqVqmdLqBDeG8RP\nloLHAA+wq5fCJSm4DTHKPgN2t/KPb/D6f7x4mmNBQ7wdlxa0i4r9lWESWGC7maq1\nSkf9W6iMyYxP4OhDaOekMaHDA3zDijq4tO10JTHG+4L5WsZ6qFxfqJmrfsr9uwgG\nzuX4m3PEau3KqSWcgPosm2vxSOJJWUja5PQi6W0xbCOtrRyF/HEWBaxBJB9CayXC\nhFvmTrViaazW7OuBNcqSeIoLktQqGOMCAwEAAaM3MDUwMwYIKwYBBQUHAQEEJzAl\nMCMGCCsGAQUFBzABhhdodHRwOi8vMTI3LjAuMC4xOjExNDUxLzANBgkqhkiG9w0B\nAQsFAAOCAYEAkHi0FLFQbPANUxXIIYjR0dVt5xb0SoAet0TxAAzoJaO1v7jrXok+\nDBZu9dOftDIX5jB8vne7JSKCl1ibpsYqpOW9AjFjUTtKkirXcsKQKs+sW1Vue0uu\nxPx1IAbI475X8emIB3vH5S/eqe8ep31pJkFxoiWSafKB9gpXzpD6NEteLr6oK67F\nbdIZHEdxIuiu1SQEeN8ShSoIWcVkWavsP5ziXhi+PxK4CKYQoHyFoBFWk7SXhCCA\nmKhnvcOjR9Cq/ZtkAe/G31x9nYQ6blJejRDxHOqgK+eke9+8qPx2oTLwraodPRVv\n0O5NpI0SQw8+5KcWpz/vq0NZFHh0SqSh82/IJvgxSab51VLdU2lxNxsllTNpDN9F\nLtXT5SRgRy/gXs6bOq6tszHTNE7t6hlCGlWfaRNUHfRsdyOfim0JwOpusmE0yR7R\nv6jYCk8LyJM+1oppp71cUtzrMxWEn/bC0M9TQuwb9fHFgEFU1VWjJrcaSfFGf61m\nuzgYQtn5uERq\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/ocsp/rsa_good.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA89dazkFv465m/wPV3FF5pliI03eBMKLpC4LsAEqr1Jy58EWF\n29PkrcjpBvOmEYlJ62Kj0BLjrO1fi2na9cDbRbLD9H3ut5W3WVhE0JJoOpWqZ0uo\nEN4bxE+WgscAD7Crl8IlKbgNMco+A3a38o9v8Pp/vHiaY0FDvB2XFrSLiv2VYRJY\nYLuZqrVKR/1bqIzJjE/g6ENo56QxocMDfMOKOri07XQlMcb7gvlaxnqoXF+omat+\nyv27CAbO5fibc8Rq7cqpJZyA+iyba/FI4klZSNrk9CLpbTFsI62tHIX8cRYFrEEk\nH0JrJcKEW+ZOtWJprNbs64E1ypJ4iguS1CoY4wIDAQABAoIBABV8oZzROVnX0W2h\nWeQLLewRmyT/P9wYTu7bv44bBl863Eum5K/FUT5bGOWq7LRY47GhRIweTf+7/xJa\n5peHQgs3QHs36aQ1xi1SUOYMMLEQ5S4rBYlO+SVoWfv2KzQ2vjgmPH4boNYFW0eU\n24q9RwD2IfFqszgR1TUralfu2ukJWWi1zuNN4jJZfXZWcPLV26wMAdQrGhf7CnQ7\n1QN6SFNBKxpiKIXCs7ki2VmzAVg01FpmVsHkuRvn80dZ1FkkfXXaAYqNEulisAq0\n+Zv+QWXrK7IpmRpJQszr7VBGLlBrMTp9wFq+ep+Bui9e26pGXYETPnchUqtBFk5k\nyylxduECgYEA/v8cJEX/W8H2//Dl2tOq+F6RKwxX56dlrs6LBSpYh9982CXrALG4\nXuq8VmnjYYpdVxSyxMkex+Dih06BpQMmQci3mzHPGAPzkiFSxsogQBlB1MF4RphD\n4UutZNdsmQb+l2NTGIyQR9TgLXjwKfEesia91HQGCTDPFzku/Xg0WBECgYEA9M0B\ns9sLb4DC6LDoGlSlmIfKmJgZgbwjRhUI3Foplpzzrc+A23MaYbuhwVszch7S0Tib\nOtEtJWTjryGAG5a/eCsVHtyAnJWxJiHV8yJ+xN1MTXIh9T6Xlablip8cBaHKZxLC\nvhu5ZEIyGddYa2B6hG/x1ydMoz5pdMFGjKkFNbMCgYEA55HNgLOAn1eac/vVAdDP\npxZaRvnCqsE+em1fmqVGGL5AphppPAwpHymVN/SZZe89rONDJapvpZz4m2AUJEKj\n74HUG8A0Dd8ox0Az6AuPFibZvdik3ZdRrbwID1gDa0UK13h/8f9U16benu0BTVWH\nRsogAlwLTzVgG/r2TYFoJ8ECgYBa/u14Fp88lmddKY1NZFOdzDQh3r/0eqO+BEmj\n5xv4cWUfIbfrWvDejWmGP0lzTUPeI6WICoM2mDcOPWyqVLHdkF4sd5iTHA2aeA9Y\nbmUi9oPLcfZvfBHKvhwrGBPJgCeFgvLCyfly7CxFcMfcOiOwoRALgv842xVGIiYA\nWT+ngwKBgAk1xBsENJlEGz6aeoby6ELx3QP9cQX5GG7YtPrrl9BziRIPc5YNJZfA\nguw8rBxO72ilChrJIMfE8PZx6L4LJ+N1VTRgZ9T8F9ZGopqUtquc4OErmAYR0rH2\nll/i9QPgHzUYm4L7kN2J/cejJnzhANnBiJbgE5wHUHsjT5sv3trO\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/ocsp/rsa_revoked.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDzTCCAjWgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjES\nMBAGA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDzANBgNVBAoMBmly\nZXN0eTERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjQwMTEyMTUwMDA5WhcNMzQwMTA5\nMTUwMDA5WjBFMQswCQYDVQQGEwJDTjEWMBQGA1UECwwNQXBhY2hlIEFQSVNJWDEe\nMBwGA1UEAwwVb2NzcC1yZXZva2VkLnRlc3QuY29tMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAtjAaX6JtUhjXkxqtOrwvpehS53gXxRioiY9sbklUKflH\n63efdi4BY0IBs7IVLMo4JUbQPe4FFWFTBMDSOoyaECSSw7AJear3XZCB9H+b41rG\nEXi574WNnMmRITwXDr7fJvA9QIvZK1rotUXqNNHwb5tP1VvJJh3jCRj9mzv51MQb\n8HtleMntGSRGLSxEdo2bL/u33FwALV5Ayqgg6bf0gPPGc4Lqc2xwFO0HvF6ooiLj\nlQjmsqkI4/jE0ElCJSHuRcbq4H0Q1riCROnmSrLRiSdEFnAUT/cf2sR5NZKsTAzO\nVwIkKjigdUjyEUN4xWberGtGzwxgfG4iiSlVht4pRQIDAQABozcwNTAzBggrBgEF\nBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly8xMjcuMC4wLjE6MTE0NTEvMA0G\nCSqGSIb3DQEBCwUAA4IBgQBBbfOcWr20MQLWjijsOPiQGWa6Z+z8qMkgSB1Ukhqj\nqhuYA/bVcMcftqCCcjeAW4fgN+xBuLnHwS+iIdCjUSV8kbN19KGJNR27kDI/ShgY\nHiFNqEjDOq46jo/CMJap3VO4ZdvH8+3Dk3KuOCNBej/Oe3XD5Aw4jedHxHgfGWqt\nFD3nA+lZOvUVe7qgSkOOPtWsyX3xx7cvWziXHFd6TUWhSfcRIORO0ZHMF80ipNgd\nKgUe7t2pOuIN8sOx98j2MHNMFQVEPZ+EweznVOvWVqbGzW5wf3pUz/Vbb+uCR1LQ\notNEEbENAEEZQ6sKpZ0pe2xuuHT+KOQ20Ty79Fs2ji9R4maiD0NTaVy2/oqYrs3G\nOFA7OrPSJ+HYKCq9QP6Cu/wY5kiG328SeoHNaXGltzCxvqE3DZNzevKh9s88SBjL\npZ1hHUH++Co3pCss+ZPDjkWUFnbg7v8altE37ksdYMXOjY1OStHUzfZ4uYeC9orx\nGm5X8AE3zIgpNdiANrO/ook=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/ocsp/rsa_revoked.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtjAaX6JtUhjXkxqtOrwvpehS53gXxRioiY9sbklUKflH63ef\ndi4BY0IBs7IVLMo4JUbQPe4FFWFTBMDSOoyaECSSw7AJear3XZCB9H+b41rGEXi5\n74WNnMmRITwXDr7fJvA9QIvZK1rotUXqNNHwb5tP1VvJJh3jCRj9mzv51MQb8Htl\neMntGSRGLSxEdo2bL/u33FwALV5Ayqgg6bf0gPPGc4Lqc2xwFO0HvF6ooiLjlQjm\nsqkI4/jE0ElCJSHuRcbq4H0Q1riCROnmSrLRiSdEFnAUT/cf2sR5NZKsTAzOVwIk\nKjigdUjyEUN4xWberGtGzwxgfG4iiSlVht4pRQIDAQABAoIBAQCQ5Sz0hl/ffTZm\nHj9LiUNz9ZOJ1+8/p97SmKiqBdPUFhfm45qFCQ29fU+RNL62gpWov+r6dgTA/khi\nbWBFhHE7CXtX+vduNlTJqxZP9/VpGlaQqq1mG5eG7KBqCDpmVdNwSnzMiuzLGGAf\nW11raNSKTsFtdLRDhl18bM212jtVxNHVJ6itFZa5Ls0/VrD2KH9PXP0J5jugKYqi\nGzHmt9RfmjhOPcAIOTYNP/5n1CiHERj5KytfXjN1BHWc3rx5uakaUntBkdevXWj+\nZSspRi/ReuVCXWeldmi6Pg9hQSX3MGVndAx8J6ilmtqSrRvaCBRU6x9KLfuTVEz8\nUXap8UkBAoGBAOkjowQb/nWI5oqDs3SduXwVE+0yn8kBCk5x48ee1Vt0eCxtnuRs\nqdPuldh6czsBj3ijmJ8Ny6aHQaaVuIlUbPjBDZGf7IVc0hj7sQ6o5Jcu9KmLLOBH\n62fQDeuzM+EkGOcnH8aPvs31p5bMklhycyyyTXvmZba3hvST+Ske/3olAoGBAMgN\ndtFrvAQoZMIV61kNTml3ehhUeWas/ry0WUHa4iaDnVMoJ0fwiMKk7kQDvJH8gxx3\nCr7dSnI17yBYEn0HkXlTMq0IgjJp+K40temMFwMFOWrTtGUVcbibv1Yjx+8i7m2+\npWnfGXGd6tolWRHAMe7B68q+x1iJEjQD/Ujx3XihAoGACJg3uj8N8mdJmHGie/oU\njG56fZQQL+jJ6HpqW0GPu/9fLsQbx2/6EsYI4CIjfVlhYKEnTzXC/DCgSvPaCbYD\nDmiPh37NyVzSofklXdT8GFayzk1DKkF8fCc/XCEPGI2sHVlj4n4KGq2jr/t6qagO\ndudb0+V6enHpl7qcxNdPs8ECgYBYM8+CUAzKjIC4Le/hCIPc7kePuJb6FSYPTzjX\nV0lEj9zqkBaZmkzB/PPsWvVmLD4ma7n6Ixkyt+LhkNM9+vtB0dPTBKBa1+xD6ouW\nGCUBOOly1zp/IvBL46d9tDLvlagoDNljj3DpbiXg3nyh3epmCWwLrQe5Wl4DPwsK\ngVETYQKBgDvVy2JvG51tmrqg3bEkJ+8RWELK3DeJphVmlD+unkA6ONwJTB8gvDIE\nmkrxulta1cgg+u3+oJ1Pbo6P19v6xOj9vr1NHmHTd6shpfx8cHVbOYo7tBX0zKcv\ncPlhtyGb3LUmaHXc22qI3ooYAKUo6r0bsK6ixBQEQUyxxHOt2fz7\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/ocsp/rsa_unknown.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDzTCCAjWgAwIBAgIBBDANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjES\nMBAGA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDzANBgNVBAoMBmly\nZXN0eTERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjQwMTEyMTUwMDM0WhcNMzQwMTA5\nMTUwMDM0WjBFMQswCQYDVQQGEwJDTjEWMBQGA1UECwwNQXBhY2hlIEFQSVNJWDEe\nMBwGA1UEAwwVb2NzcC11bmtub3duLnRlc3QuY29tMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEApuHmwZFhVmjPhNRmQtZwDpVa6BesX6Y6mBJknQVRZN52\n5Ac9usp6cGBiYY+2iNV3T5EcC0yTgJhrKK0Oz1WDEAYSH3I3TwMYmFdx4r/cuaHE\nsxXZggkGHPaVmzFOX9wSfrB+/TERByO4FIiY1/pTa/vDWy8cNYUYMqZzeJq52zpt\nXSk1njVgjQ9pJIyZmTMADqfpc/OTdiG8kXXbrAnMuMYyNExDDcTd6eJtRmks+Vjg\n95IbM8x8E/hn+QpXRCb1Nj6Fj7D0t9yC2/bQVdBJNkAxjEofuCbpR3Pn/p82Y84y\nOuhORvdXmBWm7RchOf773cxat4R+I1Dec3GU8vpQAwIDAQABozcwNTAzBggrBgEF\nBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly8xMjcuMC4wLjE6MTE0NTEvMA0G\nCSqGSIb3DQEBCwUAA4IBgQBSK6ACelC4GVbyyaN8QKwtzOkQJtbn5lMyo6YmgqAD\n00vyrjPIc5GbuA69Jinu9mty3AMMn+UFqudWHfLXGSAl0M1LUWrxg0Qa6llEB+Kh\nf6EmDNciWUK7kijuraqPxVxB4G8ZebS+SjaeptgqIW54fMQMOOzmG9DldOvIc5FF\nZMzHYNP5QTHaeGki9KxZmfxt89lTYi6ZvViW7mjpxSbecY5H2DTFWIKD7P8seHVZ\nJp4laPyAWDA157zpIvyK/zTNqnE+85ZJ2c0MrVWFXwL/7InViHASZriIOaOUBs/g\npE6RTrwpU9JhjmdYtv39SgdLAInoaxmoPeNZmr4tefLrXwn9oRHnk6RInQNSffam\nvxNxD/ZKNPDZwf40ybWH5JG/SyrQr0UJAT3PWlKxHwbAz/4f6z0E/byR5nJrdFSh\ndLTbfJZ5h0vaBrcBeg/NXSvW7znJWtX3NBiUq3Ns3gAJ1y8usKKWwbCsbKzUl9j4\nNoG6Jv5toAlmtCmhVuUkX5g=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/ocsp/rsa_unknown.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApuHmwZFhVmjPhNRmQtZwDpVa6BesX6Y6mBJknQVRZN525Ac9\nusp6cGBiYY+2iNV3T5EcC0yTgJhrKK0Oz1WDEAYSH3I3TwMYmFdx4r/cuaHEsxXZ\nggkGHPaVmzFOX9wSfrB+/TERByO4FIiY1/pTa/vDWy8cNYUYMqZzeJq52zptXSk1\nnjVgjQ9pJIyZmTMADqfpc/OTdiG8kXXbrAnMuMYyNExDDcTd6eJtRmks+Vjg95Ib\nM8x8E/hn+QpXRCb1Nj6Fj7D0t9yC2/bQVdBJNkAxjEofuCbpR3Pn/p82Y84yOuhO\nRvdXmBWm7RchOf773cxat4R+I1Dec3GU8vpQAwIDAQABAoIBAGVK6siFGKLdPVBv\np55cEGoZp7MGY38vI5OYXm+cgboK+fkQmBxfuA+rwStckrvdbezitDX7hfBhE3H+\nEOYyDjpUpP1nU0DnLS+SrDKoqC4YjY7x7TLrjUVZOpeXRu4SYzt4n6vI83/040+7\nVaKKc8Ywa3RWVPX7UiO0OpRyverdQgXKxaIKukO8v1IhMQ5cshvLStFpJSLlN+Cy\n2BO3uGSth7dLHG/OIz8SoGp/m5ofpBUny5FAb2EWZM/eGP/VZN2hVrYfbx5d3dVs\n8AtuboH/LRCbh9IE0P5mOIZFc4r1VodozkGP83fMYiEP61pyEVDvTFcvZizJRN6h\nLDNlm/kCgYEA0FdQvOjKjpogUHBJkYrH9yuSlFIwzqp+I1ZsPBUORt/czfOQapf4\nHjFtUj6JvqZW3xn7rPiwTnXys9x6KP5xzjbia6or7dL/D1q7bk0oKvqhVF1LiQ2X\n545a3zErLpkMmH9fSVVyTC4x+hIfN0PlrhXh0mH2urT2+K3v7j/4408CgYEAzQ64\ntzs1zVGdIgRmiQV0eP9qoB7y3vmcxZq0NKjNyoMbXhuo3muGY33AzCY6qSdFLigI\n8LLZms/7o5VYp9ckaYPnuYwABzXrxN4fz1vSAe0y64X8i7P09W+pB8uh07nJVTpJ\nrSC+E4fNgvnvnaVp30G0gRj29OmxMrWQt8gvqw0CgYB88mCxastQCo8mrrDwYFLc\noX0fBsvOpeFQQBxZTCdrygYaXeBWjR14vhvaHzds50ViN6sAaYUTCRmtVKTOwQpv\nqerQtxXxY4EkLD4MQKm+XOE0P191qnlXncBR6qMDJzaunnT+/ge2OF4wo32lH0s3\nxFfSXH4kKzOSoH4sXKFfcQKBgHeB8+9+B54wyYZQ0D1dO4NlQIwvXVbMXSzhO9NQ\n6hbzkBipwCJYwkrruFiCkz+QToZW+NbnNWE/g6XT3YZ8IZGJOZzu1fld2Jm05w8f\nsWZECqAvR39YExSTzgxoBllx9r/AJ75Jzd1uET0bUyYqiGiAT6XJmewk4ovuO3iQ\nqA9lAoGBAKKmSj69vgldBUuLQx5AGOb5ivIou8M4f6yJBKkU/fSuUjxy5EVvAOTe\nYYtZVcNymzSdk8SXEg9krCK6QHDa/H6M8bLu/aJHk6pXwPFRWPG2TyQcYDg/ItlV\nFK6rvRA+dxwFMi5p1TwZIE4gVkqExSWD77jgxfA8wfbSh2B/BCNi\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/ocsp/signer.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDqzCCAhOgAwIBAgIBBTANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjES\nMBAGA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDzANBgNVBAoMBmly\nZXN0eTERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjQwMTEyMTUwMDU1WhcNMzQwMTA5\nMTUwMDU1WjBDMQswCQYDVQQGEwJDTjEWMBQGA1UECwwNQXBhY2hlIEFQSVNJWDEc\nMBoGA1UEAwwTb2NzcCB0ZXN0IENBIHNpZ25lcjCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAML7FQ6P5418DkSmB6tG7Rlhc9C2rfuu+xDOZIUW2tm++6xS\nm4nh+9ZeDaOX5c9oBSN93VNaeKtd8ZAAaRjToJKFJbTQ+/ELGbUm8mISMIKrpebk\nBWp/S+VavLYZiGjj3FiFtIZ0+RL+z2XarQcnTqcR2qlYWVDTETzHmxJiDBJ82+if\n/L5VffS2RagCDUGuAfm/XcwI8pIkHNS6Bs4TL5ik7OTuX0e5YvkwCDiIhfzLm0fM\na54Z4ImvLFfGtTQRILkTltr1RGioPRQAzvpy8zpSgux5Qd96HAT6hF/xlJKHCCkr\nxBiBUUtMX5fdw2AI6sUHig5v0u+bbU3nsMF9aiECAwEAAaMXMBUwEwYDVR0lBAww\nCgYIKwYBBQUHAwkwDQYJKoZIhvcNAQELBQADggGBALd3mJXvzu9TDTqfGZMkfGqD\nhv/GdV0uaPpNlPZyOkS31t+iLd5R1EZz8wKLegvOdm3uKA8NFx9uB7O6mWSneJ4R\nVbrcJnFzl5C/SbgWIAt/N0uujO8xuC5YWlHeig5IJc8pmnacJ4y7FhTVfsw1u93d\nevGyCIJ3TyYcZTDVuZruaW+xIRa+QJt3Q+CvkMn+aaxs1ji08ZjodGVu9+jsbb7f\nDLgl3FuLf8DlKqhRh+hoOPUTI496m3ZTozb7cs0TfHFPSL0pZ2Rdbz4WGHfX0LRF\nE8lweAsyd12OySr2PwiRY8m18t9HrPNbk7bAaVJLqehUjvB1Am5+Lgjicdy4HNhh\ngDlt5hjLV2criJpq9QmKz7Veu2V42FNJR7DumnJsbUjBVlh+rgN42j8QnPAqylZ4\n4gcjhu1cGJd0miEN+TjzzqdZqjVvINepBR1j6pOF3fdfea8IFtJ1dzTpkU4Bq2P5\nCYqmKqhkAkEIYbwQ4fqehrcMrAAno3/ikW5PqxetnQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/ocsp/signer.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAwvsVDo/njXwORKYHq0btGWFz0Lat+677EM5khRba2b77rFKb\nieH71l4No5flz2gFI33dU1p4q13xkABpGNOgkoUltND78QsZtSbyYhIwgqul5uQF\nan9L5Vq8thmIaOPcWIW0hnT5Ev7PZdqtBydOpxHaqVhZUNMRPMebEmIMEnzb6J/8\nvlV99LZFqAINQa4B+b9dzAjykiQc1LoGzhMvmKTs5O5fR7li+TAIOIiF/MubR8xr\nnhngia8sV8a1NBEguROW2vVEaKg9FADO+nLzOlKC7HlB33ocBPqEX/GUkocIKSvE\nGIFRS0xfl93DYAjqxQeKDm/S75ttTeewwX1qIQIDAQABAoIBAF5EUBDjSBriYG+W\nKd0IBHeh4wGEYKdvGNkuP/EMdLCTok/U/Hf0NvKUNFnkhWn6K4nWP1weQHrxh2mM\nmUM0hcxw7SL3audF657mfoclrihu3l273lZ3xvTTIquTupyjlZOCyR28jfM+GH1w\n9Piha2hgvGvlWAE4mnvdMT75AkcpGEDvKrJec3Kkq7DfnW/AotVc8yJG0Q2JwkRJ\ny0ITJBWkA+WtLscqRyp4tvSAtAByBdynuMpEskIrOOC7WtxLJD+JJtzO7H31irC1\nON3x4czwiVKbVjxworoD5R/2Gy7JKK+pou0TNTcFrFm/+mU0Ig3VkcZTlDSTWJio\n4D3CinECgYEA62fnLuxVOP9X/lgkKjN/xtTMqcDHOXz/1tyMewj6jRzFWu42b8k1\nECDtzx1Aug8cqsA7pUvBxeC7DzyZmo3zybHRsUf9mQzUA7WK7RN4SK0O4FOZ6PbZ\n1116aZqwIiYpMhXL5syC+5yVWEJkbtClMciCbDlp5Y/+PISHl24assUCgYEA1AnU\nAsDKWysYoVqePfBoLDaep+5Q9VQr8T9AyXlvrqtpmLiBZR8Oh4iOGDJmFSsQOQWP\npeYIuf9eTXh6DH0BhIr/wSbhleiS/ibuOPEUosnwUzC64rkcgXzofbKOyHig5o8y\n45XGUzVSJQPBQM3fVEyuGV1vZKZ/2CVhnFBl360CgYBP1bMPtNLKO77J4XaSYVjK\nQ80NHPXzxzK02aNC7q6aQNGlnvgTPTejuqcsAI29C/b66arQyjpzM139Mt4dDltJ\nYebtqq6Uw0b74wu0j0/Rxe8voOqnmWATq/4h5nYpfqul8sJuCZm6X0Y+4nVRJ61+\njrO8pFQHqKfeOkwJzSt8yQKBgGCNAR8nznzpCNQgQUIPAEBxtpjdKbwsUb4OgV+8\njiBJKVJDYZg8Jg+NHLbj7BvjegWdBKYUMxEOuVApddnN6i0CZib7n2j1eEmGTJ9d\nF3pw3Z/j5pVqmRJVYEAsWFvsoceamR+MibxF4Vu9c/ggRntKV1RxeVGphzlS/DmD\nWoAZAoGBAL9bHijnEzrijuWa1M9LV1eTCQyr0bihL86Z4pEI+B2NX5lWpcOQi8IU\nW4wbN02e8g2u9DGgm6yg2eNbkVg4XWuaXUV7a3fGwnmtuWnxGhU/37Lrcou34bsM\n3TvP055+kATwZ98X2MzvAUDIKdO9k+/s41H33frdJQgwCH5ArWGp\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/openssl-test2.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n[req]\ndistinguished_name = req_distinguished_name\nx509_extensions = v3_req\nprompt = no\n\n[req_distinguished_name]\nC = CN\nST = GuangDong\nL = ZhuHai\nO = iresty\nCN = test2.com\n\n[v3_req]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = CA:TRUE\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = test2.com\nDNS.2 = *.test2.com\n\n## openssl genrsa -out test2.key 3072\n## openssl req -new -x509 -key test2.key -sha256 -config openssl-test2.conf -out test2.crt -days 36500\n"
  },
  {
    "path": "t/certs/openssl.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n[req]\ndistinguished_name = req_distinguished_name\nx509_extensions = v3_req\nprompt = no\n\n[req_distinguished_name]\nC = CN\nST = GuangDong\nL = ZhuHai\nO = iresty\nCN = test.com\n\n[v3_req]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = CA:TRUE\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = test.com\nDNS.2 = *.test.com\n\n## openssl genrsa -out apisix.key 3072 -nodes\n## openssl req -new -x509 -key apisix.key -sha256 -config openssl.conf -out apisix.crt -days 36500\n"
  },
  {
    "path": "t/certs/private.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA79XYBopfnVMKxI533oU2VFQbEdSPtWRD+xSl73lHLVboGP1l\nSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7JBUXyl6pysBPfrqC8n/MOXKaD4e8U\n5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4Clrd7shAyitB7use6DHcVCKuI4bFO\noFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHAM+47r1iv3lY3ex0P45PRd7U7rq8P\n8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1+7njrVQoWvuOTSsc9TDMhZkmmSsU\n3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbcBQIDAQABAoIBADHXy1FwqHZVr8Mx\nqI/CN4xG/mkyN7uG3unrXKDsH3K4wPuQjeAIr/bu43EOqYl3eLI3sDrpKjsUSCqe\nrE1QhE5oPwZuEe+t8aqlFQ5YwP9YS8hEm57qpg5hkBWTBWfxQWVwclilV13JT5W0\nNgpfQwJ3l2lmHFrlARHMOEom5WQrewKvLh2YXeJBFQc0shHcjC2Pt7cjR9oAUVi6\nM5h6I+eB5xd9jj2a2fXaFL1SKZXEBVT6agSQqdB0tSuVTUsTBzNnuTL5ngS1wdLa\nlEdrw8klOYWrUihKJgYH7rnQrVEVNxGyO6fVs1S9CxMwu/nW2MPcbRBY0WKYCcAO\nQFJ4j4ECgYEA+yaEEPp/SH1E+DJi3U35pGdlHqg8yP0R7sik2cvvPUk4VbPrYVDD\nNQ8gt2H+06keycfRqJTPptS79db9LpKjG59yYP3aWj2YbGsH1H3XxA3sZiWHkNl0\n7i0ZE0GSCmEMbPe3C0Z3726tD9ZyVdaE5RdvRWdz1IloA+rYr3ypnH0CgYEA9Hdl\nKY8qSthtgWsTuthpExcvfppS3Dijgd23+oZJY2JLKf8/yctuBv6rBgqDCwpnUmGR\ntnkxPD/igaBnFtaMjDKNMwWwGHyarWkI7Zc+6HUdNcA/BkI3MCxwYQg2fr7HXY0h\nFalewOHeJz2Tldaue9DrVIO49jfLtBh2DYZFvCkCgYBV7OmGPY3KqUEtgV+dw43D\nl7Ra9shFI4A9J9xuv30MhL6HY9UGKHGA97oDw71BgT0NYBX1DWS1+VaNV46rnnO7\ngaPKV0+bTDOX9E5rftqRMwpMME7fWebNjhRkKCzk7CsqJN41N1jVTBJdtsrLX2d8\nUbY6EpjogFJb9L9J2ubUqQKBgQCk6oKJJbZfJV/CJaz6qBFCOqrkmlD5lQ/ghOUf\nEUYi0GVqYHH0vNJtz5EqEx9R7GPFNGLrGRi4z1QLJF1HD9dioJuWZujjq/NgtnG6\nbgSXJqJc52Lc4wB99AyfuL2ihSrTFmjSRx7Puc9241hTha7Rgh+vNOkq2HsH9FR3\nTTRv+QKBgG5ph+SFenSE7MgYXm2NRfG1k8bp86hrt9C8vHJ7DSO2Rr833RtqEiDJ\nnD4FbR0IObaBpS2VJdOn/jBYXCG0hFuj+Shxiyg/mZN0fwPVaRWDls7jzqqPsA+b\nx3XKRAn57LY8UbsNpOIqZ8kjVLPZhgfYwfOI3yAeSMv4ZnRY/MWe\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/public.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2\nVFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J\nBUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C\nlrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA\nM+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1\n+7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc\nBQIDAQAB\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "t/certs/server_1024.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBrTCCARYCFGohAv7D46F+kSOf08X/MLwtazC7MA0GCSqGSIb3DQEBCwUAMBcx\nFTATBgNVBAMMDGNhLmxvY2FsaG9zdDAeFw0yMzA1MjIwMjQ3MzZaFw0zMzA1MTkw\nMjQ3MzZaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOB\njQAwgYkCgYEA2YEV7+FWPl2R9EOSvi2iPyymiUnSaYhIaTuSoqRISOjCSmgrmKpJ\nyDN1Cg2hqBHPkWW8BuphpV405ja+94xvOEc0qcP/Or6zDhDfWcaoqxGRAcdwHDgQ\nXYMfOxlhwWCp5+vWKep3FPXpHamE09PqUbKWqIa/16aK/1sFR7Q+JJkCAwEAATAN\nBgkqhkiG9w0BAQsFAAOBgQCA5aSfk4lZjAEFQ9mWN7Z97b9bnwLw2bv4PgAhDtas\n0IxIvhuwR4ZTFv+Pe4PNNNvfnMgTRUWFVpjywUX7Km+k9WFavrikPgAGEfzkR2HR\nWE4ETNuS7sGxczosqD+00An4lZ+58uYGEitUOJ6xO80NIhNnOGgo5N/d4fFUTyYH\nbw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/server_1024.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANmBFe/hVj5dkfRD\nkr4toj8spolJ0mmISGk7kqKkSEjowkpoK5iqScgzdQoNoagRz5FlvAbqYaVeNOY2\nvveMbzhHNKnD/zq+sw4Q31nGqKsRkQHHcBw4EF2DHzsZYcFgqefr1inqdxT16R2p\nhNPT6lGylqiGv9emiv9bBUe0PiSZAgMBAAECgYBvf5j7S4ymk9kKWsmS7FnMANuu\nbUWMC+zy5TMaZRUZKxjOg/A1ZrZEBvzslmhUfCzn4DsvYF+GInEDwvTKehdY07I+\n+hpv1M9Wa7v12ofRWNvZjsbMHfiWM/pFBZFYryV4sQ4qHFRj3TKgXu3pZWPx41wn\nayMUtxYRR3Lez4UiMQJBAP31OIC65xbWGr1W/YJ6IwOPFBgyB6O6qFTcR/lAdJ6H\n6MVVs8XEWC4o/ZTk8RGog2nWzVsRCN2pqQUGUHBGRE8CQQDbQNL6eGbsuSxM1uUS\nPjrAs5t9rfrwpx41ubjRoGIukEYeX1YXDf4WICe/51vE3jvVfVvFOJuvGoO9QqzB\nLgaXAkEAtyRG0R74VBGnSvAW9idaZNCj7yb1N2/+wOPyy59d+o2MofLCKFcGOJO6\n+8t2xgM+ce9EPO419JTLnSIGlFE4JQJAGueHfCjOKHpIj11HWse8Ge1wRSnWQzWe\npWUW4tJVefVGRW/ZdpbG+RwVBJ11S2Eh4n6xhi/+GqycQdsuq73kHQJBAPCknTOP\nKpiH5qtKw84nsF+RkWZQ6BhzvP5OuW4avQBx1jQUjpjUhOWfctFH7MLI92Ti+iUS\nMYi+HgfT9Lx4kbc=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/server_enc.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB2DCCAX6gAwIBAgIBAzAKBggqgRzPVQGDdTBFMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEPMA0GA1UEAwwGc3Vi\nIGNhMB4XDTIyMTEwMjAzMTkzNloXDTMyMTAzMDAzMTkzNlowSTELMAkGA1UEBhMC\nQUExCzAJBgNVBAgMAkJCMQswCQYDVQQKDAJDQzELMAkGA1UECwwCREQxEzARBgNV\nBAMMCnNlcnZlciBlbmMwWjAUBggqgRzPVQGCLQYIKoEcz1UBgi0DQgAED+MQrLrZ\n9PbMmz/44Kb73Qc7FlMs7u034XImjJREBAn1KzZ7jqcYfCiV/buhmu1sLhMXnB69\nmERtf1tAaXcgIaNaMFgwCQYDVR0TBAIwADALBgNVHQ8EBAMCAzgwHQYDVR0OBBYE\nFBxHDo0gHhMoYkDeHWySTIJy5BZpMB8GA1UdIwQYMBaAFCTrpmbUig3JfveqAIGJ\n6n+vAk2AMAoGCCqBHM9VAYN1A0gAMEUCIHtXgpOxcb3mZv2scRZHZz5YGFr45dfk\nVfLkF9BkrB/xAiEA8EeUg7nCFfgHzrfgB7v0wgN1Hrgj8snTUO6IDfkBKYM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/server_enc.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgPJQ040ba9lVszMF0\n7S7pzqdxyIMc2fXKr6EsU0vRk2ahRANCAAQP4xCsutn09sybP/jgpvvdBzsWUyzu\n7TfhciaMlEQECfUrNnuOpxh8KJX9u6Ga7WwuExecHr2YRG1/W0BpdyAh\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/server_sign.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB2TCCAX+gAwIBAgIBAjAKBggqgRzPVQGDdTBFMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEPMA0GA1UEAwwGc3Vi\nIGNhMB4XDTIyMTEwMjAzMTkzNloXDTMyMTAzMDAzMTkzNlowSjELMAkGA1UEBhMC\nQUExCzAJBgNVBAgMAkJCMQswCQYDVQQKDAJDQzELMAkGA1UECwwCREQxFDASBgNV\nBAMMC3NlcnZlciBzaWduMFowFAYIKoEcz1UBgi0GCCqBHM9VAYItA0IABKGSiyHA\n5oIeT13uNL3yxK+t9rKhAMHTzDTQA01ZylHYLn6XdksWugZsP6tTx/2+17NmRkaH\nH3wztf6ciD3WZnCjWjBYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgbAMB0GA1UdDgQW\nBBT28BTySSlomaLtnuex4LvaY8tM1TAfBgNVHSMEGDAWgBQk66Zm1IoNyX73qgCB\niep/rwJNgDAKBggqgRzPVQGDdQNIADBFAiB4qGB+bD47KxSfSgqcedXVTd+JlL4f\n174uhGLSzNkOZwIhAI33LdJlaw+60YlZqQxzffI+gbqXpSN82+3W4vAsONN0\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/server_sign.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgMjFhLon1CAQdzFWt\n0Mre0juQiCDbXOY8ljWqSzQN9EehRANCAAShkoshwOaCHk9d7jS98sSvrfayoQDB\n08w00ANNWcpR2C5+l3ZLFroGbD+rU8f9vtezZkZGhx98M7X+nIg91mZw\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/test-dot.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC+zCCAeOgAwIBAgIUWUtIDbrU8QF90OXlMKyClPRNRcgwDQYJKoZIhvcNAQEL\nBQAwETEPMA0GA1UEAwwGUk9PVENBMCAXDTI0MDcxNzE2MDcyM1oYDzIxMjQwNjIz\nMTYwNzIzWjAYMRYwFAYDVQQDDA13d3cudGVzdC5jb20uMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEApkX5NgwwEC/brmrUAfxSMGMaYOzjx+3BlCC23sLR\n0uQ1+KMXt/Pd2QJVqREjEAiwXCMuHbB0qWD5985SfsjeRJJ8rc8CzJfcb7QESKfK\nGdLaD8LsyAAg+Rxm0QyVFGrLJ82sjbEimLGCkLMpYsePxEDEifKPp3Z9bRUFT0zm\nxcUEXojw5pzjrjIvfqVenWNP716s7bSdOFoc4RBlAdEI3pFUasLF9Lovz7BJLvtY\naoqgCNfb78C6zreDLswET5/338AVf9yPYc5HOthmygxkYTniK47/fOW64RQKXQ2X\nEtBiIzN6dSXfTCXSpvow5XIR02rLoxsVEEwM9ODgUAJg6QIDAQABo0IwQDAdBgNV\nHQ4EFgQUAHYNW6/hFM+Bqd2KNBXbLgJLaxcwHwYDVR0jBBgwFoAUjwSzlti+ag+f\nBzoRa0wZbMaGh10wDQYJKoZIhvcNAQELBQADggEBAA1HfiDtHZV8sxJjasnNSM9f\n6XTRCjT+DcABXm7k/Dmb8q5rpyqYwkUfadgAbmPx6T/dC4z7LblkcTkwD7azpkNE\nfXY3Hx4qxSVSbSOHWnaSOX/8BRiPbSQNWGyTGh9AK/Vp/VJU2cDPqFbjQKHFq3ZI\nw3GnRDerdA8vm5qzJ5/9wMF2ZsmnMiV3zX0Xisbzx/dponz6ktfygE3bk8Pb4wKt\nD0EjbnLIXwyHv1czJrcRq0Y8irWaTY97vdff/J2aO9582zFNx6AnsU3+6fGsDyrO\nss+ggKDLK+aOBKroTNb3TgdPyPOgobUwLByFdKT/zTtWbkqyYMZzBme2SD4TWok=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/test-dot.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmRfk2DDAQL9uu\natQB/FIwYxpg7OPH7cGUILbewtHS5DX4oxe3893ZAlWpESMQCLBcIy4dsHSpYPn3\nzlJ+yN5EknytzwLMl9xvtARIp8oZ0toPwuzIACD5HGbRDJUUassnzayNsSKYsYKQ\nsylix4/EQMSJ8o+ndn1tFQVPTObFxQReiPDmnOOuMi9+pV6dY0/vXqzttJ04Whzh\nEGUB0QjekVRqwsX0ui/PsEku+1hqiqAI19vvwLrOt4MuzARPn/ffwBV/3I9hzkc6\n2GbKDGRhOeIrjv985brhFApdDZcS0GIjM3p1Jd9MJdKm+jDlchHTasujGxUQTAz0\n4OBQAmDpAgMBAAECggEAD84ctm8h5fYApDOWJ8Kp9tzCwgYekE94vEmATIw5CPqF\nqVbqbyNUmhdTWGzvN+vVhMqYzHxsmHmmBTDU7WWPYDYK+TQRbGx+iRUz54qghsQg\n04j4PDor6DYTjWlMZfqRSV0u+vCErP5JnpLTOyckUrfD3ueCUX0tRsBN5wf0s0WD\n7AiUIdVBesQwIuIin3MyhGFtQC0PNta3NdSBVbnUA69OL3QNxPoai5LACrAf1hkf\nwPD/y6y2CswdER+j+obPChjTcnJFjRCkqqO+66QZWmMmVxq4ymCQg9IOgLRWtfhI\n6Ts5RxVn12kEuPULk9oHHOjC+MVh3BmWFLb58G/gwQKBgQDUCSd/2uswTVlTYpw6\nXO3iVyoZVeo/BIiOm/kjmqmr5U/D7ZO27ElKBTe9CDQ4WB5PuisCy0/SnsJJsPpf\npWif2v0mVs3T9K7J1M1yQU2iMs+Z2stzLGe5AASImYpw9091v57A/1jI4VUoodOr\n7sMo+9ROqx6dTG/tJgUa+VZaKQKBgQDIv8CZHv4LqvQEQrGoTcKOxQP47nsbfEPW\nB0GQscykvRTWxlTfFdfFM4VG2ApERZDwjPFU84n4dH8J7P14iy2ty70krzHWNfjY\ny52CXUb295HsdcQ0bP8wztuvM/Jfh1mKKynmezvAZlTSb+GMAAMrReuG2Ga1/gp1\n5daCd4IowQKBgG//md6eCybLZIh4CN+HIJwywGj7iazZvyvc1T9qPX8vs+9g+Wpg\n6uFvWh6+S58LZI9mXbuvGq288BEuq0GERHxTlu3+YeA4WW8AubhFKDWpsyCogliG\ntw7wJHTm7Up4R3+BxOBawFHzPCEnQYCKsIlgY6deGeCqdGCGeaHi3CrpAoGAdWam\nxSW53qr4j/FNIqdvK72OaCtX9agDqAyQTIWer40gvcY5ZknI6TwLKnY38ttYO0XB\n8TOIMbQ3g1+EkNWcPjKTh/upQqRHxsm1cMMKOG5qeYYZ26sOxsWC9oCDs1hdhg9e\nLrtNI2T1IChsGEr9j3YRmse9sZtDFNX4UE6B4UECgYEAsuRRQK0tgvcsQxkX/bZb\nVTKqI4ezGRLXuavBe42xWOBLFzEujGvbZMbxzD4F4H1dfVVor3ItAEoybC37jtHI\nuEWLAQtZtNyDCOiq4UuwbmtIqtoJz556QUrwO0KdPPjg/jyZTxs5jdKRMk9bsfmR\n80vnuQpr0CZe8EgHiMoysrs=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/test2.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEsTCCAxmgAwIBAgIUMbgUUCYHkuKDaPy0bzZowlK0JG4wDQYJKoZIhvcNAQEL\nBQAwVzELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\nWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxEjAQBgNVBAMMCXRlc3QyLmNvbTAgFw0y\nMDA0MDQyMjE3NTJaGA8yMTIwMDMxMTIyMTc1MlowVzELMAkGA1UEBhMCQ04xEjAQ\nBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVz\ndHkxEjAQBgNVBAMMCXRlc3QyLmNvbTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCC\nAYoCggGBAMQGBk35V3zaNVDWzEzVGd+EkZnUOrRpXQg5mmcnoKnrQ5rQQMsQCbMO\ngFvLt/9OEZQmbE2HuEKsPzL79Yjdu8rGjSoQdbJZ9ccO32uvln1gn68iK79o7Tvm\nTCi+BayyNA+lo9IxrBm1wGBkOU1ZPasGYzgBAbMLTSDps1EYxNR8t4l9PrTTRsh6\nNZyTYoDeVIsKZ9SckpjWVnxHOkF+AzZzIJJSe2pj572TDLYA/Xw9I4X3L+SHzwTl\niGWNXb2tU367LHERHvensQzdle7mQN2kE5GpB7QPWB+t9V4mn30jc/LyDvOaei6L\n+pbl5CriGBTjaR80oXhK765K720BQeKUezri15bQlMaUGQRnzr53ZsqA4PEh6WCX\nhUT2ibO32+uZFXzVQw8y/JUkPf76pZagi8DoLV+sfSbUtnpbQ8wyV2qqTM2eCuPi\nRgUwXQi2WssKKzrqcgKil3vksHZozLtOmyZiNE4qfNxv+UGoIybJtZmB+9spY0Rw\n5zBRuULycQIDAQABo3MwcTAdBgNVHQ4EFgQUCmZefzpizPrb3VbiIDhrA48ypB8w\nHwYDVR0jBBgwFoAUCmZefzpizPrb3VbiIDhrA48ypB8wDAYDVR0TBAUwAwEB/zAh\nBgNVHREEGjAYggl0ZXN0Mi5jb22CCyoudGVzdDIuY29tMA0GCSqGSIb3DQEBCwUA\nA4IBgQA0nRTv1zm1ACugJFfYZfxZ0mLJfRUCFMmFfhy+vGiIu6QtnOFVw/tEOyMa\nm78lBiqac15n3YWYiHiC5NFffTZ7XVlOjN2i4x2z2IJsHNa8tU80AX0Q/pizGK/d\n+dzlcsGBb9MGT18h/B3/EYQFKLjUsr0zvDb1T0YDlRUsN3Bq6CvZmvfe9F7Yh4Z/\nXO5R+rX8w9c9A2jzM5isBw2qp/Ggn5RQodMwApEYkJdu80MuxaY6s3dssS4Ay8wP\nVNFEeLcdauJ00ES1OnbnuNiYSiSMOgWBsnR+c8AaSRB/OZLYQQKGGYbq0tspwRjM\nMGJRrI/jdKnvJQ8p02abdvA9ZuFChoD3Wg03qQ6bna68ZKPd9peBPpMrDDGDLkGI\nNzZ6bLJKILnQkV6b1OHVnPDsKXfXjUTTNK/QLJejTXu9RpMBakYZMzs/SOSDtFlS\nA+q25t6+46nvA8msUSBKyOGBX42mJcKvR4OgG44PfDjYfmjn2l+Dz/jNXDclpb+Q\nXAzBnfM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/test2.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIG5QIBAAKCAYEAxAYGTflXfNo1UNbMTNUZ34SRmdQ6tGldCDmaZyegqetDmtBA\nyxAJsw6AW8u3/04RlCZsTYe4Qqw/Mvv1iN27ysaNKhB1sln1xw7fa6+WfWCfryIr\nv2jtO+ZMKL4FrLI0D6Wj0jGsGbXAYGQ5TVk9qwZjOAEBswtNIOmzURjE1Hy3iX0+\ntNNGyHo1nJNigN5Uiwpn1JySmNZWfEc6QX4DNnMgklJ7amPnvZMMtgD9fD0jhfcv\n5IfPBOWIZY1dva1TfrsscREe96exDN2V7uZA3aQTkakHtA9YH631XiaffSNz8vIO\n85p6Lov6luXkKuIYFONpHzSheErvrkrvbQFB4pR7OuLXltCUxpQZBGfOvndmyoDg\n8SHpYJeFRPaJs7fb65kVfNVDDzL8lSQ9/vqllqCLwOgtX6x9JtS2eltDzDJXaqpM\nzZ4K4+JGBTBdCLZayworOupyAqKXe+SwdmjMu06bJmI0Tip83G/5QagjJsm1mYH7\n2yljRHDnMFG5QvJxAgMBAAECggGBAIELlkruwvGmlULKpWRPReEn3NJwLNVoJ56q\njUMri1FRWAgq4PzNahU+jrHfwxmHw3rMcK/5kQwTaOefh1y63E35uCThARqQroSE\n/gBeb6vKWFVrIXG5GbQ9QBXyQroV9r/2Q4q0uJ+UTzklwbNx9G8KnXbY8s1zuyrX\nrvzMWYepMwqIMSfJjuebzH9vZ4F+3BlMmF4XVUrYj8bw/SDwXB0UXXT2Z9j6PC1J\nCS0oKbgIZ8JhoF3KKjcHBGwWTIf5+byRxeG+z99PBEBafm1Puw1vLfOjD3DN/fso\n8xCEtD9pBPBJ+W97x/U+10oKetmP1VVEr2Ph8+s2VH1zsRF5jo5d0GtvJqOwIQJ7\nz3OHJ7lLODw0KAjB1NRXW4dTTUDm6EUuUMWFkGAV6YTyhNLAT0DyrUFJck9RiY48\n3QN8vSf3n/+3wwg1gzcJ9w3W4DUbvGqu86CaUQ4UegfYJlusY/3YGp5bGNQdxmws\nlgIoSRrHp6UJKsP8Yl08MIvT/oNLgQKBwQD75SuDeyE0ukhEp0t6v+22d18hfSef\nq3lLWMI1SQR9Kiem9Z1KdRkIVY8ZAHANm6D8wgjOODT4QZtiqJd2BJn3Xf+aLfCd\nCW0hPvmGTcp/E4sDZ2u0HbIrUStz7ZcgXpjD2JJAJGEKY2Z7J65gnTqbqoBDrw1q\n1+FqtikkHRte1UqxjwnWBpSdoRQFgNPHxPWffhML1xsD9Pk1B1b7JoakYcKsNoQM\noXUKPLxSZEtd0hIydqmhGYTa9QWBPNDlA5UCgcEAxzfGbOrPBAOOYZd3jORXQI6p\nH7SddTHMQyG04i+OWUd0HZFkK7/k6r26GFmImNIsQMB26H+5XoKRFKn+sUl14xHY\nFwB140j0XSav2XzT38UpJ9CptbgK1eKGQVp41xwRYjHVScE5hJuA3a1TKM0l26rp\nhny/KaP+tXuqt9QbxcUN6efubNYyFP+m6nq2/XdX74bJuGpXLq8W0oFdiocO6tmF\n4/Hsc4dCVrcwULqXQa0lJ57zZpfIPARqWM2847xtAoHBANVUNbDpg6rTJMc34722\ndAy3NhL3mqooH9aG+hsEls+l9uT4WFipqSScyU8ERuHPbt0BO1Hi2kFx1rYMUBG8\nPeT4b7NUutVUGV8xpUNv+FH87Bta6CUnjTAQUzuf+QCJ/NjIPrwh0yloG2+roIvk\nPLF/CZfI1hUpdZfZZChYmkiLXPHZURw4gH6q33j1rOYf0WFc9aZua0vDmZame6zB\n6P+oZ6VPmi/UQXoFC/y/QfDYK18fjfOI2DJTlnDoX4XErQKBwGc3M5xMz/MRcJyJ\noIwj5jzxbRibOJV2tpD1jsU9xG/nQHbtVEwCgTVKFXf2M3qSMhFeZn0xZ7ZayZY+\nOVJbcDO0lBPezjVzIAB/Qc7aCOBAQ4F4b+VRtHN6iPqlSESTK0KH9Szgas+UzeCM\no7BZEctNMu7WBSkq6ZXXu+zAfZ8q6HmPDA3hsFMG3dFQwSxzv+C/IhZlKkRqvNVV\n50QVk5oEF4WxW0PECY/qG6NH+YQylDSB+zPlYf4Of5cBCWOoxQKBwQCeo37JpEAR\nkYtqSjXkC5GpPTz8KR9lCY4SDuC1XoSVCP0Tk23GX6GGyEf4JWE+fb/gPEFx4Riu\n7pvxRwq+F3LaAa/FFTNUpY1+8UuiMO7J0B1RkVXkyJjFUF/aQxAnOoZPmzrdZhWy\nbpe2Ka+JS/aXSd1WRN1nmo/DarpWFvdLWZFwUt6zMziH40o1gyPHEuXOqVtf2QCe\nQ6WC9xnEz4lbb/fR2TF9QRA4FtoRpDe/f3ZGIpWE0RdwyZZ6uA7T1+Q=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/vector_logs_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUa34rzhtYT21pC8NwNIYf3phFciQwDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA0MjQxMzE2NDNaFw0yMzA1\nMjQxMzE2NDNaMEUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQC84FXe/8ofZB2rj5TPHasXdiBEbTCv04ti/lV92WOC\nMrHLKibI2+kI3YRQXaR5/1F2bXfROUhdRgkB8NOSM4WbaD1mtr/7mW2Tatxplxsp\n1s0zmHHl5v0VYwBahcUs6nlSe19dgfrj4s0Wn7p4E7iSq/UDAs+We/dQowusQTVs\nQ2ZhjDlFY22CV/oyCYsNq3ORRgwZRm9cmVmUUF7GX70yjT1KvLkFjc7y1vwi8XJY\nADhw/hjtEzAOkxdUai84+jyhpQYQWMOgrlP1DXnZw1bNKqo6NTkMzfNCS+ul5PMs\nNoyxcw1iyGW6Bm81LANsnMM7BLhPQATShmW7O83WUJ4vAgMBAAGjUzBRMB0GA1Ud\nDgQWBBRdFCb//WETC8mDxg/75e+RoVNoDjAfBgNVHSMEGDAWgBRdFCb//WETC8mD\nxg/75e+RoVNoDjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC8\ncKOagO+SVJBMzJppm4uGdSM6TJ2wbkn6K5/eOtZmYdKtW6fkAC9tf0DR7dVP1DUk\n24lS+atR1Oe7SukxJyd+NafCZ61uf+zrMC3wgBGnufrbPWaDDVxi6c3I0I+WNaCk\nDHHY+9UtjvSboWKG1yuEExPN6aDeytbpscG1DNi7l96Ac3Yzs007SFljA7NBrf65\nSo9SZYSdJVC/JrOnfK2HZPeAqvoyUO5JsCh02q5AskxTqfBGy6VUVQO5mN8bxYHV\nGG5XD46rpwQYNT2bWWRF5d0bRv7ecNkCoupm6hCQROg4FZHGPnqHGqDTcgCLZ59e\n8rHh2gsDMMNYvSMTi+0N\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/vector_logs_ca.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvOBV3v/KH2Qdq4+Uzx2rF3YgRG0wr9OLYv5VfdljgjKxyyom\nyNvpCN2EUF2kef9Rdm130TlIXUYJAfDTkjOFm2g9Zra/+5ltk2rcaZcbKdbNM5hx\n5eb9FWMAWoXFLOp5UntfXYH64+LNFp+6eBO4kqv1AwLPlnv3UKMLrEE1bENmYYw5\nRWNtglf6MgmLDatzkUYMGUZvXJlZlFBexl+9Mo09Sry5BY3O8tb8IvFyWAA4cP4Y\n7RMwDpMXVGovOPo8oaUGEFjDoK5T9Q152cNWzSqqOjU5DM3zQkvrpeTzLDaMsXMN\nYshlugZvNSwDbJzDOwS4T0AE0oZluzvN1lCeLwIDAQABAoIBADM7ou9fcQM80/OC\nefoIcS1nBG+rMqau+kM6/BOsERrzB1k1sNmRFVArTkXCcOgKwp0eKn8dS6zJX44g\nNjOVOCukhetDrSXhQ2DWfr1BmMOrmXPiaRrUolfXx/PGD2sUmx4tivvBUz3Xeowl\nfZ4us0VN0aMkcwy9yaMc5wCtm4Em+uMrUIvWSAl3ji09oG4NNBQHUsEWJoRMZ/AG\nGQowc7Ga850ybZlza1uWh29a3bbQqEwHExJwiCISv25PJ/xQLqH65biB4MU+ym17\nOu/MDn9cYndxBal/XI4R7HbeIjMgw2XxwXiiDOuKAn5TlCzHmySRXFj1BoT8xoXa\nvTXVlAkCgYEA+nc2GiainyW0MAASX53Ue5zsFh4T2CaA4TTHeXEK22rL1Sz3LsbX\nymBqCcNwbcSTYUzBsf6YzSsPLUwIzBGEN0p5Ywts5KtWavAxllBj2MOTP4yQfLvh\nAxOq94hqrDLMs/g7LkFrfspYMCXmegGjjXGuqirKbigXkFVQkvOUcwUCgYEAwQy8\nkl2+deq1OD9rJId596nDx6JVLBt7/VP4dOOaS2/YQeFnUdM1xMM+zovEZb3UZMpp\n8yhRE7hB7Fm0688yd+F7GpFC49LyVTitZcaIV7nMnXjJQQ27WyiAZoRKHt1gP4io\nOCZAaOEJRbGJcWR3sSPHfX93R+xEtFNAexb/eqMCgYEA8NDV7+bdzO7PhKdNAyoZ\nNpD2XX2lztmWaPH6KMWLjtPsD5cgQpVkvWxeB+0lmCS9H3xRb/Y+rGWOPhsxCiR9\nXzv34kcF+AbVHBS9WK0Kk0vXs+5Ord9mxTKP21gKWG6vawpsvFiiJlIe4IxQQVZ6\nDnETYwGpiKh7n4an5eLVBJECgYEAnviuEJnBzbiJotgWku49MgVKg4raOIgpgmMz\npo4G8TgZDadgPbGABZgCkHPoNyArVxSYSvRYT7TcFJWKtuTY2n+DsE0OmC2OAT+7\nCqSCgjsulD5y/G8iad7gXYtyvhfuumL+o75cLAGkcQ/R7t6c8fJUxLPCtieKLDSi\nVLqLh6ECgYAlk8O5Rz2bSje4F+b0PZLAGjQRytpTjrgVpxwJ4bBXYeqhfd+3Fi8s\nOraFx+kj/YOOFkk5uu75/fbccEY1DG0nmWUR0pjHM+QndB4rpkSxtp+pfVo2nRn0\npAY8ep+TFRLwmy7ZXpOFPYlGPwx+rjSm9vk9EJYjxZE8YYldiBBKHw==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/certs/vector_logs_server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDETCCAfkCFEynFsv9L6bzyJJVmjoaKuCoYZwkMA0GCSqGSIb3DQEBCwUAMEUx\nCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl\ncm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjMwNDI0MTMxNzAwWhcNMjQwNDIzMTMx\nNzAwWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE\nCgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAtGvdwIHuk6k3vphBnGIdtlZ/6ImHSVBsNHz5y6E9X31a88EH\nwtnxT5Ang8K6Y4pQt+LsjhI0NdUY2skiKDnGpo2IkaFAn9nERQ1GJstIHr7ltal6\nureV4n/Na/T6n6GPnwD4+P86XvpIwFtJZujYr2tUl4qm/t1P7zHjB/UsF9G6H/aN\noCsDkG3a7+b8uWAZLkyHS4RLF3pG6pDWns8/vC/P9nTT7o3Ha2DV7TPaY0hlsXf6\n0/SCSm7EonnVVwhnKyy5Z0FsCXClg7weN4ZKPb+ypF0o0/LLqw481lbSfAu5kpjE\nr/rHpsQonRbQrcrD9xovXmw2vdk/2jJn6wpFQwIDAQABMA0GCSqGSIb3DQEBCwUA\nA4IBAQBv9e8gsD75iySf+pam11JUujjL0gpqdzY72CKo4abYX5NZhMiBs6OCKicz\nEedR/EgRY+26RMThKC0zSy3hOO6SKPw03FLsV2B8ooDzaOa4l3F/E6NQ5yNDoK+K\nlT1G85fW3bQWtNoB8aa/r1/eExZy3kZF8GSl+/BvwLtOwtGXMO0Y1URo81Dl0da+\nF2yv6ZGziEYIWYTUK3kxOpe0Sl4wHz33olWoli2qpYlSndUUIWoVYJr4gtH/xTEV\nGHxdOhxcfyMNi6ceYG4HGWyKRFR9TJAU+PRBxHI8UUpg+BG3/DQmfA5+7xgAws37\ndEVsm725hta8vPUSMSAdRrArBlh+\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "t/certs/vector_logs_server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAtGvdwIHuk6k3vphBnGIdtlZ/6ImHSVBsNHz5y6E9X31a88EH\nwtnxT5Ang8K6Y4pQt+LsjhI0NdUY2skiKDnGpo2IkaFAn9nERQ1GJstIHr7ltal6\nureV4n/Na/T6n6GPnwD4+P86XvpIwFtJZujYr2tUl4qm/t1P7zHjB/UsF9G6H/aN\noCsDkG3a7+b8uWAZLkyHS4RLF3pG6pDWns8/vC/P9nTT7o3Ha2DV7TPaY0hlsXf6\n0/SCSm7EonnVVwhnKyy5Z0FsCXClg7weN4ZKPb+ypF0o0/LLqw481lbSfAu5kpjE\nr/rHpsQonRbQrcrD9xovXmw2vdk/2jJn6wpFQwIDAQABAoIBAQCB24lV/6759Le8\npNXEexIrpQKXGjWXXR0kgjdAiyMjUZRfETZG1prKy1TFjyiccHc8g0YD07JkdKZZ\nAp9lGICUbBY5yzg6VYDguncdgP69smSfZgaB0ZU92wK9iyvALYazyP1qKjmXFsm6\nOXoRadJcIAJYuGEN27imzt87YQmFciXj63lW4usR7rPpacW004VeWqGfXTnckJd6\nTYFq0xmdhnGxDxOlf6fs5zOEw17NrGlYxQVtdst8sGmpAPMEM7DzvDsjfEPxDuXl\nhQJE8Zk8jK3Xwrnc03NWisZ4QVhgxeR7PVcraFo623qiI/CzH9YqUqMCtIMAqz/T\nCOXXl9JxAoGBAOosUC72SM7ZRshneHHszEaZDvfLINdKGUKCDvYlLEmVFqE5iRFy\nSomVci2jtrlGH1gJAWfwkT09JVgtGosRIA0MS82HseLN/QIa01dAmmiZqM/CLbcn\nmpb0CQDkm0Bbz6fokQkFB/sBA5Kj3kOKRydCLp2S0Ugs50cKXDHP5fuVAoGBAMU8\n9rIvmNdweGTiYjHYLJBkzu7eL+5RB2zVSMuZtVivaDTfleilbRlcrBBIaM0urv2W\nUtROB9ack2Ijn/BF+tHkBRVWpaZFdHJ8qMfz2bBDgf6za/LBcvuT35i7ibPT+zfg\nUFXtArmGwPq3AZdWBwIKyN8rM7253WDnUlkN7Ed3AoGBAMPAR0b6meJPvtvHoueZ\nCyn4yIpbQxi02GjAT8FzUZIxDrm3Xt02rRhV1RxRvm0iMRFmdcZtUvveIVmUWpvl\ntOUzYiptREZT6yvXQNOvLWRDDtqdd5mjgZauaNhWQXGLTgsOXi8sBX/NWS87zJCp\nBtHKgS03jbrHzo2UG32ITLgBAoGAJRoardoWPjCB9ThAkG/BskfERVq2WXYUl3xn\nfSUk39HfIFMOt/ymUScFluqIDFDDyiAE5Lro7o31i3h4FZKUY/conaL29hgKl56r\ngTF1uZp5UZgerkOFhZ2Dag+dD57ImvIvKnqzEIMwufjC69za5J9yucg+q2nTIu9g\npi/gSnECgYEAhfJ5uq1qa+g23np02ED5ttqmyrMRGGInx3mr2QgJDTum6FujpYCM\nPwJhMwKJZXcf3eUlECSJPa+9UGI53d+JDlQdwq9Pi726KFtrBiS4t9aSyZSpkoWk\nSVdYGaOMtokDKRJibazXjpGFJQy9tAMgtqptS3kL03IuJc643y+lMFc=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "t/chaos/delayetcd/delayetcd.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage delayetcd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/chaos-mesh/chaos-mesh/api/v1alpha1\"\n\t\"github.com/gavv/httpexpect\"\n\t\"github.com/onsi/ginkgo\"\n\t\"github.com/onsi/gomega\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\n\t\"github.com/apache/apisix/t/chaos/utils\"\n)\n\nfunc getEtcdDelayChaos(delay int) *v1alpha1.NetworkChaos {\n\treturn &v1alpha1.NetworkChaos{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      \"etcd-delay\",\n\t\t\tNamespace: metav1.NamespaceDefault,\n\t\t},\n\t\tSpec: v1alpha1.NetworkChaosSpec{\n\t\t\tSelector: v1alpha1.SelectorSpec{\n\t\t\t\tLabelSelectors: map[string]string{\"app.kubernetes.io/instance\": \"etcd\"},\n\t\t\t},\n\t\t\tAction: v1alpha1.DelayAction,\n\t\t\tMode:   v1alpha1.AllPodMode,\n\t\t\tTcParameter: v1alpha1.TcParameter{\n\t\t\t\tDelay: &v1alpha1.DelaySpec{\n\t\t\t\t\tLatency: strconv.Itoa(delay) + \"ms\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc setRouteMultipleTimes(e *httpexpect.Expect, times int, status httpexpect.StatusRange) time.Duration {\n\tnow := time.Now()\n\ttimeLast := now\n\tvar timeList []string\n\tfor i := 0; i < times; i++ {\n\t\tutils.SetRoute(e, status)\n\t\ttimeList = append(timeList, time.Since(timeLast).String())\n\t\ttimeLast = time.Now()\n\t}\n\tfmt.Fprintf(ginkgo.GinkgoWriter, \"takes %v separately\\n\", timeList)\n\treturn time.Since(now) / time.Duration(times)\n}\n\nfunc setRouteMultipleTimesIgnoreError(e *httpexpect.Expect, times int) (time.Duration, int) {\n\tnow := time.Now()\n\tvar resp *httpexpect.Response\n\tfor i := 0; i < times; i++ {\n\t\tresp = utils.SetRouteIgnoreError(e)\n\t}\n\t// use status code of the last time is enough to show the accessibility of apisix\n\treturn time.Since(now) / time.Duration(times), resp.Raw().StatusCode\n}\n\nfunc deleteChaosAndCheck(eSilent *httpexpect.Expect, cliSet *utils.ClientSet, chaos *v1alpha1.NetworkChaos) {\n\terr := cliSet.CtrlCli.Delete(context.Background(), chaos)\n\tgomega.Expect(err).To(gomega.BeNil())\n\ttime.Sleep(1 * time.Second)\n\n\tvar setDuration time.Duration\n\tvar statusCode int\n\tfor range [10]int{} {\n\t\tsetDuration, statusCode = setRouteMultipleTimesIgnoreError(eSilent, 5)\n\t\tif setDuration < 15*time.Millisecond && statusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(5 * time.Second)\n\t}\n\tgomega.Ω(setDuration).Should(gomega.BeNumerically(\"<\", 15*time.Millisecond))\n\tgomega.Ω(statusCode).Should(gomega.BeNumerically(\"==\", http.StatusOK))\n}\n\nvar _ = ginkgo.Describe(\"Test APISIX Delay When Add ETCD Delay\", func() {\n\tctx := context.Background()\n\te := httpexpect.New(ginkgo.GinkgoT(), utils.Host)\n\teDataPanel := httpexpect.New(ginkgo.GinkgoT(), utils.DataPanelHost)\n\tePrometheus := httpexpect.New(ginkgo.GinkgoT(), utils.PrometheusHost)\n\teSilent := utils.GetSilentHttpexpectClient()\n\n\tvar cliSet *utils.ClientSet\n\tvar apisixPod *v1.Pod\n\tvar err error\n\tginkgo.It(\"init client set\", func() {\n\t\tcliSet, err = utils.InitClientSet()\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tlistOption := client.MatchingLabels{\"app\": \"apisix-gw\"}\n\t\tapisixPods, err := utils.GetPods(cliSet.CtrlCli, metav1.NamespaceDefault, listOption)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(len(apisixPods)).Should(gomega.BeNumerically(\">\", 0))\n\t\tapisixPod = &apisixPods[0]\n\t})\n\n\tginkgo.It(\"setup prometheus metrics public API\", func() {\n\t\tutils.SetPrometheusMetricsPublicAPI(e)\n\t})\n\n\tginkgo.It(\"check if everything works\", func() {\n\t\tutils.SetRoute(e, httpexpect.Status2xx)\n\t\tutils.GetRouteList(e, http.StatusOK)\n\n\t\tutils.WaitUntilMethodSucceed(eDataPanel, http.MethodGet, 1)\n\t\tutils.TestPrometheusEtcdMetric(ePrometheus, 1)\n\t})\n\n\t// get default\n\tginkgo.It(\"get default apisix delay\", func() {\n\t\ttimeStart := time.Now()\n\t\tsetDuration := setRouteMultipleTimes(eSilent, 5, httpexpect.Status2xx)\n\t\tgomega.Ω(setDuration).Should(gomega.BeNumerically(\"<\", 15*time.Millisecond))\n\n\t\terrorLog, err := utils.Log(apisixPod, cliSet.KubeCli, timeStart)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(errorLog).ShouldNot(gomega.ContainSubstring(\"error\"))\n\t})\n\n\t// 30ms delay\n\tginkgo.It(\"generate a 30ms delay between etcd and apisix\", func() {\n\t\ttimeStart := time.Now()\n\t\tchaos := getEtcdDelayChaos(30)\n\t\terr := cliSet.CtrlCli.Create(ctx, chaos)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tdefer deleteChaosAndCheck(eSilent, cliSet, chaos)\n\n\t\tsetDuration := setRouteMultipleTimes(eSilent, 5, httpexpect.Status2xx)\n\t\tgomega.Ω(setDuration).Should(gomega.BeNumerically(\"<\", 400*time.Millisecond))\n\n\t\terrorLog, err := utils.Log(apisixPod, cliSet.KubeCli, timeStart)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(errorLog).ShouldNot(gomega.ContainSubstring(\"error\"))\n\t})\n\n\t// 300ms delay\n\tginkgo.It(\"generate a 300ms delay between etcd and apisix\", func() {\n\t\ttimeStart := time.Now()\n\t\tchaos := getEtcdDelayChaos(300)\n\t\terr := cliSet.CtrlCli.Create(ctx, chaos)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tdefer deleteChaosAndCheck(eSilent, cliSet, chaos)\n\n\t\tsetDuration := setRouteMultipleTimes(eSilent, 5, httpexpect.Status2xx)\n\t\tgomega.Ω(setDuration).Should(gomega.BeNumerically(\"<\", 4*time.Second))\n\n\t\terrorLog, err := utils.Log(apisixPod, cliSet.KubeCli, timeStart)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(errorLog).ShouldNot(gomega.ContainSubstring(\"error\"))\n\t})\n\n\t// 3s delay and cause error\n\tginkgo.It(\"generate a 3s delay between etcd and apisix\", func() {\n\t\ttimeStart := time.Now()\n\t\tchaos := getEtcdDelayChaos(3000)\n\t\terr := cliSet.CtrlCli.Create(ctx, chaos)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tdefer deleteChaosAndCheck(eSilent, cliSet, chaos)\n\n\t\t_ = setRouteMultipleTimes(e, 2, httpexpect.Status5xx)\n\n\t\terrorLog, err := utils.Log(apisixPod, cliSet.KubeCli, timeStart)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(errorLog).Should(gomega.ContainSubstring(\"error\"))\n\t})\n\n\tginkgo.It(\"restore test environment\", func() {\n\t\tutils.WaitUntilMethodSucceed(e, http.MethodPut, 5)\n\t\tutils.DeleteRoute(e)\n\t})\n})\n"
  },
  {
    "path": "t/chaos/e2e.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage e2e\n\nimport (\n\t_ \"github.com/apache/apisix/t/chaos/delayetcd\"\n\t_ \"github.com/apache/apisix/t/chaos/killetcd\"\n)\n\nfunc runChaos() {}\n"
  },
  {
    "path": "t/chaos/e2e_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage e2e\n\nimport (\n\t\"testing\"\n\n\t\"github.com/onsi/ginkgo\"\n\t\"github.com/onsi/gomega\"\n)\n\nfunc TestRunChaos(t *testing.T) {\n\trunChaos()\n\tgomega.RegisterFailHandler(ginkgo.Fail)\n\tginkgo.RunSpecs(t, \"chaos test suites\")\n}\n"
  },
  {
    "path": "t/chaos/go.mod",
    "content": "module github.com/apache/apisix/t/chaos\n\nrequire (\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/chaos-mesh/chaos-mesh v1.1.1\n\tgithub.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect\n\tgithub.com/fatih/structs v1.1.0 // indirect\n\tgithub.com/gavv/httpexpect v2.0.0+incompatible\n\tgithub.com/imkira/go-interpol v1.1.0 // indirect\n\tgithub.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect\n\tgithub.com/moul/http2curl v1.0.0 // indirect\n\tgithub.com/onsi/ginkgo v1.12.0\n\tgithub.com/onsi/gomega v1.9.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgithub.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect\n\tgithub.com/yudai/gojsondiff v1.0.0 // indirect\n\tgithub.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect\n\tgithub.com/yudai/pp v2.0.1+incompatible // indirect\n\tk8s.io/api v0.17.0\n\tk8s.io/apimachinery v0.17.0\n\tk8s.io/client-go v0.17.0\n\tk8s.io/kubectl v0.0.0\n\tk8s.io/kubernetes v1.17.2\n\tsigs.k8s.io/controller-runtime v0.4.0\n)\n\nreplace (\n\tk8s.io/api => k8s.io/api v0.17.0\n\tk8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.17.0\n\tk8s.io/apimachinery => k8s.io/apimachinery v0.17.1-beta.0\n\tk8s.io/apiserver => k8s.io/apiserver v0.17.0\n\tk8s.io/cli-runtime => k8s.io/cli-runtime v0.17.0\n\tk8s.io/client-go => k8s.io/client-go v0.17.0\n\tk8s.io/cloud-provider => k8s.io/cloud-provider v0.17.0\n\tk8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.17.0\n\tk8s.io/code-generator => k8s.io/code-generator v0.17.1-beta.0\n\tk8s.io/component-base => k8s.io/component-base v0.17.0\n\tk8s.io/cri-api => k8s.io/cri-api v0.17.1-beta.0\n\tk8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.17.0\n\tk8s.io/kube-aggregator => k8s.io/kube-aggregator v0.17.0\n\tk8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.17.0\n\tk8s.io/kube-proxy => k8s.io/kube-proxy v0.17.0\n\tk8s.io/kube-scheduler => k8s.io/kube-scheduler v0.17.0\n\tk8s.io/kubectl => k8s.io/kubectl v0.17.0\n\tk8s.io/kubelet => k8s.io/kubelet v0.17.0\n\tk8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.17.0\n\tk8s.io/metrics => k8s.io/metrics v0.17.0\n\tk8s.io/sample-apiserver => k8s.io/sample-apiserver v0.17.0\n)\n\ngo 1.14\n"
  },
  {
    "path": "t/chaos/go.sum",
    "content": "bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=\nbitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1:Ulb78X89vxKYgdL24HMTiXYHlyHEvruOj1ZPlqeNEZM=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncode.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=\ngithub.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=\ngithub.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=\ngithub.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=\ngithub.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=\ngithub.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=\ngithub.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=\ngithub.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=\ngithub.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=\ngithub.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=\ngithub.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14=\ngithub.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA=\ngithub.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=\ngithub.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=\ngithub.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=\ngithub.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=\ngithub.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=\ngithub.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=\ngithub.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg=\ngithub.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=\ngithub.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=\ngithub.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM=\ngithub.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/bazelbuild/bazel-gazelle v0.18.2/go.mod h1:D0ehMSbS+vesFsLGiD6JXu3mVEzOlfUl8wNnq+x/9p0=\ngithub.com/bazelbuild/bazel-gazelle v0.19.1-0.20191105222053-70208cbdc798/go.mod h1:rPwzNHUqEzngx1iVBfO/2X2npKaT3tqPqqHW6rVsn/A=\ngithub.com/bazelbuild/buildtools v0.0.0-20190731111112-f720930ceb60/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=\ngithub.com/bazelbuild/buildtools v0.0.0-20190917191645-69366ca98f89/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=\ngithub.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=\ngithub.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA=\ngithub.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM=\ngithub.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E=\ngithub.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/prettybench v0.0.0-20150116022406-03b8cfe5406c/go.mod h1:Xe6ZsFhtM8HrDku0pxJ3/Lr51rwykrzgFwpmTzleatY=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=\ngithub.com/chaos-mesh/chaos-mesh v1.1.1 h1:8nQ7Z9uvRa8rBvY/uJKt8kb7D9gBvcdWcnzqu7VckHc=\ngithub.com/chaos-mesh/chaos-mesh v1.1.1/go.mod h1:BjKgSVpmbyrj5Te3/xiYaIMCfiZUhreS3nep/Fupqmk=\ngithub.com/chaos-mesh/k8s_dns_chaos v0.0.0-20200922120555-7ced93637075/go.mod h1:CB8grXv5pqxLgiI0HSZxyyykmDRekpd5M7fz+NlOdMs=\ngithub.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho=\ngithub.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=\ngithub.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clusterhq/flocker-go v0.0.0-20160920122132-2b8b7259d313/go.mod h1:P1wt9Z3DP8O6W3rvwCt0REIlshg1InHImaLW0t3ObY0=\ngithub.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=\ngithub.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=\ngithub.com/container-storage-interface/spec v1.2.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4=\ngithub.com/containerd/cgroups v0.0.0-20200404012852-53ba5634dc0f/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=\ngithub.com/containerd/console v0.0.0-20170925154832-84eeaae905fa/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=\ngithub.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.2.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=\ngithub.com/containerd/cri v1.11.1/go.mod h1:DavH5Qa8+6jOmeOMO3dhWoqksucZDe06LfuhBz/xPZs=\ngithub.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=\ngithub.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=\ngithub.com/containerd/typeurl v0.0.0-20200115183213-fe1d0d650e42/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=\ngithub.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/coredns/corefile-migration v1.0.4/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=\ngithub.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=\ngithub.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=\ngithub.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=\ngithub.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/libnetwork v0.8.0-dev.2.0.20190624125649-f0e46a78ea34/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8=\ngithub.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=\ngithub.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=\ngithub.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=\ngithub.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw=\ngithub.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=\ngithub.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=\ngithub.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc=\ngithub.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=\ngithub.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=\ngithub.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=\ngithub.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=\ngithub.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=\ngithub.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=\ngithub.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=\ngithub.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=\ngithub.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=\ngithub.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=\ngithub.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=\ngithub.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=\ngithub.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=\ngithub.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=\ngithub.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=\ngithub.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=\ngithub.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54=\ngithub.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=\ngithub.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=\ngithub.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=\ngithub.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=\ngithub.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=\ngithub.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=\ngithub.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=\ngithub.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=\ngithub.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=\ngithub.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=\ngithub.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=\ngithub.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=\ngithub.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=\ngithub.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=\ngithub.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=\ngithub.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=\ngithub.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=\ngithub.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=\ngithub.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=\ngithub.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=\ngithub.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=\ngithub.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=\ngithub.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=\ngithub.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=\ngithub.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=\ngithub.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=\ngithub.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=\ngithub.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=\ngithub.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=\ngithub.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=\ngithub.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=\ngithub.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=\ngithub.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=\ngithub.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=\ngithub.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=\ngithub.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=\ngithub.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=\ngithub.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=\ngithub.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=\ngithub.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=\ngithub.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=\ngithub.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=\ngithub.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=\ngithub.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=\ngithub.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/overalls v0.0.0-20180201144345-22ec1a223b7c/go.mod h1:UqxAgEOt89sCiXlrc/ycnx00LVvUO/eS8tMUkWX4R7w=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=\ngithub.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=\ngithub.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=\ngithub.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=\ngithub.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=\ngithub.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=\ngithub.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=\ngithub.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=\ngithub.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=\ngithub.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=\ngithub.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=\ngithub.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=\ngithub.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=\ngithub.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=\ngithub.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/googleapis v1.3.2/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=\ngithub.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=\ngithub.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=\ngithub.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=\ngithub.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=\ngithub.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=\ngithub.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=\ngithub.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=\ngithub.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg=\ngithub.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=\ngithub.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=\ngithub.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=\ngithub.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=\ngithub.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=\ngithub.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=\ngithub.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=\ngithub.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=\ngithub.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=\ngithub.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8=\ngithub.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/cadvisor v0.35.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=\ngithub.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=\ngithub.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=\ngithub.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=\ngithub.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=\ngithub.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=\ngithub.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.14.1/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=\ngithub.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/heketi/heketi v9.0.1-0.20190917153846-c2e2a4ab7ab9+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o=\ngithub.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4=\ngithub.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=\ngithub.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=\ngithub.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=\ngithub.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=\ngithub.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=\ngithub.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=\ngithub.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=\ngithub.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=\ngithub.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.4.1 h1:8VMb5+0wMgdBykOV96DwNwKFQ+WTI4pzYURP99CcB9E=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=\ngithub.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=\ngithub.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=\ngithub.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA=\ngithub.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04=\ngithub.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk=\ngithub.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao=\ngithub.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=\ngithub.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=\ngithub.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=\ngithub.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4=\ngithub.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=\ngithub.com/mgechev/revive v1.0.2-0.20200225072153-6219ca02fffb/go.mod h1:E9j8UNyHeYo/uUXIIUOAehxf5B69UwZ5u3qj7wEn8J0=\ngithub.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY=\ngithub.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=\ngithub.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=\ngithub.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=\ngithub.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=\ngithub.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=\ngithub.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=\ngithub.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=\ngithub.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=\ngithub.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=\ngithub.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=\ngithub.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=\ngithub.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/selinux v1.3.1-0.20190929122143-5215b1806f52/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=\ngithub.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=\ngithub.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc=\ngithub.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pingcap/errors v0.11.5-0.20190809092503-95897b64e011/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pingcap/failpoint v0.0.0-20200210140405-f8f9fb234798/go.mod h1:DNS3Qg7bEDhU6EXNHF+XSv/PGznQaMJ5FWvctpm6pQI=\ngithub.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=\ngithub.com/pingcap/log v0.0.0-20200117041106-d28c14d3b1cd/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=\ngithub.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=\ngithub.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI=\ngithub.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=\ngithub.com/robfig/cron v1.1.0 h1:jk4/Hud3TTdcrJgUOBgsqrZBarcxl6ADIjSC2iniwLY=\ngithub.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=\ngithub.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=\ngithub.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=\ngithub.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=\ngithub.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc=\ngithub.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=\ngithub.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=\ngithub.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=\ngithub.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=\ngithub.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=\ngithub.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=\ngithub.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=\ngithub.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=\ngithub.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=\ngithub.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=\ngithub.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=\ngithub.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=\ngithub.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=\ngithub.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=\ngithub.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=\ngithub.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=\ngithub.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=\ngithub.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.2.0 h1:dzZJf2IuMiclVjdw0kkT+f9u4YdrapbNyGAN47E/qnk=\ngithub.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=\ngithub.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=\ngithub.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=\ngithub.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=\ngithub.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=\ngithub.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=\ngithub.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=\ngithub.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=\ngithub.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE=\ngithub.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=\ngithub.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=\ngithub.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=\ngithub.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=\ngithub.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=\ngithub.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=\ngo.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=\ngo.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=\ngo.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/dig v1.9.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=\ngo.uber.org/fx v1.12.0/go.mod h1:egT3Kyg1JFYQkvKLZ3EsykxkNrZxgXS+gKoKo7abERY=\ngo.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngo.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=\ngo.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=\ngo4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=\ngolang.org/x/build v0.0.0-20190927031335-2835ba2e683f/go.mod h1:fYw7AShPAhGMdXqA9gRadk/CcMsvLlClpE5oBwnS3dM=\ngolang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=\ngolang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg=\ngolang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa h1:mQTN3ECqfsViCNBgq+A40vdwhkGykrrQlYe3mPj6BoU=\ngolang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190122202912-9c309ee22fab/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191107010934-f79515f33823/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200309202150-20ab64c0d93f h1:NbrfHxef+IfdI86qCgO/1Siq1BuMH2xG0NqgvCguRhQ=\ngolang.org/x/tools v0.0.0-20200309202150-20ab64c0d93f/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0=\ngomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU=\ngonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=\ngoogle.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=\ngopkg.in/alecthomas/gometalinter.v2 v2.0.12/go.mod h1:NDRytsqEZyolNuAgTzJkZMkSQM7FIKyzVzGhjB/qfYo=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=\ngopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=\ngopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=\ngrpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM=\nk8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI=\nk8s.io/apiextensions-apiserver v0.17.0 h1:+XgcGxqaMztkbbvsORgCmHIb4uImHKvTjNyu7b8gRnA=\nk8s.io/apiextensions-apiserver v0.17.0/go.mod h1:XiIFUakZywkUl54fVXa7QTEHcqQz9HG55nHd1DCoHj8=\nk8s.io/apimachinery v0.17.1-beta.0 h1:0Wl/KpAiFOMe9to5h8x2Y6JnjV+BEWJiTcUk1Vx7zdE=\nk8s.io/apimachinery v0.17.1-beta.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=\nk8s.io/apiserver v0.17.0 h1:XhUix+FKFDcBygWkQNp7wKKvZL030QUlH1o8vFeSgZA=\nk8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg=\nk8s.io/cli-runtime v0.17.0/go.mod h1:1E5iQpMODZq2lMWLUJELwRu2MLWIzwvMgDBpn3Y81Qo=\nk8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg=\nk8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k=\nk8s.io/cloud-provider v0.17.0/go.mod h1:Ze4c3w2C0bRsjkBUoHpFi+qWe3ob1wI2/7cUn+YQIDE=\nk8s.io/cluster-bootstrap v0.17.0/go.mod h1:KnxktBWGyKlBDaHLC8zzu0EPt/HJ9Lcs7bNM2WvUHSs=\nk8s.io/code-generator v0.17.1-beta.0/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=\nk8s.io/component-base v0.17.0 h1:BnDFcmBDq+RPpxXjmuYnZXb59XNN9CaFrX8ba9+3xrA=\nk8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc=\nk8s.io/cri-api v0.17.1-beta.0/go.mod h1:BzAkbBHHp81d+aXzbiIcUbilLkbXa40B8mUHOk6EX3s=\nk8s.io/csi-translation-lib v0.17.0/go.mod h1:HEF7MEz7pOLJCnxabi45IPkhSsE/KmxPQksuCrHKWls=\nk8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM=\nk8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=\nk8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=\nk8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=\nk8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=\nk8s.io/kube-aggregator v0.17.0/go.mod h1:Vw104PtCEuT12WTVuhRFWCHXGiVqXsTzFtrvoaHxpk4=\nk8s.io/kube-controller-manager v0.17.0/go.mod h1:uewKsjSm/Kggbn+BmimupXDDEikKQv6rX8ShiLiuXTw=\nk8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=\nk8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=\nk8s.io/kube-proxy v0.17.0/go.mod h1:pecyGyajk667mTTCT0vMP7Oh3bQMUHvEW+Z5pZUjYxU=\nk8s.io/kube-scheduler v0.17.0/go.mod h1:mZVsEg++qnq6xWm9DTh2bw9v2i9XPdkEQGDafcjG6PE=\nk8s.io/kubectl v0.17.0 h1:xD4EWlL+epc/JTO1gvSjmV9yiYF0Z2wiHK2DIek6URY=\nk8s.io/kubectl v0.17.0/go.mod h1:jIPrUAW656Vzn9wZCCe0PC+oTcu56u2HgFD21Xbfk1s=\nk8s.io/kubelet v0.17.0/go.mod h1:e/JBCxucKuEV6JO6zYW+e72ib9eMsGO2Fah3iT5tiiI=\nk8s.io/kubernetes v1.17.2 h1:g1UFZqFQsYx88xMUks4PKC6tsNcekxe0v06fcVGRwVE=\nk8s.io/kubernetes v1.17.2/go.mod h1:NbNV+69yL3eKiKDJ+ZEjqOplN3BFXKBeunzkoOy8WLo=\nk8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8=\nk8s.io/metrics v0.17.0/go.mod h1:EH1D3YAwN6d7bMelrElnLhLg72l/ERStyv2SIQVt6Do=\nk8s.io/repo-infra v0.0.1-alpha.1/go.mod h1:wO1t9WaB99V80ljbeENTnayuEEwNZt7gECYh/CEyOJ8=\nk8s.io/sample-apiserver v0.17.0/go.mod h1:SAkguNIe/gJik7VlkFu62oGlWltW3c0mAP9WQYUMEJo=\nk8s.io/system-validators v1.0.4/go.mod h1:HgSgTg4NAGNoYYjKsUyk52gdNi2PVDswQ9Iyn66R7NI=\nk8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=\nk8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=\nk8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=\nmodernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=\nmodernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=\nmodernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=\nmodernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=\nmodernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=\nmvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=\nmvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=\nmvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nsigs.k8s.io/controller-runtime v0.4.0 h1:wATM6/m+3w8lj8FXNaO6Fs/rq/vqoOjO1Q116Z9NPsg=\nsigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns=\nsigs.k8s.io/controller-tools v0.2.5/go.mod h1:+t0Hz6tOhJQCdd7IYO0mNzimmiM9sqMU0021u6UCF2o=\nsigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=\nsigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=\nsigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=\nsigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM=\nsigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=\nvbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=\n"
  },
  {
    "path": "t/chaos/killetcd/killetcd.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage killetcd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/chaos-mesh/chaos-mesh/api/v1alpha1\"\n\t\"github.com/gavv/httpexpect\"\n\t\"github.com/onsi/ginkgo\"\n\t\"github.com/onsi/gomega\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\n\t\"github.com/apache/apisix/t/chaos/utils\"\n)\n\nvar (\n\tbandwidthBefore float64\n\tdurationBefore  float64\n\tbpsBefore       float64\n\tbandwidthAfter  float64\n\tdurationAfter   float64\n\tbpsAfter        float64\n)\n\nfunc getEtcdKillChaos() *v1alpha1.PodChaos {\n\treturn &v1alpha1.PodChaos{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      \"kill-etcd\",\n\t\t\tNamespace: metav1.NamespaceDefault,\n\t\t},\n\t\tSpec: v1alpha1.PodChaosSpec{\n\t\t\tSelector: v1alpha1.SelectorSpec{\n\t\t\t\tLabelSelectors: map[string]string{\"app.kubernetes.io/instance\": \"etcd\"},\n\t\t\t},\n\t\t\tAction: v1alpha1.PodKillAction,\n\t\t\tMode:   v1alpha1.AllPodMode,\n\t\t\tScheduler: &v1alpha1.SchedulerSpec{\n\t\t\t\tCron: \"@every 10m\",\n\t\t\t},\n\t\t},\n\t}\n}\n\nvar _ = ginkgo.Describe(\"Test Get Success When Etcd Got Killed\", func() {\n\te := httpexpect.New(ginkgo.GinkgoT(), utils.Host)\n\teDataPanel := httpexpect.New(ginkgo.GinkgoT(), utils.DataPanelHost)\n\tePrometheus := httpexpect.New(ginkgo.GinkgoT(), utils.PrometheusHost)\n\n\tvar cliSet *utils.ClientSet\n\tvar apisixPod *v1.Pod\n\tvar err error\n\tginkgo.It(\"init client set\", func() {\n\t\tcliSet, err = utils.InitClientSet()\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tlistOption := client.MatchingLabels{\"app\": \"apisix-gw\"}\n\t\tapisixPods, err := utils.GetPods(cliSet.CtrlCli, metav1.NamespaceDefault, listOption)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(len(apisixPods)).Should(gomega.BeNumerically(\">\", 0))\n\t\tapisixPod = &apisixPods[0]\n\t})\n\n\tstopChan := make(chan bool)\n\n\tginkgo.It(\"setup prometheus metrics public API\", func() {\n\t\tutils.SetPrometheusMetricsPublicAPI(e)\n\t})\n\n\tginkgo.It(\"check if everything works\", func() {\n\t\tutils.SetRoute(e, httpexpect.Status2xx)\n\t\tutils.GetRouteList(e, http.StatusOK)\n\n\t\tutils.WaitUntilMethodSucceed(eDataPanel, http.MethodGet, 1)\n\t\tutils.TestPrometheusEtcdMetric(ePrometheus, 1)\n\t})\n\n\tginkgo.It(\"run request in background\", func() {\n\t\tgo func() {\n\t\t\tdefer ginkgo.GinkgoRecover()\n\t\t\tfor {\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer ginkgo.GinkgoRecover()\n\t\t\t\t\tutils.GetRoute(eDataPanel, http.StatusOK)\n\t\t\t\t}()\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\tstopLoop := false\n\t\t\t\tselect {\n\t\t\t\tcase <-stopChan:\n\t\t\t\t\tstopLoop = true\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tif stopLoop {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t})\n\t// wait 1 seconds to let first route access returns\n\ttime.Sleep(1 * time.Second)\n\n\tginkgo.It(\"get stats before kill etcd\", func() {\n\t\ttimeStart := time.Now()\n\t\tbandwidthBefore, durationBefore = utils.GetEgressBandwidthPerSecond(ePrometheus)\n\t\tbpsBefore = bandwidthBefore / durationBefore\n\t\tgomega.Expect(bpsBefore).NotTo(gomega.BeZero())\n\n\t\terrorLog, err := utils.Log(apisixPod, cliSet.KubeCli, timeStart)\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\tgomega.Ω(errorLog).ShouldNot(gomega.ContainSubstring(\"no healthy etcd endpoint available\"))\n\t})\n\n\t// apply chaos to kill all etcd pods\n\tginkgo.It(\"kill all etcd pods\", func() {\n\t\tchaos := getEtcdKillChaos()\n\t\terr := cliSet.CtrlCli.Create(context.Background(), chaos.DeepCopy())\n\t\tgomega.Expect(err).To(gomega.BeNil())\n\t\ttime.Sleep(3 * time.Second)\n\t})\n\n\t// fail to set route since etcd is all killed\n\t// while get route could still succeed\n\tginkgo.It(\"get stats after kill etcd\", func() {\n\t\tutils.SetRoute(e, httpexpect.Status5xx)\n\t\tutils.GetRoute(eDataPanel, http.StatusOK)\n\t\tutils.TestPrometheusEtcdMetric(ePrometheus, 0)\n\n\t\tbandwidthAfter, durationAfter = utils.GetEgressBandwidthPerSecond(ePrometheus)\n\t\tbpsAfter = bandwidthAfter / durationAfter\n\t})\n\n\tginkgo.It(\"ingress bandwidth per second not change much\", func() {\n\t\tfmt.Fprintf(ginkgo.GinkgoWriter, \"bandwidth before: %f, after: %f\\n\", bandwidthBefore, bandwidthAfter)\n\t\tfmt.Fprintf(ginkgo.GinkgoWriter, \"duration before: %f, after: %f\\n\", durationBefore, durationAfter)\n\t\tfmt.Fprintf(ginkgo.GinkgoWriter, \"bps before: %f, after: %f\\n\", bpsBefore, bpsAfter)\n\t\tgomega.Expect(utils.RoughCompare(bpsBefore, bpsAfter)).To(gomega.BeTrue())\n\t})\n\n\tginkgo.It(\"restore test environment\", func() {\n\t\tstopChan <- true\n\t\tcliSet.CtrlCli.Delete(context.Background(), getEtcdKillChaos())\n\t\tutils.WaitUntilMethodSucceed(e, http.MethodPut, 5)\n\t\tutils.DeleteRoute(e)\n\t})\n})\n"
  },
  {
    "path": "t/chaos/kubernetes/deployment.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\napiVersion: apps/v1  # for versions before 1.8.0 use apps/v1beta1, before 1.9.0 use apps/v1beta2\nkind: Deployment\nmetadata:\n  labels:\n    app: apisix-gw\n  name: apisix-gw-deployment\n  # namespace: default\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: apisix-gw\n  template:\n    metadata:\n      labels:\n        app: apisix-gw\n    spec:\n      affinity:\n        podAntiAffinity:\n          preferredDuringSchedulingIgnoredDuringExecution:\n            - podAffinityTerm:\n                labelSelector:\n                  matchExpressions:\n                    - key: app\n                      operator: In\n                      values:\n                        - apisix-gw\n                topologyKey: kubernetes.io/hostname\n              weight: 100\n      initContainers:\n        - command:\n            - /bin/sh\n            - -c\n            - |\n              sysctl -w net.core.somaxconn=65535\n              sysctl -w net.ipv4.ip_local_port_range=\"1024 65535\"\n              sysctl -w net.ipv4.tcp_max_syn_backlog=8192\n              sysctl -w fs.file-max=1048576\n              sysctl -w fs.inotify.max_user_instances=16384\n              sysctl -w fs.inotify.max_user_watches=524288\n              sysctl -w fs.inotify.max_queued_events=16384\n          image: busybox:latest\n          imagePullPolicy: IfNotPresent\n          name: init-sysctl\n          resources: {}\n          securityContext:\n            privileged: true\n            procMount: Default\n      restartPolicy: Always\n\n      containers:\n        - env:\n            - name: POD_NAME\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.name\n            - name: POD_NAMESPACE\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.namespace\n          image: \"apache/apisix:alpine-local\"\n          imagePullPolicy: IfNotPresent\n          name: apisix-gw-deployment\n          ports:\n            - containerPort: 9080\n              name: http\n              protocol: TCP\n            - containerPort: 9443\n              name: https\n              protocol: TCP\n            - containerPort: 9180\n              name: admin-port\n              protocol: TCP\n          readinessProbe:\n            failureThreshold: 6\n            initialDelaySeconds: 10\n            periodSeconds: 10\n            successThreshold: 1\n            tcpSocket:\n              port: 9080\n            timeoutSeconds: 1\n          volumeMounts:\n            - mountPath: /usr/local/apisix/conf/config.yaml\n              name: apisix-config-yaml-configmap\n              subPath: config.yaml\n            - mountPath: /etc/localtime\n              name: localtime\n              readOnly: true\n      volumes:\n        - configMap:\n            name: apisix-gw-config.yaml\n          name: apisix-config-yaml-configmap\n        - hostPath:\n            path: /etc/localtime\n            type: File\n          name: localtime\n"
  },
  {
    "path": "t/chaos/kubernetes/service.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: apisix-gw-lb\n  # namespace: default\n  labels:\n    app: apisix-gw  # useful for service discovery, for example, prometheus-operator.\nspec:\n  ports:\n    - name: http\n      port: 9080\n      protocol: TCP\n      targetPort: 9080\n    - name: https\n      port: 9443\n      protocol: TCP\n      targetPort: 9443\n    - name: admin-port\n      port: 9180\n      protocol: TCP\n      targetPort: 9180\n  selector:\n    app: apisix-gw\n  type: NodePort\n  externalTrafficPolicy: Local\n  # sessionAffinity: None\n"
  },
  {
    "path": "t/chaos/utils/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nARG ENABLE_PROXY=false\n\nFROM openresty/openresty:1.21.4.2-alpine-fat AS production-stage\n\nARG ENABLE_PROXY\nARG APISIX_PATH\nCOPY $APISIX_PATH ./apisix\nRUN set -x \\\n    && (test \"${ENABLE_PROXY}\" != \"true\" || /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories) \\\n    && apk add --no-cache --virtual .builddeps \\\n    automake \\\n    autoconf \\\n    libtool \\\n    pkgconfig \\\n    cmake \\\n    git \\\n    openldap-dev \\\n    pcre-dev \\\n    sudo \\\n    && cd apisix \\\n    && git config --global url.https://github.com/.insteadOf git://github.com/ \\\n    && make deps \\\n    && cp -v bin/apisix /usr/bin/ \\\n    && mv ../apisix /usr/local/apisix \\\n    && apk del .builddeps build-base make unzip\n\nFROM alpine:3.13 AS last-stage\n\nARG ENABLE_PROXY\n# add runtime for Apache APISIX\nRUN set -x \\\n    && (test \"${ENABLE_PROXY}\" != \"true\" || /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories) \\\n    && apk add --no-cache \\\n        bash \\\n        curl \\\n        libstdc++ \\\n        openldap \\\n        pcre \\\n        tzdata\n\nWORKDIR /usr/local/apisix\n\nCOPY --from=production-stage /usr/local/openresty/ /usr/local/openresty/\nCOPY --from=production-stage /usr/local/apisix/ /usr/local/apisix/\nCOPY --from=production-stage /usr/bin/apisix /usr/bin/apisix\n\n# forward request and error logs to docker log collector\nRUN mkdir -p logs && touch logs/access.log && touch logs/error.log \\\n    && ln -sf /dev/stdout /usr/local/apisix/logs/access.log \\\n    && ln -sf /dev/stderr /usr/local/apisix/logs/error.log\n\nENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin\n\nEXPOSE 9080 9180 9443\n\nCMD [\"sh\", \"-c\", \"/usr/bin/apisix init && /usr/bin/apisix init_etcd && /usr/local/openresty/bin/openresty -p /usr/local/apisix -g 'daemon off;'\"]\n\nSTOPSIGNAL SIGQUIT\n\n"
  },
  {
    "path": "t/chaos/utils/kube_utils.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage utils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/chaos-mesh/chaos-mesh/api/v1alpha1\"\n\t\"github.com/pkg/errors\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/kubernetes\"\n\tclientScheme \"k8s.io/client-go/kubernetes/scheme\"\n\t\"k8s.io/client-go/tools/remotecommand\"\n\tkubectlScheme \"k8s.io/kubectl/pkg/scheme\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client/config\"\n)\n\ntype ClientSet struct {\n\tCtrlCli client.Client\n\tKubeCli *kubernetes.Clientset\n}\n\nfunc InitClientSet() (*ClientSet, error) {\n\tscheme := runtime.NewScheme()\n\tv1alpha1.AddToScheme(scheme)\n\tclientScheme.AddToScheme(scheme)\n\n\trestConfig := config.GetConfigOrDie()\n\tctrlCli, err := client.New(restConfig, client.Options{Scheme: scheme})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkubeCli, err := kubernetes.NewForConfig(restConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ClientSet{ctrlCli, kubeCli}, nil\n}\n\nfunc GetPods(cli client.Client, ns string, listOption client.MatchingLabels) ([]corev1.Pod, error) {\n\tpodList := &corev1.PodList{}\n\terr := cli.List(context.Background(), podList, client.InNamespace(ns), listOption)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn podList.Items, nil\n}\n\nfunc ExecInPod(cli *kubernetes.Clientset, pod *corev1.Pod, cmd string) (string, error) {\n\tname := pod.GetName()\n\tnamespace := pod.GetNamespace()\n\t// only get the first container, no harm for now\n\tcontainerName := pod.Spec.Containers[0].Name\n\n\treq := cli.CoreV1().RESTClient().Post().\n\t\tResource(\"pods\").\n\t\tName(name).\n\t\tNamespace(namespace).\n\t\tSubResource(\"exec\")\n\n\treq.VersionedParams(&corev1.PodExecOptions{\n\t\tContainer: containerName,\n\t\tCommand:   []string{\"/bin/sh\", \"-c\", cmd},\n\t\tStdin:     false,\n\t\tStdout:    true,\n\t\tStderr:    true,\n\t\tTTY:       false,\n\t}, kubectlScheme.ParameterCodec)\n\n\tvar stdout, stderr bytes.Buffer\n\texec, err := remotecommand.NewSPDYExecutor(config.GetConfigOrDie(), \"POST\", req.URL())\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"error in creating NewSPDYExecutor for pod %s in ns: %s\", name, namespace)\n\t}\n\terr = exec.Stream(remotecommand.StreamOptions{\n\t\tStdin:  nil,\n\t\tStdout: &stdout,\n\t\tStderr: &stderr,\n\t})\n\tif stderr.String() != \"\" {\n\t\tstderror := errors.New(stderr.String())\n\t\treturn \"\", errors.Wrapf(stderror, \"pod: %s\\ncommand: %s\", name, cmd)\n\t}\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"error in streaming remote command: pod: %s in ns: %s\\n command: %s\", name, namespace, cmd)\n\t}\n\treturn stdout.String(), nil\n}\n\n// Log print log of pod\nfunc Log(pod *corev1.Pod, c *kubernetes.Clientset, sinceTime time.Time) (string, error) {\n\tpodLogOpts := corev1.PodLogOptions{}\n\tif !sinceTime.IsZero() {\n\t\tpodLogOpts.SinceTime = &metav1.Time{Time: sinceTime}\n\t}\n\n\treq := c.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &podLogOpts)\n\tpodLogs, err := req.Stream()\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed to open log stream for pod %s/%s\", pod.GetNamespace(), pod.GetName())\n\t}\n\tdefer podLogs.Close()\n\n\tbuf := new(bytes.Buffer)\n\t_, err = io.Copy(buf, podLogs)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed to copy information from podLogs to buf\")\n\t}\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "t/chaos/utils/setup_chaos_utils.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\nstart_minikube() {\n    # pin the version until chaos mesh solves https://github.com/chaos-mesh/chaos-mesh/issues/2172\n    curl -LO \"https://storage.googleapis.com/kubernetes-release/release/v1.21.4/bin/linux/amd64/kubectl\"\n    chmod +x ./kubectl\n    sudo mv ./kubectl /usr/local/bin/kubectl\n\n    curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb\n    sudo dpkg -i --force-architecture minikube_latest_amd64.deb\n    minikube start --kubernetes-version \"v1.21.4\"\n}\n\nmodify_config() {\n    DNS_IP=$(kubectl get svc -n kube-system -l k8s-app=kube-dns -o 'jsonpath={..spec.clusterIP}')\n    echo \"dns_resolver:\n  - ${DNS_IP}\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \\\"http://etcd.default.svc.cluster.local:2379\\\"\nplugin_attr:\n  prometheus:\n    enable_export_server: false\n  \" > ./conf/config.yaml\n}\n\nport_forward() {\n    apisix_pod_name=$(kubectl get pod -l app=apisix-gw -o 'jsonpath={.items[0].metadata.name}')\n    nohup kubectl port-forward svc/apisix-gw-lb 9080:9080 >/dev/null 2>&1 &\n    nohup kubectl port-forward svc/apisix-gw-lb 9180:9180 >/dev/null 2>&1 &\n    nohup kubectl port-forward $apisix_pod_name 9091:9091 >/dev/null 2>&1 &\n    ps aux | grep '[p]ort-forward'\n}\n\n\"$@\"\n"
  },
  {
    "path": "t/chaos/utils/utils.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage utils\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gavv/httpexpect\"\n\t\"github.com/onsi/ginkgo\"\n\t\"github.com/onsi/gomega\"\n)\n\nvar (\n\ttoken = \"edd1c9f034335f136f87ad84b625c8f1\"\n\t// TODO: refactor the code. We should move the endpoint from the expect to the http call.\n\t// So we don't need to remember to pass the correct expect.\n\tHost           = \"http://127.0.0.1:9180\"\n\tDataPanelHost  = \"http://127.0.0.1:9080\"\n\tPrometheusHost = \"http://127.0.0.1:9080\"\n\tsetRouteBody   = `{\n\t\t\"uri\": \"/get\",\n\t\t\"plugins\": {\n\t\t\t\"prometheus\": {}\n\t\t},\n\t\t\"upstream\": {\n\t\t\t\"nodes\": {\n\t\t\t\t\"httpbin.default.svc.cluster.local:8000\": 1\n\t\t\t},\n\t\t\t\"type\": \"roundrobin\"\n\t\t}\n\t}`\n\tignoreErrorFuncMap = map[string]func(e *httpexpect.Expect) *httpexpect.Response{\n\t\thttp.MethodGet: GetRouteIgnoreError,\n\t\thttp.MethodPut: SetRouteIgnoreError,\n\t}\n)\n\ntype httpTestCase struct {\n\tE                 *httpexpect.Expect\n\tMethod            string\n\tPath              string\n\tBody              string\n\tHeaders           map[string]string\n\tIgnoreError       bool\n\tExpectStatus      int\n\tExpectBody        string\n\tExpectStatusRange httpexpect.StatusRange\n}\n\nfunc caseCheck(tc httpTestCase) *httpexpect.Response {\n\te := tc.E\n\tvar req *httpexpect.Request\n\tswitch tc.Method {\n\tcase http.MethodGet:\n\t\treq = e.GET(tc.Path)\n\tcase http.MethodPut:\n\t\treq = e.PUT(tc.Path)\n\tcase http.MethodDelete:\n\t\treq = e.DELETE(tc.Path)\n\tdefault:\n\t\tpanic(\"invalid HTTP method\")\n\t}\n\n\tif req == nil {\n\t\tpanic(\"fail to init request\")\n\t}\n\tfor key, val := range tc.Headers {\n\t\treq.WithHeader(key, val)\n\t}\n\tif tc.Body != \"\" {\n\t\treq.WithText(tc.Body)\n\t}\n\n\tresp := req.Expect()\n\tif tc.IgnoreError {\n\t\treturn resp\n\t}\n\n\tif tc.ExpectStatus != 0 {\n\t\tresp.Status(tc.ExpectStatus)\n\t}\n\n\tif tc.ExpectStatusRange != 0 {\n\t\tresp.StatusRange(tc.ExpectStatusRange)\n\t}\n\n\tif tc.ExpectBody != \"\" {\n\t\tresp.Body().Contains(tc.ExpectBody)\n\t}\n\n\treturn resp\n}\n\nfunc SetRoute(e *httpexpect.Expect, expectStatusRange httpexpect.StatusRange) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:                 e,\n\t\tMethod:            http.MethodPut,\n\t\tPath:              \"/apisix/admin/routes/1\",\n\t\tHeaders:           map[string]string{\"X-API-KEY\": token},\n\t\tBody:              setRouteBody,\n\t\tExpectStatusRange: expectStatusRange,\n\t})\n}\n\nfunc SetRouteIgnoreError(e *httpexpect.Expect) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:           e,\n\t\tMethod:      http.MethodPut,\n\t\tPath:        \"/apisix/admin/routes/1\",\n\t\tHeaders:     map[string]string{\"X-API-KEY\": token},\n\t\tBody:        setRouteBody,\n\t\tIgnoreError: true,\n\t})\n}\n\nfunc GetRoute(e *httpexpect.Expect, expectStatus int) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:            e,\n\t\tMethod:       http.MethodGet,\n\t\tPath:         \"/get\",\n\t\tExpectStatus: expectStatus,\n\t})\n}\n\nfunc GetRouteIgnoreError(e *httpexpect.Expect) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:           e,\n\t\tMethod:      http.MethodGet,\n\t\tPath:        \"/get\",\n\t\tIgnoreError: true,\n\t})\n}\n\nfunc GetRouteList(e *httpexpect.Expect, expectStatus int) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:            e,\n\t\tMethod:       http.MethodGet,\n\t\tPath:         \"/apisix/admin/routes\",\n\t\tHeaders:      map[string]string{\"X-API-KEY\": token},\n\t\tExpectStatus: expectStatus,\n\t\tExpectBody:   \"httpbin.default.svc.cluster.local\",\n\t})\n}\n\nfunc DeleteRoute(e *httpexpect.Expect) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:       e,\n\t\tMethod:  http.MethodDelete,\n\t\tPath:    \"/apisix/admin/routes/1\",\n\t\tHeaders: map[string]string{\"X-API-KEY\": token},\n\t})\n}\n\nfunc SetPrometheusMetricsPublicAPI(e *httpexpect.Expect) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:       e,\n\t\tMethod:  http.MethodPut,\n\t\tPath:    \"/apisix/admin/routes/metrics\",\n\t\tHeaders: map[string]string{\"X-API-KEY\": token},\n\t\tBody: `{\n\t\t\t\"uri\": \"/apisix/prometheus/metrics\",\n\t\t\t\"plugins\": {\n\t\t\t\t\"public-api\": {}\n\t\t\t},\n\t\t\t\"upstream\": {\n\t\t\t\t\"nodes\": {\n\t\t\t\t\t\"httpbin.default.svc.cluster.local:8000\": 1\n\t\t\t\t},\n\t\t\t\t\"type\": \"roundrobin\"\n\t\t\t}\n\t\t}`,\n\t})\n}\n\nfunc TestPrometheusEtcdMetric(e *httpexpect.Expect, expectEtcd int) *httpexpect.Response {\n\treturn caseCheck(httpTestCase{\n\t\tE:          e,\n\t\tMethod:     http.MethodGet,\n\t\tPath:       \"/apisix/prometheus/metrics\",\n\t\tExpectBody: fmt.Sprintf(\"apisix_etcd_reachable %d\", expectEtcd),\n\t})\n}\n\n// get the first line which contains the key\nfunc getPrometheusMetric(e *httpexpect.Expect, key string) string {\n\tresp := caseCheck(httpTestCase{\n\t\tE:      e,\n\t\tMethod: http.MethodGet,\n\t\tPath:   \"/apisix/prometheus/metrics\",\n\t})\n\tresps := strings.Split(resp.Body().Raw(), \"\\n\")\n\tvar targetLine string\n\tfor _, line := range resps {\n\t\tif strings.Contains(line, key) {\n\t\t\ttargetLine = line\n\t\t\tbreak\n\t\t}\n\t}\n\ttargetSlice := strings.Fields(targetLine)\n\tgomega.Ω(len(targetSlice)).Should(gomega.BeNumerically(\"==\", 2))\n\treturn targetSlice[1]\n}\n\nfunc GetEgressBandwidthPerSecond(e *httpexpect.Expect) (float64, float64) {\n\tkey := \"apisix_bandwidth{type=\\\"egress\\\",\"\n\tbandWidthString := getPrometheusMetric(e, key)\n\tbandWidthStart, err := strconv.ParseFloat(bandWidthString, 64)\n\tgomega.Expect(err).To(gomega.BeNil())\n\t// after etcd got killed, it would take longer time to get the metrics\n\t// so need to calculate the duration\n\ttimeStart := time.Now()\n\n\ttime.Sleep(10 * time.Second)\n\tbandWidthString = getPrometheusMetric(e, key)\n\tbandWidthEnd, err := strconv.ParseFloat(bandWidthString, 64)\n\tgomega.Expect(err).To(gomega.BeNil())\n\tduration := time.Since(timeStart)\n\n\treturn bandWidthEnd - bandWidthStart, duration.Seconds()\n}\n\nfunc GetSilentHttpexpectClient() *httpexpect.Expect {\n\treturn httpexpect.WithConfig(httpexpect.Config{\n\t\tBaseURL:  Host,\n\t\tReporter: httpexpect.NewAssertReporter(ginkgo.GinkgoT()),\n\t\tPrinters: []httpexpect.Printer{\n\t\t\tnewSilentPrinter(ginkgo.GinkgoT()),\n\t\t},\n\t})\n}\n\nfunc WaitUntilMethodSucceed(e *httpexpect.Expect, method string, interval int) {\n\tf, ok := ignoreErrorFuncMap[method]\n\tgomega.Expect(ok).To(gomega.BeTrue())\n\tresp := f(e)\n\tif resp.Raw().StatusCode != http.StatusOK {\n\t\tfor i := range [60]int{} {\n\t\t\ttimeWait := fmt.Sprintf(\"wait for %ds\\n\", i*interval)\n\t\t\tfmt.Fprint(ginkgo.GinkgoWriter, timeWait)\n\t\t\tresp = f(e)\n\t\t\tif resp.Raw().StatusCode != http.StatusOK {\n\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tgomega.Ω(resp.Raw().StatusCode).Should(gomega.BeNumerically(\"==\", http.StatusOK))\n}\n\nfunc RoughCompare(a float64, b float64) bool {\n\tratio := a / b\n\tif ratio < 1.3 && ratio > 0.7 {\n\t\treturn true\n\t}\n\treturn false\n}\n\ntype silentPrinter struct {\n\tlogger httpexpect.Logger\n}\n\nfunc newSilentPrinter(logger httpexpect.Logger) silentPrinter {\n\treturn silentPrinter{logger}\n}\n\n// Request implements Printer.Request.\nfunc (p silentPrinter) Request(req *http.Request) {\n}\n\n// Response implements Printer.Response.\nfunc (silentPrinter) Response(*http.Response, time.Duration) {\n}\n"
  },
  {
    "path": "t/cli/cli.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# unit test for cli module\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: local_dns_resolver\n--- config\n    location /t {\n        content_by_lua_block {\n            local local_dns_resolver = require(\"apisix.cli.ops\").local_dns_resolver\n            local json_encode = require(\"toolkit.json\").encode\n            ngx.say(json_encode(local_dns_resolver(\"$TEST_NGINX_HTML_DIR/resolv.conf\")))\n        }\n    }\n--- user_files\n>>> resolv.conf\n# This file was automatically generated.\nnameserver 172.27.0.1\n\nnameserver fe80::215:5dff:fec5:8e1d\n--- response_body\n[\"172.27.0.1\",\"fe80::215:5dff:fec5:8e1d\"]\n"
  },
  {
    "path": "t/cli/cli_envsubst_confusion.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\n\n$ENV{SOME_STRING_VALUE_BUT_DIFFERENT} = 'astringvaluebutdifferent';\n$ENV{SOME_STRING_VALUE} = 'astringvalue';\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n_EOC_\n\nour $apisix_yaml = <<_EOC_;\nupstreams:\n  - id: 1\n    nodes:\n      - host: 127.0.0.1\n        port: 1980\n        weight: 1\nroutes:\n  - uri: /hello\n    upstream_id: 1\n    plugins:\n      response-rewrite:\n        headers:\n          set:\n            X-Some-String-Value-But-Different: Different \\${{SOME_STRING_VALUE_BUT_DIFFERENT}}\n            X-Some-String-Value: \\${{SOME_STRING_VALUE}}\n#END\n_EOC_\n\nour $response_headers_correct = <<_EOC_;\nX-Some-String-Value-But-Different: Different astringvaluebutdifferent\nX-Some-String-Value: astringvalue\n_EOC_\n\nour $response_headers_INCORRECT = <<_EOC_;\nX-Some-String-Value-But-Different: Different astringvalue\nX-Some-String-Value: astringvalue\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: assignment style, the PREFIX 1st - incorrect\n--- main_config\nenv SOME_STRING_VALUE=astringvalue;\nenv SOME_STRING_VALUE_BUT_DIFFERENT=astringvaluebutdifferent;\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml eval: $::apisix_yaml\n--- response_headers eval: $::response_headers_INCORRECT\n\n\n\n=== TEST 2: assignment style, the DIFF 1st - correct\n--- main_config\nenv SOME_STRING_VALUE_BUT_DIFFERENT=astringvaluebutdifferent;\nenv SOME_STRING_VALUE=astringvalue;\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml eval: $::apisix_yaml\n--- response_headers eval: $::response_headers_correct\n\n\n\n=== TEST 3: declaration style, the PREFIX 1st - correct\n--- main_config\nenv SOME_STRING_VALUE;\nenv SOME_STRING_VALUE_BUT_DIFFERENT;\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml eval: $::apisix_yaml\n--- response_headers eval: $::response_headers_correct\n\n\n\n=== TEST 4: declaration style, the DIFF 1st - also correct\n--- main_config\nenv SOME_STRING_VALUE_BUT_DIFFERENT;\nenv SOME_STRING_VALUE;\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml eval: $::apisix_yaml\n--- response_headers eval: $::response_headers_correct\n"
  },
  {
    "path": "t/cli/common.sh",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# 'make init' operates scripts and related configuration files in the current directory\n# The 'apisix' command is a command in the /usr/local/apisix,\n# and the configuration file for the operation is in the /usr/local/apisix/conf\n\nset -ex\n\ncheck_failure() {\n    cat logs/error.log\n}\n\nclean_up() {\n    if [ $? -gt 0 ]; then\n        check_failure\n    fi\n    make stop || true\n    git checkout conf/config.yaml\n}\n\ntrap clean_up EXIT\n\nexit_if_not_customed_nginx() {\n    openresty -V 2>&1 | grep apisix-nginx-module || exit 0\n}\n\nrm logs/error.log || true # clear previous error log\nunset APISIX_PROFILE\n"
  },
  {
    "path": "t/cli/docker-compose-etcd-cluster.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nversion: \"3.7\"\n\nservices:\n  etcd0:\n    image: \"gcr.io/etcd-development/etcd:v3.4.15\"\n    container_name: etcd0\n    ports:\n      - \"23800:2380\"\n      - \"23790:2379\"\n    environment:\n      - ALLOW_NONE_AUTHENTICATION=yes\n      - ETCD_NAME=etcd0\n      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380\n      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379\n      - ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:23790\n      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd0:2380\n      - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster\n      - ETCD_INITIAL_CLUSTER=etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380\n      - ETCD_INITIAL_CLUSTER_STATE=new\n      - ETCD_ENABLE_GRPC_GATEWAY=${ETCD_ENABLE_GRPC_GATEWAY:-true}\n\n  etcd1:\n    image: \"gcr.io/etcd-development/etcd:v3.4.15\"\n    container_name: etcd1\n    ports:\n      - \"23801:2380\"\n      - \"23791:2379\"\n    environment:\n      - ALLOW_NONE_AUTHENTICATION=yes\n      - ETCD_NAME=etcd1\n      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380\n      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379\n      - ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:23791\n      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd1:2380\n      - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster\n      - ETCD_INITIAL_CLUSTER=etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380\n      - ETCD_INITIAL_CLUSTER_STATE=new\n      - ETCD_ENABLE_GRPC_GATEWAY=${ETCD_ENABLE_GRPC_GATEWAY:-true}\n\n  etcd2:\n    image: \"gcr.io/etcd-development/etcd:v3.4.15\"\n    container_name: etcd2\n    ports:\n      - \"23802:2380\"\n      - \"23792:2379\"\n    environment:\n      - ALLOW_NONE_AUTHENTICATION=yes\n      - ETCD_NAME=etcd2\n      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380\n      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379\n      - ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:23792\n      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd2:2380\n      - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster\n      - ETCD_INITIAL_CLUSTER=etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380\n      - ETCD_INITIAL_CLUSTER_STATE=new\n      - ETCD_ENABLE_GRPC_GATEWAY=${ETCD_ENABLE_GRPC_GATEWAY:-true}\n"
  },
  {
    "path": "t/cli/test_access_log.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# log format\n\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  http:\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host test_access_log_format\"\n' > conf/config.yaml\n\nmake init\n\ngrep \"test_access_log_format\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: access_log_format in nginx.conf doesn't change\"\n    exit 1\nfi\n\necho \"passed: access_log_format in nginx.conf is ok\"\n\n# check enable access log\n\necho '\nnginx_config:\n  http:\n    enable_access_log: true\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host test_enable_access_log_true\"\n' > conf/config.yaml\n\nmake init\n\ncount_test_access_log=`grep -c \"test_enable_access_log_true\" conf/nginx.conf || true`\nif [ $count_test_access_log -eq 0 ]; then\n    echo \"failed: nginx.conf file doesn't find access_log_format when enable access log\"\n    exit 1\nfi\n\ncount_access_log_off=`grep -c \"access_log off;\" conf/nginx.conf || true`\nif [ $count_access_log_off -eq 5 ]; then\n    echo \"failed: nginx.conf file find access_log off; when enable access log\"\n    exit 1\nfi\n\nmake run\nsleep 0.1\ncurl http://127.0.0.1:9080/hi\nsleep 4\ntail -n 1 logs/access.log > output.log\n\ncount_grep=`grep -c \"test_enable_access_log_true\" output.log || true`\nif [ $count_grep -eq 0 ]; then\n    echo \"failed: not found test_enable_access_log in access.log \"\n    exit 1\nfi\n\nmake stop\n\necho '\nnginx_config:\n  http:\n    enable_access_log: false\n    access_log_format: \"$remote_addr - $remote_user [$time_local] $http_host test_enable_access_log_false\"\n' > conf/config.yaml\n\nmake init\n\ncount_test_access_log=`grep -c \"test_enable_access_log_false\" conf/nginx.conf || true`\nif [ $count_test_access_log -eq 1 ]; then\n    echo \"failed: nginx.conf file find access_log_format when disable access log\"\n    exit 1\nfi\n\nmake run\nsleep 0.1\ncurl http://127.0.0.1:9080/hi\nsleep 4\ntail -n 1 logs/access.log > output.log\n\ncount_grep=`grep -c \"test_enable_access_log_false\" output.log || true`\nif [ $count_grep -eq 1 ]; then\n    echo \"failed: found test_enable_access_log in access.log \"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: enable_access_log is ok\"\n\n# access log with JSON format\n\necho '\nnginx_config:\n  http:\n    access_log_format: |-\n      {\"@timestamp\": \"$time_iso8601\", \"client_ip\": \"$remote_addr\", \"status\": \"$status\"}\n    access_log_format_escape: json\n' > conf/config.yaml\n\nmake init\nmake run\nsleep 0.1\ncurl http://127.0.0.1:9080/hello2\nsleep 4\ntail -n 1 logs/access.log > output.log\n\nif [ `grep -c '\"client_ip\": \"127.0.0.1\"' output.log` -eq '0' ]; then\n    echo \"failed: invalid JSON log in access log\"\n    exit 1\nfi\n\nif [ `grep -E \"^{[^}]+}$\" output.log` -eq '0' ]; then\n    echo \"failed: invalid JSON log in access log\"\n    exit 1\nfi\n\nif [ `grep -c 'main escape=json' conf/nginx.conf` -eq '0' ]; then\n    echo \"failed: not found \\\"escape=json\\\" in conf/nginx.conf\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: access log with JSON format\"\n\n\n# access log with default log format\necho '\nnginx_config:\n  http:\n    enable_access_log: true\n' > conf/config.yaml\n\nmake init\nmake run\nsleep 0.1\ncurl http://127.0.0.1:9080/hello2\nsleep 4\ntail -n 1 logs/access.log > output.log\n\nif [ `grep -E '\"[0-9a-zA-Z]{32}\"$' output.log` -eq '0' ]; then\n    echo \"failed: access log don't match default log format\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: access log with default format\"\n\n# access log with unset llm_token JSON format\n\necho '\nnginx_config:\n  http:\n    access_log_format: |-\n      {\"@timestamp\": \"$time_iso8601\", \"client_ip\": \"$remote_addr\", \"status\": \"$status\", \"llm_prompt_tokens\": $llm_prompt_tokens}\n    access_log_format_escape: json\n' > conf/config.yaml\n\nmake init\nmake run\nsleep 0.1\ncurl http://127.0.0.1:9080/hello2\nsleep 4\ntail -n 1 logs/access.log > output.log\n\nif [ `grep -c '\"llm_prompt_tokens\": 0' output.log` -eq '0' ]; then\n    echo \"failed: invalid JSON log in access log\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: access log with JSON format and llm_prompt_tokens\"\n\n# check uninitialized variable in access log when access admin\ngit checkout conf/config.yaml\n\nrm logs/error.log\nmake init\nmake run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nmake stop\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: failed to access admin\"\n    exit 1\nfi\n\nif grep -E 'using uninitialized \".+\" variable while logging request' logs/error.log; then\n    echo \"failed: uninitialized variable found during writing access log\"\n    exit 1\nfi\n\necho \"pass: uninitialized variable not found during writing access log\"\n\n# don't log uninitialized access log variable when the HTTP request is malformed\n\ngit checkout conf/config.yaml\n\nrm logs/error.log\n./bin/apisix start\nsleep 1 # wait for apisix starts\n\ncurl -v -k -i -m 20 -o /dev/null -s https://127.0.0.1:9080 || true\nif grep -E 'using uninitialized \".+\" variable while logging request' logs/error.log; then\n    echo \"failed: log uninitialized access log variable when the HTTP request is malformed\"\n    exit 1\nfi\n\nmake stop\n\necho \"don't log uninitialized access log variable when the HTTP request is malformed\"\n\n# TLS upstream\n\necho \"\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\n        https_admin: true\n        admin_api_mtls:\n            admin_ssl_cert: '../t/certs/apisix_admin_ssl.crt'\n            admin_ssl_cert_key: '../t/certs/apisix_admin_ssl.key'\nnginx_config:\n  http:\n    access_log_format: '\\\"\\$upstream_scheme://\\$upstream_host\\\" \\$ssl_server_name'\n\" > conf/config.yaml\n\nmake run\nsleep 2\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -k -i https://127.0.0.1:9180/apisix/admin/routes/1  -H \"X-API-KEY: $admin_key\" -X PUT -d \\\n    '{\"uri\":\"/apisix/admin/routes/1\", \"upstream\":{\"nodes\":{\"localhost:9180\":1},\"scheme\":\"https\",\"type\":\"roundrobin\",\"pass_host\":\"node\"}}'\n\ncurl -i http://127.0.0.1:9080/apisix/admin/routes/1\nsleep 4\ntail -n 2 logs/access.log > output.log\n\n# APISIX\nif ! grep '\"https://localhost:9180\" -' output.log; then\n    echo \"failed: should find upstream scheme\"\n    cat output.log\n    exit 1\nfi\n\n# admin\nif ! grep '\"http://localhost:9180\" localhost' output.log; then\n    echo \"failed: should find upstream scheme\"\n    cat output.log\n    exit 1\nfi\n\nmake stop\necho \"passed: should find upstream scheme\"\n\n# check stream logs\necho '\napisix:\n    proxy_mode: stream\n    stream_proxy:                  # UDP proxy\n     udp:\n       - \"127.0.0.1:9200\"\n\nnginx_config:\n  stream:\n    enable_access_log: true\n    access_log_format: \"$remote_addr $protocol test_stream_access_log_format\"\n' > conf/config.yaml\n\nmake init\n\ngrep \"test_stream_access_log_format\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: stream access_log_format in nginx.conf doesn't change\"\n    exit 1\nfi\necho \"passed: stream access_log_format in nginx.conf is ok\"\n\n# check if logs are being written\nmake run\nsleep 0.1\n# sending single udp packet\necho -n \"hello\" | nc -4u -w1 localhost 9200\nsleep 4\ntail -n 1 logs/access_stream.log > output.log\n\nif ! grep '127.0.0.1 UDP test_stream_access_log_format' output.log; then\n    echo \"failed: should have found udp log entry\"\n    cat output.log\n    exit 1\nfi\necho \"passed: logs are being dumped for stream proxy\"\n"
  },
  {
    "path": "t/cli/test_admin.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# check admin https enabled\n\ngit checkout conf/config.yaml\n\necho \"\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\n        https_admin: true\n        admin_api_mtls:\n            admin_ssl_cert: '../t/certs/apisix_admin_ssl.crt'\n            admin_ssl_cert_key: '../t/certs/apisix_admin_ssl.key'\n\" > conf/config.yaml\n\nmake init\n\ngrep \"listen 0.0.0.0:9180 ssl\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: failed to enable https for admin\"\n    exit 1\nfi\n\nmake run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\necho \"admin key is \" $admin_key\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} https://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nif [ ! $code -eq 200 ]; then\n    echo \"failed: failed to enable https for admin\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: admin https enabled\"\n\necho '\napisix:\n  enable_admin: true\ndeployment:\n  admin:\n    admin_listen:\n      ip: 127.0.0.2\n      port: 9181\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"listen 127.0.0.2:9181;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: customize address for admin server\"\n    exit 1\nfi\n\nmake run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.2:9181/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: failed to access admin\"\n    exit 1\nfi\n\nmake stop\n\n# rollback to the default\n\ngit checkout conf/config.yaml\n\nmake init\n\nset +ex\n\ngrep \"listen 0.0.0.0:9080 ssl\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 1 ]; then\n    echo \"failed: failed to rollback to the default admin config\"\n    exit 1\nfi\n\nset -ex\n\necho \"passed: rollback to the default admin config\"\n\n# set allow_admin in conf/config.yaml\n\necho \"\ndeployment:\n    admin:\n        allow_admin:\n            - 127.0.0.9\n\" > conf/config.yaml\n\nmake init\n\ncount=`grep -c \"allow 127.0.0.9\" conf/nginx.conf`\nif [ $count -eq 0 ]; then\n    echo \"failed: not found 'allow 127.0.0.9;' in conf/nginx.conf\"\n    exit 1\nfi\n\necho \"\ndeployment:\n    admin:\n        allow_admin: ~\n\" > conf/config.yaml\n\nmake init\n\ncount=`grep -c \"allow all;\" conf/nginx.conf`\nif [ $count -eq 0 ]; then\n    echo \"failed: not found 'allow all;' in conf/nginx.conf\"\n    exit 1\nfi\n\necho \"passed: empty allow_admin in conf/config.yaml\"\n\n# missing admin key, allow any IP to access admin api\n\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  admin:\n    admin_key: ~\n    allow_admin: ~\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\ngrep -E \"ERROR: missing valid Admin API token.\" output.log > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: should show 'ERROR: missing valid Admin API token.'\"\n    exit 1\nfi\n\necho \"pass: missing admin key and show ERROR message\"\n\n# missing admin key, only allow 127.0.0.0/24 to access admin api\n\necho '\ndeployment:\n  admin:\n    admin_key: ~\n    allow_admin:\n      - 127.0.0.0/24\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\nif grep -E \"ERROR: missing valid Admin API token.\" output.log > /dev/null; then\n    echo \"failed: should not show 'ERROR: missing valid Admin API token.'\"\n    exit 1\nfi\n\necho '\ndeployment:\n  admin:\n    admin_key: ~\n    allow_admin:\n      - 0.0.0.0/0\n      - 127.0.0.0/24\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\nif ! grep -E \"ERROR: missing valid Admin API token.\" output.log > /dev/null; then\n    echo \"failed: should show 'ERROR: missing valid Admin API token.'\"\n    exit 1\nfi\n\necho \"pass: missing admin key and only allow 127.0.0.0/24 to access admin api\"\n\n# allow any IP to access admin api with empty admin_key, when admin_key_required=true\n\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  admin:\n    admin_key_required: true\n    admin_key: ~\n    allow_admin:\n      - 0.0.0.0/0\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\nif ! grep -E \"ERROR: missing valid Admin API token.\" output.log > /dev/null; then\n    echo \"failed: should show 'ERROR: missing valid Admin API token.'\"\n    exit 1\nfi\n\necho '\ndeployment:\n  admin:\n    admin_key_required: false\n    admin_key: ~\n    allow_admin:\n      - 0.0.0.0/0\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\nif grep -E \"ERROR: missing valid Admin API token.\" output.log > /dev/null; then\n    echo \"failed: should not show 'ERROR: missing valid Admin API token.'\"\n    exit 1\nfi\n\nif ! grep -E \"Warning! Admin key is bypassed\" output.log > /dev/null; then\n    echo \"failed: should show 'Warning! Admin key is bypassed'\"\n    exit 1\nfi\n\necho '\ndeployment:\n  admin:\n    admin_key_required: invalid-value\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\nif grep -E \"path[deployment->admin->admin_key_required] expect: boolean, but got: string\" output.log > /dev/null; then\n    echo \"check admin_key_required value failed: should show 'expect: boolean, but got: string'\"\n    exit 1\nfi\n\necho \"pass: allow empty admin_key, when admin_key_required=false\"\n\n# admin api, allow any IP but use default key\n\necho '\ndeployment:\n  admin:\n    allow_admin: ~\n    admin_key:\n      - name: \"admin\"\n        key: ''\n        role: admin\n' > conf/config.yaml\n\nmake init > output.log 2>&1 | true\n\ngrep -E \"WARNING: using empty Admin API.\" output.log > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: need to show `WARNING: using fixed Admin API token has security risk`\"\n    exit 1\nfi\n\necho \"pass: show WARNING message if the user uses empty key\"\n\n# admin_listen set\necho '\ndeployment:\n  admin:\n    admin_listen:\n      port: 9180\n' > conf/config.yaml\n\nrm logs/error.log\nmake init\nmake run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nmake stop\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: failed to access admin\"\n    exit 1\nfi\n\nif grep -E 'using uninitialized \".+\" variable while logging request' logs/error.log; then\n    echo \"failed: uninitialized variable found during writing access log\"\n    exit 1\nfi\n\necho \"pass: uninitialized variable not found during writing access log (admin_listen set)\"\n\n# Admin API can only be used with etcd config_provider\n## if role is data_plane, and config_provider is yaml, then enable_admin is set to false\necho '\napisix:\n    enable_admin: true\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep \"Admin API can only be used with etcd config_provider\"; then\n    echo \"failed: Admin API can only be used with etcd config_provider\"\n    exit 1\nfi\n\necho \"passed: Admin API can only be used with etcd config_provider\"\n\n# disable Admin API and init plugins syncer\necho '\napisix:\n  enable_admin: false\n' > conf/config.yaml\n\nrm logs/error.log\nmake init\nmake run\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n\n\nmake init\n\nif grep -E \"failed to fetch data from etcd\" logs/error.log; then\n    echo \"failed: should sync /apisix/plugins from etcd when disabling admin normal\"\n    exit 1\nfi\n\nmake stop\n\necho \"pass: sync /apisix/plugins from etcd when disabling admin successfully\"\n\n\n\n# ignore changes to /apisix/plugins/ due to init_etcd\necho '\napisix:\n  enable_admin: true\nplugins:\n  - public-api\n  - node-status\nnginx_config:\n  error_log_level:  info\n' > conf/config.yaml\n\nrm logs/error.log\nmake init\nmake run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n# initialize node-status public API routes #1\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} -X PUT http://127.0.0.1:9180/apisix/admin/routes/node-status \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d \"{\n        \\\"uri\\\": \\\"/apisix/status\\\",\n        \\\"plugins\\\": {\n            \\\"public-api\\\": {}\n        }\n    }\")\nif [ ! $code -lt 300 ]; then\n    echo \"failed: initialize node status public API failed #1\"\n    exit 1\nfi\n\nsleep 0.5\n\n# first time check node status api\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: first time check node status api failed #1\"\n    exit 1\nfi\n\n# mock another instance init etcd dir\nmake init\nsleep 1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n# initialize node-status public API routes #2\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} -X PUT http://127.0.0.1:9180/apisix/admin/routes/node-status \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d \"{\n        \\\"uri\\\": \\\"/apisix/status\\\",\n        \\\"plugins\\\": {\n            \\\"public-api\\\": {}\n        }\n    }\")\nif [ ! $code -eq 200 ]; then\n    echo \"failed: initialize node status public API failed #2\"\n    exit 1\nfi\n\nsleep 0.5\n\n# second time check node status api\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: second time check node status api failed #1\"\n    exit 1\nfi\n\nmake stop\n\necho \"pass: ignore changes to /apisix/plugins/ due to init_etcd successfully\"\n\n\n# accept changes to /apisix/plugins when enable_admin is false\necho '\napisix:\n  enable_admin: false\nplugins:\n  - public-api\n  - node-status\nstream_plugins:\n' > conf/config.yaml\n\nrm logs/error.log\nmake init\nmake run\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n\n\n# first time check node status api\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: first time check node status api failed #2\"\n    exit 1\nfi\n\nsleep 0.5\n\n# check http plugins load list\nif ! grep logs/error.log -E -e 'new plugins: {\"public-api\":true,\"node-status\":true}' \\\n   -e 'new plugins: {\"node-status\":true,\"public-api\":true}'; then\n    echo \"failed: first time load http plugins list failed\"\n    exit 1\nfi\n\n# check stream plugins(no plugins under stream, it will be added below)\nif grep -E 'failed to read stream plugin list from local file' logs/error.log; then\n    echo \"failed: first time load stream plugins list failed\"\n    exit 1\nfi\n\n# mock another instance add /apisix/plugins\nres=$(etcdctl put \"/apisix/plugins\" '[{\"name\":\"node-status\"},{\"name\":\"example-plugin\"},{\"name\":\"public-api\"},{\"stream\":true,\"name\":\"mqtt-proxy\"}]')\nif [[ $res != \"OK\" ]]; then\n    echo \"failed: failed to set /apisix/plugins to add more plugins\"\n    exit 1\nfi\n\nsleep 0.5\n\n# second time check node status api\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: second time check node status api failed #2\"\n    exit 1\nfi\n\n# check http plugins load list\nif ! grep logs/error.log -E -e 'new plugins: {\"public-api\":true,\"node-status\":true}' \\\n   -e 'new plugins: {\"node-status\":true,\"public-api\":true}'; then\n    echo \"failed: second time load http plugins list failed\"\n    exit 1\nfi\n\n# check stream plugins load list\nif ! grep -E 'new plugins: {.*example-plugin' logs/error.log; then\n    echo \"failed: second time load stream plugins list failed\"\n    exit 1\nfi\n\n\nif grep -E 'new plugins: {}' logs/error.log; then\n    echo \"failed: second time load plugins list failed\"\n    exit 1\nfi\n\nmake stop\n\necho \"pass: accept changes to /apisix/plugins successfully\"\n"
  },
  {
    "path": "t/cli/test_admin_mtls.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# The 'admin.apisix.dev' is injected by ci/common.sh@set_coredns\necho '\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\n        https_admin: true\n        admin_api_mtls:\n            admin_ssl_cert: \"../t/certs/mtls_server.crt\"\n            admin_ssl_cert_key: \"../t/certs/mtls_server.key\"\n            admin_ssl_ca_cert: \"../t/certs/mtls_ca.crt\"\n\n' > conf/config.yaml\n\nmake run\n\nsleep 1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n# correct certs\ncode=$(curl -i -o /dev/null -s -w %{http_code}  --cacert ./t/certs/mtls_ca.crt --key ./t/certs/mtls_client.key --cert ./t/certs/mtls_client.crt -H \"X-API-KEY: $admin_key\" https://admin.apisix.dev:9180/apisix/admin/routes)\nif [ ! \"$code\" -eq 200 ]; then\n    echo \"failed: failed to enabled mTLS for admin\"\n    exit 1\nfi\n\n# skip\ncode=$(curl -i -o /dev/null -s -w %{http_code} -k -H \"X-API-KEY: $admin_key\" https://admin.apisix.dev:9180/apisix/admin/routes)\nif [ ! \"$code\" -eq 400 ]; then\n    echo \"failed: failed to enabled mTLS for admin\"\n    exit 1\nfi\n\necho \"passed: enabled mTLS for admin\"\n"
  },
  {
    "path": "t/cli/test_admin_ui.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# check admin ui enabled\n\ngit checkout conf/config.yaml\n\nmake init\n\ngrep \"location ^~ /ui/\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: failed to enable embedded admin ui\"\n    exit 1\nfi\n\n# build apisix-dashboard\ncorepack enable pnpm\n\n## prepare apisix-dashboard source code\nsource .requirements\ngit clone --revision=${APISIX_DASHBOARD_COMMIT} --depth 1 https://github.com/apache/apisix-dashboard.git\npushd apisix-dashboard\n\n## compile\npnpm install --frozen-lockfile && pnpm run build\npopd\n\n## copy the dist files to the ui directory\nmkdir ui\ncp -R apisix-dashboard/dist/* ui/ && rm -r apisix-dashboard\n\nmake run\n\n## check /ui redirects to /ui/ with 301\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/ui)\nif [ ! $code -eq 301 ]; then\n    echo \"failed: failed to redirect /ui to /ui/\"\n    exit 1\nfi\n\n## check /ui redirects to /ui/ with correct Location header\nlocation_header=$(curl -k -i -m 20 -s http://127.0.0.1:9180/ui | grep -i '^Location:' | awk -F': ' '{print $2}' | tr -d '\\r')\n\nif [ \"${location_header}\" != \"/ui/\" ]; then\n    echo \"failed: incorrect redirect Location header when accessing /ui, expected /ui/, got ${location_header}\"\n    exit 1\nfi\n\n## check /ui/ accessible\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/ui/)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: /ui/ not accessible\"\n    exit 1\nfi\n\n## check /ui/index.html accessible\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/ui/index.html)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: /ui/index.html not accessible\"\n    exit 1\nfi\n\n## check /ui/assets/*.js accessible\n\njs_file=$(find ui/assets -name \"*.js\" | head -n 1)\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/${js_file})\nif [ ! $code -eq 200 ]; then\n    echo \"failed: ${js_file} not accessible\"\n    exit 1\nfi\n\n## check /ui/assets/*.css accessible\n\ncss_file=$(find ui/assets -name \"*.css\" | head -n 1)\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/${css_file})\nif [ ! $code -eq 200 ]; then\n    echo \"failed: ${css_file} not accessible\"\n    exit 1\nfi\n\n## check /ui/assets/*.svg accessible\n\nsvg_file=$(find ui/assets -name \"*.svg\" | head -n 1)\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/${svg_file})\nif [ ! $code -eq 200 ]; then\n    echo \"failed: ${svg_file} not accessible\"\n    exit 1\nfi\n\n## check /ui/ single-page-application fallback\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/ui/not_exist)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: /ui/not_exist not accessible\"\n    exit 1\nfi\n\nmake stop\n\n# test ip restriction\n\ngit checkout conf/config.yaml\n\necho \"\ndeployment:\n    admin:\n        enable_admin_ui: true\n        allow_admin:\n            - 1.1.1.1/32\n\" > conf/config.yaml\n\nmake run\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/ui/)\nif [ ! $code -eq 403 ]; then\n    echo \"failed: ip restriction not working, expected 403, got $code\"\n    exit 1\nfi\n\nmake stop\n\n# test admin ui disabled\n\ngit checkout conf/config.yaml\n\necho \"\ndeployment:\n    admin:\n        enable_admin_ui: false\n\" > conf/config.yaml\n\nmake init\n\n#### When grep cannot find the value, it uses 1 as the exit code.\n#### Due to the use of set -e, any non-zero exit will terminate the\n#### script, so grep is written inside the if statement here.\nif grep \"location ^~ /ui/\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to disable embedded admin ui\"\n    exit 1\nfi\n\n# test admin UI explicitly enabled\n\ngit checkout conf/config.yaml\n\necho \"\ndeployment:\n    admin:\n        enable_admin_ui: true\n\" > conf/config.yaml\n\nmake init\n\nif ! grep \"location ^~ /ui/\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to explicitly enable embedded admin ui\"\n    exit 1\nfi\n"
  },
  {
    "path": "t/cli/test_apisix_mirror.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nexit_if_not_customed_nginx\n\necho '\nnginx_config:\n  http:\n    enable_access_log: false\n' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\nsleep 0.1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -k -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/get\"\n}'\n\nsleep 0.1\n\ncurl -k -i http://127.0.0.1:9080/get\n\nsleep 0.1\n\nif ! grep \"apisix_mirror_on_demand on;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: apisix_mirror_on_demand should on when running on apisix-runtime\"\n    exit 1\nfi\n\nif grep -E \"invalid URL prefix\" logs/error.log > /dev/null; then\n    echo \"failed: apisix_mirror_on_demand should on when running on apisix-runtime\"\n    exit 1\nfi\n\necho \"passed: apisix_mirror_on_demand is on when running on apisix-runtime\"\n"
  },
  {
    "path": "t/cli/test_ci_only.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This file is like other test_*.sh, but requires extra dependencies which\n# you don't need in daily development.\n\n. ./t/cli/common.sh\n\n# check error handling when connecting to old etcd\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:3379\"\n    prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'etcd cluster version 3.3.0 is less than the required version 3.4.0'; then\n    echo \"failed: properly handle the error when connecting to old etcd\"\n    exit 1\nfi\n\necho \"passed: properly handle the error when connecting to old etcd\"\n\n# It is forbidden to run apisix under the \"/root\" directory.\ngit checkout conf/config.yaml\n\nmkdir /root/apisix\n\ncp -R ./*  /root/apisix\ncd /root/apisix\nmake init\n\nout=$(make run 2>&1 || true)\nif ! echo \"$out\" | grep \"Error: It is forbidden to run APISIX in the /root directory\"; then\n    echo \"failed: should echo It is forbidden to run APISIX in the /root directory\"\n    exit 1\nfi\n\ncd -\n\necho \"passed: successfully prohibit APISIX from running in the /root directory\"\n\nrm -rf /root/apisix\n"
  },
  {
    "path": "t/cli/test_cmd.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\ngit checkout conf/config.yaml\n\n# check restart with old nginx.pid exist\necho \"-1\" > logs/nginx.pid\nout=$(./bin/apisix start 2>&1 || true)\nif echo \"$out\" | grep \"the old APISIX is still running\"; then\n    rm logs/nginx.pid\n    echo \"failed: should reject bad nginx.pid\"\n    exit 1\nfi\n\n./bin/apisix stop\nsleep 0.5\nrm logs/nginx.pid || true\n\n# check no corresponding process\nmake run\noldpid=$(< logs/nginx.pid)\nmake stop\nsleep 0.5\necho $oldpid > logs/nginx.pid\nout=$(make run || true)\nif ! echo \"$out\" | grep \"nginx.pid exists but there's no corresponding process with pid\"; then\n    echo \"failed: should find no corresponding process\"\n    exit 1\nfi\nmake stop\necho \"pass: no corresponding process\"\n\n# check running when run repeatedly\nout=$(make run; make run || true)\nif ! echo \"$out\" | grep \"the old APISIX is still running\"; then\n    echo \"failed: should find APISIX running\"\n    exit 1\nfi\n\nmake stop\necho \"pass: check APISIX running\"\n\n# check customized config\n\ngit checkout conf/config.yaml\n\n# start with not existed customized config\nmake init\n\nif ./bin/apisix start -c conf/not_existed_config.yaml; then\n    echo \"failed: apisix still start with invalid customized config.yaml\"\n    exit 1\nfi\n\n# start with customized config\necho \"\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\n        https_admin: true\n        admin_api_mtls:\n            admin_ssl_cert: '../t/certs/apisix_admin_ssl.crt'\n            admin_ssl_cert_key: '../t/certs/apisix_admin_ssl.key'\n        admin_key_required: true   # Enable Admin API authentication by default for security.\n        admin_key:\n        -\n            name: admin                             # admin: write access to configurations.\n            key: edd1c9f034335f136f87ad84b625c8f1\n            role: admin\n\" > conf/customized_config.yaml\n\n./bin/apisix start -c conf/customized_config.yaml\n\n# check if .customized_config_path has been created\nif [ ! -e conf/.customized_config_path ]; then\n    rm conf/customized_config.yaml\n    echo \".customized_config_path should exits\"\n    exit 1\nfi\n\n# check if the custom config is used\ncode=$(curl -k -i -m 20 -o /dev/null -s -w %{http_code} https://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\")\nif [ ! $code -eq 200 ]; then\n    rm conf/customized_config.yaml\n    echo \"failed: customized config.yaml not be used\"\n    exit 1\nfi\n\nmake stop\n\n# check if .customized_config_path has been removed\nif [ -e conf/.customized_config_path ]; then\n    rm conf/customized_config_path.yaml\n    echo \".customized_config_path should be removed\"\n    exit 1\nfi\n\n# start with invalied config\necho \"abc\" > conf/customized_config.yaml\n\nif ./bin/apisix start -c conf/customized_config.yaml ; then\n    rm conf/customized_config.yaml\n    echo \"start should be failed\"\n    exit 1\nfi\n\n# check if apisix can be started use correctly default config. (https://github.com/apache/apisix/issues/9700)\n./bin/apisix start\nsleep 1\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\necho \"look here\" $admin_key\ncode=$(curl -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nif [ ! $code -eq 200 ]; then\n    rm conf/customized_config.yaml\n    echo \"failed: should use default config\"\n    exit 1\nfi\n\nmake stop\n\n# check if apisix can be started after multiple start failures. (https://github.com/apache/apisix/issues/9171)\necho \"\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\n        https_admin: true\n        admin_api_mtls:\n            admin_ssl_cert: '../t/certs/apisix_admin_ssl.crt'\n            admin_ssl_cert_key: '../t/certs/apisix_admin_ssl.key'\n    etcd:\n        host:\n         - http://127.0.0.1:22379\n\" > conf/customized_config.yaml\n\n./bin/apisix start -c conf/customized_config.yaml || true\n./bin/apisix start -c conf/customized_config.yaml || true\n./bin/apisix start -c conf/customized_config.yaml || true\n\necho \"\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\n        https_admin: true\n        admin_api_mtls:\n            admin_ssl_cert: '../t/certs/apisix_admin_ssl.crt'\n            admin_ssl_cert_key: '../t/certs/apisix_admin_ssl.key'\n        admin_key_required: true   # Enable Admin API authentication by default for security.\n        admin_key:\n        -\n            name: admin                             # admin: write access to configurations.\n            key: edd1c9f034335f136f87ad84b625c8f1\n            role: admin\n\" > conf/customized_config.yaml\n\n./bin/apisix start -c conf/customized_config.yaml\n\ncode=$(curl -k -i -m 20 -o /dev/null -s -w %{http_code} https://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\")\nif [ ! $code -eq 200 ]; then\n    rm conf/customized_config.yaml\n    echo \"failed: should use default config\"\n    exit 1\nfi\n\nrm conf/customized_config.yaml\necho \"passed: test customized config successful\"\n\n# test quit command\nbin/apisix start\n\nif ! ps -ef | grep \"apisix\" | grep \"master process\" | grep -v \"grep\"; then\n    echo \"apisix not started\"\n    exit 1\nfi\n\nbin/apisix quit\n\nsleep 2\n\nif ps -ef | grep \"worker process is shutting down\" | grep -v \"grep\"; then\n    echo \"all workers should exited\"\n    exit 1\nfi\n\necho \"passed: test quit command successful\"\n\n# test reload command\nbin/apisix start\n\nif ! ps -ef | grep \"apisix\" | grep \"master process\" | grep -v \"grep\"; then\n    echo \"apisix not started\"\n    exit 1\nfi\n\nbin/apisix reload\n\nsleep 3\n\nif ps -ef | grep \"worker process is shutting down\" | grep -v \"grep\"; then\n    echo \"old workers should exited\"\n    exit 1\nfi\n\necho \"passed: test reload command successful\"\n"
  },
  {
    "path": "t/cli/test_control.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# control server\necho '\napisix:\n  enable_control: true\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"listen 127.0.0.1:9090;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: find default address for control server\"\n    exit 1\nfi\n\nmake run\n\nsleep 0.1\n\nset +e\ntimes=1\ncode=000\nwhile [ $code -eq 000 ] && [ $times -lt 10 ]\ndo\n  code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9090/v1/schema)\n  sleep 0.2\n  times=$(($times+1))\ndone\nset -e\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: access control server\"\n    exit 1\nfi\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9090/v0/schema)\n\nif [ ! $code -eq 404 ]; then\n    echo \"failed: handle route not found\"\n    exit 1\nfi\n\nmake stop\n\necho '\napisix:\n  enable_control: true\n  control:\n    ip: 127.0.0.2\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"listen 127.0.0.2:9090;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: customize address for control server\"\n    exit 1\nfi\n\nmake run\n\nsleep 0.1\n\nset +e\ntimes=1\ncode=000\nwhile [ $code -eq 000 ] && [ $times -lt 10 ]\ndo\n  code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.2:9090/v1/schema)\n  sleep 0.2\n  times=$(($times+1))\ndone\nset -e\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: access control server\"\n    exit 1\nfi\n\nmake stop\n\necho '\napisix:\n  enable_control: true\n  control:\n    port: 9092\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"listen 127.0.0.1:9092;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: customize address for control server\"\n    exit 1\nfi\n\nmake run\n\nsleep 0.1\n\nset +e\ntimes=1\ncode=000\nwhile [ $code -eq 000 ] && [ $times -lt 10 ]\ndo\n  code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9092/v1/schema)\n  sleep 0.2\n  times=$(($times+1))\ndone\nset -e\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: access control server\"\n    exit 1\nfi\n\nmake stop\n\necho '\napisix:\n  enable_control: false\n' > conf/config.yaml\n\nmake init\n\nif grep \"listen 127.0.0.1:9090;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: disable control server\"\n    exit 1\nfi\n\necho '\napisix:\n  node_listen: 9090\n  enable_control: true\n  control:\n    port: 9090\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"http listen port 9090 conflicts with control\"; then\n    echo \"failed: can't detect port conflicts\"\n    exit 1\nfi\n\necho '\napisix:\n  node_listen: 9080\n  enable_control: true\n  control:\n    port: 9091\nplugin_attr:\n  prometheus:\n    export_addr:\n      ip: \"127.0.0.1\"\n      port: 9091\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"prometheus port 9091 conflicts with control\"; then\n    echo \"failed: can't detect port conflicts\"\n    exit 1\nfi\n\necho \"pass: access control server\"\n"
  },
  {
    "path": "t/cli/test_core_config.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho \"\nnginx_config:\n    max_pending_timers: 10240\n    max_running_timers: 2561\n\" > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"lua_max_pending_timers 10240;\" conf/nginx.conf)\nif [ \"$count\" -ne 1 ]; then\n    echo \"failed: failed to set lua_max_pending_timers\"\n    exit 1\nfi\n\necho \"passed: set lua_max_pending_timers successfully\"\n\ncount=$(grep -c \"lua_max_running_timers 2561;\" conf/nginx.conf)\nif [ \"$count\" -ne 1 ]; then\n    echo \"failed: failed to set lua_max_running_timers\"\n    exit 1\nfi\n\necho \"passed: set lua_max_running_timers successfully\"\n\necho \"\napisix:\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\nnginx_config:\n    max_pending_timers: 10240\n    max_running_timers: 2561\n\" > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"lua_max_pending_timers 10240;\" conf/nginx.conf)\nif [ \"$count\" -ne 2 ]; then\n    echo \"failed: failed to set lua_max_pending_timers in stream proxy\"\n    exit 1\nfi\n\necho \"passed: set lua_max_pending_timers successfully in stream proxy\"\n\ncount=$(grep -c \"lua_max_running_timers 2561;\" conf/nginx.conf)\nif [ \"$count\" -ne 2 ]; then\n    echo \"failed: failed to set lua_max_running_timers in stream proxy\"\n    exit 1\nfi\n\necho \"passed: set lua_max_running_timers successfully in stream proxy\"\n"
  },
  {
    "path": "t/cli/test_deployment_control_plane.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# The 'admin.apisix.dev' is injected by ci/common.sh@set_coredns\necho '\napisix:\n    enable_admin: false\ndeployment:\n    role: control_plane\n    role_control_plane:\n        config_provider: etcd\n    etcd:\n        prefix: \"/apisix\"\n        host:\n            - http://127.0.0.1:2379\n' > conf/config.yaml\n\nmake run\nsleep 1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: control_plane should enable Admin API\"\n    exit 1\nfi\n\necho \"passed: control_plane should enable Admin API\"\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"httpbin.org:80\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/*\"\n}'\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/c -H \"X-API-KEY: $admin_key\")\nmake stop\nif [ ! $code -eq 404 ]; then\n    echo \"failed: should disable request proxy\"\n    exit 1\nfi\n\necho \"passed: should disable request proxy\"\n"
  },
  {
    "path": "t/cli/test_deployment_data_plane.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# clean etcd data\netcdctl del / --prefix\n\n# data_plane does not write data to etcd\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: etcd\n    etcd:\n        host:\n            - https://127.0.0.1:12379\n        prefix: \"/apisix\"\n        timeout: 30\n        tls:\n            verify: false\n' > conf/config.yaml\n\nmake run\n\nsleep 1\n\nres=$(etcdctl get / --prefix | wc -l)\n\nif [ ! $res -eq 0 ]; then\n    echo \"failed: data_plane should not write data to etcd\"\n    exit 1\nfi\n\necho \"passed: data_plane does not write data to etcd\"\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nmake stop\n\nif [ ! $code -eq 404 ]; then\n    echo \"failed: data_plane should not enable Admin API\"\n    exit 1\nfi\n\necho \"passed: data_plane should not enable Admin API\"\n\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: etcd\n    etcd:\n        host:\n            - https://127.0.0.1:12379\n        prefix: \"/apisix\"\n        timeout: 30\n' > conf/config.yaml\n\nout=$(make run 2>&1 || true)\nmake stop\nif ! echo \"$out\" | grep 'failed to load the configuration: https://127.0.0.1:12379: certificate verify failed'; then\n    echo \"failed: should verify certificate by default\"\n    exit 1\nfi\n\necho \"passed: should verify certificate by default\"\n"
  },
  {
    "path": "t/cli/test_deployment_data_plane_with_readonly_etcd.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# clean etcd data\netcdctl del / --prefix\n\n# non data_plane can prepare dirs when init etcd\necho '\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    etcd:\n        host:\n            - http://127.0.0.1:2379\n        prefix: /apisix\n        timeout: 30\n' >conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'trying to initialize the data of etcd'; then\n    echo \"failed: non data_plane should init the data of etcd\"\n    exit 1\nfi\necho \"passed: non data_plane can init the data of etcd\"\n\n# start apisix to test non data_plane can work with etcd\nmake run\nsleep 3\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -o /dev/null -s -w %{http_code} -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n    },\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"serverless-pre-function\": {\n            \"phase\": \"rewrite\",\n            \"functions\": [\"\n                return function(conf, ctx)\n                    local core = require(\\\"apisix.core\\\")\n                    return core.response.exit(200)\n                end\n            \"]\n        }\n    }\n}'\n\n# check can access the route\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\nif [ ! \"$code\" -eq 200 ]; then\n    echo \"failed: non data_plane should be able to access the route\"\n    exit 1\nfi\necho \"passed: non data_plane can work with etcd\"\n\n# prepare for data_plane with etcd\n# stop apisix\nmake stop\nsleep 3\n\n# data_plane can skip initializing the data of etcd\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: etcd\n    etcd:\n        host:\n            - http://127.0.0.1:2379\n        prefix: /apisix\n        timeout: 30\n' >conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep 'trying to initialize the data of etcd'; then\n    echo \"failed: data_plane should not init the data of etcd\"\n    exit 1\nfi\nif ! echo \"$out\" | grep 'access from the data plane to etcd should be read-only, skip initializing the data of etcd'; then\n    echo \"failed: data_plane should skip initializing the data of etcd\"\n    exit 1\nfi\necho \"passed: data_plane can skip initializing the data of etcd\"\n\n# start apisix to test data_plane can work with etcd\nmake run\nsleep 3\n\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\nif [ ! \"$code\" -eq 200 ]; then\n    echo \"failed: data_plane should be able to access the route when using etcd\"\n    exit 1\nfi\necho \"passed: data_plane can work with etcd\"\n\n# prepare for data_plane with read-only etcd\n# stop apisix\nmake stop\nsleep 3\n# add root user to help disable auth\netcdctl user add \"root:test\"\netcdctl role add root\netcdctl user grant-role root root\n# add readonly user\netcdctl user add \"apisix-data-plane:test\"\netcdctl role add data-plane-role\netcdctl role grant-permission --prefix=true data-plane-role read /apisix\netcdctl user grant-role apisix-data-plane data-plane-role\n# enable auth\netcdctl auth enable\n\n# data_plane can skip initializing the data when using read-only etcd\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: etcd\n    etcd:\n        host:\n            - http://127.0.0.1:2379\n        user: apisix-data-plane\n        password: test\n        prefix: /apisix\n        timeout: 30\n' >conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep 'trying to initialize the data of etcd'; then\n    echo \"failed: data_plane should not init the data of etcd (read-only)\"\n    exit 1\nfi\nif ! echo \"$out\" | grep 'access from the data plane to etcd should be read-only, skip initializing the data of etcd'; then\n    echo \"failed: data_plane should skip initializing the data of etcd (read-only)\"\n    exit 1\nfi\necho \"passed: data_plane can skip initializing the data of etcd (read-only)\"\n\n# start apisix to test data_plane can work with read-only etcd\nmake run\nsleep 3\n\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\nif [ ! \"$code\" -eq 200 ]; then\n    echo \"failed: data_plane should be able to access the route when using read-only etcd\"\n    exit 1\nfi\necho \"passed: data_plane can work with read-only etcd\"\n\n# clean up\netcdctl --user=root:test auth disable\netcdctl user delete apisix-data-plane\netcdctl role delete data-plane-role\netcdctl user delete root\netcdctl role delete root\n"
  },
  {
    "path": "t/cli/test_deployment_traditional.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# HTTP\necho '\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    etcd:\n        prefix: \"/apisix\"\n        host:\n            - http://127.0.0.1:2379\n' > conf/config.yaml\n\nmake run\nsleep 1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nmake stop\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: could not connect to etcd with http enabled\"\n    exit 1\nfi\n\n# Both HTTP and Stream\necho '\napisix:\n    proxy_mode: http&stream\n    enable_admin: true\n    stream_proxy:\n        tcp:\n            - addr: 9100\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    etcd:\n        prefix: \"/apisix\"\n        host:\n            - http://127.0.0.1:2379\n' > conf/config.yaml\n\nmake run\nsleep 1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nmake stop\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: could not connect to etcd with http & stream enabled\"\n    exit 1\nfi\n\n# Stream\necho '\napisix:\n    enable_admin: false\n    proxy_mode: stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    etcd:\n        prefix: \"/apisix\"\n        host:\n            - http://127.0.0.1:2379\n' > conf/config.yaml\n\nmake run\nsleep 1\nmake stop\n\nif grep '\\[error\\]' logs/error.log; then\n    echo \"failed: could not connect to etcd with stream enabled\"\n    exit 1\nfi\n\necho \"passed: could connect to etcd\"\n\necho '\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    etcd:\n        host:\n            - \"https://admin.apisix.dev:22379\"\n        prefix: \"/apisix\"\n        tls:\n            verify: false\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || echo \"ouch\")\nif ! echo \"$out\" | grep \"bad certificate\"; then\n    echo \"failed: apisix should echo \\\"bad certificate\\\"\"\n    exit 1\nfi\n\necho \"passed: certificate verify fail expectedly\"\n"
  },
  {
    "path": "t/cli/test_dns.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# 'make init' operates scripts and related configuration files in the current directory\n# The 'apisix' command is a command in the /usr/local/apisix,\n# and the configuration file for the operation is in the /usr/local/apisix/conf\n\n. ./t/cli/common.sh\n\n# dns_resolver_valid\necho '\napisix:\n  dns_resolver:\n    - 127.0.0.1\n    - \"[::1]:5353\"\n  dns_resolver_valid: 30\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"resolver 127.0.0.1 \\[::1\\]:5353 valid=30 ipv6=on;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: dns_resolver_valid doesn't take effect\"\n    exit 1\nfi\n\necho '\napisix:\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - 9100\n  dns_resolver:\n    - 127.0.0.1\n    - \"[::1]:5353\"\n  dns_resolver_valid: 30\n' > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"resolver 127.0.0.1 \\[::1\\]:5353 valid=30 ipv6=on;\" conf/nginx.conf)\nif [ \"$count\" -ne 2 ]; then\n    echo \"failed: dns_resolver_valid doesn't take effect\"\n    exit 1\nfi\n\necho \"pass: dns_resolver_valid takes effect\"\n\necho '\napisix:\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - 9100\n  dns_resolver:\n    - 127.0.0.1\n    - \"::1\"\n    - \"[::2]\"\n' > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"resolver 127.0.0.1 \\[::1\\] \\[::2\\] ipv6=on;\" conf/nginx.conf)\nif [ \"$count\" -ne 2 ]; then\n    echo \"failed: can't handle IPv6 resolver w/o bracket\"\n    exit 1\nfi\n\necho \"pass: handle IPv6 resolver w/o bracket\"\n\n# ipv6 config test\necho '\napisix:\n  enable_ipv6: false\n  dns_resolver:\n    - 127.0.0.1\n  dns_resolver_valid: 30\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"resolver 127.0.0.1 valid=30 ipv6=off;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: ipv6 config doesn't take effect\"\n    exit 1\nfi\n\n# check dns resolver address\necho '\napisix:\n  dns_resolver:\n    - 127.0.0.1\n    - \"fe80::21c:42ff:fe00:18%eth0\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\n\nif ! echo \"$out\" | grep \"unsupported DNS resolver\"; then\n    echo \"failed: should check dns resolver is unsupported\"\n    exit 1\nfi\n\nif ! grep \"resolver 127.0.0.1 ipv6=on;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: should skip unsupported DNS resolver\"\n    exit 1\nfi\n\nif grep \"fe80::21c:42ff:fe00:18%eth0\" conf/nginx.conf > /dev/null; then\n    echo \"failed: should skip unsupported DNS resolver\"\n    exit 1\nfi\n\necho \"passed: check dns resolver\"\n\n# dns resolver in stream subsystem\nrm logs/error.log || true\n\necho \"\napisix:\n    enable_admin: true\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\n    dns_resolver:\n        - 127.0.0.1:1053\nnginx_config:\n    error_log_level: info\n\" > conf/config.yaml\n\nmake run\nsleep 0.5\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -v -k -i -m 20 -o /dev/null -s -X PUT http://127.0.0.1:9180/apisix/admin/stream_routes/1 \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": [{\n                \"host\": \"sd.test.local\",\n                \"port\": 1995,\n                \"weight\": 1\n            }]\n        }\n    }'\n\ncurl http://127.0.0.1:9100 || true\nmake stop\nsleep 0.1 # wait for logs output\n\nif grep -E 'dns client error: 101 empty record received while prereading client data' logs/error.log; then\n    echo \"failed: resolve upstream host in stream subsystem should works fine\"\n    exit 1\nfi\n\nif ! grep -E 'dns resolver domain: sd.test.local to 127.0.0.(1|2) while prereading client data' logs/error.log; then\n    echo \"failed: resolve upstream host in preread phase should works fine\"\n    exit 1\nfi\n\necho \"success: resolve upstream host in stream subsystem works fine\"\n"
  },
  {
    "path": "t/cli/test_dubbo.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nexit_if_not_customed_nginx\n\n# enable dubbo\necho '\nplugins:\n    - dubbo-proxy\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"location @dubbo_pass \" conf/nginx.conf > /dev/null; then\n    echo \"failed: dubbo location not found in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found dubbo location in nginx.conf\"\n\n# dubbo multiplex configuration\necho '\nplugins:\n    - dubbo-proxy\nplugin_attr:\n    dubbo-proxy:\n        upstream_multiplex_count: 16\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"multi 16;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: dubbo multiplex configuration not found in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found dubbo multiplex configuration in nginx.conf\"\n"
  },
  {
    "path": "t/cli/test_etcd.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# check etcd while enable auth\ngit checkout conf/config.yaml\n\nexport ETCDCTL_API=3\netcdctl version\netcdctl --endpoints=127.0.0.1:2379 user add \"root:apache-api6\"\netcdctl --endpoints=127.0.0.1:2379 role add root\netcdctl --endpoints=127.0.0.1:2379 user grant-role root root\netcdctl --endpoints=127.0.0.1:2379 user get root\netcdctl --endpoints=127.0.0.1:2379 auth enable\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 del /apisix --prefix\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - http://127.0.0.1:2379\n    prefix: /apisix\n    timeout: 30\n    user: root\n    password: apache-api6\n' > conf/config.yaml\n\nmake init\ncmd_res=`etcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 get /apisix --prefix`\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 auth disable\netcdctl --endpoints=127.0.0.1:2379 role delete root\netcdctl --endpoints=127.0.0.1:2379 user delete root\n\ninit_kv=(\n\"/apisix/consumers/ init_dir\"\n\"/apisix/global_rules/ init_dir\"\n\"/apisix/plugin_metadata/ init_dir\"\n\"/apisix/plugins/ init_dir\"\n\"/apisix/protos/ init_dir\"\n\"/apisix/routes/ init_dir\"\n\"/apisix/services/ init_dir\"\n\"/apisix/ssls/ init_dir\"\n\"/apisix/stream_routes/ init_dir\"\n\"/apisix/upstreams/ init_dir\"\n)\n\nIFS=$'\\n'\nfor kv in ${init_kv[@]}\ndo\ncount=`echo $cmd_res | grep -c ${kv} || true`\nif [ $count -ne 1 ]; then\n    echo \"failed: failed to match ${kv}\"\n    exit 1\nfi\ndone\n\necho \"passed: etcd auth enabled and init kv has been set up correctly\"\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'authentication is not enabled'; then\n    echo \"failed: properly handle the error when connecting to etcd without auth\"\n    exit 1\nfi\n\necho \"passed: properly handle the error when connecting to etcd without auth\"\n\n# Check etcd retry if connect failed\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - http://127.0.0.1:2389\n    prefix: /apisix\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"retry time\"; then\n    echo \"failed: apisix should echo \\\"retry time\\\"\"\n    exit 1\nfi\n\necho \"passed: Show retry time info successfully\"\n\n# Check etcd connect refused\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - http://127.0.0.1:2389\n    prefix: /apisix\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"connection refused\"; then\n    echo \"failed: apisix should echo \\\"connection refused\\\"\"\n    exit 1\nfi\n\necho \"passed: Show connection refused info successfully\"\n\n# Check etcd auth error\ngit checkout conf/config.yaml\n\nexport ETCDCTL_API=3\netcdctl version\netcdctl --endpoints=127.0.0.1:2379 user add \"root:apache-api6\"\netcdctl --endpoints=127.0.0.1:2379 role add root\netcdctl --endpoints=127.0.0.1:2379 user grant-role root root\netcdctl --endpoints=127.0.0.1:2379 user get root\netcdctl --endpoints=127.0.0.1:2379 auth enable\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 del /apisix --prefix\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - http://127.0.0.1:2379\n    prefix: /apisix\n    timeout: 30\n    user: root\n    password: apache-api7\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"invalid user ID or password\"; then\n    echo \"failed: should echo \\\"invalid user ID or password\\\"\"\n    exit 1\nfi\n\necho \"passed: show password error successfully\"\n\n# clean etcd auth\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 auth disable\netcdctl --endpoints=127.0.0.1:2379 role delete root\netcdctl --endpoints=127.0.0.1:2379 user delete root\n\n# check connect to etcd with ipv6 address\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - http://[::1]:2379\n    prefix: /apisix\n    timeout: 30\n' > conf/config.yaml\n\nrm logs/error.log || true\nmake run\nsleep 0.1\n\nif grep \"update endpoint: http://\\[::1\\]:2379 to unhealthy\" logs/error.log; then\n    echo \"failed: connect to etcd via ipv6 address failed\"\n    exit 1\nfi\n\nif grep \"host or service not provided, or not known\" logs/error.log; then\n    echo \"failed: luasocket resolve ipv6 addresses failed\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: connect to etcd via ipv6 address successfully\"\n"
  },
  {
    "path": "t/cli/test_etcd_healthcheck.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# create 3 node etcd cluster in docker\nETCD_NAME_0=etcd0\nETCD_NAME_1=etcd1\nETCD_NAME_2=etcd2\nHEALTH_CHECK_RETRY_TIMEOUT=10\n\nif [ -z \"logs/error.log\" ]; then\n    git checkout logs/error.log\nfi\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:23790\"\n      - \"http://127.0.0.1:23791\"\n      - \"http://127.0.0.1:23792\"\n  health_check_timeout: '\"$HEALTH_CHECK_RETRY_TIMEOUT\"'\n  timeout: 2\n' > conf/config.yaml\n\ndocker compose -f ./t/cli/docker-compose-etcd-cluster.yaml up -d\n\n# case 1: Check apisix not got effected when one etcd node disconnected\nmake init && make run\n\ndocker stop ${ETCD_NAME_0}\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nif [ ! $code -eq 200 ]; then\n    echo \"failed: apisix got effect when one etcd node out of a cluster disconnected\"\n    exit 1\nfi\ndocker start ${ETCD_NAME_0}\n\ndocker stop ${ETCD_NAME_1}\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nif [ ! $code -eq 200 ]; then\n    echo \"failed: apisix got effect when one etcd node out of a cluster disconnected\"\n    exit 1\nfi\ndocker start ${ETCD_NAME_1}\n\nmake stop\n\necho \"passed: apisix not got effected when one etcd node disconnected\"\n\n# case 2: Check when all etcd nodes disconnected, apisix trying to reconnect with backoff, and could successfully recover when reconnected\nmake init && make run\n\ndocker stop ${ETCD_NAME_0} && docker stop ${ETCD_NAME_1} && docker stop ${ETCD_NAME_2}\n\nsleep_till=$(date +%s -d \"$DATE + $HEALTH_CHECK_RETRY_TIMEOUT second\")\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nif [ $code -eq 200 ]; then\n    echo \"failed: apisix not got effect when all etcd nodes disconnected\"\n    exit 1\nfi\n\ndocker start ${ETCD_NAME_0} && docker start ${ETCD_NAME_1} && docker start ${ETCD_NAME_2}\n\n# case 3: sleep till etcd health check try to check again\ncurrent_time=$(date +%s)\nsleep_seconds=$(( $sleep_till - $current_time + 3))\nif [ \"$sleep_seconds\" -gt 0 ]; then\n    sleep $sleep_seconds\nfi\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncode=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H \"X-API-KEY: $admin_key\")\nif [ ! $code -eq 200 ]; then\n    echo \"failed: apisix could not recover when etcd node recover\"\n    docker ps\n    cat logs/error.log\n    exit 1\nfi\n\nmake stop\n\necho \"passed: when all etcd nodes disconnected, apisix trying to reconnect with backoff, and could successfully recover when reconnected\"\n\n# case 4: stop one etcd node (result: start successful)\ndocker stop ${ETCD_NAME_0}\n\nout=$(make init 2>&1)\nif echo \"$out\" | grep \"23790\" | grep \"connection refused\"; then\n    echo \"passed: APISIX successfully to start, stop only one etcd node\"\nelse\n    echo \"failed: stop only one etcd node APISIX should start normally\"\n    exit 1\nfi\n\n# case 5: stop two etcd nodes (result: start failure)\ndocker stop ${ETCD_NAME_1}\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep \"23791\" | grep \"connection refused\"; then\n    echo \"passed: APISIX failed to start, etcd cluster must have two or more healthy nodes\"\nelse\n    echo \"failed: two etcd nodes have been stopped, APISIX should fail to start\"\n    exit 1\nfi\n\n# case 6: stop all etcd nodes (result: start failure)\ndocker stop ${ETCD_NAME_2}\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep \"23792\" | grep \"connection refused\"; then\n    echo \"passed: APISIX failed to start, all etcd nodes have stopped\"\nelse\n    echo \"failed: all etcd nodes have stopped, APISIX should not be able to start\"\n    exit 1\nfi\n\n# stop etcd docker container\ndocker compose -f ./t/cli/docker-compose-etcd-cluster.yaml down\n"
  },
  {
    "path": "t/cli/test_etcd_mtls.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nexit_if_not_customed_nginx\n\n# The 'admin.apisix.dev' is injected by ci/common.sh@set_coredns\n\n# etcd mTLS verify\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://admin.apisix.dev:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n      verify: false\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || echo \"ouch\")\nif echo \"$out\" | grep \"bad certificate\"; then\n    echo \"failed: apisix should not echo \\\"bad certificate\\\"\"\n    exit 1\nfi\n\necho \"passed: certificate verify success expectedly\"\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://admin.apisix.dev:22379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || echo \"ouch\")\nif ! echo \"$out\" | grep \"bad certificate\"; then\n    echo \"failed: apisix should echo \\\"bad certificate\\\"\"\n    exit 1\nfi\n\necho \"passed: certificate verify fail expectedly\"\n\n# etcd mTLS verify with CA\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://admin.apisix.dev:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || echo \"ouch\")\nif echo \"$out\" | grep \"certificate verify failed\"; then\n    echo \"failed: apisix should not echo \\\"certificate verify failed\\\"\"\n    exit 1\nfi\n\nif echo \"$out\" | grep \"ouch\"; then\n    echo \"failed: apisix should not fail\"\n    exit 1\nfi\n\necho \"passed: certificate verify with CA success expectedly\"\n\n# etcd mTLS in stream subsystem\necho '\napisix:\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - addr: 9100\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://admin.apisix.dev:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || echo \"ouch\")\nif echo \"$out\" | grep \"certificate verify failed\"; then\n    echo \"failed: apisix should not echo \\\"certificate verify failed\\\"\"\n    exit 1\nfi\n\nif echo \"$out\" | grep \"ouch\"; then\n    echo \"failed: apisix should not fail\"\n    exit 1\nfi\n\nrm logs/error.log || true\nmake run\nsleep 1\nmake stop\n\nif grep \"\\[error\\]\" logs/error.log; then\n    echo \"failed: veirfy etcd certificate during sync should not fail\"\nfi\n\necho \"passed: certificate verify in stream subsystem successfully\"\n\n# use host in etcd.host as sni by default\ngit checkout conf/config.yaml\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n  ' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\nsleep 1\nmake stop\n\nif ! grep -F 'certificate host mismatch' logs/error.log; then\n    echo \"failed: should got certificate host mismatch when use host in etcd.host as sni\"\n    exit 1\nfi\n\n\necho \"passed: use host in etcd.host as sni by default\"\n\n# specify custom sni instead of using etcd.host\ngit checkout conf/config.yaml\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n      sni: \"admin.apisix.dev\"\n  ' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\nsleep 1\nmake stop\n\nif grep -E 'certificate host mismatch' logs/error.log; then\n    echo \"failed: should use specify custom sni\"\n    exit 1\nfi\n\necho \"passed: specify custom sni instead of using etcd.host\"\n"
  },
  {
    "path": "t/cli/test_etcd_sync_event_handle.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# check etcd while enable auth\ngit checkout conf/config.yaml\n\n# Make new routes\netcdctl --endpoints=127.0.0.1:2379 del --prefix /apisix/routes/\netcdctl --endpoints=127.0.0.1:2379 put /apisix/routes/ init_dir\netcdctl --endpoints=127.0.0.1:2379 put /apisix/routes/1 '{\"uri\":\"/1\",\"plugins\":{}}'\netcdctl --endpoints=127.0.0.1:2379 put /apisix/routes/2 '{\"uri\":\"/2\",\"plugins\":{}}'\netcdctl --endpoints=127.0.0.1:2379 put /apisix/routes/3 '{\"uri\":\"/3\",\"plugins\":{}}'\netcdctl --endpoints=127.0.0.1:2379 put /apisix/routes/4 '{\"uri\":\"/4\",\"plugins\":{}}'\netcdctl --endpoints=127.0.0.1:2379 put /apisix/routes/5 '{\"uri\":\"/5\",\"plugins\":{}}'\n\n# Connect by unauthenticated\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - http://127.0.0.1:2379\n    prefix: /apisix\nnginx_config:\n  error_log_level: info\n  worker_processes: 1\n' > conf/config.yaml\n\n# Initialize and start APISIX without password\nmake init\nmake run\n\n# Test request\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/1 | grep 503 || (echo \"failed: Round 1 Request 1 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/2 | grep 503 || (echo \"failed: Round 1 Request 2 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/3 | grep 503 || (echo \"failed: Round 1 Request 3 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/4 | grep 503 || (echo \"failed: Round 1 Request 4 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/5 | grep 503 || (echo \"failed: Round 1 Request 5 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/6 | grep 404 || (echo \"failed: Round 1 Request 6 unexpected\"; exit 1)\n\n# Enable auth to block APISIX connect\nexport ETCDCTL_API=3\netcdctl version\netcdctl --endpoints=127.0.0.1:2379 user add \"root:apache-api6-sync\"\netcdctl --endpoints=127.0.0.1:2379 role add root\netcdctl --endpoints=127.0.0.1:2379 user grant-role root root\netcdctl --endpoints=127.0.0.1:2379 user get root\netcdctl --endpoints=127.0.0.1:2379 auth enable\nsleep 3\n\n# Restart etcd services to make sure that APISIX cannot be synchronized\nproject_compose_ci=ci/pod/docker-compose.common.yml make ci-env-stop\nproject_compose_ci=ci/pod/docker-compose.common.yml make ci-env-up\n\n# Make some changes when APISIX cannot be synchronized\n# Authentication ensures that only etcdctl can access etcd at this time\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6-sync put /apisix/routes/1 '{\"uri\":\"/1\",\"plugins\":{\"fault-injection\":{\"abort\":{\"http_status\":204}}}}'\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6-sync put /apisix/routes/2 '{\"uri\":\"/2\"}' ## set incorrect configuration\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6-sync put /apisix/routes/3 '{\"uri\":\"/3\",\"plugins\":{\"fault-injection\":{\"abort\":{\"http_status\":204}}}}'\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6-sync put /apisix/routes/4 '{\"uri\":\"/4\",\"plugins\":{\"fault-injection\":{\"abort\":{\"http_status\":204}}}}'\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6-sync put /apisix/routes/5 '{\"uri\":\"/5\",\"plugins\":{\"fault-injection\":{\"abort\":{\"http_status\":204}}}}'\n\n# Resume APISIX synchronization by disable auth\n# Since APISIX will not be able to access etcd until authentication is disable,\n# watch will be temporarily disabled, so when authentication is disable,\n# the backlog events will be sent at once at an offset from when APISIX disconnects.\n# When APISIX resumes the connection, it still has not met its mandatory full\n# synchronization condition, so it will be \"watch\" that resumes, not \"readdir\".\netcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6-sync auth disable\netcdctl --endpoints=127.0.0.1:2379 user delete root\netcdctl --endpoints=127.0.0.1:2379 role delete root\nsleep 5 # wait resync by watch\n\n# Test request\n# All but the intentionally incoming misconfigurations should be applied,\n# and non-existent routes will remain non-existent.\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/1 | grep 204 || (echo \"failed: Round 2 Request 1 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/2 | grep 503 || (echo \"failed: Round 2 Request 2 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/3 | grep 204 || (echo \"failed: Round 2 Request 3 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/4 | grep 204 || (echo \"failed: Round 2 Request 4 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/5 | grep 204 || (echo \"failed: Round 2 Request 5 unexpected\"; exit 1)\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:9080/6 | grep 404 || (echo \"failed: Round 2 Request 6 unexpected\"; exit 1)\n\n# Check logs\n## Case1: Ensure etcd is disconnected\ncat logs/error.log | grep \"watchdir err: has no healthy etcd endpoint available\" || (echo \"Log case 1 unexpected\"; exit 1)\n\n## Case2: Ensure events are sent in bulk after connection is restored\n## It is extracted from the structure of following type\n## result = {\n##   events = { {\n##     {\n##       kv = {\n##         key = \"/apisix/routes/1\",\n##         ...\n##       }\n####   }, {\n##       kv = {\n##         key = \"/apisix/routes/2\",\n##         ...\n##       }\n##     },\n##     ...\n##   } },\n##   header = {\n##     ...\n##   }\n## }\n## After check, it only appears when watch recovers and returns events in bulk.\ncat logs/error.log | grep \"etcd events sent in bulk\" || (echo \"failed: Log case 2 unexpected\"; exit 1)\n\n## Case3: Ensure that the check schema error is actually triggered.\ncat logs/error.log | grep \"failed to check item data\" || (echo \"failed: Log case 3 unexpected\"; exit 1)\n"
  },
  {
    "path": "t/cli/test_etcd_tls.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# 'make init' operates scripts and related configuration files in the current directory\n# The 'apisix' command is a command in the /usr/local/apisix,\n# and the configuration file for the operation is in the /usr/local/apisix/conf\n\n. ./t/cli/common.sh\n\n# Check etcd tls verify failure\ngit checkout conf/config.yaml\n\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:12379\"\n    prefix: \"/apisix\"\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"certificate verify failed\"; then\n    echo \"failed: apisix should echo \\\"certificate verify failed\\\"\"\n    exit 1\nfi\n\necho \"passed: Show certificate verify failed info successfully\"\n\n\n# Check etcd tls without verification\ngit checkout conf/config.yaml\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:12379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n  ' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep \"certificate verify failed\"; then\n    echo \"failed: apisix should not echo \\\"certificate verify failed\\\"\"\n    exit 1\nfi\n\necho \"passed: Certificate verification successfully\"\n"
  },
  {
    "path": "t/cli/test_http_config.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  http:\n    custom_lua_shared_dict:\n      my_dict: 1m\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"lua_shared_dict my_dict 1m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: define custom shdict\"\n    exit 1\nfi\n\necho \"passed: define custom shdict\"\n\ngit checkout conf/config.yaml\n\necho \"\nplugins:\n    - ip-restriction\n\" > conf/config.yaml\n\nmake init\n\nif grep \"plugin-limit-conn\" conf/nginx.conf > /dev/null; then\n    echo \"failed: enable shdict on demand\"\n    exit 1\nfi\n\necho \"\nplugins:\n    - limit-conn\n\" > conf/config.yaml\n\nmake init\n\nif ! grep \"plugin-limit-conn\" conf/nginx.conf > /dev/null; then\n    echo \"failed: enable shdict on demand\"\n    exit 1\nfi\n\necho \"passed: enable shdict on demand\"\n"
  },
  {
    "path": "t/cli/test_kubernetes.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho '\ndiscovery:\n    kubernetes:\n      service:\n        host: ${HOST_ENV}\n      client:\n        token: ${TOKEN_ENV}\n' >conf/config.yaml\n\nmake init\n\nif ! grep \"env HOST_ENV\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env KUBERNETES_SERVICE_PORT\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env TOKEN_ENV\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"lua_shared_dict kubernetes 1m;\" conf/nginx.conf; then\n  echo \"kubernetes discovery lua_shared_dict inject failed\"\n  exit 1\nfi\n\necho '\ndiscovery:\n    kubernetes:\n      - id: dev\n        service:\n          host: ${DEV_HOST}\n          port: ${DEV_PORT}\n        client:\n          token: ${DEV_TOKEN}\n      - id: pro\n        service:\n          host: ${PRO_HOST}\n          port: ${PRO_PORT}\n        client:\n          token: ${PRO_TOKEN}\n        shared_size: 2m\n' >conf/config.yaml\n\nmake init\n\nif ! grep \"env DEV_HOST\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env DEV_PORT\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env DEV_TOKEN\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env PRO_HOST\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env PRO_PORT\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"env PRO_TOKEN\" conf/nginx.conf; then\n  echo \"kubernetes discovery env inject failed\"\n  exit 1\nfi\n\nif ! grep \"lua_shared_dict kubernetes-dev 1m;\" conf/nginx.conf; then\n  echo \"kubernetes discovery lua_shared_dict inject failed\"\n  exit 1\nfi\n\nif ! grep \"lua_shared_dict kubernetes-pro 2m;\" conf/nginx.conf; then\n  echo \"kubernetes discovery lua_shared_dict inject failed\"\n  exit 1\nfi\n\necho \"kubernetes discovery inject success\"\n"
  },
  {
    "path": "t/cli/test_limit_conn_redis_ttl.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# Enable limit-conn plugin\n\nrm logs/worker_events.sock || true\n\necho '\nnginx_config:\n  worker_processes: 1\n  error_log_level: info\ndeployment:\n  admin:\n    admin_key:\n      - name: \"admin\"\n        key: edd1c9f034335f136f87ad84b625c8f1\n        role: admin\n\napisix:\n  enable_admin: true\n  control:\n    port: 9110\nplugins:\n  - limit-conn\n' > conf/config.yaml\n\nmake init\nmake run\n\nadmin_key=\"edd1c9f034335f136f87ad84b625c8f1\"\n\n# Create a route with limit-conn and redis policy\n# key_ttl is set to 2 seconds\ncurl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"limit-conn\": {\n            \"conn\": 1,\n            \"burst\": 0,\n            \"default_conn_delay\": 0.1,\n            \"key\": \"remote_addr\",\n            \"policy\": \"redis\",\n            \"redis_host\": \"127.0.0.1\",\n            \"redis_timeout\": 1000,\n            \"key_ttl\": 2\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1995\": 1\n        },\n        \"timeout\": {\n            \"connect\": 300,\n            \"send\": 300,\n            \"read\": 300\n        }\n    }\n}'\n\nif [ $? -ne 0 ]; then\n    echo \"failed: verify route creation\"\n    exit 1\nfi\n\nsleep 0.5\n\n# Start a mock upstream server (perl) that hangs.\n# This ensures the connection stays open and limit-conn count remains 1.\nperl -e 'use IO::Socket::INET; my $s = IO::Socket::INET->new(LocalPort => 1995, Listen => 1, Reuse => 1) or die; my $c = $s->accept(); sleep 10;' &\nPERL_PID=$!\nsleep 1\n\n# Start the request in background.\n# This request consumes the 1 allowed connection.\ncurl -v http://127.0.0.1:9080/hello > /dev/null 2>&1 &\nCURL_PID=$!\n\nsleep 1\n\n# Kill APISIX hard (-9) to prevent limit-conn from decrementing the count.\n# This simulates a crash where the Redis key is left with value 1.\nif [ -f logs/nginx.pid ]; then\n    pid=$(cat logs/nginx.pid)\n    workers=$(pgrep -P $pid)\n    kill -9 $pid || true\n    echo \"Killed APISIX master $pid\"\n    if [ -n \"$workers\" ]; then\n        kill -9 $workers 2>/dev/null || true\n    fi\nfi\n\n# Clean up the background tasks\nkill $PERL_PID || echo \"failed to kill upstream process\"\nkill $CURL_PID || echo \"failed to kill curl process\"\n\n# Wait for key_ttl (2s) to expire in Redis.\n# If key_ttl works, the key should expire and vanish.\necho \"Waiting for key_ttl expiration...\"\nsleep 3\n\n# Start APISIX again\nrm logs/worker_events.sock || true\nmake run\nsleep 1\n\n# Start upstream again for the verification request\nperl -e 'use IO::Socket::INET; my $s = IO::Socket::INET->new(LocalPort => 1995, Listen => 1, Reuse => 1) or die; my $c = $s->accept(); print $c \"HTTP/1.1 200 OK\\r\\nContent-Length: 0\\r\\n\\r\\n\";' &\nNC_2_PID=$!\nsleep 1\n\n# check connection\n# If the key expired, this request should be allowed (we might get timeout or empty reply from nc, but NOT 503).\n# If the key persisted (ttl features broken), connection count would still be 1, so this new request would result in 1+1 > 1 -> 503.\nstatus_code=$(curl -s -o /dev/null -w \"%{http_code}\" --max-time 1 http://127.0.0.1:9080/hello)\n\necho \"Status code: $status_code\"\n\n# Cleanup\nkill $NC_2_PID || true\n\nif [ \"$status_code\" == \"503\" ]; then\n    echo \"failed: request blocked (503), limit-conn key did not expire\"\n    exit 1\nfi\n\necho \"pass: request not blocked, key_ttl works\"\n"
  },
  {
    "path": "t/cli/test_limit_req_redis_ttl.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# Enable limit-req plugin\n\nrm logs/worker_events.sock || true\n\necho '\nnginx_config:\n  worker_processes: 1\n  error_log_level: info\ndeployment:\n  admin:\n    admin_key:\n      - name: \"admin\"\n        key: edd1c9f034335f136f87ad84b625c8f1\n        role: admin\n\napisix:\n  enable_admin: true\n  control:\n    port: 9110\nplugins:\n  - limit-req\n' > conf/config.yaml\n\nmake init\nmake run\n\nadmin_key=\"edd1c9f034335f136f87ad84b625c8f1\"\n\n# Create a route with limit-req and redis policy\n# rate=1, burst=1 -> ttl = ceil(1/1) + 1 = 2s\ncurl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n    \"methods\": [\"GET\"],\n    \"uri\": \"/hello\",\n    \"plugins\": {\n        \"limit-req\": {\n            \"rate\": 1,\n            \"burst\": 1,\n            \"key\": \"remote_addr\",\n            \"policy\": \"redis\",\n            \"redis_host\": \"127.0.0.1\",\n            \"redis_timeout\": 1000\n        }\n    },\n    \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        }\n    }\n}'\n\nif [ $? -ne 0 ]; then\n    echo \"failed: verify route creation\"\n    exit 1\nfi\n\nsleep 0.5\n\n# Make a request to create the Redis keys\ncurl -v http://127.0.0.1:9080/hello > /dev/null 2>&1\n\n# Verify keys exist\n# Keys pattern: plugin-limit-req*\nkeys=$(redis-cli keys \"limit_req:*\" | wc -l)\nif [ \"$keys\" -eq 0 ]; then\n    echo \"failed: keys not found in Redis immediately after request\"\n    exit 1\nfi\necho \"pass: keys found in Redis\"\n\n# Wait for 3 seconds (TTL is 2s)\necho \"Waiting for 3s...\"\nsleep 3\n\n# Verify keys are gone\nkeys_list=$(redis-cli keys \"limit_req:*\")\n\nif [ -n \"$keys_list\" ]; then\n    echo \"failed: keys still exist in Redis after TTL expiration\"\n    echo \"Keys found:\"\n    echo \"$keys_list\"\n\n    first_key=$(echo \"$keys_list\" | head -n 1)\n    echo \"TTL of $first_key:\"\n    redis-cli ttl \"$first_key\"\n\n    exit 1\nfi\n\necho \"pass: keys expired correctly\"\nmake stop\n"
  },
  {
    "path": "t/cli/test_load_full_data_init_worker.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nrm logs/error.log || true\n\ngit checkout conf/config.yaml\n\necho '\napisix:\n  worker_startup_time_threshold: 3\nnginx_config:\n  worker_processes: 1\n  http_configuration_snippet: |\n    server {\n        listen 1980;\n        location /hello {\n            return 200 \"hello world\";\n        }\n    }\n' > conf/config.yaml\n\nmake run\n\nsleep 5\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -k -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:1980\": 1\n        },\n        \"scheme\": \"http\",\n        \"type\": \"roundrobin\"\n    }\n}'\n\nsleep 1\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: request failed, http status $code\"\n    exit 1\nfi\n\nMASTER_PID=$(cat logs/nginx.pid)\n\nworker_pid=$(pgrep -P \"$MASTER_PID\" -f \"nginx: worker process\" || true)\n\nif [ -n \"$worker_pid\" ]; then\n    echo \"killing worker $worker_pid (master $MASTER_PID)\"\n    kill \"$worker_pid\"\nelse\n    echo \"failed: no worker process found for master $MASTER_PID\"\n    exit 1\nfi\n\nsleep 2\n\nif ! grep 'master process has been running for a long time, reloading the full configuration from etcd for this new worker' logs/error.log; then\n    echo \"failed: could not detect new worker be started\"\n    exit 1\nfi\n\nif grep 'API disabled in the context of init_worker_by_lua' logs/error.log; then\n    echo \"failed: cannot access etcd in init_worker phase\"\n    exit 1\nfi\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: request failed for new worker, http status $code\"\n    exit 1\nfi\n\necho \"passed: load full configuration for new worker\"\n\nmake stop\n"
  },
  {
    "path": "t/cli/test_main.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# 'make init' operates scripts and related configuration files in the current directory\n# The 'apisix' command is a command in the /usr/local/apisix,\n# and the configuration file for the operation is in the /usr/local/apisix/conf\n\n. ./t/cli/common.sh\n\ngit checkout conf/config.yaml\n\n# check 'Server: APISIX' is not in nginx.conf. We already added it in Lua code.\nmake init\n\nif grep \"Server: APISIX\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'Server: APISIX' should not be added twice\"\n    exit 1\nfi\n\necho \"passed: 'Server: APISIX' not in nginx.conf\"\n\n#make init <- no need to re-run since we don't change the config yet.\n\n# check the error_log directive uses warn level by default.\nif ! grep \"error_log logs/error.log warn;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: error_log directive doesn't use warn level by default\"\n    exit 1\nfi\n\necho \"passed: error_log directive uses warn level by default\"\n\n# check whether the 'reuseport' is in nginx.conf .\n\ngrep -E \"listen 0.0.0.0:9080.*reuseport\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: nginx.conf file is missing reuseport configuration\"\n    exit 1\nfi\n\necho \"passed: nginx.conf file contains reuseport configuration\"\n\n# check default ssl port\necho \"\napisix:\n    ssl:\n        listen:\n            - port: 8443\n\n\" > conf/config.yaml\n\nmake init\n\ngrep \"listen 0.0.0.0:8443 ssl\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: failed to update ssl port\"\n    exit 1\nfi\n\ngrep \"listen \\[::\\]:8443 ssl\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: failed to update ssl port\"\n    exit 1\nfi\n\necho \"passed: change default ssl port\"\n\n# check support multiple ports listen in http and https\n\necho \"\napisix:\n  node_listen:\n    - 9080\n    - 9081\n    - 9082\n  ssl:\n    enable: true\n    listen:\n        - port: 9443\n        - port: 9444\n        - port: 9445\n\" > conf/config.yaml\n\nmake init\n\ncount_http_ipv4=`grep -c \"listen 0.0.0.0:908.\" conf/nginx.conf || true`\nif [ $count_http_ipv4 -ne 3 ]; then\n    echo \"failed: failed to support multiple ports listen in http with ipv4\"\n    exit 1\nfi\n\ncount_http_ipv6=`grep -c \"listen \\[::\\]:908.\" conf/nginx.conf || true`\nif [ $count_http_ipv6 -ne 3 ]; then\n    echo \"failed: failed to support multiple ports listen in http with ipv6\"\n    exit 1\nfi\n\ncount_https_ipv4=`grep -c \"listen 0.0.0.0:944. ssl\" conf/nginx.conf || true`\nif [ $count_https_ipv4 -ne 3 ]; then\n    echo \"failed: failed to support multiple ports listen in https with ipv4\"\n    exit 1\nfi\n\ncount_https_ipv6=`grep -c \"listen \\[::\\]:944. ssl\" conf/nginx.conf || true`\nif [ $count_https_ipv6 -ne 3 ]; then\n    echo \"failed: failed to support multiple ports listen in https with ipv6\"\n    exit 1\nfi\n\necho \"passed: support multiple ports listen in http and https\"\n\n# check support specific IP listen in http and https\n\necho \"\napisix:\n  node_listen:\n    - ip: 127.0.0.1\n      port: 9081\n    - ip: 127.0.0.2\n      port: 9082\n  ssl:\n    listen:\n      - ip: 127.0.0.3\n        port: 9444\n      - ip: 127.0.0.4\n        port: 9445\n        enable_http3: true\n  enable_http2: true\n\" > conf/config.yaml\n\nmake init\n\ncount_http_specific_ip=`grep -c \"listen 127.0.0..:908.\" conf/nginx.conf || true`\nif [ $count_http_specific_ip -ne 2 ]; then\n    echo \"failed: failed to support specific IP listen in http\"\n    exit 1\nfi\n\ncount_https_specific_ip=`grep -c \"listen 127.0.0..:944. ssl\" conf/nginx.conf || true`\nif [ $count_https_specific_ip -ne 2 ]; then\n    echo \"failed: failed to support specific IP listen in https\"\n    exit 1\nfi\n\ncount_enable_http2=`grep -c \"http2 on\" conf/nginx.conf || true`\nif [ $count_enable_http2 -ne 1 ]; then\n    echo \"failed: failed to enable http2\"\n    exit 1\nfi\n\ncount_https_specific_ip_and_enable_quic=`grep -c \"listen 127.0.0..:944. quic\" conf/nginx.conf || true`\nif [ $count_https_specific_ip_and_enable_quic -ne 1 ]; then\n    echo \"failed: failed to support specific IP and enable quic listen in https\"\n    exit 1\nfi\n\ncount_https_specific_ip_and_enable_http3=`grep -c \"http3 on\" conf/nginx.conf || true`\nif [ $count_https_specific_ip_and_enable_http3 -ne 1 ]; then\n    echo \"failed: failed to support specific IP and enable http3 listen in https\"\n    exit 1\nfi\n\necho \"passed: support specific IP listen in http and https\"\n\n# check deprecated enable_http2 in node_listen\necho \"\napisix:\n  node_listen:\n    - ip: 127.0.0.1\n      port: 9081\n      enable_http2: true\n\" > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'port level enable_http2 in node_listen is deprecated'; then\n    echo \"failed: failed to detect deprecated enable_http2 in node_listen\"\n    exit 1\nfi\n\necho \"passed: check deprecated enable_http2 in node_listen\"\n\n\n# check deprecated enable_http2 in ssl.listen\necho \"\napisix:\n  node_listen:\n    - ip: 127.0.0.1\n      port: 9081\n  ssl:\n    enable: true\n    listen:\n      - ip: 127.0.0.1\n        port: 9444\n        enable_http2: true\n\" > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'port level enable_http2 in ssl.listen is deprecated'; then\n    echo \"failed: failed to detect deprecated enable_http2 in ssl.listen\"\n    exit 1\nfi\n\necho \"passed: check deprecated enable_http2 in node_listen\"\n\n# check default env\necho \"\nnginx_config:\n    envs:\n        - TEST\n\" > conf/config.yaml\n\nmake init\n\ngrep \"env TEST;\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: failed to update env\"\n    exit 1\nfi\n\necho \"passed: change default env\"\n\n# support environment variables\necho '\nnginx_config:\n    envs:\n        - ${{var_test}}_${{FOO}}\n' > conf/config.yaml\n\nvar_test=TEST FOO=bar make init\n\nif ! grep \"env TEST_bar;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to resolve variables\"\n    exit 1\nfi\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"can't find environment variable\"; then\n    echo \"failed: failed to resolve variables\"\n    exit 1\nfi\n\necho \"passed: resolve variables\"\n\n# support reserved environment variable APISIX_DEPLOYMENT_ETCD_HOST\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2333\"\n' > conf/config.yaml\n\nfailed_msg=\"failed: failed to configure etcd host with reserved environment variable\"\n\nout=$(APISIX_DEPLOYMENT_ETCD_HOST='[\"http://127.0.0.1:2379\"]' make init 2>&1 || true)\nif echo \"$out\" | grep \"connection refused\" > /dev/null; then\n    echo $failed_msg\n    exit 1\nfi\n\nout=$(APISIX_DEPLOYMENT_ETCD_HOST='[\"http://127.0.0.1:2379\"]' make run 2>&1 || true)\nif echo \"$out\" | grep \"connection refused\" > /dev/null; then\n    echo $failed_msg\n    exit 1\nfi\n\nif ! grep \"env APISIX_DEPLOYMENT_ETCD_HOST;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'env APISIX_DEPLOYMENT_ETCD_HOST;' not in nginx.conf\"\n    echo $failed_msg\n    exit 1\nfi\n\nmake stop\n\necho \"passed: configure etcd host with reserved environment variable\"\n\necho '\nnginx_config:\n    worker_rlimit_nofile: ${{nofile9}}\n' > conf/config.yaml\n\nnofile9=99999 make init\n\nif ! grep \"worker_rlimit_nofile 99999;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to resolve variables as integer\"\n    exit 1\nfi\n\necho \"passed: resolve variables as integer\"\n\necho '\napisix:\n    enable_admin: ${{admin}}\n' > conf/config.yaml\n\nadmin=false make init\n\nif grep \"location /apisix/admin\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to resolve variables as boolean\"\n    exit 1\nfi\n\necho \"passed: resolve variables as boolean\"\n\necho '\nnginx_config:\n    envs:\n        - ${{ var_test}}_${{ FOO }}\n' > conf/config.yaml\n\nvar_test=TEST FOO=bar make init\n\nif ! grep \"env TEST_bar;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to resolve variables wrapped with whitespace\"\n    exit 1\nfi\n\necho \"passed: resolve variables wrapped with whitespace\"\n\n# support environment variables in local_conf\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://${{ETCD_HOST}}:${{ETCD_PORT}}\"\n' > conf/config.yaml\n\nETCD_HOST=127.0.0.1 ETCD_PORT=2379 make init\n\nif ! grep \"env ETCD_HOST;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: support environment variables in local_conf\"\n    exit 1\nfi\n\n# don't override user's envs configuration\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://${{ETCD_HOST}}:${{ETCD_PORT}}\"\nnginx_config:\n    envs:\n        - ETCD_HOST\n' > conf/config.yaml\n\nETCD_HOST=127.0.0.1 ETCD_PORT=2379 make init\n\nif grep \"env ETCD_HOST=.*;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: support environment variables in local_conf\"\n    exit 1\nfi\n\nif ! grep \"env ETCD_HOST;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: support environment variables in local_conf\"\n    exit 1\nfi\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://${{ETCD_HOST}}:${{ETCD_PORT}}\"\nnginx_config:\n    envs:\n        - ETCD_HOST=1.1.1.1\n' > conf/config.yaml\n\nETCD_HOST=127.0.0.1 ETCD_PORT=2379 make init\n\nif grep \"env ETCD_HOST;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: support environment variables in local_conf\"\n    exit 1\nfi\n\nif ! grep \"env ETCD_HOST=1.1.1.1;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: support environment variables in local_conf\"\n    exit 1\nfi\n\necho \"pass: support environment variables in local_conf\"\n\n# support default value when environment not set\necho '\ntests:\n    key: ${{TEST_ENV:=1.1.1.1}}\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"env TEST_ENV;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: should use default value when environment not set\"\n    exit 1\nfi\n\necho '\ntests:\n    key: ${{TEST_ENV:=very-long-domain-with-many-symbols.absolutely-non-exists-123ss.com:1234/path?param1=value1}}\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"env TEST_ENV;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: should use default value when environment not set\"\n    exit 1\nfi\n\necho '\ntests:\n    key: ${{TEST_ENV:=192.168.1.1}}\n' > conf/config.yaml\n\nTEST_ENV=127.0.0.1 make init\n\nif ! grep \"env TEST_ENV;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: should use environment variable when environment is set\"\n    exit 1\nfi\n\necho \"pass: support default value when environment not set\"\n\n# support merging worker_processes\necho '\nnginx_config:\n    worker_processes: 1\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"worker_processes 1;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to merge worker_processes\"\n    exit 1\nfi\n\necho '\nnginx_config:\n    worker_processes: ${{nproc}}\n' > conf/config.yaml\n\nnproc=1 make init\n\nif ! grep \"worker_processes 1;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to merge worker_processes\"\n    exit 1\nfi\n\necho '\nnginx_config:\n    worker_processes: true\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'path\\[nginx_config->worker_processes\\] expect'; then\n    echo \"failed: failed to merge worker_processes\"\n    exit 1\nfi\n\necho '\nnginx_config:\n    worker_processes: ${{nproc}}\n' > conf/config.yaml\n\nout=$(nproc=false make init 2>&1 || true)\nif ! echo \"$out\" | grep 'path\\[nginx_config->worker_processes\\] expect'; then\n    echo \"failed: failed to merge worker_processes\"\n    exit 1\nfi\n\necho \"passed: merge worker_processes\"\n\n# check nameserver imported\ngit checkout conf/config.yaml\n\nmake init\n\ni=`grep  -E '^nameserver[[:space:]]+(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4]0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[[:space:]]?$' /etc/resolv.conf | awk '{print $2}'`\nfor ip in $i\ndo\n  echo $ip\n  grep $ip conf/nginx.conf > /dev/null\n  if [ ! $? -eq 0 ]; then\n    echo \"failed: system DNS \"$ip\" unimported\"\n    exit 1\n  fi\ndone\n\necho \"passed: system nameserver imported\"\n\n# enable enable_dev_mode\ngit checkout conf/config.yaml\n\necho \"\napisix:\n    enable_dev_mode: true\n\" > conf/config.yaml\n\nmake init\n\ncount=`grep -c \"worker_processes 1;\" conf/nginx.conf`\nif [ $count -ne 1 ]; then\n    echo \"failed: worker_processes is not 1 when enable enable_dev_mode\"\n    exit 1\nfi\n\ncount=`grep -c \"listen 0.0.0.0:9080.*reuseport\" conf/nginx.conf || true`\nif [ $count -ne 0 ]; then\n    echo \"failed: reuseport should be disabled when enable enable_dev_mode\"\n    exit 1\nfi\n\necho \"passed: enable enable_dev_mode\"\n\n# check whether the 'worker_cpu_affinity' is in nginx.conf\n\ngit checkout conf/config.yaml\n\nmake init\n\ncount=`grep -c \"worker_cpu_affinity\" conf/nginx.conf  || true`\nif [ $count -ne 0 ]; then\n    echo \"failed: nginx.conf file found worker_cpu_affinity when disabling it\"\n    exit 1\nfi\n\necho \"passed: nginx.conf file disables cpu affinity\"\n\n# check the 'worker_shutdown_timeout' in 'nginx.conf' .\n\nmake init\n\ngrep -E \"worker_shutdown_timeout 240s\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: worker_shutdown_timeout in nginx.conf is required 240s\"\n    exit 1\nfi\n\necho \"passed: worker_shutdown_timeout in nginx.conf is ok\"\n\n# check the 'client_max_body_size' in 'nginx.conf' .\n\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n    http:\n        client_max_body_size: 512m\n' > conf/config.yaml\n\nmake init\n\nif ! grep -E \"client_max_body_size 512m\" conf/nginx.conf > /dev/null; then\n    echo \"failed: client_max_body_size in nginx.conf doesn't change\"\n    exit 1\nfi\n\necho \"passed: client_max_body_size in nginx.conf is ok\"\n\n# check worker processes number is configurable.\n\ngit checkout conf/config.yaml\n\necho \"\nnginx_config:\n    worker_processes: 2\n\" > conf/config.yaml\n\nmake init\n\ngrep \"worker_processes 2;\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: worker_processes in nginx.conf doesn't change\"\n    exit 1\nfi\n\nsed -i 's/worker_processes: 2/worker_processes: auto/'  conf/config.yaml\necho \"passed: worker_processes number is configurable\"\n\n# check disable cpu affinity\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  enable_cpu_affinity: true\n' > conf/config.yaml\n\nmake init\n\ngrep -E \"worker_cpu_affinity\" conf/nginx.conf > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: nginx.conf file is missing worker_cpu_affinity configuration\"\n    exit 1\nfi\n\necho \"passed: nginx.conf file contains worker_cpu_affinity configuration\"\n\n# set worker processes with env\ngit checkout conf/config.yaml\n\nexport APISIX_WORKER_PROCESSES=8\n\nmake init\n\ncount=`grep -c \"worker_processes 8;\" conf/nginx.conf || true`\nif [ $count -ne 1 ]; then\n    echo \"failed: worker_processes is not 8 when using env to set worker processes\"\n    exit 1\nfi\n\necho \"passed: using env to set worker processes\"\n\n# set worker processes with env\ngit checkout conf/config.yaml\n\nmake init\n\ncount=`grep -c \"ssl_session_tickets off;\" conf/nginx.conf || true `\nif [ $count -eq 0 ]; then\n    echo \"failed: ssl_session_tickets is off when ssl.ssl_session_tickets is false.\"\n    exit 1\nfi\n\necho '\napisix:\n    ssl:\n        ssl_session_tickets: true\n' > conf/config.yaml\n\nmake init\n\ncount=`grep -c \"ssl_session_tickets on;\" conf/nginx.conf || true `\nif [ $count -eq 0 ]; then\n    echo \"failed: ssl_session_tickets is on when ssl.ssl_session_tickets is true.\"\n    exit 1\nfi\n\necho \"passed: disable ssl_session_tickets by default\"\n\n# support 3rd-party plugin\necho '\napisix:\n    extra_lua_path: \"$prefix/example/?.lua\"\n    extra_lua_cpath: \"$prefix/example/?.lua\"\nplugins:\n    - 3rd-party\nstream_plugins:\n    - 3rd-party\n' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\n\nsleep 0.5\nmake stop\n\nif grep \"failed to load plugin [3rd-party]\" logs/error.log > /dev/null; then\n    echo \"failed: 3rd-party plugin can not be loaded\"\n    exit 1\nfi\necho \"passed: 3rd-party plugin can be loaded\"\n\n# validate extra_lua_path\necho '\napisix:\n    extra_lua_path: \";\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'invalid extra_lua_path'; then\n    echo \"failed: can't detect invalid extra_lua_path\"\n    exit 1\nfi\n\necho \"passed: detect invalid extra_lua_path\"\n\n# support hooking into APISIX methods\necho '\napisix:\n    lua_module_hook: \"example/my_hook\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"lua_module_hook\" validation failed'; then\n    echo \"failed: bad lua_module_hook should be rejected\"\n    exit 1\nfi\n\necho \"passed: bad lua_module_hook should be rejected\"\n\necho '\napisix:\n    proxy_mode: http&stream\n    extra_lua_path: \"$prefix/example/?.lua\"\n    lua_module_hook: \"my_hook\"\n    stream_proxy:\n        tcp:\n            - addr: 9100\n' > conf/config.yaml\n\nrm logs/error.log\nmake init\nmake run\n\nsleep 0.5\nmake stop\n\nif ! grep \"my hook works in http\" logs/error.log > /dev/null; then\n    echo \"failed: hook can take effect\"\n    exit 1\nfi\n\nif ! grep \"my hook works in stream\" logs/error.log > /dev/null; then\n    echo \"failed: hook can take effect\"\n    exit 1\nfi\n\necho \"passed: hook can take effect\"\n\n# check the keepalive related parameter settings in the upstream\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  http:\n    upstream:\n      keepalive: 32\n      keepalive_requests: 100\n      keepalive_timeout: 6s\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"keepalive 32;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'keepalive 32;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"keepalive_requests 100;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'keepalive_requests 100;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"keepalive_timeout 6s;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'keepalive_timeout 6s;' not in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found the keepalive related parameter in nginx.conf\"\n\n# check the charset setting\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  http:\n    charset: gbk\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"charset gbk;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'charset gbk;' not in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found the 'charset gbk;' in nginx.conf\"\n\n# check realip recursive setting\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n    http:\n        real_ip_recursive: \"on\"\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"real_ip_recursive on;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'real_ip_recursive on;' not in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found 'real_ip_recursive on' in nginx.conf\"\n\n# check the variables_hash_max_size setting\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  http:\n    variables_hash_max_size: 1024\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"variables_hash_max_size 1024;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'variables_hash_max_size 1024;' not in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found the 'variables_hash_max_size 1024;' in nginx.conf\"\n\n# test disk_path without quotes\ngit checkout conf/config.yaml\n\necho '\napisix:\n  proxy_cache:\n    zones:\n      - name: disk_cache_one\n        disk_path: /tmp/disk_cache_one\n        disk_size: 100m\n        memory_size: 20m\n        cache_levels: \"1:2\"\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"proxy_cache_path /tmp/disk_cache_one\" conf/nginx.conf > /dev/null; then\n    echo \"failed: disk_path could not work without quotes\"\n    exit 1\nfi\n\necho \"passed: disk_path could work without quotes\"\n\n# check the stream lua_shared_dict lrucache_lock value\ngit checkout conf/config.yaml\n\necho '\napisix:\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - addr: 9100\n        tls: true\n      - addr: \"127.0.0.1:9101\"\n    udp:\n      - 9200\n      - \"127.0.0.1:9201\"\nnginx_config:\n  stream:\n    lua_shared_dict:\n      lrucache-lock-stream: 20m\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"lrucache-lock-stream 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'lrucache-lock-stream 20m;' not in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found the 'lrucache-lock-stream 20m;' in nginx.conf\"\n\n# check the http lua_shared_dict variables value\ngit checkout conf/config.yaml\n\necho '\nnginx_config:\n  meta:\n    lua_shared_dict:\n      upstream-healthcheck: 20m\n  http:\n    lua_shared_dict:\n      internal-status: 20m\n      plugin-limit-req: 20m\n      plugin-limit-count: 20m\n      prometheus-metrics: 20m\n      plugin-limit-conn: 20m\n      worker-events: 20m\n      lrucache-lock: 20m\n      balancer-ewma: 20m\n      balancer-ewma-locks: 20m\n      balancer-ewma-last-touched-at: 20m\n      plugin-limit-count-redis-cluster-slot-lock: 2m\n      tracing_buffer: 20m\n      plugin-api-breaker: 20m\n      etcd-cluster-health-check: 20m\n      discovery: 2m\n      jwks: 2m\n      introspection: 20m\n      access-tokens: 2m\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"internal-status 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'internal-status 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"plugin-limit-req 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'plugin-limit-req 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"plugin-limit-count 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'plugin-limit-count 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"prometheus-metrics 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'prometheus-metrics 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"plugin-limit-conn 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'plugin-limit-conn 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"upstream-healthcheck 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'upstream-healthcheck 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"worker-events 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'worker-events 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"lrucache-lock 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'lrucache-lock 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"balancer-ewma 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'balancer-ewma 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"balancer-ewma-locks 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'balancer-ewma-locks 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"balancer-ewma-last-touched-at 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'balancer-ewma-last-touched-at 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"plugin-limit-count-redis-cluster-slot-lock 2m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'plugin-limit-count-redis-cluster-slot-lock 2m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"plugin-api-breaker 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'plugin-api-breaker 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"etcd-cluster-health-check 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'etcd-cluster-health-check 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"discovery 2m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'discovery 2m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"jwks 2m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'jwks 2m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"introspection 20m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'introspection 20m;' not in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"access-tokens 2m;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: 'access-tokens 2m;' not in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: found the http lua_shared_dict related parameter in nginx.conf\"\n"
  },
  {
    "path": "t/cli/test_makefile.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nmake run\n\necho \"\ndeployment:\n    admin:\n        admin_listen:\n            ip: 127.0.0.2\n            port: 9181\napisix:\n  enable_admin: true\n\" > conf/config.yaml\n\nmake reload\nmake stop\n\nif ! grep \"listen 127.0.0.2:9181;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: regenerate nginx conf in 'make reload'\"\n    exit 1\nfi\n\necho \"passed: regenerate nginx conf in 'make reload'\"\n"
  },
  {
    "path": "t/cli/test_opentelemetry_set_ngx_var.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho '\nplugins:\n    - opentelemetry\nplugin_attr:\n  opentelemetry:\n    set_ngx_var: true\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"set \\$opentelemetry_context_traceparent          '';\" conf/nginx.conf > /dev/null; then\n    echo \"failed: opentelemetry_context_traceparent not found in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"set \\$opentelemetry_trace_id                     '';\" conf/nginx.conf > /dev/null; then\n    echo \"failed: opentelemetry_trace_id not found in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"set \\$opentelemetry_span_id                      '';\" conf/nginx.conf > /dev/null; then\n    echo \"failed: opentelemetry_span_id not found in nginx.conf\"\n    exit 1\nfi\n\n\necho \"passed: opentelemetry_set_ngx_var configuration is validated\"\n"
  },
  {
    "path": "t/cli/test_prometheus.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\ngit checkout conf/config.yaml\n\nsleep 1\n\nmake run\n\n# The privileged agent may be ready later than the normal workers,\n# so we need to wait for a while.\nsleep 1\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/prometheus/metrics)\nif [ ! $code -eq 404 ]; then\n    echo \"failed: should listen at default prometheus address\"\n    exit 1\nfi\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9091/apisix/prometheus/metrics)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: should listen at default prometheus address\"\n    exit 1\nfi\n\nif ! curl -i http://127.0.0.1:9091/apisix/prometheus/metrics | grep \"apisix_nginx_http_current_connections\" > /dev/null; then\n    echo \"failed: should listen at default prometheus address\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: should listen at default prometheus address\"\n\necho '\nplugin_attr:\n  prometheus:\n    export_addr:\n        ip: ${{IP}}\n        port: ${{PORT}}\n' > conf/config.yaml\n\nIP=127.0.0.1 PORT=9092 make run\n\nsleep 1\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9092/apisix/prometheus/metrics)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: should listen at configured prometheus address\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: should listen at configured prometheus address\"\n\necho '\nplugin_attr:\n  prometheus:\n    enable_export_server: false\n    export_uri: /prometheus/metrics\n    export_addr:\n        ip: ${{IP}}\n        port: ${{PORT}}\n' > conf/config.yaml\n\nIP=127.0.0.1 PORT=9092 make run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n# initialize prometheus metrics public API route #1\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} -X PUT http://127.0.0.1:9180/apisix/admin/routes/metrics1 \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d \"{\n        \\\"uri\\\": \\\"/prometheus/metrics\\\",\n        \\\"plugins\\\": {\n            \\\"public-api\\\": {}\n        }\n    }\")\nif [ ! $code -eq 201 ]; then\n    echo \"failed: initialize prometheus metrics public API failed #1\"\n    exit 1\nfi\n\nsleep 0.5\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s http://127.0.0.1:9092/prometheus/metrics || echo 'ouch')\nif [ \"$code\" != \"ouch\" ]; then\n    echo \"failed: should listen at previous prometheus address\"\n    exit 1\nfi\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/prometheus/metrics)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: should listen at previous prometheus address\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: should listen at previous prometheus address\"\n\necho '\nplugin_attr:\n  prometheus:\n    export_addr:\n      ip: ${{IP}}\n      port: ${{PORT}}\n' > conf/config.yaml\n\nout=$(IP=127.0.0.1 PORT=9090 make init 2>&1 || true)\nif ! echo \"$out\" | grep \"prometheus port 9090 conflicts with control\"; then\n    echo \"failed: can't detect port conflicts\"\n    exit 1\nfi\n\necho '\napisix:\n  node_listen: ${{PORT}}\nplugin_attr:\n  prometheus:\n    export_addr:\n      ip: ${{IP}}\n      port: ${{PORT}}\n' > conf/config.yaml\n\nout=$(IP=127.0.0.1 PORT=9092 make init 2>&1 || true)\nif ! echo \"$out\" | grep \"http listen port 9092 conflicts with prometheus\"; then\n    echo \"failed: can't detect port conflicts\"\n    exit 1\nfi\n\necho \"passed: should detect port conflicts\"\n\necho '\nplugin_attr:\n  prometheus:\n    metric_prefix: apisix_ci_prefix_\n    export_addr:\n      ip: ${{IP}}\n      port: ${{PORT}}\n' > conf/config.yaml\n\nIP=127.0.0.1 PORT=9092 make run\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\n# initialize prometheus metrics public API route #2\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} -X PUT http://127.0.0.1:9180/apisix/admin/routes/metrics2 \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d \"{\n        \\\"uri\\\": \\\"/apisix/prometheus/metrics\\\",\n        \\\"plugins\\\": {\n            \\\"public-api\\\": {}\n        }\n    }\")\nif [ ! $code -eq 201 ]; then\n    echo \"failed: initialize prometheus metrics public API failed #2\"\n    exit 1\nfi\n\nsleep 0.5\n\nif ! curl -s http://127.0.0.1:9092/apisix/prometheus/metrics | grep \"apisix_ci_prefix_\" | wc -l; then\n    echo \"failed: should use custom metric prefix\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: should use custom metric prefix\"\n"
  },
  {
    "path": "t/cli/test_prometheus_reload.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\ngit checkout conf/config.yaml\n\nmake run\n\nsleep 2\n\necho \"removing prometheus from the plugins list\"\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n  - ip-restriction' > conf/config.yaml\n\necho \"fetch metrics, should not contain {}\"\n\nif curl -i http://127.0.0.1:9091/apisix/prometheus/metrics | grep \"{}\" > /dev/null; then\n    echo \"failed: metrics should not contain '{}' when prometheus is enabled\"\n    exit 1\nfi\n\necho \"calling reload API to actually disable prometheus\"\n\ncurl -i http://127.0.0.1:9090/v1/plugins/reload -XPUT\n\nsleep 2\n\necho \"fetch metrics after reload should contain {}\"\n\nif ! curl -i http://127.0.0.1:9091/apisix/prometheus/metrics | grep \"{}\" > /dev/null; then\n    echo \"failed: metrics should contain '{}' when prometheus is disabled\"\n    exit 1\nfi\n\necho \"re-enable prometheus\"\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n  - prometheus' > conf/config.yaml\n\necho \"fetching metrics without reloading should give same result as before\"\n\nif ! curl -i http://127.0.0.1:9091/apisix/prometheus/metrics | grep \"{}\" > /dev/null; then\n    echo \"failed: metrics should contain '{}' when prometheus is disabled\"\n    exit 1\nfi\n\necho \"calling reload API to actually enable prometheus\"\n\ncurl -i http://127.0.0.1:9090/v1/plugins/reload -XPUT\n\nsleep 2\n\nif curl -i http://127.0.0.1:9091/apisix/prometheus/metrics | grep \"{}\" > /dev/null; then\n    echo \"failed: metrics should not contain '{}' when prometheus is enabled\"\n    exit 1\nfi\n"
  },
  {
    "path": "t/cli/test_prometheus_stream.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nexit_if_not_customed_nginx\n\necho \"\napisix:\n    proxy_mode: http&stream\n    enable_admin: true\n    stream_proxy:\n        tcp:\n            - addr: 9100\nstream_plugins:\n    - prometheus\nplugin_attr:\n    prometheus:\n        refresh_interval: 1\n\" > conf/config.yaml\n\nmake run\nsleep 0.5\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -v -k -i -m 20 -o /dev/null -s -X PUT http://127.0.0.1:9180/apisix/admin/stream_routes/1 \\\n    -H \"X-API-KEY: $admin_key\" \\\n    -d '{\n        \"plugins\": {\n            \"prometheus\": {}\n        },\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": [{\n                \"host\": \"127.0.0.1\",\n                \"port\": 1995,\n                \"weight\": 1\n            }]\n        }\n    }'\n\ncurl http://127.0.0.1:9100 || true\nsleep 1 # wait for sync\n\nout=\"$(curl http://127.0.0.1:9091/apisix/prometheus/metrics)\"\nif ! echo \"$out\" | grep \"apisix_stream_connection_total{route=\\\"1\\\"} 1\" > /dev/null; then\n    echo \"failed: prometheus can't work in stream subsystem\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: prometheus works when both http & stream are enabled\"\n\necho \"\napisix:\n    proxy_mode: stream\n    enable_admin: false\n    stream_proxy:\n        tcp:\n            - addr: 9100\nstream_plugins:\n    - prometheus\nplugin_attr:\n    prometheus:\n        refresh_interval: 1\n\" > conf/config.yaml\n\nmake run\nsleep 0.5\n\ncurl http://127.0.0.1:9100 || true\nsleep 1 # wait for sync\n\nout=\"$(curl http://127.0.0.1:9091/apisix/prometheus/metrics)\"\nif ! echo \"$out\" | grep \"apisix_stream_connection_total{route=\\\"1\\\"} 1\" > /dev/null; then\n    echo \"failed: prometheus can't work in stream subsystem\"\n    exit 1\nfi\n\nif ! echo \"$out\" | grep \"apisix_node_info{hostname=\" > /dev/null; then\n    echo \"failed: prometheus can't work in stream subsystem\"\n    exit 1\nfi\n\necho \"passed: prometheus works when only stream is enabled\"\n"
  },
  {
    "path": "t/cli/test_proxy_mirror_timeout.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho '\nplugin_attr:\n  proxy-mirror:\n    timeout:\n        connect: 2000ms\n        read: 2s\n        send: 2000ms\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"proxy_connect_timeout 2000ms;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: proxy_connect_timeout not found in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"proxy_read_timeout 2s;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: proxy_read_timeout not found in nginx.conf\"\n    exit 1\nfi\n\necho \"passed: proxy timeout configuration is validated\"\n"
  },
  {
    "path": "t/cli/test_route_match_with_graphql.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho '\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n\napisix:\n  router:\n    http: radixtree_uri\n\nnginx_config:\n  worker_processes: 1\n\n' > conf/config.yaml\n\necho '\nroutes:\n  - uri: \"/hello\"\n    hosts:\n      - test.com\n    vars:\n      - - \"graphql_name\"\n        - \"==\"\n        - \"createAccount\"\n    priority: 30\n    id: \"graphql1\"\n    upstream_id: \"invalid\"\n\n  - uri: \"/hello\"\n    hosts:\n      - test.com\n    plugins:\n      echo:\n        body: \"test server\"\n    priority: 20\n    id: \"graphql2\"\n    upstream_id: \"invalid\"\n\n  - uri: \"/hello\"\n    hosts:\n      - test2.com\n    plugins:\n      echo:\n        body: \"test2\"\n    priority: 20\n    id: \"graphql3\"\n    upstream_id: \"invalid\"\n\nupstreams:\n  - nodes:\n      127.0.0.1:1999: 1\n    id: \"invalid\"\n#END\n' > conf/apisix.yaml\n\nmake run\n\ndd if=/dev/urandom of=tmp_data.json bs=300K count=1\n\nfor i in {1..100}; do\n    curl -s http://127.0.0.1:9080/hello -H \"Host: test.com\" -H \"Content-Type: application/json\" -X POST -d @tmp_data.json > /tmp/graphql_request1.txt &\n    curl -s http://127.0.0.1:9080/hello -H \"Host: test2.com\" -H \"Content-Type: application/json\" -X POST -d @tmp_data.json > /tmp/graphql_request2.txt &\n\n    wait\n\n    if diff /tmp/graphql_request1.txt /tmp/graphql_request2.txt > /dev/null; then\n        make stop\n        echo \"failed: route match error in GraphQL requests, route should not be the same\"\n        exit 1\n    fi\ndone\n\nmake stop\n\nrm tmp_data.json /tmp/graphql_request1.txt /tmp/graphql_request2.txt\n\necho \"passed: GraphQL requests can be correctly matched to the route\"\n"
  },
  {
    "path": "t/cli/test_serverless.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nserverless_clean_up() {\n    clean_up\n    git checkout conf/apisix.yaml\n}\n\ntrap serverless_clean_up EXIT\n\nrm logs/error.log || echo ''\n\necho '\napisix:\n  enable_admin: false\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n' > conf/config.yaml\n\nmake init\n\necho '\nroutes:\n  -\n    uri: /log_request\n    plugins:\n      serverless-pre-function:\n        phase: before_proxy\n        functions:\n        - \"return function(conf, ctx) ctx.count = (ctx.count or 0) + 1 end\"\n        - \"return function(conf, ctx) ngx.log(ngx.WARN, \\\"run before_proxy phase \\\", ctx.count, \\\" with \\\", ctx.balancer_ip) end\"\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n        \"0.0.0.0:1979\": 100000\n      type: chash\n      key: remote_addr\n#END\n' > conf/apisix.yaml\n\nmake run\nsleep 0.1\ncurl -v -k -i -m 20 -o /dev/null http://127.0.0.1:9080/log_request\n\nif ! grep \"run before_proxy phase 1 with 0.0.0.0\" logs/error.log; then\n    echo \"failed: before_proxy phase runs incorrect time\"\n    exit 1\nfi\n\nif ! grep \"run before_proxy phase 2 with 127.0.0.1\" logs/error.log; then\n    echo \"failed: before_proxy phase runs incorrect time\"\n    exit 1\nfi\n\nmake stop\n\necho '\nroutes:\n  -\n    uri: /log_request\n    plugins:\n      serverless-pre-function:\n        phase: before_proxy\n        functions:\n        - \"return function(conf, ctx) ngx.exit(403) end\"\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n        \"0.0.0.0:1979\": 100000\n      type: chash\n      key: remote_addr\n#END\n' > conf/apisix.yaml\n\nmake run\nsleep 0.1\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/log_request)\nmake stop\n\nif [ ! $code -eq 403 ]; then\n    echo \"failed: failed to exit in the before_proxy phase\"\n    exit 1\nfi\n\nmake stop\n\necho \"pass: run code in the before_proxy phase of serverless plugin\"\n"
  },
  {
    "path": "t/cli/test_snippet.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# allow injecting configuration snippets\n\necho '\napisix:\n    node_listen: 9080\n    enable_admin: true\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - 9100\nnginx_config:\n    main_configuration_snippet: |\n        daemon on;\n    http_configuration_snippet: |\n        chunked_transfer_encoding on;\n    http_server_configuration_snippet: |\n        set $my \"var\";\n    http_server_location_configuration_snippet: |\n        set $upstream_name -;\n    http_admin_configuration_snippet: |\n        log_format admin \"$request_time $pipe\";\n    http_end_configuration_snippet: |\n        server_names_hash_bucket_size 128;\n    stream_configuration_snippet: |\n        tcp_nodelay off;\n' > conf/config.yaml\n\nmake init\n\ngrep \"daemon on;\" -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject main configuration\"\n    exit 1\nfi\n\ngrep \"chunked_transfer_encoding on;\" -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject http configuration\"\n    exit 1\nfi\n\ngrep 'set $my \"var\";' -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject http server configuration\"\n    exit 1\nfi\n\ngrep 'set $upstream_name -;' -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject http server location configuration\"\n    exit 1\nfi\n\ngrep 'log_format admin \"$request_time $pipe\";' -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject admin server configuration\"\n    exit 1\nfi\n\ngrep 'server_names_hash_bucket_size 128;' -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject http end configuration\"\n    exit 1\nfi\n\ngrep 'server_names_hash_bucket_size 128;' -A 3 conf/nginx.conf | grep \"}\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject http end configuration\"\n    exit 1\nfi\n\ngrep 'tcp_nodelay off;' -A 2 conf/nginx.conf | grep \"configuration snippet ends\" > /dev/null\nif [ ! $? -eq 0 ]; then\n    echo \"failed: can't inject stream configuration\"\n    exit 1\nfi\n\n# use the builtin server by default\n\necho '\napisix:\n    node_listen: 9080\nnginx_config:\n    http_configuration_snippet: |\n        server {\n            listen 9080;\n            server_name qa.com www.qa.com;\n            location / {\n                return 503 \"ouch\";\n            }\n        }\n' > conf/config.yaml\n\nmake run\n\nsleep 1\ncode=$(curl -k -i -o /dev/null -s -w %{http_code} http://127.0.0.1:9080 -H 'Host: m.qa.com')\nif [ ! $code -eq 404 ]; then\n    echo \"failed: use the builtin server by default\"\n    exit 1\nfi\ncode=$(curl -k -i -o /dev/null -s -w %{http_code} http://127.0.0.1:9080 -H 'Host: www.qa.com')\nif [ ! $code -eq 503 ]; then\n    echo \"failed: use the builtin server by default\"\n    exit 1\nfi\n\nmake stop\n\necho \"passed: use the builtin server by default\"\n"
  },
  {
    "path": "t/cli/test_standalone.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nstandalone() {\n    rm -f conf/apisix.yaml.link\n    clean_up\n    git checkout conf/apisix.yaml\n}\n\ntrap standalone EXIT\n\n# support environment variables in yaml values\necho '\napisix:\n  enable_admin: false\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n' > conf/config.yaml\n\necho '\nroutes:\n  -\n    uri: ${{var_test_path}}\n    plugins:\n      proxy-rewrite:\n        uri: ${{var_test_proxy_rewrite_uri:=/apisix/nginx_status}}\n    upstream:\n      nodes:\n        \"127.0.0.1:9091\": 1\n      type: roundrobin\n#END\n' > conf/apisix.yaml\n\n# check for resolve variables\nvar_test_path=/test make init\n\nif ! grep \"env var_test_path;\" conf/nginx.conf > /dev/null; then\n    echo \"failed: failed to resolve variables\"\n    exit 1\nfi\n\n# variable is valid\nvar_test_path=/test make run\nsleep 0.1\ncode=$(curl -o /dev/null -s -m 5 -w %{http_code} http://127.0.0.1:9080/test)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: resolve variables in apisix.yaml conf failed\"\n    exit 1\nfi\n\necho \"passed: resolve variables in apisix.yaml conf success\"\n\n# support environment variables in yaml keys\necho '\nroutes:\n  -\n    uri: \"/test\"\n    plugins:\n      proxy-rewrite:\n        uri: \"/apisix/nginx_status\"\n    upstream:\n      nodes:\n        \"${{HOST_IP}}:${{PORT}}\": 1\n      type: roundrobin\n#END\n' > conf/apisix.yaml\n\n# variable is valid\nHOST_IP=\"127.0.0.1\" PORT=\"9091\" make init\nHOST_IP=\"127.0.0.1\" PORT=\"9091\" make run\nsleep 0.1\n\ncode=$(curl -o /dev/null -s -m 5 -w %{http_code} http://127.0.0.1:9080/test)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: resolve variables in apisix.yaml conf failed\"\nfi\n\necho \"passed: resolve variables in apisix.yaml conf success\"\n\n# configure standalone via deployment\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n       config_provider: yaml\n' > conf/config.yaml\n\nvar_test_path=/test make run\nsleep 0.1\ncode=$(curl -o /dev/null -s -m 5 -w %{http_code} http://127.0.0.1:9080/apisix/admin/routes)\nif [ ! $code -eq 404 ]; then\n    echo \"failed: admin API should be disabled automatically\"\n    exit 1\nfi\n\necho \"passed: admin API should be disabled automatically\"\n\n# support environment variables\necho '\nroutes:\n  -\n    uri: ${{var_test_path}}\n    plugins:\n      proxy-rewrite:\n        uri: ${{var_test_proxy_rewrite_uri:=/apisix/nginx_status}}\n    upstream:\n      nodes:\n        \"127.0.0.1:9091\": 1\n      type: roundrobin\n#END\n' > conf/apisix.yaml\n\nvar_test_path=/test make run\nsleep 0.1\ncode=$(curl -o /dev/null -s -m 5 -w %{http_code} http://127.0.0.1:9080/test)\nif [ ! $code -eq 200 ]; then\n    echo \"failed: resolve variables in apisix.yaml conf failed\"\n    exit 1\nfi\n\necho \"passed: resolve variables in apisix.yaml conf success\"\n\n# Avoid unnecessary config reloads\n## Wait for a second else `st_ctime` won't increase\nsleep 1\nexpected_config_reloads=$(grep \"config file $(pwd)/conf/apisix.yaml reloaded.\" logs/error.log | wc -l)\n\n## Create a symlink to change the link count and as a result `st_ctime`\nln conf/apisix.yaml conf/apisix.yaml.link\nsleep 1\n\nactual_config_reloads=$(grep \"config file $(pwd)/conf/apisix.yaml reloaded.\" logs/error.log | wc -l)\nif [ $expected_config_reloads -ne $actual_config_reloads ]; then\n    echo \"failed: apisix.yaml was reloaded\"\n    exit 1\nfi\necho \"passed: apisix.yaml was not reloaded\"\n"
  },
  {
    "path": "t/cli/test_standalone_docker.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nstandalone() {\n    clean_up\n    docker rm -f apisix-test-standalone 2>/dev/null || true\n    git checkout conf/apisix.yaml\n}\n\nDOCKER_IMAGE=\"${DOCKER_IMAGE:-apache/apisix:master-debian-dev}\"\ntrap standalone EXIT\n\necho 'routes: []\n#END' > conf/apisix.yaml\n\nrun_docker_test() {\n    local standalone_flag=$1\n    local config_mode=${2:-ro}\n\n    if [ \"$standalone_flag\" = \"true\" ]; then\n        docker run -d --name apisix-test-standalone \\\n            -e APISIX_STAND_ALONE=true \\\n            -v $(pwd)/conf/config.yaml:/usr/local/apisix/conf/config.yaml:${config_mode} \\\n            -v $(pwd)/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro \\\n            ${DOCKER_IMAGE} > /dev/null 2>&1\n    else\n        docker run -d --name apisix-test-standalone \\\n            -v $(pwd)/conf/config.yaml:/usr/local/apisix/conf/config.yaml:${config_mode} \\\n            -v $(pwd)/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro \\\n            ${DOCKER_IMAGE} > /dev/null 2>&1\n    fi\n\n    sleep 5\n\n    if ! docker ps | grep -q apisix-test-standalone; then\n        echo \"Container failed to start. Logs:\"\n        docker logs apisix-test-standalone\n        docker rm -f apisix-test-standalone > /dev/null 2>&1\n        return 1\n    fi\n\n    docker rm -f apisix-test-standalone > /dev/null 2>&1\n    return 0\n}\n\n# normal YAML format\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nif ! run_docker_test \"true\"; then\n    echo \"failed: normal YAML format 'role: data_plane' was rejected\"\n    exit 1\nfi\n\necho \"passed: normal YAML format accepted\"\n\n# double-quoted format\necho '\ndeployment:\n    role: \"data_plane\"\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nif ! run_docker_test \"true\"; then\n    echo \"failed: double-quoted 'role: \\\"data_plane\\\"' was rejected\"\n    exit 1\nfi\n\necho \"passed: double-quoted format accepted\"\n\n# single-quoted format\necho \"\ndeployment:\n    role: 'data_plane'\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n\" > conf/config.yaml\n\nif ! run_docker_test \"true\"; then\n    echo \"failed: single quoted \\\"role: 'data_plane'\\\" was rejected\"\n    exit 1\nfi\n\necho \"passed: single-quoted format accepted\"\n\n# flow syntax\necho '\ndeployment: {\"role\": \"data_plane\", \"role_data_plane\": {\"config_provider\": \"yaml\"}}\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nif ! run_docker_test \"true\"; then\n    echo \"failed: flow syntax 'role: {\\\"data_plane\\\"}' was rejected\"\n    exit 1\nfi\n\necho \"passed: flow syntax format accepted\"\n\n# mixed quotes\necho \"\ndeployment:\n    role: \\\"data_plane\\\"\n    role_data_plane:\n        config_provider: 'yaml'\napisix:\n    node_listen: 9080\n\" > conf/config.yaml\n\nif ! run_docker_test \"true\"; then\n    echo \"failed: mixed quotes format was rejected\"\n    exit 1\nfi\n\necho \"passed: mixed quotes format accepted\"\n\n# etcd config_provider should fail in standalone mode\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: etcd\n    etcd:\n        host:\n            - \"http://127.0.0.1:2379\"\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nif run_docker_test \"true\"; then\n    echo \"failed: 'config_provider: etcd' should be rejected in standalone mode\"\n    exit 1\nfi\n\necho \"passed: 'config_provider: etcd' was correctly rejected in standalone mode\"\n\n# traditional role with yaml config_provider should work\necho '\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: yaml\n    admin:\n        admin_key_required: false\napisix:\n    node_listen: 9080\n    enable_admin: true\n' > conf/config.yaml\n\nif ! run_docker_test \"true\" \"rw\"; then\n    echo \"failed: traditional role with 'config_provider: yaml' was rejected\"\n    exit 1\nfi\n\necho \"passed: traditional role with 'config_provider: yaml' accepted\"\n\n# check is APISIX_PROFILE is respected in standalone mode\necho '\n  deployment:\n    role: data_plane\n    role_data_plane:\n      config_provider: yaml\n  apisix:\n    node_listen: 9080\n  ' > conf/config-prod.yaml\n\n  docker run -d --name apisix-test-standalone \\\n      -e APISIX_STAND_ALONE=true \\\n      -e APISIX_PROFILE=prod \\\n      -v $(pwd)/conf/config-prod.yaml:/usr/local/apisix/conf/config-prod.yaml:ro \\\n      -v $(pwd)/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro \\\n      ${DOCKER_IMAGE} > /dev/null 2>&1\n\n  sleep 5\n\n  if ! docker ps | grep -q apisix-test-standalone; then\n      echo \"failed: APISIX_PROFILE=prod in standalone mode was rejected\"\n      docker logs apisix-test-standalone\n      docker rm -f apisix-test-standalone > /dev/null 2>&1\n      exit 1\n  fi\n\n  docker rm -f apisix-test-standalone > /dev/null 2>&1\n\n  echo \"passed: APISIX_PROFILE=prod in standalone mode accepted\"\n"
  },
  {
    "path": "t/cli/test_standalone_yaml_format.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nstandalone() {\n    clean_up\n    git checkout conf/apisix.yaml\n    rm -f conf/config-prod.yaml\n    unset APISIX_STAND_ALONE\n    unset APISIX_PROFILE\n}\n\ntrap standalone EXIT\n\n# normal YAML format\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nexport APISIX_STAND_ALONE=true\n\nif ! make init > /dev/null 2>&1; then\n    echo \"failed: normal YAML format 'role: data_plane' was rejected\"\n    exit 1\nfi\n\necho \"passed: normal YAML format accepted\"\n\n# double-quoted format\necho '\ndeployment:\n    role: \"data_plane\"\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nmake clean > /dev/null 2>&1\n\nif ! make init > /dev/null 2>&1; then\n    echo \"failed: double-quoted 'role: \\\"data_plane\\\"' was rejected\"\n    exit 1\nfi\n\necho \"passed: double-quoted format accepted\"\n\n# single-quoted format\necho \"\ndeployment:\n    role: 'data_plane'\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n\" > conf/config.yaml\n\nmake clean > /dev/null 2>&1\n\nif ! make init > /dev/null 2>&1; then\n    echo \"failed: single quoted \"role: 'data_plane'\" was rejected\"\n    exit 1\nfi\n\necho \"passed: single-quoted format accepted\"\n\n# flow syntax\necho '\ndeployment: {\"role\": \"data_plane\", \"role_data_plane\": {\"config_provider\": \"yaml\"}}\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nmake clean > /dev/null 2>&1\n\nif ! make init > /dev/null 2>&1; then\n    echo \"failed: flow syntax was rejected\"\n    exit 1\nfi\n\n# should fail - etcd config_provider in standalone mode\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: etcd\n    etcd:\n        host:\n          - \"http://127.0.0.1:2379\"\napisix:\n    node_listen: 9080\n' > conf/config.yaml\n\nmake clean > /dev/null 2>&1\n\nif make init > /dev/null 2>&1; then\n    echo \"failed: 'config_provider: etcd' should be rejected in standalone mode\"\n    exit 1\nfi\n\necho \"passed: 'config_provider: etcd' was correctly rejected in standalone mode\"\n\n# traditional role with yaml config_provider\necho '\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: yaml\n    admin:\n        admin_key_required: false\napisix:\n    node_listen: 9080\n    enable_admin: true\n' > conf/config.yaml\n\nmake clean > /dev/null 2>&1\n\nif ! make init > /dev/null 2>&1; then\n    echo \"failed: 'role: traditional' with 'config_provider: yaml' was rejected\"\n    exit 1\nfi\n\necho \"passed: 'role: traditional' with 'config_provider: yaml' accepted\"\n\n# check APISIX_PROFILE is respected\necho '\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\napisix:\n    node_listen: 9080\n' > conf/config-prod.yaml\n\necho 'routes: []\n#END' > conf/apisix.yaml\n\nexport APISIX_PROFILE=prod\n\nmake clean > /dev/null 2>&1\n\nif ! make init > /dev/null 2>&1; then\n    echo \"failed: APISIX_PROFILE=prod is not respected in standalone mode\"\n    exit 1\nfi\n\necho \"passed: APISIX_PROFILE=prod is respected in standalone mode\"\n"
  },
  {
    "path": "t/cli/test_status_api.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\ngit checkout conf/config.yaml\n\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:23790\"\n      - \"http://127.0.0.1:23791\"\n      - \"http://127.0.0.1:23792\"\n    prefix: /apisix\nnginx_config:\n  error_log_level: info\napisix:\n  status:\n    ip: 127.0.0.1\n    port: 7085\n' > conf/config.yaml\n\n# create 3 node etcd cluster in docker\nETCD_NAME_0=etcd0\nETCD_NAME_1=etcd1\nETCD_NAME_2=etcd2\ndocker compose -f ./t/cli/docker-compose-etcd-cluster.yaml up -d\n\nmake run\n\nsleep 0.5\n\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status | grep 200 \\\n|| (echo \"failed: status api didn't return 200\"; exit 1)\n\nsleep 2\n\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status/ready | grep 200 \\\n|| (echo \"failed: status/ready api didn't return 200\"; exit 1)\n\n# stop two etcd endpoints but status api should return 200 as all workers are synced\ndocker stop ${ETCD_NAME_0}\ndocker stop ${ETCD_NAME_1}\n\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status | grep 200 \\\n|| (echo \"failed: status api didn't return 200\"; exit 1)\n\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status/ready | grep 200 \\\n|| (echo \"failed: status/ready api didn't return 200\"; exit 1)\n\ndocker stop ${ETCD_NAME_2}\n\necho \"/status/ready returns 200 even when etcd endpoints are down as all workers are synced\"\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status/ready | grep 200 \\\n|| (echo \"failed: status/ready api didn't return 200\"; exit 1)\n\ndocker compose -f ./t/cli/docker-compose-etcd-cluster.yaml down\n"
  },
  {
    "path": "t/cli/test_status_api_standalone.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nstandalone() {\n    clean_up\n    git checkout conf/apisix.yaml\n}\n\ntrap standalone EXIT\n\n# support environment variables\necho '\napisix:\n  enable_admin: false\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\nnginx_config:\n  error_log_level: warn\napisix:\n  status:\n    ip: 127.0.0.1\n    port: 7085\n' > conf/config.yaml\n\necho '\nroutes:\n  -\n    uri: /get\n    upstream:\n      nodes:\n        \"httpbin.local:8280\": 1\n      type: roundrobin\n#END\n' > conf/apisix.yaml\n\n# check for resolve variables\nmake run\n\nsleep 0.5\n\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status/ready | grep 200 \\\n|| (echo \"failed: status/ready api didn't return 200\"; exit 1)\n\nsleep 0.5\n\ncurl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:7085/status | grep 200 \\\n|| (echo \"failed: status api didn't return 200\"; exit 1)\n"
  },
  {
    "path": "t/cli/test_stream_config.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho \"\napisix:\n    enable_admin: false\n    proxy_mode: stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\n\" > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"lua_package_path\" conf/nginx.conf)\nif [ \"$count\" -ne 1 ]; then\n    echo \"failed: failed to enable stream proxy only by default\"\n    exit 1\nfi\n\necho \"passed: enable stream proxy only by default\"\n\necho \"\napisix:\n    enable_admin: false\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\n\" > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"lua_package_path\" conf/nginx.conf)\nif [ \"$count\" -ne 2 ]; then\n    echo \"failed: failed to enable stream proxy and http proxy\"\n    exit 1\nfi\n\necho \"\napisix:\n    enable_admin: true\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\n\" > conf/config.yaml\n\nmake init\n\ncount=$(grep -c \"lua_package_path\" conf/nginx.conf)\nif [ \"$count\" -ne 2 ]; then\n    echo \"failed: failed to enable stream proxy and http proxy when admin is enabled\"\n    exit 1\nfi\n\necho \"passed: enable stream proxy and http proxy\"\n\necho \"\napisix:\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\nstream_plugins:\n    - ip-restriction\n\" > conf/config.yaml\n\nmake init\n\nif grep \"plugin-limit-conn-stream\" conf/nginx.conf > /dev/null; then\n    echo \"failed: enable shdict on demand\"\n    exit 1\nfi\n\necho \"\napisix:\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\nstream_plugins:\n    - limit-conn\n\" > conf/config.yaml\n\nmake init\n\nif ! grep \"plugin-limit-conn-stream\" conf/nginx.conf > /dev/null; then\n    echo \"failed: enable shdict on demand\"\n    exit 1\nfi\n\necho \"passed: enable shdict on demand\"\n"
  },
  {
    "path": "t/cli/test_tls_over_tcp.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\n# check tls over tcp proxy\necho \"\napisix:\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - addr: 9100\n              tls: true\nnginx_config:\n    stream_configuration_snippet: |\n        server {\n            listen 9101;\n            return \\\"OK FROM UPSTREAM\\\";\n        }\n\n\" > conf/config.yaml\n\nmake run\nsleep 0.1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl http://127.0.0.1:9180/apisix/admin/ssls/1 \\\n-H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n     \"cert\" : \"'\"$(cat t/certs/mtls_server.crt)\"'\",\n     \"key\": \"'\"$(cat t/certs/mtls_server.key)\"'\",\n     \"snis\": [\"test.com\"]\n}'\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -k -i http://127.0.0.1:9180/apisix/admin/stream_routes/1  \\\n    -H \"X-API-KEY: $admin_key\" -X PUT -d \\\n    '{\"upstream\":{\"nodes\":{\"127.0.0.1:9101\":1},\"type\":\"roundrobin\"}}'\n\nsleep 0.1\nif ! echo -e 'mmm' | \\\n    openssl s_client -connect 127.0.0.1:9100 -servername test.com -CAfile t/certs/mtls_ca.crt \\\n        -ign_eof | \\\n    grep 'OK FROM UPSTREAM';\nthen\n    echo \"failed: should proxy tls over tcp\"\n    exit 1\nfi\n\nmake stop\necho \"passed: proxy tls over tcp\"\n"
  },
  {
    "path": "t/cli/test_upstream_mtls.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# validate the config.yaml\n\n. ./t/cli/common.sh\n\n# test proxy_ssl_trusted_certificate success\ngit checkout conf/config.yaml\n\nexit_if_not_customed_nginx\n\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/apisix.crt\nnginx_config:\n  http_configuration_snippet: |\n    server {\n        listen 1983 ssl;\n        server_name test.com;\n        ssl_certificate             ../t/certs/apisix.crt;\n        ssl_certificate_key         ../t/certs/apisix.key;\n        location /hello {\n            return 200 \"hello world\";\n        }\n    }\n  http_server_configuration_snippet: |\n    proxy_ssl_verify on;\n' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\nsleep 0.1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -k -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"pass_host\": \"rewrite\",\n        \"nodes\": {\n            \"127.0.0.1:1983\": 1\n        },\n        \"scheme\": \"https\",\n        \"hash_on\": \"vars\",\n        \"upstream_host\": \"test.com\",\n        \"type\": \"roundrobin\",\n        \"tls\": {\n            \"client_cert\": \"-----BEGIN CERTIFICATE-----\\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\\n-----END CERTIFICATE-----\\n\",\n            \"client_key\": \"HrMHUvE9Esvn7GnZ+vAynaIg/8wlB3r0zm0htmnwofYLp1VhtLeU1EmMJkPLUkcn2+v6Uav9bOQMkPdSpUMcEpRplLSXs+miu+B07CCUnsMrXkfQawRMIoePJZSLH5+PfDAlWIK2Q+ruYnjtnpNziiAtXf/HRRwHHMelnfedXqD8kn3Toe46ZYyBir99o/r/do5ludez5oY7qhOgNSWKCfnZE8Ip82g7t7n7jsAf5tTdRulUGBQ4ITV2zM3cxpD0PWnWMbOfygZIDxR8QU9wj8ihuFL1s1NM8PplcKbUxC4QlrSN+ZNkr6mxy+akPmXlABwcFIiSK7c/xvU1NjoILnhPpL6aRpbhmQX/a1XUCl+2INlQ5QbXbTN+JmDBhrU9NiYecRJMfmA1N/lhwgt01tUnxMoAhfpUVgEbZNalCJt+wn8TC+Xp3DZ0bCpXrfzqsprGKan9qC3mCN03jj50JyGFL+xt8wX8D0uaIsu4cVk4et7kbTIj9rvucsh0cfKn8va8/cdjw5QhFSRBkW5Vuz9NwvzVQ6DHWs1a8VZbN/hERxcbWNk/p1VgGLHioqZZTOd5CYdN4dGjnksjXa0Z77mTSoNx3U79FQPAgUMEA1phnO/jdryM3g5M+UvESXA/75we435xg5tLRDvNwJw2NlosQsGY7fzUi2+HFo436htydRFv8ChHezs2v99mjfCUijrWYoeJ5OB2+KO9XiOIz7gpqhTef9atajSYRhxhcwdCVupC1PrPGn9MzhdQLeqQCJj3kyazPfO3xPkNpMAqd2lXnLR4HGd9SBHe75Sik3jW9W1sUqrn2fDjyWd0jz57pl4qyHjbzjd3uE5qbH/QuYZBIzI9tEn7tj12brWrwHsMt+/4M7zp8Opsia64V3Y7ICLIi7fiYfr70RujXyn8Ik5TB1QC98JrnDjgQlTPDhHLk1r8XhZXqIIg6DmaN7UUjIuZhKxARTs8b5WMPvVV4GownlPN28sHIMAX84BNbP0597Fxipwp2oTMFKTzvxm+QUtbWvIPzF3n25L4sPCyUx5PRIRCJ5kDNQfhiN6o3Y/fAY0PyxI06PWYoNvSn3uO24XNXbF3RkpwKtV8n/iNo5dyM1VqFPWDuKRSLHY7E4lQTdqx4/n+rrnoH6SlmQ0zwxwxBeAz/TvkmiW7WLe3C5cUDKF9yYwvAe8ek4oTR3GxaiDWjNFsu7DUoDjpH5f3IxrX2IN4FyzE47hMeg4muPov7h74WwosqgnfmwoAEFV4+ldmzpdSjghZoF2M9EZI24Xa9rVdd6j2t6IjX20oL+SLQL/9HppMi1nC+3Zby1WOvuTR4g8K1QP75OeY4xTD1iEAXpd0WOX7C3ndceVF4THLCI4Imcf9FH9MBrE55FPMEsAk54HiAoyMd6tgqv/akRqmuAmnSsrWALhqiCnAVh2uzk644gSzmsFbh7zF33qrcafPpU4PxUEvpqbLz7asoNUDf4YB4gCcgZx30eK/w9FpMaLveiNq77EW7qcvJQPcjZ4uLaKkQVODJsd+1CbZF6370aiLxouXLFT3eQI7Ovu6be8D3MmazRPgCV36qzMwONqrXE/JbMFMKe5l1e4Y6avMejrj43BMgGo2u8LimCWkBeNwqIjH7plwbpDKo4OKZVbrzSZ0hplUDd/jMrb6Ulbc04uMeEigehrhSsZ0ZwoDiZcf/fDIclaTGNMl40N2wBiqdnw9uKTqD1YxzqDQ7vgiXG55ae31lvevPTgk/lLvpwzlyitjGs+6LJPu/wSCKA2VIyhJfK+8EnItEKjBUrXdOklBdOmTpUpdQ+zfd2NCrFRDJZKl26Uh412adFEkqY37O/0FbSCpAIsUCvaItcqK7qh5Rq26hVR0nS1MRs+MjGBzGqudXPQZHy+Yp7AlAa5UgJUaAwn2b/id6kNdv6hNWqSzHvOAVKdgC9/j0yN1VJD92+IoJTTiXsMQELcgm1Ehj2GZpTHu+GPuaOovHBnZMq/Kg4nUS+ig86X01jV28uGGtglERf1HqVQpdZwbrXtUqH0cbjlvUwQ1j7zp9yhs+0ta87v0I+elAZhXzqvehMiLJu2o9/k2+4dPvkEscduHOU6jZqe8ndNEMQWiaZEYJKxNWPTaQ6nZSlFTsT7GlENeJlFzlw8QkyRJPMBWkXuaymQUcu43Pm+gAjinHSAGUeaSaIdL2Yb0M88qNwG+UlNEslx/J37pA1oMJyxb7XOeySxkP7dXi5JvygLIfkEA3ENC4NHU9nsUvTvp5AZidZCxxtYCNYfjY6xyrlfnE+V+us31LA9Wc/tKa4y3Ldj30IT2sssUrdZ0l7UbwfcZT42ZeJpxDofpZ2rjgswTs0Upr72VuOCzjpKa1CJwxhVVtPVJJovcXp4bsNPJers+yIYfTl1aqaf4qSzU5OL/cze2e6qAh7622zEa/q6klpUx9b1f8YGlQhjQcy3++JnwwsHR71Ofh9woXq57LDCHFA6f95zdkadDDhwgRcvWVnbA2Szps8iJv7h2m25qZPFtN6puJj3RlmT6hnfBeYCjpfy/2TxyCqm6bG3HZxGuhzWs2ZGxzsjBJ3ueO1pAOjtDhkRqzoWt/v2o367IYP7iTcp4pi+qJHIWCN1ElDI0BVoZ+Xq9iLfKmjrjcxQ7EYGHfQDE52QaCQ3nMB7oiqncZ1Q5n/ICDHha9RkPP9V9vWiJIZwgOJtPfGzsGQ9AigH6po65IJyxmY5upuhg7DTmsLQnKC/fwjkBF9So/4cdZuqDbxGrDDOgpL7uvWXANRNMrqYoMFUG7M90QJHj7NgSL+B6mSNwa9ctTua7Estkoyvavda3Bl3qHQ0Hva5gjSg6elL6PQ4ksqhESvjztuy58qk9aZHsQB8ZKRu8VSay40a/3ueX6bnd0hwsYy42aWJR1z+uie3yTWPuG2JZ7DjkgDduWdC+cxfvTVTG58E5luafy5j/t85UVoB2nr46VHlt/vg4M9G8/4F0d0Y6ThI4/XTfg6l1vq5ouzhQxd+SRwnuXieZy+4/2XKJnrV6t+JbNAvwdGR1V9VPLlnb+IqpvOCYyL1YLYSlNubb9HU0wxVPppGSpJLmi+njQzl71PBgMm6QV9j889wPUo387fRbJjXbSSVLon61xk/4dNvjsgfv9rF+/qEML0q4tXBJVOJ1iwKjn84Nk6vdHM3Hu8knp0hYFa4AECYKInSTVXajWAKFx4SOq8G8MA/0YlIN872LBjUm2GKs17wsJuWID+mSyVE5pV5gQ+r92YvPcC+yIvB8hTTaRclAP/KyJesDTA==\"\n        }\n    }\n}'\n\nsleep 1\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: connection to upstream with mTLS failed\"\n    exit 1\nfi\n\nsleep 0.1\n\nmake stop\n\necho \"passed: connection to upstream with mTLS success\"\n\n# test proxy_ssl_trusted_certificate and use incorrect ca cert\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/apisix_ecc.crt\nnginx_config:\n  http_configuration_snippet: |\n    server {\n        listen 1983 ssl;\n        server_name test.com;\n        ssl_certificate             ../t/certs/apisix.crt;\n        ssl_certificate_key         ../t/certs/apisix.key;\n        location /hello {\n            return 200 \"hello world\";\n        }\n    }\n  http_server_configuration_snippet: |\n    proxy_ssl_verify on;\n' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\nsleep 0.1\n\nadmin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/\"//g')\ncurl -k -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: $admin_key\" -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"pass_host\": \"rewrite\",\n        \"nodes\": {\n            \"127.0.0.1:1983\": 1\n        },\n        \"scheme\": \"https\",\n        \"hash_on\": \"vars\",\n        \"upstream_host\": \"test.com\",\n        \"type\": \"roundrobin\",\n        \"tls\": {\n            \"client_cert\": \"-----BEGIN CERTIFICATE-----\\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\\n-----END CERTIFICATE-----\\n\",\n            \"client_key\": \"HrMHUvE9Esvn7GnZ+vAynaIg/8wlB3r0zm0htmnwofYLp1VhtLeU1EmMJkPLUkcn2+v6Uav9bOQMkPdSpUMcEpRplLSXs+miu+B07CCUnsMrXkfQawRMIoePJZSLH5+PfDAlWIK2Q+ruYnjtnpNziiAtXf/HRRwHHMelnfedXqD8kn3Toe46ZYyBir99o/r/do5ludez5oY7qhOgNSWKCfnZE8Ip82g7t7n7jsAf5tTdRulUGBQ4ITV2zM3cxpD0PWnWMbOfygZIDxR8QU9wj8ihuFL1s1NM8PplcKbUxC4QlrSN+ZNkr6mxy+akPmXlABwcFIiSK7c/xvU1NjoILnhPpL6aRpbhmQX/a1XUCl+2INlQ5QbXbTN+JmDBhrU9NiYecRJMfmA1N/lhwgt01tUnxMoAhfpUVgEbZNalCJt+wn8TC+Xp3DZ0bCpXrfzqsprGKan9qC3mCN03jj50JyGFL+xt8wX8D0uaIsu4cVk4et7kbTIj9rvucsh0cfKn8va8/cdjw5QhFSRBkW5Vuz9NwvzVQ6DHWs1a8VZbN/hERxcbWNk/p1VgGLHioqZZTOd5CYdN4dGjnksjXa0Z77mTSoNx3U79FQPAgUMEA1phnO/jdryM3g5M+UvESXA/75we435xg5tLRDvNwJw2NlosQsGY7fzUi2+HFo436htydRFv8ChHezs2v99mjfCUijrWYoeJ5OB2+KO9XiOIz7gpqhTef9atajSYRhxhcwdCVupC1PrPGn9MzhdQLeqQCJj3kyazPfO3xPkNpMAqd2lXnLR4HGd9SBHe75Sik3jW9W1sUqrn2fDjyWd0jz57pl4qyHjbzjd3uE5qbH/QuYZBIzI9tEn7tj12brWrwHsMt+/4M7zp8Opsia64V3Y7ICLIi7fiYfr70RujXyn8Ik5TB1QC98JrnDjgQlTPDhHLk1r8XhZXqIIg6DmaN7UUjIuZhKxARTs8b5WMPvVV4GownlPN28sHIMAX84BNbP0597Fxipwp2oTMFKTzvxm+QUtbWvIPzF3n25L4sPCyUx5PRIRCJ5kDNQfhiN6o3Y/fAY0PyxI06PWYoNvSn3uO24XNXbF3RkpwKtV8n/iNo5dyM1VqFPWDuKRSLHY7E4lQTdqx4/n+rrnoH6SlmQ0zwxwxBeAz/TvkmiW7WLe3C5cUDKF9yYwvAe8ek4oTR3GxaiDWjNFsu7DUoDjpH5f3IxrX2IN4FyzE47hMeg4muPov7h74WwosqgnfmwoAEFV4+ldmzpdSjghZoF2M9EZI24Xa9rVdd6j2t6IjX20oL+SLQL/9HppMi1nC+3Zby1WOvuTR4g8K1QP75OeY4xTD1iEAXpd0WOX7C3ndceVF4THLCI4Imcf9FH9MBrE55FPMEsAk54HiAoyMd6tgqv/akRqmuAmnSsrWALhqiCnAVh2uzk644gSzmsFbh7zF33qrcafPpU4PxUEvpqbLz7asoNUDf4YB4gCcgZx30eK/w9FpMaLveiNq77EW7qcvJQPcjZ4uLaKkQVODJsd+1CbZF6370aiLxouXLFT3eQI7Ovu6be8D3MmazRPgCV36qzMwONqrXE/JbMFMKe5l1e4Y6avMejrj43BMgGo2u8LimCWkBeNwqIjH7plwbpDKo4OKZVbrzSZ0hplUDd/jMrb6Ulbc04uMeEigehrhSsZ0ZwoDiZcf/fDIclaTGNMl40N2wBiqdnw9uKTqD1YxzqDQ7vgiXG55ae31lvevPTgk/lLvpwzlyitjGs+6LJPu/wSCKA2VIyhJfK+8EnItEKjBUrXdOklBdOmTpUpdQ+zfd2NCrFRDJZKl26Uh412adFEkqY37O/0FbSCpAIsUCvaItcqK7qh5Rq26hVR0nS1MRs+MjGBzGqudXPQZHy+Yp7AlAa5UgJUaAwn2b/id6kNdv6hNWqSzHvOAVKdgC9/j0yN1VJD92+IoJTTiXsMQELcgm1Ehj2GZpTHu+GPuaOovHBnZMq/Kg4nUS+ig86X01jV28uGGtglERf1HqVQpdZwbrXtUqH0cbjlvUwQ1j7zp9yhs+0ta87v0I+elAZhXzqvehMiLJu2o9/k2+4dPvkEscduHOU6jZqe8ndNEMQWiaZEYJKxNWPTaQ6nZSlFTsT7GlENeJlFzlw8QkyRJPMBWkXuaymQUcu43Pm+gAjinHSAGUeaSaIdL2Yb0M88qNwG+UlNEslx/J37pA1oMJyxb7XOeySxkP7dXi5JvygLIfkEA3ENC4NHU9nsUvTvp5AZidZCxxtYCNYfjY6xyrlfnE+V+us31LA9Wc/tKa4y3Ldj30IT2sssUrdZ0l7UbwfcZT42ZeJpxDofpZ2rjgswTs0Upr72VuOCzjpKa1CJwxhVVtPVJJovcXp4bsNPJers+yIYfTl1aqaf4qSzU5OL/cze2e6qAh7622zEa/q6klpUx9b1f8YGlQhjQcy3++JnwwsHR71Ofh9woXq57LDCHFA6f95zdkadDDhwgRcvWVnbA2Szps8iJv7h2m25qZPFtN6puJj3RlmT6hnfBeYCjpfy/2TxyCqm6bG3HZxGuhzWs2ZGxzsjBJ3ueO1pAOjtDhkRqzoWt/v2o367IYP7iTcp4pi+qJHIWCN1ElDI0BVoZ+Xq9iLfKmjrjcxQ7EYGHfQDE52QaCQ3nMB7oiqncZ1Q5n/ICDHha9RkPP9V9vWiJIZwgOJtPfGzsGQ9AigH6po65IJyxmY5upuhg7DTmsLQnKC/fwjkBF9So/4cdZuqDbxGrDDOgpL7uvWXANRNMrqYoMFUG7M90QJHj7NgSL+B6mSNwa9ctTua7Estkoyvavda3Bl3qHQ0Hva5gjSg6elL6PQ4ksqhESvjztuy58qk9aZHsQB8ZKRu8VSay40a/3ueX6bnd0hwsYy42aWJR1z+uie3yTWPuG2JZ7DjkgDduWdC+cxfvTVTG58E5luafy5j/t85UVoB2nr46VHlt/vg4M9G8/4F0d0Y6ThI4/XTfg6l1vq5ouzhQxd+SRwnuXieZy+4/2XKJnrV6t+JbNAvwdGR1V9VPLlnb+IqpvOCYyL1YLYSlNubb9HU0wxVPppGSpJLmi+njQzl71PBgMm6QV9j889wPUo387fRbJjXbSSVLon61xk/4dNvjsgfv9rF+/qEML0q4tXBJVOJ1iwKjn84Nk6vdHM3Hu8knp0hYFa4AECYKInSTVXajWAKFx4SOq8G8MA/0YlIN872LBjUm2GKs17wsJuWID+mSyVE5pV5gQ+r92YvPcC+yIvB8hTTaRclAP/KyJesDTA==\"\n        }\n    }\n}'\n\nsleep 0.1\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\n\nif [ ! $code -eq 502 ]; then\n    echo \"failed: should fail when proxy_ssl_verify is enabled and ssl_trusted_certificate is wrong ca cert\"\n    exit 1\nfi\n\nsleep 0.1\n\nmake stop\n\nif ! grep -E 'self-signed certificate' logs/error.log; then\n    echo \"failed: should got 'self-signed certificate' when ssl_trusted_certificate is wrong ca cert\"\n    exit 1\nfi\n\necho \"passed: when proxy_ssl_verify is enabled and ssl_trusted_certificate is wrong ca cert, got 502\"\n\n\n#  test combined proxy_ssl_trusted_certificate success\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/apisix.crt\nnginx_config:\n  http_configuration_snippet: |\n    server {\n        listen 1983 ssl;\n        server_name test.com;\n        ssl_certificate             ../t/certs/apisix.crt;\n        ssl_certificate_key         ../t/certs/apisix.key;\n        location /hello {\n            return 200 \"hello world\";\n        }\n    }\n  http_server_configuration_snippet: |\n    proxy_ssl_verify on;\n' > conf/config.yaml\n\nrm logs/error.log || true\nmake init\nmake run\nsleep 0.1\n\ncurl -k -i http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '\n{\n    \"uri\": \"/hello\",\n    \"upstream\": {\n        \"pass_host\": \"rewrite\",\n        \"nodes\": {\n            \"127.0.0.1:1983\": 1\n        },\n        \"scheme\": \"https\",\n        \"hash_on\": \"vars\",\n        \"upstream_host\": \"test.com\",\n        \"type\": \"roundrobin\",\n        \"tls\": {\n            \"client_cert\": \"-----BEGIN CERTIFICATE-----\\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\\n-----END CERTIFICATE-----\\n\",\n            \"client_key\": \"HrMHUvE9Esvn7GnZ+vAynaIg/8wlB3r0zm0htmnwofYLp1VhtLeU1EmMJkPLUkcn2+v6Uav9bOQMkPdSpUMcEpRplLSXs+miu+B07CCUnsMrXkfQawRMIoePJZSLH5+PfDAlWIK2Q+ruYnjtnpNziiAtXf/HRRwHHMelnfedXqD8kn3Toe46ZYyBir99o/r/do5ludez5oY7qhOgNSWKCfnZE8Ip82g7t7n7jsAf5tTdRulUGBQ4ITV2zM3cxpD0PWnWMbOfygZIDxR8QU9wj8ihuFL1s1NM8PplcKbUxC4QlrSN+ZNkr6mxy+akPmXlABwcFIiSK7c/xvU1NjoILnhPpL6aRpbhmQX/a1XUCl+2INlQ5QbXbTN+JmDBhrU9NiYecRJMfmA1N/lhwgt01tUnxMoAhfpUVgEbZNalCJt+wn8TC+Xp3DZ0bCpXrfzqsprGKan9qC3mCN03jj50JyGFL+xt8wX8D0uaIsu4cVk4et7kbTIj9rvucsh0cfKn8va8/cdjw5QhFSRBkW5Vuz9NwvzVQ6DHWs1a8VZbN/hERxcbWNk/p1VgGLHioqZZTOd5CYdN4dGjnksjXa0Z77mTSoNx3U79FQPAgUMEA1phnO/jdryM3g5M+UvESXA/75we435xg5tLRDvNwJw2NlosQsGY7fzUi2+HFo436htydRFv8ChHezs2v99mjfCUijrWYoeJ5OB2+KO9XiOIz7gpqhTef9atajSYRhxhcwdCVupC1PrPGn9MzhdQLeqQCJj3kyazPfO3xPkNpMAqd2lXnLR4HGd9SBHe75Sik3jW9W1sUqrn2fDjyWd0jz57pl4qyHjbzjd3uE5qbH/QuYZBIzI9tEn7tj12brWrwHsMt+/4M7zp8Opsia64V3Y7ICLIi7fiYfr70RujXyn8Ik5TB1QC98JrnDjgQlTPDhHLk1r8XhZXqIIg6DmaN7UUjIuZhKxARTs8b5WMPvVV4GownlPN28sHIMAX84BNbP0597Fxipwp2oTMFKTzvxm+QUtbWvIPzF3n25L4sPCyUx5PRIRCJ5kDNQfhiN6o3Y/fAY0PyxI06PWYoNvSn3uO24XNXbF3RkpwKtV8n/iNo5dyM1VqFPWDuKRSLHY7E4lQTdqx4/n+rrnoH6SlmQ0zwxwxBeAz/TvkmiW7WLe3C5cUDKF9yYwvAe8ek4oTR3GxaiDWjNFsu7DUoDjpH5f3IxrX2IN4FyzE47hMeg4muPov7h74WwosqgnfmwoAEFV4+ldmzpdSjghZoF2M9EZI24Xa9rVdd6j2t6IjX20oL+SLQL/9HppMi1nC+3Zby1WOvuTR4g8K1QP75OeY4xTD1iEAXpd0WOX7C3ndceVF4THLCI4Imcf9FH9MBrE55FPMEsAk54HiAoyMd6tgqv/akRqmuAmnSsrWALhqiCnAVh2uzk644gSzmsFbh7zF33qrcafPpU4PxUEvpqbLz7asoNUDf4YB4gCcgZx30eK/w9FpMaLveiNq77EW7qcvJQPcjZ4uLaKkQVODJsd+1CbZF6370aiLxouXLFT3eQI7Ovu6be8D3MmazRPgCV36qzMwONqrXE/JbMFMKe5l1e4Y6avMejrj43BMgGo2u8LimCWkBeNwqIjH7plwbpDKo4OKZVbrzSZ0hplUDd/jMrb6Ulbc04uMeEigehrhSsZ0ZwoDiZcf/fDIclaTGNMl40N2wBiqdnw9uKTqD1YxzqDQ7vgiXG55ae31lvevPTgk/lLvpwzlyitjGs+6LJPu/wSCKA2VIyhJfK+8EnItEKjBUrXdOklBdOmTpUpdQ+zfd2NCrFRDJZKl26Uh412adFEkqY37O/0FbSCpAIsUCvaItcqK7qh5Rq26hVR0nS1MRs+MjGBzGqudXPQZHy+Yp7AlAa5UgJUaAwn2b/id6kNdv6hNWqSzHvOAVKdgC9/j0yN1VJD92+IoJTTiXsMQELcgm1Ehj2GZpTHu+GPuaOovHBnZMq/Kg4nUS+ig86X01jV28uGGtglERf1HqVQpdZwbrXtUqH0cbjlvUwQ1j7zp9yhs+0ta87v0I+elAZhXzqvehMiLJu2o9/k2+4dPvkEscduHOU6jZqe8ndNEMQWiaZEYJKxNWPTaQ6nZSlFTsT7GlENeJlFzlw8QkyRJPMBWkXuaymQUcu43Pm+gAjinHSAGUeaSaIdL2Yb0M88qNwG+UlNEslx/J37pA1oMJyxb7XOeySxkP7dXi5JvygLIfkEA3ENC4NHU9nsUvTvp5AZidZCxxtYCNYfjY6xyrlfnE+V+us31LA9Wc/tKa4y3Ldj30IT2sssUrdZ0l7UbwfcZT42ZeJpxDofpZ2rjgswTs0Upr72VuOCzjpKa1CJwxhVVtPVJJovcXp4bsNPJers+yIYfTl1aqaf4qSzU5OL/cze2e6qAh7622zEa/q6klpUx9b1f8YGlQhjQcy3++JnwwsHR71Ofh9woXq57LDCHFA6f95zdkadDDhwgRcvWVnbA2Szps8iJv7h2m25qZPFtN6puJj3RlmT6hnfBeYCjpfy/2TxyCqm6bG3HZxGuhzWs2ZGxzsjBJ3ueO1pAOjtDhkRqzoWt/v2o367IYP7iTcp4pi+qJHIWCN1ElDI0BVoZ+Xq9iLfKmjrjcxQ7EYGHfQDE52QaCQ3nMB7oiqncZ1Q5n/ICDHha9RkPP9V9vWiJIZwgOJtPfGzsGQ9AigH6po65IJyxmY5upuhg7DTmsLQnKC/fwjkBF9So/4cdZuqDbxGrDDOgpL7uvWXANRNMrqYoMFUG7M90QJHj7NgSL+B6mSNwa9ctTua7Estkoyvavda3Bl3qHQ0Hva5gjSg6elL6PQ4ksqhESvjztuy58qk9aZHsQB8ZKRu8VSay40a/3ueX6bnd0hwsYy42aWJR1z+uie3yTWPuG2JZ7DjkgDduWdC+cxfvTVTG58E5luafy5j/t85UVoB2nr46VHlt/vg4M9G8/4F0d0Y6ThI4/XTfg6l1vq5ouzhQxd+SRwnuXieZy+4/2XKJnrV6t+JbNAvwdGR1V9VPLlnb+IqpvOCYyL1YLYSlNubb9HU0wxVPppGSpJLmi+njQzl71PBgMm6QV9j889wPUo387fRbJjXbSSVLon61xk/4dNvjsgfv9rF+/qEML0q4tXBJVOJ1iwKjn84Nk6vdHM3Hu8knp0hYFa4AECYKInSTVXajWAKFx4SOq8G8MA/0YlIN872LBjUm2GKs17wsJuWID+mSyVE5pV5gQ+r92YvPcC+yIvB8hTTaRclAP/KyJesDTA==\"\n        }\n    }\n}'\n\nsleep 1\n\ncode=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/hello)\n\nif [ ! $code -eq 200 ]; then\n    echo \"failed: connection to upstream with mTLS failed\"\n    exit 1\nfi\n\nsleep 0.1\n\nmake stop\n\necho \"passed: connection to upstream with mTLS success\"\n"
  },
  {
    "path": "t/cli/test_validate_config.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# validate the config.yaml\n\n. ./t/cli/common.sh\n\necho '\ndiscovery:\n    nacos:\n        host: \"127.0.0.1\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"host\" validation failed: wrong type: expected array, got string'; then\n    echo \"failed: should check discovery schema during init\"\n    exit 1\nfi\n\necho '\ndiscovery:\n    unknown:\n        host: \"127.0.0.1\"\n' > conf/config.yaml\n\nif ! make init; then\n    echo \"failed: should ignore discovery without schema\"\n    exit 1\nfi\n\necho \"passed: check discovery schema during init\"\n\necho '\napisix:\n  dns_resolver_valid: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"dns_resolver_valid\" validation failed: wrong type: expected integer, got string'; then\n    echo \"failed: dns_resolver_valid should be a number\"\n    exit 1\nfi\n\necho \"passed: dns_resolver_valid should be a number\"\n\necho '\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\n' > conf/config.yaml\n\nout=$(make run 2>&1)\nif echo \"$out\" | grep 'no such file'; then\n    echo \"failed: find the certificate correctly\"\n    exit 1\nfi\nmake stop\n\necho \"passed: find the certificate correctly\"\n\necho '\ndeployment:\n    admin:\n        admin_listen:\n            port: 9180\napisix:\n    node_listen: 9080\n    enable_admin: true\n    proxy_mode: http&stream\n    stream_proxy:\n        tcp:\n            - \"localhost:9100\"\n        udp:\n            - \"127.0.0.1:9101\"\n' > conf/config.yaml\n\nout=$(make run 2>&1 || echo \"ouch\")\nif echo \"$out\" | grep 'ouch'; then\n    echo \"failed: allow configuring address in stream_proxy\"\n    exit 1\nfi\nmake stop\n\necho \"passed: allow configuring address in stream_proxy\"\n\n# apisix test\ngit checkout conf/config.yaml\n\nout=$(./bin/apisix test 2>&1 || true)\nif ! echo \"$out\" | grep \"configuration test is successful\"; then\n    echo \"failed: configuration test should be successful\"\n    exit 1\nfi\n\necho \"pass: apisix test\"\n\n./bin/apisix start\nsleep 1 # wait for apisix starts\n\n# set invalid configuration\necho '\nnginx_config:\n    main_configuration_snippet: |\n        notexist on;\n' > conf/config.yaml\n\n# apisix restart\nout=$(./bin/apisix restart 2>&1 || true)\nif ! (echo \"$out\" | grep \"\\[emerg\\] unknown directive \\\"notexist\\\"\") && ! (echo \"$out\" | grep \"the old APISIX is still running\"); then\n    echo \"failed: should restart failed when configuration invalid\"\n    exit 1\nfi\n\necho \"passed: apisix restart\"\n\n# apisix test - failure scenario\nout=$(./bin/apisix test 2>&1 || true)\nif ! echo \"$out\" | grep \"configuration test failed\"; then\n    echo \"failed: should test failed when configuration invalid\"\n    exit 1\nfi\n\n# apisix test failure should not affect apisix stop\nout=$(./bin/apisix stop 2>&1 || true)\nif echo \"$out\" | grep \"\\[emerg\\] unknown directive \\\"notexist\\\"\"; then\n    echo \"failed: `apisix test` failure should not affect `apisix stop`\"\n    exit 1\nfi\n\necho \"passed: apisix test(failure scenario)\"\n\n# apisix plugin batch-requests real_ip_from invalid - failure scenario\necho '\nplugins:\n- batch-requests\nnginx_config:\n    http:\n        real_ip_from:\n        - \"128.0.0.2\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep \"missing loopback or unspecified in the nginx_config.http.real_ip_from for plugin batch-requests\"; then\n    echo \"failed: should check the realip configuration for batch-requests\"\n    exit 1\nfi\n\necho \"passed: apisix plugin batch-requests real_ip_from(failure scenario)\"\n\n# apisix plugin batch-requests real_ip_from valid\necho '\nplugins:\n- batch-requests\nnginx_config:\n    http:\n        real_ip_from:\n        - \"127.0.0.1\"\n        - \"127.0.0.2/8\"\n        - \"0.0.0.0\"\n        - \"0.0.0.0/0\"\n        - \"::\"\n        - \"::/0\"\n        - \"unix:\"\n' > conf/config.yaml\n\nout=$(make init 2>&1)\nif echo \"$out\" | grep \"missing loopback or unspecified in the nginx_config.http.real_ip_from for plugin batch-requests\"; then\n    echo \"failed: should check the realip configuration for batch-requests\"\n    exit 1\nfi\n\necho \"passed: check the realip configuration for batch-requests\"\n\necho '\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - 127.0.0.1\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"host\" validation failed'; then\n    echo \"failed: should check etcd schema during init\"\n    exit 1\nfi\n\necho \"passed: check etcd schema during init\"\n\n# Test trusted_addresses with non-array value (string instead of array)\necho '\napisix:\n  trusted_addresses: \"127.0.0.1\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: wrong type: expected array, got string'; then\n    echo \"failed: trusted_addresses should reject string value\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses rejects non-array value\"\n\n# Test trusted_addresses with empty array (should be rejected)\necho '\napisix:\n  trusted_addresses: []\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: expect array to have at least 1 items'; then\n    echo \"failed: trusted_addresses should reject empty array\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses rejects empty array\"\n\n# Test trusted_addresses with non-string items in array\necho '\napisix:\n  trusted_addresses:\n    - \"127.0.0.1\"\n    - 123\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: failed to validate item 2: object matches none of the required'; then\n    echo \"failed: trusted_addresses should reject non-string items\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses rejects non-string array items\"\n\n# Test trusted_addresses with duplicate items (should be rejected)\necho '\napisix:\n  trusted_addresses:\n    - \"127.0.0.1\"\n    - \"192.168.1.1\"\n    - \"127.0.0.1\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"trusted_addresses\" validation failed.*equal'; then\n    echo \"failed: trusted_addresses should reject duplicate items\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses rejects duplicate items\"\n\n# Test trusted_addresses with invalid IP\necho '\napisix:\n  trusted_addresses:\n    - \"127.0.0\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: failed to validate item 1: object matches none of the required'; then\n    echo \"failed: trusted_addresses should reject invalid IP\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses rejects invalid IP\"\n\n# Test trusted_addresses with invalid CIDR\necho '\napisix:\n  trusted_addresses:\n    - \"127.0.0.0/33\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: failed to validate item 1: object matches none of the required'; then\n    echo \"failed: trusted_addresses should reject invalid CIDR\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses rejects invalid CIDR\"\n\n# Test trusted_addresses with valid IP\necho '\napisix:\n  trusted_addresses:\n    - \"127.0.0.1\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: failed to validate item 1: object matches none of the required'; then\n    echo \"failed: trusted_addresses should accept valid IP\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses accepts valid IP\"\n\n# Test trusted_addresses with valid CIDR\necho '\napisix:\n  trusted_addresses:\n    - \"127.0.0.0/24\"\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\netcd:\n  host:\n    - \"http://127.0.0.1:2379\"\n  prefix: \"/apisix\"\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif echo \"$out\" | grep 'property \"trusted_addresses\" validation failed: failed to validate item 1: object matches none of the required'; then\n    echo \"failed: trusted_addresses should accept valid CIDR\"\n    exit 1\nfi\n\necho \"passed: trusted_addresses accepts valid CIDR\"\n"
  },
  {
    "path": "t/cli/test_wasm.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\nexit_if_not_customed_nginx\n\necho '\nwasm:\n    plugins:\n        - name: wasm_log\n          file: t/wasm/log/main.go.wasm\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"priority\" is required'; then\n    echo \"failed: priority is required\"\n    exit 1\nfi\n\necho '\nwasm:\n    plugins:\n        - name: wasm_log\n          priority: 888\n' > conf/config.yaml\n\nout=$(make init 2>&1 || true)\nif ! echo \"$out\" | grep 'property \"file\" is required'; then\n    echo \"failed: file is required\"\n    exit 1\nfi\n\necho \"passed: wasm configuration is validated\"\n\necho '\nwasm:\n    plugins:\n        - name: wasm_log\n          priority: 7999\n          file: t/wasm/log/main.go.wasm\n  ' > conf/config.yaml\n\nmake init\nif ! grep \"wasm_vm \" conf/nginx.conf; then\n    echo \"failed: wasm isn't enabled\"\n    exit 1\nfi\n\necho \"passed: wasm is enabled\"\n"
  },
  {
    "path": "t/cli/test_zipkin_set_ngx_var.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n. ./t/cli/common.sh\n\necho '\nplugins:\n    - zipkin\nplugin_attr:\n  zipkin:\n    set_ngx_var: true\n' > conf/config.yaml\n\nmake init\n\nif ! grep \"set \\$zipkin_context_traceparent          '';\" conf/nginx.conf > /dev/null; then\n    echo \"failed: zipkin_context_traceparent not found in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"set \\$zipkin_trace_id                     '';\" conf/nginx.conf > /dev/null; then\n    echo \"failed: zipkin_trace_id not found in nginx.conf\"\n    exit 1\nfi\n\nif ! grep \"set \\$zipkin_span_id                      '';\" conf/nginx.conf > /dev/null; then\n    echo \"failed: zipkin_span_id not found in nginx.conf\"\n    exit 1\nfi\n\n\necho \"passed: zipkin_set_ngx_var configuration is validated\"\n"
  },
  {
    "path": "t/config-center-json/consumer-group.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello?apikey=one\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"key-auth\": {}\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"consumer_groups\": [\n    {\n      \"id\": \"foobar\",\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"consumers\": [\n    {\n      \"username\": \"one\",\n      \"group_id\": \"foobar\",\n      \"plugins\": {\n        \"key-auth\": {\n          \"key\": \"one\"\n        }\n      }\n    }\n  ]\n}\n--- response_body\nhello\n\n\n\n=== TEST 2: consumer group not found\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"key-auth\": {}\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"consumers\": [\n    {\n      \"username\": \"one\",\n      \"group_id\": \"invalid_group\",\n      \"plugins\": {\n        \"key-auth\": {\n          \"key\": \"one\"\n        }\n      }\n    }\n  ]\n}\n--- error_code: 503\n--- error_log\nfailed to fetch consumer group config by id: invalid_group\n\n\n\n=== TEST 3: plugin priority\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"key-auth\": {}\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"consumer_groups\": [\n    {\n      \"id\": \"foobar\",\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"consumers\": [\n    {\n      \"username\": \"one\",\n      \"group_id\": \"foobar\",\n      \"plugins\": {\n        \"key-auth\": {\n          \"key\": \"one\"\n        },\n        \"response-rewrite\": {\n          \"body\": \"world\\n\"\n        }\n      }\n    }\n  ]\n}\n--- response_body\nworld\n\n\n\n=== TEST 4: invalid plugin\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"key-auth\": {}\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"consumer_groups\": [\n    {\n      \"id\": \"foobar\",\n      \"plugins\": {\n        \"example-plugin\": {\n          \"skey\": \"s\"\n        },\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"consumers\": [\n    {\n      \"username\": \"one\",\n      \"group_id\": \"foobar\",\n      \"plugins\": {\n        \"key-auth\": {\n          \"key\": \"one\"\n        }\n      }\n    }\n  ]\n}\n--- error_code: 503\n--- error_log\nfailed to check the configuration of plugin example-plugin\nfailed to fetch consumer group config by id: foobar\n"
  },
  {
    "path": "t/config-center-json/consumer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: validate consumer\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"consumers\": [\n    {\n      \"username\": \"jwt#auth\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nproperty \"username\" validation failed\n\n\n\n=== TEST 2: consumer restriction\n--- apisix_json\n{\n  \"consumers\": [\n    {\n      \"username\": \"jack\",\n      \"plugins\": {\n        \"key-auth\": {\n          \"key\": \"user-key\"\n        }\n      }\n    }\n  ],\n  \"routes\": [\n    {\n      \"id\": \"1\",\n      \"methods\": [\"POST\"],\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"key-auth\": {},\n        \"consumer-restriction\": {\n          \"whitelist\": [\"jack\"]\n        }\n      },\n      \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        }\n      }\n    }\n  ]\n}\n--- more_headers\napikey: user-key\n--- request\nPOST /hello\n"
  },
  {
    "path": "t/config-center-json/global-rule.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"global_rules\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ]\n}\n--- response_body\nhello\n\n\n\n=== TEST 2: global rule with bad plugin\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"global_rules\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": 4\n        }\n      }\n    }\n  ]\n}\n--- response_body\nhello world\n--- error_log\nproperty \"body\" validation failed\n\n\n\n=== TEST 3: fix global rule with default value\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"global_rules\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"uri-blocker\": {\n          \"block_rules\": [\n            \"/h*\"\n          ]\n        }\n      }\n    }\n  ]\n}\n--- error_code: 403\n\n\n\n=== TEST 4: common phase without matched route\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/apisix/prometheus/metrics\",\n      \"plugins\": {\n        \"public-api\": {}\n      }\n    }\n  ],\n  \"global_rules\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"cors\": {\n          \"allow_origins\": \"http://a.com,http://b.com\"\n        }\n      }\n    }\n  ]\n}\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n"
  },
  {
    "path": "t/config-center-json/plugin-configs.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"plugin_configs\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugin_config_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- response_body\nhello\n\n\n\n=== TEST 2: plugin_config not found\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugin_config_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- error_code: 503\n--- error_log\nfailed to fetch plugin config by id: 1\n\n\n\n=== TEST 3: mix plugins & plugin_config_id\n--- apisix_json\n{\n  \"plugin_configs\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"example-plugin\": {\n          \"i\": 1\n        },\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/echo\",\n      \"plugin_config_id\": 1,\n      \"plugins\": {\n        \"proxy-rewrite\": {\n          \"headers\": {\n            \"in\": \"out\"\n          }\n        },\n        \"response-rewrite\": {\n          \"body\": \"world\\n\"\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /echo\n--- response_body\nworld\n--- response_headers\nin: out\n--- error_log eval\nqr/conf_version: \\d+#\\d+,/\n\n\n\n=== TEST 4: invalid plugin\n--- apisix_json\n{\n  \"plugin_configs\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"example-plugin\": {\n          \"skey\": \"s\"\n        },\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    }\n  ],\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugin_config_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- error_code: 503\n--- error_log\nfailed to check the configuration of plugin example-plugin\nfailed to fetch plugin config by id: 1\n"
  },
  {
    "path": "t/config-center-json/plugin-metadata.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ],\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1,\n      \"plugins\": {\n        \"http-logger\": {\n          \"batch_max_size\": 1,\n          \"uri\": \"http://127.0.0.1:1980/log\"\n        }\n      }\n    }\n  ],\n  \"plugin_metadata\": [\n    {\n      \"id\": \"http-logger\",\n      \"log_format\": {\n        \"host\": \"$host\",\n        \"remote_addr\": \"$remote_addr\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_log\n\"remote_addr\":\"127.0.0.1\"\n--- no_error_log\nfailed to get schema for plugin:\n\n\n\n=== TEST 2: sanity\n--- apisix_json\n{\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ],\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"plugin_metadata\": [\n    {\n      \"id\": \"authz-casbin\",\n      \"model\": 123\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_log\nfailed to check item data of [plugin_metadata]\n"
  },
  {
    "path": "t/config-center-json/plugin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $debug_config = t::APISIX::read_file(\"conf/debug.yaml\");\n$debug_config =~ s/basic:\\n  enable: false/basic:\\n  enable: true/;\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"plugins\": [\n    {\n      \"name\": \"ip-restriction\"\n    },\n    {\n      \"name\": \"jwt-auth\"\n    },\n    {\n      \"name\": \"mqtt-proxy\",\n      \"stream\": true\n    }\n  ]\n}\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n--- error_log\nuse config_provider: yaml\nload(): loaded plugin and sort by priority: 3000 name: ip-restriction\nload(): loaded plugin and sort by priority: 2510 name: jwt-auth\nload_stream(): loaded stream plugin and sort by priority: 1000 name: mqtt-proxy\n--- grep_error_log eval\nqr/load\\(\\): new plugins/\n--- grep_error_log_out\nload(): new plugins\nload(): new plugins\nload(): new plugins\nload(): new plugins\n\n\n\n=== TEST 2: plugins not changed, but still need to reload\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\nplugins:\n    - ip-restriction\n    - jwt-auth\nstream_plugins:\n    - mqtt-proxy\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"plugins\": [\n    {\n      \"name\": \"ip-restriction\"\n    },\n    {\n      \"name\": \"jwt-auth\"\n    },\n    {\n      \"name\": \"mqtt-proxy\",\n      \"stream\": true\n    }\n  ]\n}\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n--- grep_error_log eval\nqr/loaded plugin and sort by priority: \\d+ name: [^,]+/\n--- grep_error_log_out eval\nqr/(loaded plugin and sort by priority: (3000 name: ip-restriction|2510 name: jwt-auth)\n){4}/\n\n\n\n=== TEST 3: disable plugin and its router\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"plugins\": [\n    {\n      \"name\": \"jwt-auth\"\n    }\n  ]\n}\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 404\n\n\n\n=== TEST 4: enable plugin and its router\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/apisix/prometheus/metrics\",\n      \"plugins\": {\n        \"public-api\": {}\n      }\n    }\n  ],\n  \"plugins\": [\n    {\n      \"name\": \"public-api\"\n    },\n    {\n      \"name\": \"prometheus\"\n    }\n  ]\n}\n--- request\nGET /apisix/prometheus/metrics\n\n\n\n=== TEST 5: invalid plugin config\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\nplugins:\n    - ip-restriction\n    - jwt-auth\nstream_plugins:\n    - mqtt-proxy\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"plugins\": [\n    {\n      \"name\": \"xxx\",\n      \"stream\": \"ip-restriction\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nproperty \"stream\" validation failed: wrong type: expected boolean, got string\n--- no_error_log\nload(): plugins not changed\n\n\n\n=== TEST 6: empty plugin list\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"plugins\": [],\n  \"stream_plugins\": []\n}\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n--- error_log\nuse config_provider: yaml\nload(): new plugins: {}\nload_stream(): new plugins: {}\n"
  },
  {
    "path": "t/config-center-json/route-service.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: hit route\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"service_id\": 1,\n      \"id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 2: not found service\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"id\": 1,\n      \"service_id\": 1111\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to fetch service configuration by id: 1111\n\n\n\n=== TEST 3: service upstream priority\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"service_id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1977\": 1\n        },\n        \"type\": \"roundrobin\"\n      },\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: route service upstream priority\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"service_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1977\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1977\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: route service upstream by upstream_id priority\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"service_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1977\": 1\n        },\n        \"type\": \"roundrobin\"\n      },\n      \"upstream_id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1977\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: route service upstream priority\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"service_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1977\": 1\n        },\n        \"type\": \"roundrobin\"\n      },\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1977\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 7: two routes with the same service\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uris\": [\"/hello\"],\n      \"service_id\": 1,\n      \"id\": 1,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"hello\\n\"\n        }\n      }\n    },\n    {\n      \"uris\": [\"/world\"],\n      \"service_id\": 1,\n      \"id\": 2,\n      \"plugins\": {\n        \"response-rewrite\": {\n          \"body\": \"world\\n\"\n        }\n      }\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello\n\n\n\n=== TEST 8: service with bad plugin\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"service_id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"proxy-rewrite\": {\n          \"uri\": 1\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 9: fix service with default value\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"service_id\": 1\n    }\n  ],\n  \"services\": [\n    {\n      \"id\": 1,\n      \"plugins\": {\n        \"uri-blocker\": {\n          \"block_rules\": [\n            \"/h*\"\n          ]\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 403\n"
  },
  {
    "path": "t/config-center-json/route-upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: hit route\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 2: not found upstream\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1111\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code_like: ^(?:50\\d)$\n--- error_log\nfailed to find upstream by id: 1111\n\n\n\n=== TEST 3: upstream_id priority upstream\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1977\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1981\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: enable healthcheck\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1\n      },\n      \"type\": \"roundrobin\",\n      \"retries\": 2,\n      \"checks\": {\n        \"active\": {\n          \"http_path\": \"/status\",\n          \"healthy\": {\n            \"interval\": 2,\n            \"successes\": 1\n          }\n        }\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: upstream domain\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"test.com:1980\": 1\n      },\n      \"type\": \"roundrobin\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 6: upstream hash_on (bad)\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"test.com:1980\": 1\n      },\n      \"type\": \"chash\",\n      \"hash_on\": \"header\",\n      \"key\": \"$aaa\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\ninvalid configuration: failed to match pattern\n\n\n\n=== TEST 7: upstream hash_on (good)\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream_id\": 1\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"id\": 1,\n      \"nodes\": {\n        \"127.0.0.1:1980\": 1,\n        \"127.0.0.2:1980\": 1\n      },\n      \"type\": \"chash\",\n      \"hash_on\": \"header\",\n      \"key\": \"test\"\n    }\n  ]\n}\n--- request\nGET /hello\n--- more_headers\ntest: one\n--- error_log\nproxy request to 127.0.0.1:1980\n"
  },
  {
    "path": "t/config-center-json/route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 2: route:uri + host (missing host, not hit)\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"host\": \"foo.com\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 3: route:uri + host\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"host\": \"foo.com\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- more_headers\nhost: foo.com\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: route with bad plugin\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"proxy-rewrite\": {\n          \"uri\": 1\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 5: ignore unknown plugin\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"x-rewrite\": {\n          \"uri\": 1\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: route with bad plugin, radixtree_host_uri\n--- yaml_config\napisix:\n    node_listen: 1984\n    router:\n        http: \"radixtree_host_uri\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"proxy-rewrite\": {\n          \"uri\": 1\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 7: fix route with default value\n--- yaml_config\napisix:\n    node_listen: 1984\n    router:\n        http: \"radixtree_host_uri\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"plugins\": {\n        \"uri-blocker\": {\n          \"block_rules\": [\n            \"/h*\"\n          ]\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 8: invalid route, bad vars operator\n--- yaml_config\napisix:\n    node_listen: 1984\n    router:\n        http: \"radixtree_host_uri\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"vars\": [\n        [\"remote_addr\", \"=\", \"1\"]\n      ],\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to validate the 'vars' expression\n\n\n\n=== TEST 9: script with id\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"script\": \"local ngx = ngx\",\n      \"script_id\": \"1\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 200\n--- error_log\nmissing loaded script object\n\n\n\n=== TEST 10: hosts with '_' is valid\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"hosts\": [\n        \"foo.com\",\n        \"v1_test-api.com\"\n      ],\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- more_headers\nhost: v1_test-api.com\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 11: script with plugin_config_id\n--- yaml_config eval: $::yaml_config\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"id\": 1,\n      \"uri\": \"/hello\",\n      \"script\": \"local ngx = ngx\",\n      \"plugin_config_id\": \"1\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to check item data of [routes]\n"
  },
  {
    "path": "t/config-center-json/secret.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->apisix_json) {\n        my $json_config = <<_EOC_;\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n_EOC_\n\n        $block->set_value(\"apisix_json\", $json_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: validate secret/vault: wrong schema\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"secrets\": [\n    {\n      \"id\": \"vault/1\",\n      \"prefix\": \"kv/apisix\",\n      \"token\": \"root\",\n      \"uri\": \"127.0.0.1:8200\"\n    }\n  ]\n}\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local values = secret.secrets()\n            ngx.say(#values)\n        }\n    }\n--- request\nGET /t\n--- response_body\n0\n--- error_log\nproperty \"uri\" validation failed: failed to match pattern \"^[^\\\\/]+:\\\\/\\\\/([\\\\da-zA-Z.-]+|\\\\[[\\\\da-fA-F:]+\\\\])(:\\\\d+)?\"\n\n\n\n=== TEST 2: validate secrets: manager not exits\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"secrets\": [\n    {\n      \"id\": \"hhh/1\",\n      \"prefix\": \"kv/apisix\",\n      \"token\": \"root\",\n      \"uri\": \"127.0.0.1:8200\"\n    }\n  ]\n}\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local values = secret.secrets()\n            ngx.say(#values)\n        }\n    }\n--- request\nGET /t\n--- response_body\n0\n--- error_log\nsecret manager not exits\n\n\n\n=== TEST 3: load config normal\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"secrets\": [\n    {\n      \"id\": \"vault/1\",\n      \"prefix\": \"kv/apisix\",\n      \"token\": \"root\",\n      \"uri\": \"http://127.0.0.1:8200\"\n    }\n  ]\n}\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local values = secret.secrets()\n            ngx.say(\"len: \", #values)\n\n            ngx.say(\"id: \", values[1].value.id)\n            ngx.say(\"prefix: \", values[1].value.prefix)\n            ngx.say(\"token: \", values[1].value.token)\n            ngx.say(\"uri: \", values[1].value.uri)\n        }\n    }\n--- request\nGET /t\n--- response_body\nlen: 1\nid: vault/1\nprefix: kv/apisix\ntoken: root\nuri: http://127.0.0.1:8200\n\n\n\n=== TEST 4: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key key=value\n--- response_body\nSuccess! Data written to: kv/apisix/apisix-key\n\n\n\n=== TEST 5: secret.fetch_by_uri: start with $secret://\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"secrets\": [\n    {\n      \"id\": \"vault/1\",\n      \"prefix\": \"kv/apisix\",\n      \"token\": \"root\",\n      \"uri\": \"http://127.0.0.1:8200\"\n    }\n  ]\n}\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://vault/1/apisix-key/key\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 6: secret.fetch_by_uri, wrong ref format: wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(1)\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror secret_uri type: number\n\n\n\n=== TEST 7: secret.fetch_by_uri, wrong ref format: wrong prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"secret://\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror secret_uri prefix: secret://\n\n\n\n=== TEST 8: secret.fetch_by_uri, error format: no secret manager\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror format: no secret manager\n\n\n\n=== TEST 9: secret.fetch_by_uri, error format: no secret conf id\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://vault/\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror format: no secret conf id\n\n\n\n=== TEST 10: secret.fetch_by_uri, error format: no secret key id\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://vault/2/\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror format: no secret key id\n\n\n\n=== TEST 11: secret.fetch_by_uri, no config\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://vault/2/bar\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nno secret conf, secret_uri: $secret://vault/2/bar\n\n\n\n=== TEST 12: secret.fetch_by_uri, no sub key value\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"secrets\": [\n    {\n      \"id\": \"vault/1\",\n      \"prefix\": \"kv/apisix\",\n      \"token\": \"root\",\n      \"uri\": \"http://127.0.0.1:8200\"\n    }\n  ]\n}\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://vault/1/apisix-key/bar\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnil\n\n\n\n=== TEST 13: fetch_secrets env: no cache\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = {\n                key = \"jack\",\n                secret = \"$env://secret\"\n            }\n            local new_refs = secret.fetch_secrets(refs)\n            assert(new_refs ~= refs)\n            ngx.say(refs.secret)\n            ngx.say(new_refs.secret)\n            ngx.say(new_refs.key)\n        }\n    }\n--- request\nGET /t\n--- response_body\n$env://secret\napisix\njack\n--- error_log_like\nqr/retrieve secrets refs/\n\n\n\n=== TEST 14: fetch_secrets env: cache (fetch data should be only called once and next call return from cache)\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = {\n                key = \"jack\",\n                secret = \"$env://secret\"\n            }\n            local refs_1 = secret.fetch_secrets(refs, true)\n            local refs_2 = secret.fetch_secrets(refs, true)\n            ngx.say(refs_1.secret)\n            ngx.say(refs_2.secret)\n        }\n    }\n--- request\nGET /t\n--- response_body\napisix\napisix\n--- grep_error_log eval\nqr/fetching data from env uri/\n--- grep_error_log_out\nfetching data from env uri\n\n\n\n=== TEST 15: fetch_secrets env: table nesting\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = {\n                key = \"jack\",\n                user = {\n                    username = \"apisix\",\n                    passsword = \"$env://secret\"\n                }\n            }\n            local new_refs = secret.fetch_secrets(refs)\n            ngx.say(new_refs.user.passsword)\n        }\n    }\n--- request\nGET /t\n--- response_body\napisix\n\n\n\n=== TEST 16: fetch_secrets: wrong refs type\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = \"wrong\"\n            local new_refs = secret.fetch_secrets(refs)\n            ngx.say(new_refs)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnil\n"
  },
  {
    "path": "t/config-center-json/ssl.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('debug');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    if ($block->sslhandshake) {\n        my $sslhandshake = $block->sslhandshake;\n\n        $block->set_value(\"config\", <<_EOC_)\nlisten unix:\\$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:\\$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            $sslhandshake\n            local req = \"GET /hello HTTP/1.0\\\\r\\\\nHost: test.com\\\\r\\\\nConnection: close\\\\r\\\\n\\\\r\\\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            local line, err = sock:receive()\n            if not line then\n                ngx.say(\"failed to receive: \", err)\n                return\n            end\n\n            ngx.say(\"received: \", line)\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n_EOC_\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"ssls\": [\n    {\n      \"cert\": \"-----BEGIN CERTIFICATE-----\\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\\ns9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\\ntdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\\nD44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\\nNFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\\nquDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\\nbnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\\nMB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\\nDQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\\nqvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\\nrAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\\nHCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\\ngeAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\\n2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\\n-----END CERTIFICATE-----\",\n      \"key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\\n5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\\nBctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\\n0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\\nSXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\\ngf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\\nSF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\\nyLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\\n2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\\ng0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\\nQS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\\nL/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\\nLR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\\n7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\\nt1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\\nbe7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\\nV3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\\nzAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\\nIeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\\nr8yiEiskqRmy7P7MY9hDmEbG\\n-----END PRIVATE KEY-----\",\n      \"snis\": [\n        \"t.com\",\n        \"test.com\"\n      ]\n    }\n  ]\n}\n--- sslhandshake\nlocal sess, err = sock:sslhandshake(nil, \"test.com\", false)\nif not sess then\n    ngx.say(\"failed to do SSL handshake: \", err)\n    return\nend\n--- response_body\nreceived: HTTP/1.1 200 OK\nclose: 1 nil\n--- error_log\nserver name: \"test.com\"\n\n\n\n=== TEST 2: single sni\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1980\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ],\n  \"ssls\": [\n    {\n      \"cert\": \"-----BEGIN CERTIFICATE-----\\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\\ns9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\\ntdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\\nD44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\\nNFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\\nquDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\\nbnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\\nMB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\\nDQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\\nqvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\\nrAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\\nHCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\\ngeAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\\n2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\\n-----END CERTIFICATE-----\",\n      \"key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\\n5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\\nBctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\\n0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\\nSXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\\ngf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\\nSF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\\nyLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\\n2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\\ng0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\\nQS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\\nL/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\\nLR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\\n7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\\nt1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\\nbe7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\\nV3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\\nzAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\\nIeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\\nr8yiEiskqRmy7P7MY9hDmEbG\\n-----END PRIVATE KEY-----\",\n      \"sni\": \"test.com\"\n    }\n  ]\n}\n--- sslhandshake\nlocal sess, err = sock:sslhandshake(nil, \"test.com\", false)\nif not sess then\n    ngx.say(\"failed to do SSL handshake: \", err)\n    return\nend\n--- response_body\nreceived: HTTP/1.1 200 OK\nclose: 1 nil\n--- error_log\nserver name: \"test.com\"\n\n\n\n=== TEST 3: bad cert\n--- apisix_json\n{\n  \"routes\": [\n    {\n      \"uri\": \"/hello\",\n      \"upstream\": {\n        \"type\": \"roundrobin\",\n        \"nodes\": {\n          \"httpbin.org:80\": 1\n        }\n      },\n      \"ssl_enable\": true\n    }\n  ],\n  \"ssls\": [\n    {\n      \"cert\": \"-----BEGIN CERTIFICATE-----\\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\\ns9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\\ntdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\\nD44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\\nquDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\\nbnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\\nMB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\\nDQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\\nqvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\\nrAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\\nHCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\\ngeAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\\n2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\\n-----END CERTIFICATE-----\",\n      \"key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\\n5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\\nBctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\\n0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\\nSXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\\ngf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\\nSF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\\nyLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\\n2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\\ng0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\\nQS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\\nL/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\\nLR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\\n7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\\nt1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\\nbe7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\\nV3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\\nzAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\\nIeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\\nr8yiEiskqRmy7P7MY9hDmEbG\\n-----END PRIVATE KEY-----\",\n      \"snis\": [\n        \"t.com\",\n        \"test.com\"\n      ]\n    }\n  ]\n}\n\n--- error_log\nfailed to parse cert\n--- error_code: 404\n"
  },
  {
    "path": "t/config-center-json/stream-route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_enable\", 1);\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"mmm\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_json\n{\n  \"stream_routes\": [\n    {\n      \"server_addr\": \"127.0.0.1\",\n      \"server_port\": 1985,\n      \"id\": 1,\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- stream_response\nhello world\n\n\n\n=== TEST 2: rule with bad plugin\n--- apisix_json\n{\n  \"stream_routes\": [\n    {\n      \"server_addr\": \"127.0.0.1\",\n      \"server_port\": 1985,\n      \"id\": 1,\n      \"plugins\": {\n        \"mqtt-proxy\": {\n          \"uri\": 1\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- error_log eval\nqr/property \"\\w+\" is required/\n\n\n\n=== TEST 3: ignore unknown plugin\n--- apisix_json\n{\n  \"stream_routes\": [\n    {\n      \"server_addr\": \"127.0.0.1\",\n      \"server_port\": 1985,\n      \"id\": 1,\n      \"plugins\": {\n        \"x-rewrite\": {\n          \"uri\": 1\n        }\n      },\n      \"upstream\": {\n        \"nodes\": {\n          \"127.0.0.1:1995\": 1\n        },\n        \"type\": \"roundrobin\"\n      }\n    }\n  ]\n}\n--- stream_response\nhello world\n\n\n\n=== TEST 4: sanity with plugin\n--- apisix_json\n{\n  \"stream_routes\": [\n    {\n      \"server_addr\": \"127.0.0.1\",\n      \"server_port\": 1985,\n      \"id\": 1,\n      \"upstream_id\": 1,\n      \"plugins\": {\n        \"mqtt-proxy\": {\n          \"protocol_name\": \"MQTT\",\n          \"protocol_level\": 4\n        }\n      }\n    }\n  ],\n  \"upstreams\": [\n    {\n      \"nodes\": {\n        \"127.0.0.1:1995\": 1\n      },\n      \"type\": \"roundrobin\",\n      \"id\": 1\n    }\n  ]\n}\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n"
  },
  {
    "path": "t/config-center-yaml/consumer-group.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    my $routes = <<_EOC_;\nroutes:\n  -\n    uri: /hello\n    plugins:\n        key-auth:\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $routes);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello?apikey=one\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nconsumer_groups:\n    - id: foobar\n      plugins:\n          response-rewrite:\n              body: \"hello\\n\"\nconsumers:\n    - username: one\n      group_id: foobar\n      plugins:\n          key-auth:\n              key: one\n#END\n--- response_body\nhello\n\n\n\n=== TEST 2: consumer group not found\n--- apisix_yaml\nconsumers:\n   - username: one\n     group_id: invalid_group\n     plugins:\n       key-auth:\n         key: one\n#END\n--- error_code: 503\n--- error_log\nfailed to fetch consumer group config by id: invalid_group\n\n\n\n=== TEST 3: plugin priority\n--- apisix_yaml\nconsumer_groups:\n    - id: foobar\n      plugins:\n        response-rewrite:\n          body: \"hello\\n\"\nconsumers:\n  - username: one\n    group_id: foobar\n    plugins:\n      key-auth:\n        key: one\n      response-rewrite:\n        body: \"world\\n\"\n#END\n--- response_body\nworld\n\n\n\n=== TEST 4: invalid plugin\n--- apisix_yaml\nconsumer_groups:\n    - id: foobar\n      plugins:\n        example-plugin:\n          skey: \"s\"\n        response-rewrite:\n          body: \"hello\\n\"\nconsumers:\n  - username: one\n    group_id: foobar\n    plugins:\n      key-auth:\n        key: one\n#END\n--- error_code: 503\n--- error_log\nfailed to check the configuration of plugin example-plugin\nfailed to fetch consumer group config by id: foobar\n"
  },
  {
    "path": "t/config-center-yaml/consumer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: validate consumer\n--- apisix_yaml\nconsumers:\n  - username: jwt&auth\nroutes:\n  - uri: /hello\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nproperty \"username\" validation failed\n\n\n\n=== TEST 2: consumer restriction\n--- apisix_yaml\nconsumers:\n  - username: jack\n    plugins:\n        key-auth:\n            key: user-key\nroutes:\n    - id: 1\n      methods:\n          - POST\n      uri: \"/hello\"\n      plugins:\n          key-auth:\n          consumer-restriction:\n              whitelist:\n                  - jack\n      upstream:\n          type: roundrobin\n          nodes:\n              \"127.0.0.1:1980\": 1\n#END\n--- more_headers\napikey: user-key\n--- request\nPOST /hello\n"
  },
  {
    "path": "t/config-center-yaml/global-rule.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\nglobal_rules:\n    -\n        id: 1\n        plugins:\n            response-rewrite:\n                body: \"hello\\n\"\n#END\n--- response_body\nhello\n\n\n\n=== TEST 2: global rule with bad plugin\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\nglobal_rules:\n    -\n        id: 1\n        plugins:\n            response-rewrite:\n                body: 4\n#END\n--- response_body\nhello world\n--- error_log\nproperty \"body\" validation failed\n\n\n\n=== TEST 3: fix global rule with default value\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n\nglobal_rules:\n    -\n        id: 1\n        plugins:\n            uri-blocker:\n                block_rules:\n                    - /h*\n#END\n--- error_code: 403\n\n\n\n=== TEST 4: common phase without matched route\n--- apisix_yaml\nroutes:\n    -\n        uri: /apisix/prometheus/metrics\n        plugins:\n            public-api: {}\nglobal_rules:\n    -\n        id: 1\n        plugins:\n            cors:\n                allow_origins: \"http://a.com,http://b.com\"\n#END\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n"
  },
  {
    "path": "t/config-center-yaml/plugin-configs.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nplugin_configs:\n    -\n        id: 1\n        plugins:\n            response-rewrite:\n                body: \"hello\\n\"\nroutes:\n    - id: 1\n      uri: /hello\n      plugin_config_id: 1\n      upstream:\n        nodes:\n          \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- response_body\nhello\n\n\n\n=== TEST 2: plugin_config not found\n--- apisix_yaml\nroutes:\n    - id: 1\n      uri: /hello\n      plugin_config_id: 1\n      upstream:\n        nodes:\n          \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- error_code: 503\n--- error_log\nfailed to fetch plugin config by id: 1\n\n\n\n=== TEST 3: mix plugins & plugin_config_id\n--- apisix_yaml\nplugin_configs:\n    -\n        id: 1\n        plugins:\n            example-plugin:\n                i: 1\n            response-rewrite:\n                body: \"hello\\n\"\nroutes:\n    - id: 1\n      uri: /echo\n      plugin_config_id: 1\n      plugins:\n        proxy-rewrite:\n          headers:\n            \"in\": \"out\"\n        response-rewrite:\n          body: \"world\\n\"\n      upstream:\n        nodes:\n          \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /echo\n--- response_body\nworld\n--- response_headers\nin: out\n--- error_log eval\nqr/conf_version: \\d+#\\d+,/\n\n\n\n=== TEST 4: invalid plugin\n--- apisix_yaml\nplugin_configs:\n    -\n        id: 1\n        plugins:\n            example-plugin:\n                skey: \"s\"\n            response-rewrite:\n                body: \"hello\\n\"\nroutes:\n    - id: 1\n      uri: /hello\n      plugin_config_id: 1\n      upstream:\n        nodes:\n          \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- error_code: 503\n--- error_log\nfailed to check the configuration of plugin example-plugin\nfailed to fetch plugin config by id: 1\n"
  },
  {
    "path": "t/config-center-yaml/plugin-metadata.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n      \"127.0.0.1:1980\": 1\n    type: roundrobin\nroutes:\n  -\n    uri: /hello\n    upstream_id: 1\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\nplugin_metadata:\n  - id: http-logger\n    log_format:\n        host: \"$host\"\n        remote_addr: \"$remote_addr\"\n#END\n--- request\nGET /hello\n--- error_log\n\"remote_addr\":\"127.0.0.1\"\n\n\n\n=== TEST 2: sanity\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n      \"127.0.0.1:1980\": 1\n    type: roundrobin\nroutes:\n  -\n    uri: /hello\n    upstream_id: 1\nplugin_metadata:\n  - id: authz-casbin\n    model: 123\n#END\n--- request\nGET /hello\n--- error_log\nfailed to check item data of [plugin_metadata]\n"
  },
  {
    "path": "t/config-center-yaml/plugin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->apisix_yaml) {\n        my $routes = <<_EOC_;\nroutes:\n  - uri: /hello\n    plugins:\n      ip-restriction:\n        whitelist:\n          - \"127.0.0.1\"\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->extra_apisix_yaml . $routes);\n    }\n});\n\nour $debug_config = t::APISIX::read_file(\"conf/debug.yaml\");\n$debug_config =~ s/basic:\\n  enable: false/basic:\\n  enable: true/;\n\nrun_tests();\n\n## TODO: extra_apisix_yaml is specific to this document and is not standard behavior for\n##       the APISIX testing framework, so it should be standardized or replaced later.\n\n__DATA__\n\n=== TEST 1: sanity\n--- extra_apisix_yaml\nplugins:\n  - name: ip-restriction\n  - name: jwt-auth\n  - name: mqtt-proxy\n    stream: true\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n--- error_log\nuse config_provider: yaml\nload(): loaded plugin and sort by priority: 3000 name: ip-restriction\nload(): loaded plugin and sort by priority: 2510 name: jwt-auth\nload_stream(): loaded stream plugin and sort by priority: 1000 name: mqtt-proxy\n--- grep_error_log eval\nqr/load\\(\\): new plugins/\n--- grep_error_log_out\nload(): new plugins\nload(): new plugins\nload(): new plugins\nload(): new plugins\n\n\n\n=== TEST 2: plugins not changed, but still need to reload\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\nplugins:\n    - ip-restriction\n    - jwt-auth\nstream_plugins:\n    - mqtt-proxy\n--- extra_apisix_yaml\nplugins:\n  - name: ip-restriction\n  - name: jwt-auth\n  - name: mqtt-proxy\n    stream: true\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n--- grep_error_log eval\nqr/loaded plugin and sort by priority: \\d+ name: [^,]+/\n--- grep_error_log_out eval\nqr/(loaded plugin and sort by priority: (3000 name: ip-restriction|2510 name: jwt-auth)\n){4}/\n\n\n\n=== TEST 3: disable plugin and its router\n--- extra_apisix_yaml\nplugins:\n  - name: jwt-auth\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 404\n--- error_log\nerr:unknown plugin [ip-restriction]\n\n\n\n=== TEST 4: enable plugin and its router\n--- apisix_yaml\nroutes:\n  - uri: /apisix/prometheus/metrics\n    plugins:\n        public-api: {}\nplugins:\n  - name: public-api\n  - name: prometheus\n#END\n--- request\nGET /apisix/prometheus/metrics\n\n\n\n=== TEST 5: invalid plugin config\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\nplugins:\n    - ip-restriction\n    - jwt-auth\nstream_plugins:\n    - mqtt-proxy\n--- extra_apisix_yaml\nplugins:\n  - name: xxx\n    stream: ip-restriction\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nproperty \"stream\" validation failed: wrong type: expected boolean, got string\n--- no_error_log\nload(): plugins not changed\n\n\n\n=== TEST 6: empty plugin list\n--- extra_apisix_yaml\nplugins:\nstream_plugins:\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- error_log\nuse config_provider: yaml\nload(): new plugins: {}\nload_stream(): new plugins: {}\n\n\n\n=== TEST 7: route with plugin not in plugins list\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\nplugins:\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- error_log\nfailed to check item data of [routes] err:unknown plugin [ip-restriction]\n"
  },
  {
    "path": "t/config-center-yaml/route-service.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: hit route\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        uri: /hello\n        service_id: 1\n        id: 1\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 2: not found service\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        uri: /hello\n        id: 1\n        service_id: 1111\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to fetch service configuration by id: 1111\n\n\n\n=== TEST 3: service upstream priority\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        service_id: 1\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1977\": 1\n            type: roundrobin\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: route service upstream priority\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        service_id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1977\": 1\n            type: roundrobin\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1977\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: route service upstream by upstream_id priority\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        service_id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1977\": 1\n            type: roundrobin\n        upstream_id: 1\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1977\": 1\n            type: roundrobin\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: route service upstream priority\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        service_id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1977\": 1\n            type: roundrobin\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1977\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 7: two routes with the same service\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    - uris:\n        - /hello\n      service_id: 1\n      id: 1\n      plugins:\n        response-rewrite:\n            body: \"hello\\n\"\n    - uris:\n        - /world\n      service_id: 1\n      id: 2\n      plugins:\n        response-rewrite:\n            body: \"world\\n\"\nservices:\n    -\n        id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello\n\n\n\n=== TEST 8: service with bad plugin\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        service_id: 1\nservices:\n    -\n        id: 1\n        plugins:\n            proxy-rewrite:\n                uri: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 9: fix service with default value\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        service_id: 1\nservices:\n    -\n        id: 1\n        plugins:\n            uri-blocker:\n                block_rules:\n                    - /h*\n        upstream:\n            nodes:\n                \"127.0.0.1:1980\": 1\n            type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 403\n"
  },
  {
    "path": "t/config-center-yaml/route-upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: hit route\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 2: not found upstream\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1111\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code_like: ^(?:50\\d)$\n--- error_log\nfailed to find upstream by id: 1111\n\n\n\n=== TEST 3: upstream_id priority upstream\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1\n        upstream:\n            nodes:\n                \"127.0.0.1:1977\": 1\n            type: roundrobin\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: enable healthcheck\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n        retries: 2\n        checks:\n            active:\n                http_path: \"/status\"\n                healthy:\n                    interval: 2\n                    successes: 1\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: upstream domain\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"test.com:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 6: upstream hash_on (bad)\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"test.com:1980\": 1\n        type: chash\n        hash_on: header\n        key: \"$aaa\"\n#END\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\ninvalid configuration: failed to match pattern\n\n\n\n=== TEST 7: upstream hash_on (good)\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n    -\n        id: 1\n        uri: /hello\n        upstream_id: 1\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:1980\": 1\n            \"127.0.0.2:1980\": 1\n        type: chash\n        hash_on: header\n        key: \"test\"\n#END\n--- request\nGET /hello\n--- more_headers\ntest: one\n--- error_log\nproxy request to 127.0.0.1:1980\n"
  },
  {
    "path": "t/config-center-yaml/route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 2: route:uri + host (missing host, not hit)\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    host: foo.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 3: route:uri + host\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    host: foo.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- more_headers\nhost: foo.com\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: route with bad plugin\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    plugins:\n        proxy-rewrite:\n            uri: 1\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 5: ignore unknown plugin\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    plugins:\n        x-rewrite:\n            uri: 1\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- error_log\nfailed to check item data of [routes] err:unknown plugin\n\n\n\n=== TEST 6: route with bad plugin, radixtree_host_uri\n--- yaml_config\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    plugins:\n        proxy-rewrite:\n            uri: 1\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 7: fix route with default value\n--- yaml_config\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    plugins:\n        uri-blocker:\n            block_rules:\n                - /h*\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 8: invalid route, bad vars operator\n--- yaml_config\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    vars:\n        - remote_addr\n        - =\n        - 1\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to validate the 'vars' expression\n\n\n\n=== TEST 9: script with id\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    script: \"local ngx = ngx\"\n    script_id: \"1\"\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 200\n--- error_log\nmissing loaded script object\n\n\n\n=== TEST 10: hosts with '_' is valid\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    hosts:\n        - foo.com\n        - v1_test-api.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- more_headers\nhost: v1_test-api.com\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 11: script with plugin_config_id\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    script: \"local ngx = ngx\"\n    plugin_config_id: \"1\"\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to check item data of [routes]\n"
  },
  {
    "path": "t/config-center-yaml/secret.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->apisix_yaml) {\n        my $routes = <<_EOC_;\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $routes);\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: validate secret/vault: wrong schema\n--- apisix_yaml\nsecrets:\n  - id: vault/1\n    prefix: kv/apisix\n    token: root\n    uri: 127.0.0.1:8200\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local values = secret.secrets()\n            ngx.say(#values)\n        }\n    }\n--- request\nGET /t\n--- response_body\n0\n--- error_log\nproperty \"uri\" validation failed: failed to match pattern \"^[^\\\\/]+:\\\\/\\\\/([\\\\da-zA-Z.-]+|\\\\[[\\\\da-fA-F:]+\\\\])(:\\\\d+)?\"\n\n\n\n=== TEST 2: validate secrets: manager not exits\n--- apisix_yaml\nsecrets:\n  - id: hhh/1\n    prefix: kv/apisix\n    token: root\n    uri: 127.0.0.1:8200\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local values = secret.secrets()\n            ngx.say(#values)\n        }\n    }\n--- request\nGET /t\n--- response_body\n0\n--- error_log\nsecret manager not exits\n\n\n\n=== TEST 3: load config normal\n--- apisix_yaml\nsecrets:\n  - id: vault/1\n    prefix: kv/apisix\n    token: root\n    uri: http://127.0.0.1:8200\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local values = secret.secrets()\n            ngx.say(\"len: \", #values)\n\n            ngx.say(\"id: \", values[1].value.id)\n            ngx.say(\"prefix: \", values[1].value.prefix)\n            ngx.say(\"token: \", values[1].value.token)\n            ngx.say(\"uri: \", values[1].value.uri)\n        }\n    }\n--- request\nGET /t\n--- response_body\nlen: 1\nid: vault/1\nprefix: kv/apisix\ntoken: root\nuri: http://127.0.0.1:8200\n\n\n\n=== TEST 4: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key key=value\n--- response_body\nSuccess! Data written to: kv/apisix/apisix-key\n\n\n\n=== TEST 5: secret.fetch_by_uri: start with $secret://\n--- apisix_yaml\nsecrets:\n  - id: vault/1\n    prefix: kv/apisix\n    token: root\n    uri: http://127.0.0.1:8200\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://vault/1/apisix-key/key\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 6: secret.fetch_by_uri, wrong ref format: wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(1)\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror secret_uri type: number\n\n\n\n=== TEST 7: secret.fetch_by_uri, wrong ref format: wrong prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"secret://\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror secret_uri prefix: secret://\n\n\n\n=== TEST 8: secret.fetch_by_uri, error format: no secret manager\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror format: no secret manager\n\n\n\n=== TEST 9: secret.fetch_by_uri, error format: no secret conf id\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://vault/\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror format: no secret conf id\n\n\n\n=== TEST 10: secret.fetch_by_uri, error format: no secret key id\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://vault/2/\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror format: no secret key id\n\n\n\n=== TEST 11: secret.fetch_by_uri, no config\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local _, err = secret.fetch_by_uri(\"$secret://vault/2/bar\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nno secret conf, secret_uri: $secret://vault/2/bar\n\n\n\n=== TEST 12: secret.fetch_by_uri, no sub key value\n--- apisix_yaml\nsecrets:\n  - id: vault/1\n    prefix: kv/apisix\n    token: root\n    uri: http://127.0.0.1:8200\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://vault/1/apisix-key/bar\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnil\n\n\n\n=== TEST 13: fetch_secrets env: no cache\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = {\n                key = \"jack\",\n                secret = \"$env://secret\"\n            }\n            local new_refs = secret.fetch_secrets(refs)\n            assert(new_refs ~= refs)\n            ngx.say(refs.secret)\n            ngx.say(new_refs.secret)\n            ngx.say(new_refs.key)\n        }\n    }\n--- request\nGET /t\n--- response_body\n$env://secret\napisix\njack\n--- error_log_like\nqr/retrieve secrets refs/\n\n\n\n=== TEST 14: fetch_secrets env: cache\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = {\n                key = \"jack\",\n                secret = \"$env://secret\"\n            }\n            local refs_1 = secret.fetch_secrets(refs, true)\n            local refs_2 = secret.fetch_secrets(refs, true)\n            ngx.say(refs_1.secret)\n            ngx.say(refs_2.secret)\n        }\n    }\n--- request\nGET /t\n--- response_body\napisix\napisix\n--- grep_error_log eval\nqr/fetching data from env uri/\n--- grep_error_log_out\nfetching data from env uri\n\n\n\n=== TEST 15: fetch_secrets env: table nesting\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = {\n                key = \"jack\",\n                user = {\n                    username = \"apisix\",\n                    passsword = \"$env://secret\"\n                }\n            }\n            local new_refs = secret.fetch_secrets(refs)\n            ngx.say(new_refs.user.passsword)\n        }\n    }\n--- request\nGET /t\n--- response_body\napisix\n\n\n\n=== TEST 16: fetch_secrets: wrong refs type\n--- main_config\nenv secret=apisix;\n--- config\n    location /t {\n        content_by_lua_block {\n            local secret = require(\"apisix.secret\")\n            local refs = \"wrong\"\n            local new_refs = secret.fetch_secrets(refs)\n            ngx.say(new_refs)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnil\n"
  },
  {
    "path": "t/config-center-yaml/ssl.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('debug');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    my $routes = <<_EOC_;\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $routes);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    if ($block->sslhandshake) {\n        my $sslhandshake = $block->sslhandshake;\n\n        $block->set_value(\"config\", <<_EOC_)\nlisten unix:\\$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:\\$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            $sslhandshake\n            local req = \"GET /hello HTTP/1.0\\\\r\\\\nHost: test.com\\\\r\\\\nConnection: close\\\\r\\\\n\\\\r\\\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            local line, err = sock:receive()\n            if not line then\n                ngx.say(\"failed to receive: \", err)\n                return\n            end\n\n            ngx.say(\"received: \", line)\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n_EOC_\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nssls:\n    -\n        cert: |\n            -----BEGIN CERTIFICATE-----\n            MIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\n            s9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\n            tdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\n            D44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\n            NFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\n            quDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\n            bnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\n            MB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\n            DQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\n            qvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\n            rAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\n            HCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\n            geAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\n            2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\n            -----END CERTIFICATE-----\n        key: |\n            -----BEGIN PRIVATE KEY-----\n            MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\n            lZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\n            FF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\n            Exnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\n            uhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n            5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\n            cyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\n            5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\n            BctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\n            0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\n            SXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\n            gf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\n            SF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\n            yLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\n            2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\n            g0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\n            QS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\n            L/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\n            LR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\n            7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\n            t1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\n            be7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\n            V3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\n            zAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\n            IeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\n            r8yiEiskqRmy7P7MY9hDmEbG\n            -----END PRIVATE KEY-----\n        snis:\n            - \"t.com\"\n            - \"test.com\"\n--- sslhandshake\nlocal sess, err = sock:sslhandshake(nil, \"test.com\", false)\nif not sess then\n    ngx.say(\"failed to do SSL handshake: \", err)\n    return\nend\n--- response_body\nreceived: HTTP/1.1 200 OK\nclose: 1 nil\n--- error_log\nserver name: \"test.com\"\n\n\n\n=== TEST 2: single sni\n--- apisix_yaml\nssls:\n    -\n        cert: |\n            -----BEGIN CERTIFICATE-----\n            MIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\n            s9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\n            tdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\n            D44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\n            NFy6EdgG9fkwcIalutjrUnGl9moGjwKYu4eXW2Zt5el0d1AHXUsqK4voe0p+U2Nz\n            quDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\n            bnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\n            MB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\n            DQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\n            qvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\n            rAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\n            HCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\n            geAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\n            2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\n            -----END CERTIFICATE-----\n        key: |\n            -----BEGIN PRIVATE KEY-----\n            MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\n            lZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\n            FF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\n            Exnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\n            uhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n            5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\n            cyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\n            5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\n            BctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\n            0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\n            SXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\n            gf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\n            SF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\n            yLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\n            2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\n            g0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\n            QS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\n            L/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\n            LR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\n            7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\n            t1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\n            be7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\n            V3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\n            zAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\n            IeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\n            r8yiEiskqRmy7P7MY9hDmEbG\n            -----END PRIVATE KEY-----\n        sni: \"test.com\"\n--- sslhandshake\nlocal sess, err = sock:sslhandshake(nil, \"test.com\", false)\nif not sess then\n    ngx.say(\"failed to do SSL handshake: \", err)\n    return\nend\n--- response_body\nreceived: HTTP/1.1 200 OK\nclose: 1 nil\n--- error_log\nserver name: \"test.com\"\n\n\n\n=== TEST 3: bad cert\n--- apisix_yaml\nssls:\n    -\n        cert: |\n            -----BEGIN CERTIFICATE-----\n            MIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\n            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\n            BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\n            ci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S\n            s9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt\n            tdUUXh5Wvgi/2OA8HBBzaQFQL1Av9qWwyES5cx6p0ZBwIrcXQIsl1XfNSUpQNTSS\n            D44TGduXUIdeshukPvMvLWLezynf2/WlgVh/haWtDG99r/Gj3uBdjl0m/xGvKvIv\n            quDmvxteXWdlsz8o5kQT6a4DUtWhpPIfNj9oZfPRs3LhBFQ74N70kVxMOCdec1lU\n            bnFzLIMGlz0CAwEAAaNQME4wHQYDVR0OBBYEFFHeljijrr+SPxlH5fjHRPcC7bv2\n            MB8GA1UdIwQYMBaAFFHeljijrr+SPxlH5fjHRPcC7bv2MAwGA1UdEwQFMAMBAf8w\n            DQYJKoZIhvcNAQELBQADggEBAG6NNTK7sl9nJxeewVuogCdMtkcdnx9onGtCOeiQ\n            qvh5Xwn9akZtoLMVEdceU0ihO4wILlcom3OqHs9WOd6VbgW5a19Thh2toxKidHz5\n            rAaBMyZsQbFb6+vFshZwoCtOLZI/eIZfUUMFqMXlEPrKru1nSddNdai2+zi5rEnM\n            HCot43+3XYuqkvWlOjoi9cP+C4epFYrxpykVbcrtbd7TK+wZNiK3xtDPnVzjdNWL\n            geAEl9xrrk0ss4nO/EreTQgS46gVU+tLC+b23m2dU7dcKZ7RDoiA9bdVc4a2IsaS\n            2MvLL4NZ2nUh8hAEHiLtGMAV3C6xNbEyM07hEpDW6vk6tqk=\n            -----END CERTIFICATE-----\n        key: |\n            -----BEGIN PRIVATE KEY-----\n            MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\n            lZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\n            FF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\n            Exnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\n            uhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n            5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\n            cyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk186lHI3z7K1\n            5nB7zt+bwVY0AUpagv3wvXoB5lrYVOsJpa9y5iAb3GqYMc/XDCKfD/KLea5hwfcn\n            BctEn0LjsPVKLDrLs2t2gBDWG2EU+udunwQh7XTdp2Nb6V3FdOGbGAg2LgrSwP1g\n            0r4z14F70oWGYyTQ5N8UGuyryVrzQH525OYl38Yt7R6zJ/44FVi/2TvdfHM5ss39\n            SXWi00Q30fzaBEf4AdHVwVCRKctwSbrIOyM53kiScFDmBGRblCWOxXbiFV+d3bjX\n            gf2zxs7QYZrFOzOO7kLtHGua4itEB02497v+1oKDwQKBgQDOBvCVGRe2WpItOLnj\n            SF8iz7Sm+jJGQz0D9FhWyGPvrN7IXGrsXavA1kKRz22dsU8xdKk0yciOB13Wb5y6\n            yLsr/fPBjAhPb4h543VHFjpAQcxpsH51DE0b2oYOWMmz+rXGB5Jy8EkP7Q4njIsc\n            2wLod1dps8OT8zFx1jX3Us6iUQKBgQDGtKkfsvWi3HkwjFTR+/Y0oMz7bSruE5Z8\n            g0VOHPkSr4XiYgLpQxjbNjq8fwsa/jTt1B57+By4xLpZYD0BTFuf5po+igSZhH8s\n            QS5XnUnbM7d6Xr/da7ZkhSmUbEaMeHONSIVpYNgtRo4bB9Mh0l1HWdoevw/w5Ryt\n            L/OQiPhfLQKBgQCh1iG1fPh7bbnVe/HI71iL58xoPbCwMLEFIjMiOFcINirqCG6V\n            LR91Ytj34JCihl1G4/TmWnsH1hGIGDRtJLCiZeHL70u32kzCMkI1jOhFAWqoutMa\n            7obDkmwraONIVW/kFp6bWtSJhhTQTD4adI9cPCKWDXdcCHSWj0Xk+U8HgQKBgBng\n            t1HYhaLzIZlP/U/nh3XtJyTrX7bnuCZ5FhKJNWrYjxAfgY+NXHRYCKg5x2F5j70V\n            be7pLhxmCnrPTMKZhik56AaTBOxVVBaYWoewhUjV4GRAaK5Wc8d9jB+3RizPFwVk\n            V3OU2DJ1SNZ+W2HBOsKrEfwFF/dgby6i2w6MuAP1AoGBAIxvxUygeT/6P0fHN22P\n            zAHFI4v2925wYdb7H//D8DIADyBwv18N6YH8uH7L+USZN7e4p2k8MGGyvTXeC6aX\n            IeVtU6fH57Ddn59VPbF20m8RCSkmBvSdcbyBmqlZSBE+fKwCliKl6u/GH0BNAWKz\n            r8yiEiskqRmy7P7MY9hDmEbG\n            -----END PRIVATE KEY-----\n        snis:\n            - \"t.com\"\n            - \"test.com\"\n--- error_log\nfailed to parse cert\n--- error_code: 404\n"
  },
  {
    "path": "t/config-center-yaml/stream-route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    $block->set_value(\"stream_enable\", 1);\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"mmm\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      nodes:\n        \"127.0.0.1:1995\": 1\n      type: roundrobin\n#END\n--- stream_response\nhello world\n\n\n\n=== TEST 2: rule with bad plugin\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    plugins:\n        mqtt-proxy:\n            uri: 1\n    upstream:\n      nodes:\n        \"127.0.0.1:1995\": 1\n      type: roundrobin\n#END\n--- error_log eval\nqr/property \"\\w+\" is required/\n\n\n\n=== TEST 3: ignore unknown plugin\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    plugins:\n        x-rewrite:\n            uri: 1\n    upstream:\n      nodes:\n        \"127.0.0.1:1995\": 1\n      type: roundrobin\n#END\n--- error_log\nerr:unknown plugin [x-rewrite]\n\n\n\n=== TEST 4: sanity with plugin\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream_id: 1\n    plugins:\n      mqtt-proxy:\n        protocol_name: \"MQTT\"\n        protocol_level: 4\nupstreams:\n  - nodes:\n      \"127.0.0.1:1995\": 1\n    type: roundrobin\n    id: 1\n#END\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n"
  },
  {
    "path": "t/control/control-healthcheck-bug-fix.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1,\n                            \"mockbin.org:80\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit the route\n--- request\nGET /status/403\n--- error_code: 403\n\n\n\n=== TEST 3: hit control api\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local passed = true\n\n            for i = 1, 40 do\n                local code, body, res = t.test('/v1/routes/1', ngx.HTTP_GET)\n                if code ~= ngx.HTTP_OK then\n                    passed = code\n                    break\n                end\n            end\n\n            if passed then\n                ngx.say(\"passed\")\n            else\n                ngx.say(\"failed. got status code: \", passed)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit the route again\n--- request\nGET /status/403\n--- error_code: 403\n\n\n\n=== TEST 5: hit control api\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local passed = true\n\n            for i = 1, 40 do\n                local code, body, res = t.test('/v1/routes/1', ngx.HTTP_GET)\n                if code ~= ngx.HTTP_OK then\n                    passed = code\n                    break\n                end\n            end\n\n            if passed then\n                ngx.say(\"passed\")\n            else\n                ngx.say(\"failed. got status code: \", passed)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/control/discovery.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\n\n# Because this whole test file is only used to verify the configuration set or not,\n# but the configuration content is invalid, which contains non-exist consul server address,\n# so we have to ignore consul connect errors in some test cases.\n\n\nour $yaml_config = <<_EOC_;\napisix:\n  enable_control: true\n  node_listen: 1984\ndiscovery:\n  eureka:\n    host:\n      - \"http://127.0.0.1:8761\"\n    prefix: \"/eureka/\"\n    fetch_interval: 10\n    weight: 80\n    timeout:\n      connect: 1500\n      send: 1500\n      read: 1500\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n  dns:\n    servers:\n      - \"127.0.0.1:1053\"\n_EOC_\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: test consul_kv dump_data api\n--- yaml_config eval: $::yaml_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, body, res = t.test('/v1/discovery/consul_kv/dump',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n            ngx.say(json.encode(entity.config))\n        }\n    }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\n{}\n{\"fetch_interval\":3,\"keepalive\":true,\"prefix\":\"upstreams\",\"servers\":[\"http://127.0.0.1:8500\",\"http://127.0.0.1:8600\"],\"timeout\":{\"connect\":2000,\"read\":2000,\"wait\":60},\"token\":\"\",\"weight\":1}\n--- error_log\nconnect consul\n\n\n\n=== TEST 2: test eureka dump_data api\n--- yaml_config eval: $::yaml_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, body, res = t.test('/v1/discovery/eureka/dump',\n                ngx.HTTP_GET, nil,\n                [[{\n                    \"config\": {\n                        \"fetch_interval\": 10,\n                        \"host\": [\n                            \"http://127.0.0.1:8761\"\n                        ],\n                        \"prefix\": \"/eureka/\",\n                        \"timeout\": {\n                            \"connect\": 1500,\n                            \"read\": 1500,\n                            \"send\": 1500\n                        },\n                        \"weight\": 80\n                    },\n                    \"services\": {}\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n--- error_log\nconnect consul\n\n\n\n=== TEST 3: test dns api\n--- yaml_config eval: $::yaml_config\n--- request\nGET /v1/discovery/dns/dump\n--- error_code: 404\n--- error_log\nconnect consul\n\n\n\n=== TEST 4: test unconfigured eureka dump_data api\n--- yaml_config\napisix:\n  enable_control: true\n  node_listen: 1984\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n#END\n--- request\nGET /v1/discovery/eureka/dump\n--- error_code: 404\n--- error_log\nconnect consul\n\n\n\n=== TEST 5: prepare consul kv register nodes\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"DELETE /consul1/upstreams/?recurse=true\",\n    \"DELETE /consul2/upstreams/?recurse=true\",\n    \"PUT /consul1/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul1/upstreams/webpages/127.0.0.1:30512\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul2/upstreams/webpages/127.0.0.1:30513\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul2/upstreams/webpages/127.0.0.1:30514\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n]\n--- response_body eval\n[\"true\", \"true\", \"true\", \"true\", \"true\", \"true\"]\n\n\n\n=== TEST 6: dump consul_kv services\n--- yaml_config eval: $::yaml_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul_kv/dump',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\n{\"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1},{\"host\":\"127.0.0.1\",\"port\":30512,\"weight\":1}],\"http://127.0.0.1:8600/v1/kv/upstreams/webpages/\":[{\"host\":\"127.0.0.1\",\"port\":30513,\"weight\":1},{\"host\":\"127.0.0.1\",\"port\":30514,\"weight\":1}]}\n\n\n\n=== TEST 7: clean consul kv register nodes\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"DELETE /consul1/upstreams/?recurse=true\",\n    \"DELETE /consul2/upstreams/?recurse=true\"\n]\n--- response_body eval\n[\"true\", \"true\"]\n"
  },
  {
    "path": "t/control/gc.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: trigger full gc\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local before = collectgarbage(\"count\")\n            do\n                local tab = {}\n                for i = 1, 10000 do\n                    tab[i] = {\"a\", 1}\n                end\n            end\n            local after_alloc = collectgarbage(\"count\")\n            local code = t.test('/v1/gc',\n                ngx.HTTP_POST\n            )\n            local after_gc = collectgarbage(\"count\")\n            if code == 200 then\n                if after_alloc - after_gc > 0.9 * (after_alloc - before) then\n                    ngx.say(\"ok\")\n                else\n                    ngx.say(before, \" \", after_alloc, \" \", after_gc)\n                end\n            end\n        }\n    }\n--- response_body\nok\n"
  },
  {
    "path": "t/control/healthcheck.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: upstreams\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\nupstreams:\n    - nodes:\n        \"127.0.0.1:1980\": 1\n        \"127.0.0.2:1988\": 0\n      type: roundrobin\n      id: 1\n      checks:\n        active:\n            http_path: \"/status\"\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n\n            ngx.sleep(4)\n\n            local _, _, res = t.test('/v1/healthcheck',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            assert(#res == 1, \"invalid number of results\")\n            table.sort(res[1].nodes, function(a, b)\n                return a.ip < b.ip\n            end)\n            ngx.log(ngx.WARN, core.json.stably_encode(res[1].nodes))\n            ngx.say(core.json.stably_encode(res[1].nodes))\n\n            local _, _, res = t.test('/v1/healthcheck/upstreams/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            table.sort(res.nodes, function(a, b)\n                return a.ip < b.ip\n            end)\n            ngx.say(core.json.stably_encode(res.nodes))\n\n            local _, _, res = t.test('/v1/healthcheck/upstreams/1',\n                ngx.HTTP_GET, nil, nil, {[\"Accept\"] = \"text/html\"})\n            ngx.sleep(4)\n            local xml2lua = require(\"xml2lua\")\n            local xmlhandler = require(\"xmlhandler.tree\")\n            local handler = xmlhandler:new()\n            local parser = xml2lua.parser(handler)\n            parser.parse(parser, res)\n            local matches = 0\n            for _, td in ipairs(handler.root.html.body.table.tr) do\n                if td.td then\n                    if td.td[4] == \"127.0.0.2:1988\" then\n                        assert(td.td[5] == \"unhealthy\", \"127.0.0.2:1988 is not unhealthy\")\n                        matches = matches + 1\n                    end\n                    if td.td[4] == \"127.0.0.1:1980\" then\n                        assert(td.td[5] == \"healthy\", \"127.0.0.1:1980 is not healthy\")\n                        matches = matches + 1\n                    end\n                end\n            end\n            ngx.sleep(4)\n            assert(matches == 2, \"unexpected html\")\n        }\n    }\n--- grep_error_log eval\nqr/unhealthy TCP increment \\(.+\\) for '[^']+'/\n--- grep_error_log_out\nunhealthy TCP increment (1/2) for '127.0.0.2(127.0.0.2:1988)'\nunhealthy TCP increment (2/2) for '127.0.0.2(127.0.0.2:1988)'\n--- response_body\n[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1980,\"status\":\"healthy\"},{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":2,\"timeout_failure\":0},\"hostname\":\"127.0.0.2\",\"ip\":\"127.0.0.2\",\"port\":1988,\"status\":\"unhealthy\"}]\n[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1980,\"status\":\"healthy\"},{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":2,\"timeout_failure\":0},\"hostname\":\"127.0.0.2\",\"ip\":\"127.0.0.2\",\"port\":1988,\"status\":\"unhealthy\"}]\n--- timeout: 14\n\n\n\n=== TEST 2: routes\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /hello\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n        \"127.0.0.1:1988\": 1\n      type: roundrobin\n      checks:\n        active:\n            http_path: \"/status\"\n            host: \"127.0.0.1\"\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n\n            ngx.sleep(4)\n\n            local code, body, res = t.test('/v1/healthcheck',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            table.sort(res[1].nodes, function(a, b)\n                return a.port < b.port\n            end)\n            ngx.say(json.encode(res))\n\n            local code, body, res = t.test('/v1/healthcheck/routes/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            table.sort(res.nodes, function(a, b)\n                return a.port < b.port\n            end)\n            ngx.say(json.encode(res))\n            ngx.sleep(4)\n        }\n    }\n--- grep_error_log eval\nqr/unhealthy TCP increment \\(.+\\) for '[^']+'/\n--- grep_error_log_out\nunhealthy TCP increment (1/2) for '127.0.0.1(127.0.0.1:1988)'\nunhealthy TCP increment (2/2) for '127.0.0.1(127.0.0.1:1988)'\n--- response_body\n[{\"name\":\"/routes/1\",\"nodes\":[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1980,\"status\":\"healthy\"},{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":2,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1988,\"status\":\"unhealthy\"}],\"type\":\"http\"}]\n{\"name\":\"/routes/1\",\"nodes\":[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1980,\"status\":\"healthy\"},{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":2,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1988,\"status\":\"unhealthy\"}],\"type\":\"http\"}\n--- timeout: 10\n\n\n\n=== TEST 3: services\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  - id: 1\n    service_id: 1\n    uris:\n        - /hello\n\nservices:\n  -\n    id: 1\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n        \"127.0.0.1:1988\": 1\n      type: roundrobin\n      checks:\n        active:\n            http_path: \"/status\"\n            host: \"127.0.0.1\"\n            port: 1988\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n\n            ngx.sleep(4)\n\n            local code, body, res = t.test('/v1/healthcheck',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            table.sort(res[1].nodes, function(a, b)\n                return a.port < b.port\n            end)\n            ngx.say(json.encode(res))\n\n            local code, body, res = t.test('/v1/healthcheck/services/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            table.sort(res.nodes, function(a, b)\n                return a.port < b.port\n            end)\n            ngx.say(json.encode(res))\n            ngx.sleep(4)\n        }\n    }\n--- grep_error_log eval\nqr/unhealthy TCP increment \\(.+\\) for '[^']+'/\n--- grep_error_log_out\nunhealthy TCP increment (1/2) for '127.0.0.1(127.0.0.1:1988)'\nunhealthy TCP increment (2/2) for '127.0.0.1(127.0.0.1:1988)'\n--- response_body\n[{\"name\":\"/services/1\",\"nodes\":[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":2,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1988,\"status\":\"unhealthy\"}],\"type\":\"http\"}]\n{\"name\":\"/services/1\",\"nodes\":[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":2,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1988,\"status\":\"unhealthy\"}],\"type\":\"http\"}\n--- timeout: 9\n\n\n\n=== TEST 4: no checkers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(4)\n            local code, body, res = t.test('/v1/healthcheck',\n                ngx.HTTP_GET)\n            ngx.print(res)\n        }\n    }\n--- response_body\n{}\n--- timeout: 5\n\n\n\n=== TEST 5: no checker\n--- request\nGET /v1/healthcheck/routes/1\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"routes[1] not found\"}\n\n\n\n=== TEST 6: invalid src type\n--- request\nGET /v1/healthcheck/route/1\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid src type route\"}\n\n\n\n=== TEST 7: passive health check status\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /specific_status\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n        \"127.0.0.2:1980\": 1\n      type: roundrobin\n      checks:\n        active:\n            healthy:\n                interval: 999 # large interval to avoid active check influence\n            unhealthy:\n                interval: 999\n        passive:\n          healthy:\n            http_statuses:\n              - 200\n            successes: 1\n          unhealthy:\n            http_statuses:\n              - 500\n            http_failures: 3\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local http = require \"resty.http\"\n\n            -- first request to trigger health checker manager startup\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/specific_status\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"x-test-upstream-status\"] = \"500\"\n                }\n            })\n            if not res then\n                ngx.say(\"failed to request: \", err)\n                return\n            end\n\n            ngx.sleep(1)\n\n            for i = 1, 6 do\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"x-test-upstream-status\"] = \"500\"\n                    }\n                })\n                if not res then\n                    ngx.say(\"failed to request: \", err)\n                    return\n                end\n            end\n\n            local code, body, res = t.test('/v1/healthcheck/routes/1',\n                ngx.HTTP_GET)\n            ngx.log(ngx.ERR, \"healthcheck response: \", res)\n            res = json.decode(res)\n            table.sort(res.nodes, function(a, b)\n                return a.ip < b.ip\n            end)\n            ngx.say(json.encode(res))\n        }\n    }\n--- grep_error_log eval\nqr/unhealthy HTTP increment \\(.+\\) for '127.0.0.1\\(127.0.0.1:1980\\)'/\n--- grep_error_log_out\nunhealthy HTTP increment (1/3) for '127.0.0.1(127.0.0.1:1980)'\nunhealthy HTTP increment (2/3) for '127.0.0.1(127.0.0.1:1980)'\nunhealthy HTTP increment (3/3) for '127.0.0.1(127.0.0.1:1980)'\n--- response_body\n{\"name\":\"/routes/1\",\"nodes\":[{\"counter\":{\"http_failure\":3,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1980,\"status\":\"unhealthy\"},{\"counter\":{\"http_failure\":3,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.2\",\"ip\":\"127.0.0.2\",\"port\":1980,\"status\":\"unhealthy\"}],\"type\":\"http\"}\n"
  },
  {
    "path": "t/control/plugin-api.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- request\nGET /v1/plugin/example-plugin/hello\n--- response_body\nworld\n\n\n\n=== TEST 2: set Content-Type for table response\n--- request\nGET /v1/plugin/example-plugin/hello?json\n--- response_body\n{\"msg\":\"world\"}\n--- response_headers\nContent-Type: application/json\n"
  },
  {
    "path": "t/control/plugin-metadata.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadatas\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/plugin_metadata/example-plugin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skey\": \"val\",\n                    \"ikey\": 1\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code = t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_PUT,\n                [[\n                {\"log_format\": {\"upstream_response_time\": \"$upstream_response_time\"}}\n                ]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 2: dump all plugin metadatas\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local _, _, res = t('/v1/plugin_metadatas', ngx.HTTP_GET)\n            local json = require(\"toolkit.json\")\n            res = json.decode(res)\n            for _, metadata in ipairs(res) do\n                if metadata.id == \"file-logger\" then\n                    ngx.say(\"check log_format: \", metadata.log_format.upstream_response_time == \"$upstream_response_time\")\n                elseif metadata.id == \"example-plugin\" then\n                    ngx.say(\"check skey: \", metadata.skey == \"val\")\n                    ngx.say(\"check ikey: \", metadata.ikey == 1)\n                end\n            end\n        }\n    }\n--- response_body\ncheck log_format: true\ncheck skey: true\ncheck ikey: true\n\n\n\n=== TEST 3: dump file-logger metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local _, _, res = t('/v1/plugin_metadata/file-logger', ngx.HTTP_GET)\n            local json = require(\"toolkit.json\")\n            metadata = json.decode(res)\n            if metadata.id == \"file-logger\" then\n                ngx.say(\"check log_format: \", metadata.log_format.upstream_response_time == \"$upstream_response_time\")\n            end\n        }\n    }\n--- response_body\ncheck log_format: true\n\n\n\n=== TEST 4: plugin without metadata\n--- request\nGET /v1/plugin_metadata/batch-requests\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"plugin metadata[batch-requests] not found\"}\n"
  },
  {
    "path": "t/control/plugins-reload.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\nworkers(2);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: reload plugins\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_control: true\n    control:\n      ip: \"127.0.0.1\"\n      port: 9090\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\")\n\n        local code, body, res = t.test('/v1/plugins/reload',\n            ngx.HTTP_PUT)\n        ngx.say(res)\n        ngx.sleep(1)\n    }\n}\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nload plugin times: 2\nload plugin times: 2\nstart to hot reload plugins\nstart to hot reload plugins\n\n\n\n=== TEST 2: reload plugins when attributes changed\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_admin: true\n  node_listen: 1984\nplugins:\n    - example-plugin\nplugin_attr:\n    example-plugin:\n        val: 0\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require \"apisix.core\"\n        ngx.sleep(0.1)\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\n  enable_control: true\n  control:\n    ip: \"127.0.0.1\"\n    port: 9090\nplugins:\n    - example-plugin\nplugin_attr:\n    example-plugin:\n        val: 1\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        local t = require(\"lib.test_admin\").test\n        local code, _, org_body = t('/v1/plugins/reload',\n                                    ngx.HTTP_PUT)\n\n        ngx.status = code\n        ngx.say(org_body)\n        ngx.sleep(0.1)\n\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n    - example-plugin\nplugin_attr:\n    example-plugin:\n        val: 1\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        local t = require(\"lib.test_admin\").test\n        local code, _, org_body = t('/v1/plugins/reload',\n                                    ngx.HTTP_PUT)\n        ngx.say(org_body)\n        ngx.sleep(0.1)\n    }\n}\n--- request\nGET /t\n--- response_body\ndone\ndone\n--- grep_error_log eval\nqr/example-plugin get plugin attr val: \\d+/\n--- grep_error_log_out\nexample-plugin get plugin attr val: 0\nexample-plugin get plugin attr val: 0\nexample-plugin get plugin attr val: 0\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\nexample-plugin get plugin attr val: 1\n\n\n\n=== TEST 3: reload plugins to change prometheus' export uri\n--- yaml_config\napisix:\n  node_listen: 1984\nplugins:\n  - public-api\n  - prometheus\nplugin_attr:\n  prometheus:\n    export_uri: /metrics\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require \"apisix.core\"\n        ngx.sleep(0.1)\n        local t = require(\"lib.test_admin\").test\n\n        -- setup public API route\n        local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/metrics\"\n                 }]]\n                )\n        ngx.say(code)\n\n        local code, _, org_body = t('/apisix/metrics',\n                                    ngx.HTTP_GET)\n        ngx.say(code)\n\n        local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\n  enable_control: true\n  control:\n    ip: \"127.0.0.1\"\n    port: 9090\nplugins:\n  - public-api\n  - prometheus\nplugin_attr:\n  prometheus:\n    export_uri: /apisix/metrics\n        ]]\n        require(\"lib.test_admin\").set_config_yaml(data)\n\n        local code, _, org_body = t('/v1/plugins/reload',\n                                    ngx.HTTP_PUT)\n\n        ngx.say(org_body)\n\n        ngx.sleep(0.1)\n        local code, _, org_body = t('/apisix/metrics',\n                                    ngx.HTTP_GET)\n        ngx.say(code)\n    }\n}\n--- request\nGET /t\n--- response_body\n201\n404\ndone\n200\n\n\n\n=== TEST 4: wrong method to reload plugins\n--- request\nGET /v1/plugins/reload\n--- error_code: 404\n\n\n\n=== TEST 5: wrong method to reload plugins\n--- request\nPOST /v1/plugins/reload\n--- error_code: 404\n\n\n\n=== TEST 6: reload plugin with data_plane deployment\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\")\n\n        local code, body, res = t.test('/v1/plugins/reload',\n            ngx.HTTP_PUT)\n        ngx.say(res)\n        ngx.sleep(1)\n    }\n}\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nload plugin times: 2\nload plugin times: 2\nstart to hot reload plugins\nstart to hot reload plugins\n"
  },
  {
    "path": "t/control/routes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: routes\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /hello\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/routes',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            if res[1] then\n                local data = {}\n                data.uris = res[1].value.uris\n                data.upstream = res[1].value.upstream\n                ngx.say(json.encode(data))\n            end\n        }\n    }\n--- response_body eval\nqr/\\{\"upstream\":\\{\"hash_on\":\"vars\",\"nodes\":\\[\\{\"host\":\"127.0.0.1\",\"port\":1980,\"weight\":1\\}\\],\"pass_host\":\"pass\",.*\"scheme\":\"http\",\"type\":\"roundrobin\"\\},\"uris\":\\[\"\\/hello\"\\]\\}/\n\n\n\n=== TEST 2: get route with id 1\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /hello\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/route/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            if res then\n                local data = {}\n                data.uris = res.value.uris\n                data.upstream = res.value.upstream\n                ngx.say(json.encode(data))\n            end\n        }\n    }\n--- response_body eval\nqr/\\{\"upstream\":\\{\"hash_on\":\"vars\",\"nodes\":\\[\\{\"host\":\"127.0.0.1\",\"port\":1980,\"weight\":1\\}\\],\"pass_host\":\"pass\",.*\"scheme\":\"http\",\"type\":\"roundrobin\"\\},\"uris\":\\[\"\\/hello\"\\]\\}/\n\n\n\n=== TEST 3: routes with invalid id\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /hello\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/route/2',\n                ngx.HTTP_GET)\n            local data = {}\n            data.status = code\n            ngx.say(json.encode(data))\n            return\n        }\n    }\n--- response_body\n{\"status\":404}\n"
  },
  {
    "path": "t/control/schema.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local code, body, res = t.test('/v1/schema',\n                ngx.HTTP_GET,\n                nil,\n                [[{\n                    \"main\": {\n                        \"consumer\": {\"type\":\"object\"},\n                        \"consumer_group\": {\"type\":\"object\"},\n                        \"global_rule\": {\"type\":\"object\"},\n                        \"plugin_config\": {\"type\":\"object\"},\n                        \"plugins\": {\"type\":\"array\"},\n                        \"proto\": {\"type\":\"object\"},\n                        \"route\": {\"type\":\"object\"},\n                        \"service\": {\"type\":\"object\"},\n                        \"ssl\": {\"type\":\"object\"},\n                        \"stream_route\": {\"type\":\"object\"},\n                        \"upstream\": {\"type\":\"object\"},\n                        \"upstream_hash_header_schema\": {\"type\":\"string\"},\n                        \"upstream_hash_vars_schema\": {\"type\":\"string\"},\n                    },]] .. [[\n                    \"plugins\": {\n                        \"example-plugin\": {\n                            \"version\": 0.1,\n                            \"priority\": 0,\n                            \"schema\": {\n                                \"type\":\"object\",\n                                \"properties\": {\n                                    \"_meta\": {\n                                         \"properties\": {\n                                             \"disable\": {\"type\": \"boolean\"}\n                                         }\n                                    }\n                                }\n                            },\n                            \"metadata_schema\": {\"type\":\"object\"}\n                        },\n                        \"basic-auth\": {\n                            \"type\": \"auth\",\n                            \"consumer_schema\": {\"type\":\"object\"}\n                        }\n                    },\n                    \"stream_plugins\": {\n                        \"mqtt-proxy\": {\n                            \"schema\": {\n                                \"type\":\"object\",\n                                \"properties\": {\n                                    \"_meta\": {\n                                         \"properties\": {\n                                             \"disable\": {\"type\": \"boolean\"}\n                                         }\n                                    }\n                                }\n                            },\n                            \"priority\": 1000\n                        }\n                    }\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: confirm the scope of plugin\n--- extra_yaml_config\nplugins:\n  - batch-requests\n  - error-log-logger\n  - server-info\n  - example-plugin\n  - node-status\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message, res = t('/v1/schema',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            res = json.decode(res)\n            local global_plugins = {}\n            local plugins = res[\"plugins\"]\n            for k, v in pairs(plugins) do\n                if v.scope == \"global\" then\n                    global_plugins[k] = v.scope\n                end\n            end\n            ngx.say(json.encode(global_plugins))\n        }\n    }\n--- response_body\n{\"batch-requests\":\"global\",\"error-log-logger\":\"global\",\"node-status\":\"global\",\"server-info\":\"global\"}\n"
  },
  {
    "path": "t/control/services.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: services\n--- apisix_yaml\nservices:\n  -\n    id: 200\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/services',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            if res[1] then\n                local data = {}\n                data.id = res[1].value.id\n                data.plugins = res[1].value.plugins\n                data.upstream = res[1].value.upstream\n                ngx.say(json.encode(data))\n            end\n            return\n        }\n    }\n--- response_body eval\nqr/\\{\"id\":\"200\",\"upstream\":\\{\"hash_on\":\"vars\",\"nodes\":\\[\\{\"host\":\"127.0.0.1\",\"port\":1980,\"weight\":1\\}\\],\"pass_host\":\"pass\".*,\"scheme\":\"http\",\"type\":\"roundrobin\"\\}\\}/\n\n\n\n=== TEST 2: multiple services\n--- apisix_yaml\nservices:\n  -\n    id: 200\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n  -\n    id: 201\n    upstream:\n      nodes:\n        \"127.0.0.2:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local code, body, res = t.test('/v1/services',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            local g_data = {}\n            for _, r in core.config_util.iterate_values(res) do\n                local data = {}\n                data.id = r.value.id\n                data.plugins = r.value.plugins\n                data.upstream = r.value.upstream\n                core.table.insert(g_data, data)\n            end\n            ngx.say(json.encode(g_data))\n            return\n        }\n    }\n--- response_body eval\nqr/\\{\"id\":\"200\",\"upstream\":\\{\"hash_on\":\"vars\",\"nodes\":\\[\\{\"host\":\"127.0.0.1\",\"port\":1980,\"weight\":1\\}\\],\"pass_host\":\"pass\".*,\"scheme\":\"http\",\"type\":\"roundrobin\"\\}\\}/\n\n\n\n=== TEST 3:  get service with id 5\n--- apisix_yaml\nservices:\n  -\n    id: 5\n    plugins:\n      limit-count:\n        count: 2\n        time_window: 60\n        rejected_code: 503\n        key: remote_addr\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/service/5',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            if res then\n                local data = {}\n                data.id = res.value.id\n                data.plugins = res.value.plugins\n                data.upstream = res.value.upstream\n                ngx.say(json.encode(data))\n            end\n            return\n        }\n    }\n--- response_body eval\nqr/\\{\"id\":\"5\",\"plugins\":\\{\"limit-count\":\\{\"_meta\":\\{\\},\"allow_degradation\":false,\"count\":2,\"key\":\"remote_addr\",\"key_type\":\"var\",\"policy\":\"local\",\"rejected_code\":503,\"show_limit_quota_header\":true,\"time_window\":60\\}\\},\"upstream\":\\{\"hash_on\":\"vars\",\"nodes\":\\[\\{\"host\":\"127.0.0.1\",\"port\":1980,\"weight\":1\\}\\],\"pass_host\":\"pass\",.*\"scheme\":\"http\",\"type\":\"roundrobin\"\\}\\}/\n\n\n\n=== TEST 4: services with invalid id\n--- apisix_yaml\nservices:\n  -\n    id: 1\n    upstream:\n      nodes:\n        \"127.0.0.1:1980\": 1\n      type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/service/2',\n                ngx.HTTP_GET)\n            local data = {}\n            data.status = code\n            ngx.say(json.encode(data))\n            return\n        }\n    }\n--- response_body\n{\"status\":404}\n"
  },
  {
    "path": "t/control/upstreams.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: dump all upstreams\n--- apisix_yaml\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:8001\": 1\n        type: roundrobin\n    -\n        id: 2\n        nodes:\n            \"127.0.0.1:8002\": 1\n        type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/upstreams',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            if res[2] and table.getn(res) == 2 then\n                local data = {}\n                data.nodes = res[2].value.nodes\n                ngx.say(json.encode(data))\n            end\n        }\n    }\n--- response_body\n{\"nodes\":[{\"host\":\"127.0.0.1\",\"port\":8002,\"weight\":1}]}\n\n\n\n=== TEST 2: dump specific upstream with id 1\n--- apisix_yaml\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:8001\": 1\n        type: roundrobin\n    -\n        id: 2\n        nodes:\n            \"127.0.0.1:8002\": 1\n        type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/upstream/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            if res then\n                local data = {}\n                data.nodes = res.value.nodes\n                ngx.say(json.encode(data))\n            end\n        }\n    }\n--- response_body\n{\"nodes\":[{\"host\":\"127.0.0.1\",\"port\":8001,\"weight\":1}]}\n\n\n\n=== TEST 3: upstreams with invalid id\n--- apisix_yaml\nupstreams:\n    -\n        id: 1\n        nodes:\n            \"127.0.0.1:8001\": 1\n        type: roundrobin\n    -\n        id: 2\n        nodes:\n            \"127.0.0.1:8002\": 1\n        type: roundrobin\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local code, body, res = t.test('/v1/upstream/3',\n                ngx.HTTP_GET)\n            local data = {}\n            data.status = code\n            ngx.say(json.encode(data))\n            return\n        }\n    }\n--- response_body\n{\"status\":404}\n"
  },
  {
    "path": "t/core/config-default.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local encode_json = require(\"toolkit.json\").encode\n            local config = require(\"apisix.core\").config.local_conf()\n\n            ngx.say(\"node_listen: \", config.apisix.node_listen)\n            ngx.say(\"stream_proxy: \", encode_json(config.apisix.stream_proxy))\n            ngx.say(\"admin_key: \", encode_json(config.deployment.admin.admin_key))\n        }\n    }\n--- request\nGET /t\n--- response_body\nnode_listen: 1984\nstream_proxy: {\"tcp\":[9100]}\nadmin_key: null\n\n\n\n=== TEST 2: wrong type: expect: table, but got: string\n--- yaml_config\napisix:\n  node_listen: xxxx\n--- must_die\n--- error_log\nfailed to parse yaml config: failed to merge, path[apisix->node_listen] expect: table, but got: string\n\n\n\n=== TEST 3: use `null` means delete\n--- yaml_config\ndeployment:\n    admin:\n        admin_key: null\n--- config\n  location /t {\n    content_by_lua_block {\n        local encode_json = require(\"toolkit.json\").encode\n        local config = require(\"apisix.core\").config.local_conf()\n\n        ngx.say(\"admin_key: \", encode_json(config.deployment.admin.admin_key))\n    }\n}\n--- request\nGET /t\n--- response_body\nadmin_key: null\n\n\n\n=== TEST 4: use `~` means delete\n--- yaml_config\ndeployment:\n    admin:\n        admin_key: null\n--- config\n  location /t {\n    content_by_lua_block {\n        local encode_json = require(\"toolkit.json\").encode\n        local config = require(\"apisix.core\").config.local_conf()\n\n        ngx.say(\"admin_key: \", encode_json(config.deployment.admin.admin_key))\n    }\n}\n--- request\nGET /t\n--- response_body\nadmin_key: null\n\n\n\n=== TEST 5: support listen multiple ports with array\n--- yaml_config\napisix:\n  node_listen:\n    - 1985\n    - 1986\n--- config\n  location /t {\n    content_by_lua_block {\n        local encode_json = require(\"toolkit.json\").encode\n        local config = require(\"apisix.core\").config.local_conf()\n\n        ngx.say(\"node_listen: \", encode_json(config.apisix.node_listen))\n    }\n}\n--- request\nGET /t\n--- response_body\nnode_listen: [1985,1986]\n\n\n\n=== TEST 6: support listen multiple ports with array table\n--- yaml_config\napisix:\n  node_listen:\n    - port: 1985\n    - port: 1986\n  enable_http2: true\n--- config\n  location /t {\n    content_by_lua_block {\n        local encode_json = require(\"toolkit.json\").encode\n        local config = require(\"apisix.core\").config.local_conf()\n\n        ngx.say(\"node_listen: \", encode_json(config.apisix.node_listen))\n    }\n}\n--- request\nGET /t\n--- response_body\nnode_listen: [{\"port\":1985},{\"port\":1986}]\n"
  },
  {
    "path": "t/core/config.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local encode_json = require(\"toolkit.json\").encode\n            local config = require(\"apisix.core\").config.local_conf()\n\n            ngx.say(\"etcd host: \", config.etcd.host)\n            ngx.say(\"first plugin: \", encode_json(config.plugins[1]))\n        }\n    }\n--- request\nGET /t\n--- response_body\netcd host: http://127.0.0.1:2379\nfirst plugin: \"real-ip\"\n\n\n\n=== TEST 2: different elements in yaml\n--- config\n    location /t {\n        content_by_lua_block {\n            local encode_json = require(\"toolkit.json\").encode\n            local config = require(\"apisix.core\").config.local_conf()\n\n            ngx.say(\"etcd host: \", config.etcd.host)\n            ngx.say(\"first plugin: \", encode_json(config.plugins[1]))\n            ngx.say(\"seq: \", encode_json(config.seq))\n        }\n    }\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\" # etcd address\n    prefix: \"/apisix\"           # apisix configurations prefix\n    timeout: 1\nplugins:\n  - example-plugin\n\n# Collection Types #############################################################\n################################################################################\n\n# http://yaml.org/type/map.html -----------------------------------------------#\n\nmap:\n  # Unordered set of key: value pairs.\n  Block style: !!map\n    Clark : Evans\n    Ingy  : döt Net\n    Oren  : Ben-Kiki\n  Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki }\n\n# http://yaml.org/type/omap.html ----------------------------------------------#\n\nomap:\n  # Explicitly typed ordered map (dictionary).\n  Bestiary: !!omap\n    - aardvark: African pig-like ant eater. Ugly.\n    - anteater: South-American ant eater. Two species.\n    - anaconda: South-American constrictor snake. Scaly.\n    # Etc.\n  # Flow style\n  Numbers: !!omap [ one: 1, two: 2, three : 3 ]\n\n# http://yaml.org/type/pairs.html ---------------------------------------------#\n\npairs:\n  # Explicitly typed pairs.\n  Block tasks: !!pairs\n    - meeting: with team.\n    - meeting: with boss.\n    - break: lunch.\n    - meeting: with client.\n  Flow tasks: !!pairs [ meeting: with team, meeting: with boss ]\n\n# http://yaml.org/type/set.html -----------------------------------------------#\n\nset:\n  # Explicitly typed set.\n  baseball players: !!set\n    ? Mark McGwire\n    ? Sammy Sosa\n    ? Ken Griffey\n  # Flow style\n  baseball teams: !!set { Boston Red Sox, Detroit Tigers, New York Yankees }\n\n# http://yaml.org/type/seq.html -----------------------------------------------#\n\nseq:\n  # Ordered sequence of nodes\n  Block style: !!seq\n  - Mercury   # Rotates - no light/dark sides.\n  - Venus     # Deadliest. Aptly named.\n  - Earth     # Mostly dirt.\n  - Mars      # Seems empty.\n  - Jupiter   # The king.\n  - Saturn    # Pretty.\n  - Uranus    # Where the sun hardly shines.\n  - Neptune   # Boring. No rings.\n  - Pluto     # You call this a planet?\n  Flow style: !!seq [ Mercury, Venus, Earth, Mars,      # Rocks\n                      Jupiter, Saturn, Uranus, Neptune, # Gas\n                      Pluto ]                           # Overrated\n\n\n# Scalar Types #################################################################\n################################################################################\n\n# http://yaml.org/type/bool.html ----------------------------------------------#\n\nbool:\n  - true\n  - True\n  - TRUE\n  - false\n  - False\n  - FALSE\n\n# http://yaml.org/type/float.html ---------------------------------------------#\n\nfloat:\n  canonical: 6.8523015e+5\n  exponential: 685.230_15e+03\n  fixed: 685_230.15\n  sexagesimal: 190:20:30.15\n  negative infinity: -.inf\n  not a number: .NaN\n\n# http://yaml.org/type/int.html -----------------------------------------------#\n\nint:\n  canonical: 685230\n  decimal: +685_230\n  octal: 02472256\n  hexadecimal: 0x_0A_74_AE\n  binary: 0b1010_0111_0100_1010_1110\n  sexagesimal: 190:20:30\n\n# http://yaml.org/type/merge.html ---------------------------------------------#\n\nmerge:\n  - &CENTER { x: 1, y: 2 }\n  - &LEFT { x: 0, y: 2 }\n  - &BIG { r: 10 }\n  - &SMALL { r: 1 }\n\n  # All the following maps are equal:\n\n  - # Explicit keys\n    x: 1\n    y: 2\n    r: 10\n    label: nothing\n\n  - # Merge one map\n    << : *CENTER\n    r: 10\n    label: center\n\n  - # Merge multiple maps\n    << : [ *CENTER, *BIG ]\n    label: center/big\n\n  - # Override\n    << : [ *BIG, *LEFT, *SMALL ]\n    x: 1\n    label: big/left/small\n\n# http://yaml.org/type/null.html ----------------------------------------------#\n\nnull:\n  # This mapping has four keys,\n  # one has a value.\n  empty:\n  canonical: ~\n  english: null\n  ~: null key\n  # This sequence has five\n  # entries, two have values.\n  sparse:\n    - ~\n    - 2nd entry\n    -\n    - 4th entry\n    - Null\n\n# http://yaml.org/type/str.html -----------------------------------------------#\n\nstring:\n  inline1: abcd\n  inline2: \"abcd\"\n  inline3: 'abcd'\n  block1: |\n    aaa\n    bbb\n    ccc\n  block2: |+\n    aaa\n    bbb\n    ccc\n  block3: |-\n    aaa\n    bbb\n    ccc\n  block4: >\n    aaa\n    bbb\n    ccc\n  text5: >+\n    aaa\n    bbb\n    ccc\n  text6: >-\n    aaa\n    bbb\n    ccc\n# http://yaml.org/type/timestamp.html -----------------------------------------#\n\ntimestamp:\n  canonical:        2001-12-15T02:59:43.1Z\n  valid iso8601:    2001-12-14t21:59:43.10-05:00\n  space separated:  2001-12-14 21:59:43.10 -5\n  no time zone (Z): 2001-12-15 2:59:43.10\n  date (00:00:00Z): 2002-12-14\n\n\n# JavaScript Specific Types ####################################################\n################################################################################\n\n# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/RegExp\n\nregexp:\n  simple: !!js/regexp      foobar\n  modifiers: !!js/regexp   /foobar/mi\n\n# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/undefined\n\nundefined: !!js/undefined ~\n\n# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function\n\nfunction: !!js/function >\n  function foobar() {\n    return 'Wow! JS-YAML Rocks!';\n  }\n\n\n# Custom types #################################################################\n################################################################################\n\n\n# JS-YAML allows you to specify a custom YAML types for your structures.\n# This is a simple example of custom constructor defined in `js/demo.js` for\n# custom `!sexy` type:\n#\n# var SexyYamlType = new jsyaml.Type('!sexy', {\n#   kind: 'sequence',\n#   construct: function (data) {\n#     return data.map(function (string) { return 'sexy ' + string; });\n#   }\n# });\n#\n# var SEXY_SCHEMA = jsyaml.Schema.create([ SexyYamlType ]);\n#\n# result = jsyaml.load(yourData, { schema: SEXY_SCHEMA });\n\nfoobar: !sexy\n  - bunny\n  - chocolate\n--- request\nGET /t\n--- response_body\netcd host: http://127.0.0.1:2379\nfirst plugin: \"example-plugin\"\nseq: {\"Block style\":[\"Mercury\",\"Venus\",\"Earth\",\"Mars\",\"Jupiter\",\"Saturn\",\"Uranus\",\"Neptune\",\"Pluto\"],\"Flow style\":[\"Mercury\",\"Venus\",\"Earth\",\"Mars\",\"Jupiter\",\"Saturn\",\"Uranus\",\"Neptune\",\"Pluto\"]}\n\n\n\n=== TEST 3: allow environment variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = require(\"apisix.core\").config.local_conf()\n\n            ngx.say(config.apisix.id)\n        }\n    }\n--- main_config\nenv AID=3;\n--- yaml_config\n#nginx_config:\n    #env: AID=3\napisix:\n    id: ${{ AID }}\n--- request\nGET /t\n--- response_body\n3\n\n\n\n=== TEST 4: allow integer worker processes\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = require(\"apisix.core\").config.local_conf()\n\n            ngx.say(config.nginx_config.worker_processes)\n        }\n    }\n--- extra_yaml_config\nnginx_config:\n    worker_processes: 1\n--- request\nGET /t\n--- response_body\n1\n"
  },
  {
    "path": "t/core/config_etcd.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: wrong etcd port\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    prefix: \"/apisix\"\n    host:\n      - \"http://127.0.0.1:7777\" # wrong etcd port\n    timeout: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(8)\n            ngx.say(body)\n        }\n    }\n--- timeout: 12\n--- request\nGET /t\n--- grep_error_log eval\nqr{connection refused}\n--- grep_error_log_out eval\nqr/(connection refused){1,}/\n\n\n\n=== TEST 2: originate TLS connection to etcd cluster without TLS configuration\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:2379\"\n--- extra_init_by_lua\nlocal health_check = require(\"resty.etcd.health_check\")\nhealth_check.get_target_status = function()\n    return true\nend\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(4)\n            ngx.say(\"ok\")\n        }\n    }\n--- timeout: 5\n--- request\nGET /t\n--- grep_error_log chop\npeer closed connection in SSL handshake\n--- grep_error_log_out eval\nqr/(peer closed connection in SSL handshake){1,}/\n\n\n\n=== TEST 3: originate plain connection to etcd cluster which enables TLS\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:12379\"\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(4)\n            ngx.say(\"ok\")\n        }\n    }\n--- timeout: 5\n--- request\nGET /t\n--- grep_error_log chop\nclosed\n--- grep_error_log_out eval\nqr/(closed){1,}/\n\n\n\n=== TEST 4: set route(id: 1) to etcd cluster with TLS\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\n  etcd:\n    host:\n      - \"https://127.0.0.1:12379\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:8080\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new route\",\n                    \"uri\": \"/index.html\"\n                }]]\n            )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: get route(id: 1) from etcd cluster with TLS\n--- yaml_config\napisix:\n  node_listen: 1984\n  admin_key: null\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: ~\n  etcd:\n    host:\n      - \"https://127.0.0.1:12379\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_GET,\n                 nil\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: ensure only one auth request per subsystem for all the etcd sync\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:1980\" # fake server port\n    timeout: 1\n    user: root                  # root username for etcd\n    password: 5tHkHhYkjr6cQY    # root password for etcd\n--- extra_init_by_lua\nlocal health_check = require(\"resty.etcd.health_check\")\nhealth_check.get_target_status = function()\n    return true\nend\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.5)\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/etcd auth failed/\n--- grep_error_log_out\netcd auth failed\netcd auth failed\netcd auth failed\n\n\n\n=== TEST 7: ensure add prefix automatically for _M.getkey\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local config = core.config.new()\n            local res = config:getkey(\"/routes/\")\n            if res and res.status == 200 and res.body\n               and res.body.count and tonumber(res.body.count) >= 1 then\n                ngx.say(\"passed\")\n              else\n                ngx.say(\"failed\")\n            end\n\n            local res = config:getkey(\"/phantomkey\")\n            if res and res.status == 404 then\n                ngx.say(\"passed\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\npassed\n\n\n\n=== TEST 8: Test ETCD health check mode switch during APISIX startup\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/healthy check use \\S+ \\w+/\n--- grep_error_log_out eval\nqr/healthy check use round robin\n(healthy check use ngx.shared dict){1,}/\n\n\n\n=== TEST 9: last_err can be nil when the reconnection is successful\n--- config\n    location /t {\n        content_by_lua_block {\n            local config_etcd = require(\"apisix.core.config_etcd\")\n            local count = 0\n            config_etcd.inject_sync_data(function()\n                if count % 2 == 0 then\n                    count = count + 1\n                    return nil, \"has no healthy etcd endpoint available\"\n                else\n                    return true\n                end\n            end)\n            config_etcd.test_automatic_fetch(false, {\n                running = true,\n                resync_delay = 1,\n            })\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- error_log\nreconnected to etcd\n--- response_body\npassed\n\n\n\n=== TEST 10: reloaded data may be in res.body.node (special kvs structure)\n--- yaml_config\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    admin:\n        admin_key: null\n--- config\n    location /t {\n        content_by_lua_block {\n            local config_etcd = require(\"apisix.core.config_etcd\")\n            local etcd_cli = {}\n            function etcd_cli.readdir()\n                return {\n                    status = 200,\n                    headers = {},\n                    body = {\n                        header = {revision = 1},\n                        kvs = {{key = \"foo\", value = \"bar\"}},\n                    }\n                }\n            end\n            config_etcd.test_sync_data({\n                etcd_cli = etcd_cli,\n                key = \"fake\",\n                single_item = true,\n                -- need_reload because something wrong happened before\n                need_reload = true,\n                upgrade_version = function() end,\n                conf_version = 1,\n            })\n        }\n    }\n--- request\nGET /t\n--- log_level: debug\n--- grep_error_log eval\nqr/readdir key: fake res: .+/\n--- grep_error_log_out eval\nqr/readdir key: fake res: \\[\\{(\"value\":\"bar\",\"key\":\"foo\"|\"key\":\"foo\",\"value\":\"bar\")\\}\\]/\n--- wait: 1\n--- no_error_log\n[error]\n\n\n\n=== TEST 11: reloaded data may be in res.body.node (admin_api_version is v2)\n--- yaml_config\ndeployment:\n    role: traditional\n    role_traditional:\n        config_provider: etcd\n    admin:\n        admin_key: null\n        admin_api_version: v2\n--- config\n    location /t {\n        content_by_lua_block {\n            local config_etcd = require(\"apisix.core.config_etcd\")\n            local etcd_cli = {}\n            function etcd_cli.readdir()\n                return {\n                    status = 200,\n                    headers = {},\n                    body = {\n                        header = {revision = 1},\n                        kvs = {\n                            {key = \"/foo\"},\n                            {key = \"/foo/bar\", value = {\"bar\"}}\n                        },\n                    }\n                }\n            end\n            config_etcd.test_sync_data({\n                etcd_cli = etcd_cli,\n                key = \"fake\",\n                -- need_reload because something wrong happened before\n                need_reload = true,\n                upgrade_version = function() end,\n                conf_version = 1,\n            })\n        }\n    }\n--- request\nGET /t\n--- log_level: debug\n--- grep_error_log eval\nqr/readdir key: fake res: .+/\n--- grep_error_log_out eval\nqr/readdir key: fake res: \\{.*\"nodes\":\\[\\{.*\"value\":\\[\"bar\"\\].*\\}\\].*\\}/\n--- wait: 1\n--- no_error_log\n[error]\n\n\n\n=== TEST 12: test route with special character \"-\"\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\n  etcd:\n    prefix: \"/apisix-test\"\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n\n            -- hit\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\"\n            })\n\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.print(res.body)\n\n            -- delete route\n            code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            -- hit\n            res, err = httpc:request_uri(uri, {\n                method = \"GET\"\n            })\n\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\nhello world\npassed\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 13: the main watcher should be initialised once\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    watch_timeout: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/main etcd watcher initialised, revision=/\n--- grep_error_log_out\nmain etcd watcher initialised, revision=\nmain etcd watcher initialised, revision=\n\n\n\n=== TEST 14: watch revision should be upgraded when timeout occurs\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    watch_timeout: 1\n    prefix: /apisix\n--- extra_yaml_config\nnginx_config:\n    worker_processes: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local etcd = require(\"resty.etcd\")\n            local etcd_cli, err = etcd.new({\n                http_host = \"http://127.0.0.1:2379\",\n            })\n            if not etcd_cli then\n                ngx.say(\"failed to create etcd client: \", err)\n                return\n            end\n            ngx.sleep(2)\n            -- we will assert 4 lines of revision upgrade log because we have one worker and one privileged agent\n            for i = 1, 2 do\n               local _, err = etcd_cli:set(\"/apache\", \"apisix\")\n               if err then\n                   ngx.say(\"failed to set key: \", err)\n                   return\n               end\n               ngx.sleep(1)\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/etcd watch timeout, upgrade revision to/\n--- grep_error_log_out\netcd watch timeout, upgrade revision to\netcd watch timeout, upgrade revision to\netcd watch timeout, upgrade revision to\netcd watch timeout, upgrade revision to\n"
  },
  {
    "path": "t/core/config_util.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: parse_time_unit\n--- config\n    location /t {\n        content_by_lua_block {\n            local parse_time_unit = require(\"apisix.core.config_util\").parse_time_unit\n            for _, case in ipairs({\n                {exp = 1, input = \"1\"},\n                {exp = 1, input = \"1s\"},\n                {exp = 60, input = \"60s\"},\n                {exp = 1.1, input = \"1s100ms\"},\n                {exp = 10.001, input = \"10s1ms\"},\n                {exp = 3600, input = \"60m\"},\n                {exp = 3600.11, input = \"60m110ms\"},\n                {exp = 3710, input = \"1h110\"},\n                {exp = 5400, input = \"1h  30m\"},\n                {exp = 34822861.001, input = \"1y1M1w1d1h1m1s1ms\"},\n            }) do\n                assert(case.exp == parse_time_unit(case.input),\n                       string.format(\"input %s, got %s\", case.input,\n                            parse_time_unit(case.input)))\n            end\n\n            for _, case in ipairs({\n                {exp = \"invalid data: -\", input = \"-1\"},\n                {exp = \"unexpected unit: h\", input = \"1m1h\"},\n                {exp = \"invalid data: \", input = \"\"},\n                {exp = \"specific unit conflicts with the default unit second\", input = \"1s1\"},\n            }) do\n                local _, err = parse_time_unit(case.input)\n                assert(case.exp == err,\n                       string.format(\"input %s, got %s\", case.input, err))\n            end\n        }\n    }\n\n\n\n=== TEST 2: add_clean_handler / cancel_clean_handler / fire_all_clean_handlers\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.core.config_util\")\n            local function setup()\n                local item = {clean_handlers = {}}\n                local idx1 = util.add_clean_handler(item, function()\n                    ngx.log(ngx.WARN, \"fire one\")\n                end)\n                local idx2 = util.add_clean_handler(item, function()\n                    ngx.log(ngx.WARN, \"fire two\")\n                end)\n                return item, idx1, idx2\n            end\n\n            local function setup_to_false()\n                local item = false\n                return item\n            end\n\n            local item, idx1, idx2 = setup()\n            util.cancel_clean_handler(item, idx1, true)\n            util.cancel_clean_handler(item, idx2, true)\n\n            local item, idx1, idx2 = setup()\n            util.fire_all_clean_handlers(item)\n\n            local item, idx1, idx2 = setup()\n            util.cancel_clean_handler(item, idx2)\n            util.fire_all_clean_handlers(item)\n\n            local item, idx1, idx2 = setup()\n            util.cancel_clean_handler(item, idx1)\n            util.fire_all_clean_handlers(item)\n\n            local item = setup_to_false()\n            util.fire_all_clean_handlers(item)\n        }\n    }\n--- grep_error_log eval\nqr/fire \\w+/\n--- grep_error_log_out eval\n\"fire one\\nfire two\\n\" x 3\n"
  },
  {
    "path": "t/core/ctx.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ctx = {}\n            core.ctx.set_vars_meta(ctx)\n\n            ngx.say(\"remote_addr: \", ctx.var[\"remote_addr\"])\n            ngx.say(\"server_port: \", ctx.var[\"server_port\"])\n        }\n    }\n--- request\nGET /t\n--- response_body\nremote_addr: 127.0.0.1\nserver_port: 1984\n\n\n\n=== TEST 2: http header\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ctx = {}\n            core.ctx.set_vars_meta(ctx)\n\n            ngx.say(\"http_host: \", ctx.var[\"http_host\"])\n        }\n    }\n--- request\nGET /t\n--- response_body\nhttp_host: localhost\n\n\n\n=== TEST 3: cookie + no cookie\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ctx = {}\n            core.ctx.set_vars_meta(ctx)\n\n            ngx.say(\"cookie_host: \", ctx.var[\"cookie_host\"])\n        }\n    }\n--- request\nGET /t?a=aaa\n--- response_body\ncookie_host: nil\n--- no_error_log\nfailed to fetch cookie value by key\n\n\n\n=== TEST 4: cookie\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ctx = {}\n            core.ctx.set_vars_meta(ctx)\n\n            ngx.say(\"cookie_a: \", ctx.var[\"cookie_a\"])\n            ngx.say(\"cookie_b: \", ctx.var[\"cookie_b\"])\n            ngx.say(\"cookie_c: \", ctx.var[\"cookie_c\"])\n            ngx.say(\"cookie_d: \", ctx.var[\"cookie_d\"])\n            ngx.say(\"cookie with dash and uppercase: \", ngx.var[\"cookie_X-user-id\"])\n            ngx.say(\"cookie with []: \", ngx.var[\"cookie_user[id]\"])\n            ngx.say(\"cookie with .: \", ngx.var[\"cookie_user.id\"])\n        }\n    }\n--- more_headers\nCookie: a=a; b=bb; c=ccc; X-user-id=2; user[id]=3; user.id=4\n--- request\nGET /t?a=aaa\n--- response_body\ncookie_a: a\ncookie_b: bb\ncookie_c: ccc\ncookie_d: nil\ncookie with dash and uppercase: 2\ncookie with []: 3\ncookie with .: 4\n\n\n\n=== TEST 5: key is nil\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ctx = {}\n            core.ctx.set_vars_meta(ctx)\n\n            ngx.say(\"cookie_a: \", ctx.var[nil])\n        }\n    }\n--- more_headers\nCookie: a=a; b=bb; c=ccc\n--- request\nGET /t?a=aaa\n--- error_code: 500\n--- error_log\ninvalid argument, expect string value\n\n\n\n=== TEST 6: key is number\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ctx = {}\n            core.ctx.set_vars_meta(ctx)\n\n            ngx.say(\"cookie_a: \", ctx.var[2222])\n        }\n    }\n--- more_headers\nCookie: a=a; b=bb; c=ccc\n--- request\nGET /t?a=aaa\n--- error_code: 500\n--- error_log\ninvalid argument, expect string value\n\n\n\n=== TEST 7: add route and get `route_id`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"route_id: \\\", ngx.ctx.api_ctx.var.route_id) end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: `url` exist and `route_id` is 1\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nroute_id: 1\n\n\n\n=== TEST 9: create a service and `service_id` is 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"desc\": \"new_service\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: the route object not bind any service object\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"service_id: \\\", ngx.ctx.api_ctx.var.service_id or 'empty route_id') end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: service_id is empty\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nservice_id: empty route_id\n\n\n\n=== TEST 12: update route and binding service_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"service_id\": 1,\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"service_id: \\\", ngx.ctx.api_ctx.var.service_id) end\"]\n                        }\n                    },\n                    \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: service_id is 1\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nservice_id: 1\n\n\n\n=== TEST 14: create consumer and bind key-auth plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"consumer_jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: create route and consumer_name is consumer_jack\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"consumer_name: \\\", ngx.ctx.api_ctx.var.consumer_name) end\"]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: consumer_name is `consumer_jack`\n--- request\nGET /hello\n--- more_headers\napikey: auth-jack\n--- response_body\nhello world\n--- error_log\nconsumer_name: consumer_jack\n\n\n\n=== TEST 17: update the route, and the consumer_name is nil\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"consumer_name: \\\", ngx.ctx.api_ctx.var.consumer_name or 'consumer_name is nil') end\"]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: consumer_name is empty\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nconsumer_name: consumer_name is nil\n\n\n\n=== TEST 19: create route and consumer_name is consumer_jack\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"consumer_name: \\\", ngx.ctx.api_ctx.var.consumer_name) end\"]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: consumer_name is `consumer_jack`\n--- request\nGET /hello\n--- more_headers\napikey: auth-jack\n--- response_body\nhello world\n--- error_log\nconsumer_name: consumer_jack\n\n\n\n=== TEST 21: update the route, and the consumer_name is nil\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"consumer_name: \\\", ngx.ctx.api_ctx.var.consumer_name or 'consumer_name is nil') end\"]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: consumer_name is nil\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nconsumer_name: consumer_name is nil\n\n\n\n=== TEST 23: add plugin metadata `service_name`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"service_name\": \"$service_name\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: add `http-logger` plugin on service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"name\": \"ctx_var-support-service_name\",\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"max_retry_count\": 1,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 25: route binding service and concat_method is json\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_id\": 1,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: hit route and report http logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log: \\{\"route_id\":\"1\",\"service_id\":\"1\",\"service_name\":\"ctx_var-support-service_name\"\\}/\n\n\n\n=== TEST 27: log_format is configured with `service_name`, but there is no matching service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"max_retry_count\": 1,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: hit route but there is no matching service\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log: \\{\"route_id\":\"1\"\\}/\n\n\n\n=== TEST 29: add plugin metadata `route_name`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"route_name\": \"$route_name\"\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: sanity, batch_max_size=1 and concat_method is json\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"name\": \"ctx_var-support-route_name\",\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"max_retry_count\": 1,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 31: hit route and report http logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log: \\{\"route_id\":\"1\",\"route_name\":\"ctx_var-support-route_name\"\\}/\n\n\n\n=== TEST 32: missing `name` field, batch_max_size=1 and concat_method is json\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"max_retry_count\": 1,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 33: hit route and report http logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log: \\{\"route_id\":\"1\"\\}/\n\n\n\n=== TEST 34: add metadata, service and route, and the service is bound to the route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"route_name\": \"$route_name\",\n                        \"service_name\": \"$service_name\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"my_service\",\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"max_retry_count\": 1,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"concat_method\": \"json\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"my_route\",\n                    \"uri\": \"/hello\",\n                    \"service_id\": 1,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 35: hit route and route_name and service_name are different\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log: \\{\"route_id\":\"1\",\"route_name\":\"my_route\",\"service_id\":\"1\",\"service_name\":\"my_service\"\\}/\n"
  },
  {
    "path": "t/core/ctx2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: should update cached ctx.var\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.log(ngx.WARN, 'pre uri: ', ctx.var.upstream_uri);\n                                        ctx.var.upstream_uri = '/server_port';\n                                        ngx.log(ngx.WARN, 'post uri: ', ctx.var.upstream_uri);\n                                        end\"]\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/xxx\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 2: check\n--- request\nGET /xxx\n--- response_body chomp\n1980\n--- error_log\npre uri: /hello\npost uri: /server_port\n\n\n\n=== TEST 3: get balancer_ip and balancer_port through ctx.var\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"phase\": \"log\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.log(ngx.WARN, 'balancer_ip: ', ctx.var.balancer_ip)\n                                        ngx.log(ngx.WARN, 'balancer_port: ', ctx.var.balancer_port)\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 4: check(balancer_ip is 127.0.0.1 and balancer_port is 1980)\n--- request\nGET /hello\n--- response_body\nhello world\n--- grep_error_log eval\nqr/balancer_ip: 127.0.0.1|balancer_port: 1980/\n--- grep_error_log_out\nbalancer_ip: 127.0.0.1\nbalancer_port: 1980\n\n\n\n=== TEST 5: parsed graphql is cached under ctx\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"POST\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"header_filter\",\n                                \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, 'find ctx._graphql: ', ctx._graphql ~= nil) end\"]\n                            }\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"graphql_name\", \"==\", \"repo\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- request\nPOST /hello\nquery repo {\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n--- error_log\nfind ctx._graphql: true\n\n\n\n=== TEST 7: support dash in the args\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"arg_a-b\", \"==\", \"ab\"]]\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 8: check (support dash in the args)\n--- request\nGET /hello?a-b=ab\n--- response_body\nhello world\n\n\n\n=== TEST 9: support dash in the args(Multi args with the same name, only fetch the first one)\n--- request\nGET /hello?a-b=ab&a-b=ccc\n--- response_body\nhello world\n\n\n\n=== TEST 10: support dash in the args(arg is missing)\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 11: parsed post args is cached under ctx\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, 'find ctx.req_post_args.test: ', ctx.req_post_args.test ~= nil) end\"]\n                            }\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"post_arg_test\", \"==\", \"test\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit\n--- request\nPOST /hello\ntest=test\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- response_body\nhello world\n--- error_log\nfind ctx.req_post_args.test: true\n\n\n\n=== TEST 13: hit with charset\n--- request\nPOST /hello\ntest=test\n--- more_headers\nContent-Type: application/x-www-form-urlencoded;charset=utf-8\n--- response_body\nhello world\n--- error_log\nfind ctx.req_post_args.test: true\n\n\n\n=== TEST 14: missed (post_arg_test is missing)\n--- request\nPOST /hello\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 15: missed (post_arg_test is mismatch)\n--- request\nPOST /hello\ntest=tesy\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 16: register custom variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\" : [\"return function(conf, ctx) ngx.say('find ctx.var.a6_labels_zone: ', ctx.var.a6_labels_zone) end\"]\n                            }\n                        },\n                        \"uri\": \"/hello\",\n                        \"labels\": {\n                            \"zone\": \"Singapore\"\n                        }\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 17: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local core = require \"apisix.core\"\n            core.ctx.register_var(\"a6_labels_zone\", function(ctx)\n                local route = ctx.matched_route and ctx.matched_route.value\n                if route and route.labels then\n                    return route.labels.zone\n                end\n                return nil\n            end)\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res = assert(httpc:request_uri(uri))\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nfind ctx.var.a6_labels_zone: Singapore\n\n\n\n=== TEST 18: register custom variable with no cacheable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\" : [\"return function(conf, ctx) ngx.say('find ctx.var.a6_count: ', ctx.var.a6_count) end\"]\n                            },\n                            \"serverless-post-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\" : [\"return function(conf, ctx) ngx.say('find ctx.var.a6_count: ', ctx.var.a6_count) end\"]\n                            }\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 19: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local core = require \"apisix.core\"\n            core.ctx.register_var(\"a6_count\", function(ctx)\n                if not ctx.a6_count then\n                    ctx.a6_count = 0\n                end\n                ctx.a6_count = ctx.a6_count + 1\n                return ctx.a6_count\n            end, {no_cacheable = true})\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res = assert(httpc:request_uri(uri))\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nfind ctx.var.a6_count: 1\nfind ctx.var.a6_count: 2\n"
  },
  {
    "path": "t/core/ctx3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('debug');\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: parse graphql only once and use subsequent from cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"POST\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"header_filter\",\n                                \"functions\" : [\"return function(conf, ctx)\n                                                ngx.log(ngx.WARN, 'find ctx._graphql: ', ctx.var.graphql_name == \\\"repo\\\");\n                                                ngx.log(ngx.WARN, 'find ctx._graphql: ', ctx.var.graphql_name == \\\"repo\\\");\n                                                ngx.log(ngx.WARN, 'find ctx._graphql: ', ctx.var.graphql_name == \\\"repo\\\");\n                                                end\"]\n                            }\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"graphql_name\", \"==\", \"repo\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nPOST /hello\nquery repo {\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n--- error_log\n--- grep_error_log eval\nqr/serving ctx value from cache for key: graphql_name/\n--- grep_error_log_out\nserving ctx value from cache for key: graphql_name\nserving ctx value from cache for key: graphql_name\nserving ctx value from cache for key: graphql_name\n"
  },
  {
    "path": "t/core/ctx4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route with serverless function to verify apisix_upstream_response_time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"phase\": \"log\",\n                            \"functions\": [\n                                \"return function(conf, ctx)\n                                    local apisix_urt = ctx.var.apisix_upstream_response_time\n                                    local ngx_urt = ngx.var.upstream_response_time\n                                    if apisix_urt and ngx_urt and apisix_urt == ngx_urt then\n                                        ngx.log(ngx.WARN, 'SUCCESS: apisix_upstream_response_time matches')\n                                    else\n                                        ngx.log(ngx.ERR, 'ERROR: apisix_upstream_response_time mismatch. APISIX: ', tostring(apisix_urt), ' NGX: ', tostring(ngx_urt))\n                                    end\n                                end\"\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: verify apisix_upstream_response_time matches ngx.upstream_response_time\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nSUCCESS: apisix_upstream_response_time matches\n--- no_error_log\nERROR: apisix_upstream_response_time mismatch\n"
  },
  {
    "path": "t/core/ctx_with_params.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    router:\n        http: 'radixtree_uri_with_parameter'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add route and get `uri_param_`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"uri_param_id: \\\", ngx.ctx.api_ctx.var.uri_param_id) end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/:id\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: `uri_param_id` exist (hello)\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nuri_param_id: hello\n\n\n\n=== TEST 3: `uri_param_id` exist (hello1)\n--- request\nGET /hello1\n--- response_body\nhello1 world\n--- error_log\nuri_param_id: hello1\n\n\n\n=== TEST 4: `uri_param_id` nonexisting route\n--- request\nGET /not_a_route\n--- error_code: 404\n--- error_log\nuri_param_id: not_a_route\n\n\n\n=== TEST 5: add route and get unknown `uri_param_id`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function() ngx.log(ngx.INFO, \\\"uri_param_id: \\\", ngx.ctx.api_ctx.var.uri_param_id) end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: `uri_param_id` not in uri\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nuri_param_id:\n"
  },
  {
    "path": "t/core/env.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{TEST_ENV_VAR} = \"test-value\";\n    $ENV{TEST_ENV_SUB_VAR} = '{\"main\":\"main_value\",\"sub\":\"sub_value\"}';\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity: start with $env://\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local value = env.fetch_by_uri(\"$env://TEST_ENV_VAR\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntest-value\n\n\n\n=== TEST 2: sanity: start with $ENV://\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local value = env.fetch_by_uri(\"$ENV://TEST_ENV_VAR\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntest-value\n\n\n\n=== TEST 3: env var case sensitive\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local value = env.fetch_by_uri(\"$ENV://test_env_var\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnil\n\n\n\n=== TEST 4: wrong format: wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local _, err = env.fetch_by_uri(1)\n            ngx.say(err)\n\n            local _, err = env.fetch_by_uri(true)\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror env_uri type: number\nerror env_uri type: boolean\n\n\n\n=== TEST 5: wrong format: wrong prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local _, err = env.fetch_by_uri(\"env://\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror env_uri prefix: env://\n\n\n\n=== TEST 6: sub value\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local value = env.fetch_by_uri(\"$ENV://TEST_ENV_SUB_VAR/main\")\n            ngx.say(value)\n            local value = env.fetch_by_uri(\"$ENV://TEST_ENV_SUB_VAR/sub\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nmain_value\nsub_value\n\n\n\n=== TEST 7: wrong sub value: error json\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local _, err = env.fetch_by_uri(\"$ENV://TEST_ENV_VAR/main\")\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- response_body\ndecode failed, err: Expected value but found invalid token at character 1, value: test-value\n\n\n\n=== TEST 8: wrong sub value: not exits\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local value = env.fetch_by_uri(\"$ENV://TEST_ENV_VAR/no\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnil\n\n\n\n=== TEST 9: use nginx env\n--- main_config\nenv ngx_env=apisix-nice;\n--- config\n    location /t {\n        content_by_lua_block {\n            local env = require(\"apisix.core.env\")\n            local value = env.fetch_by_uri(\"$ENV://ngx_env\")\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\napisix-nice\n"
  },
  {
    "path": "t/core/etcd-auth-fail.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{\"ETCD_ENABLE_AUTH\"} = \"false\";\n    delete $ENV{\"FLUSH_ETCD\"};\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\n# Authentication is enabled at etcd and credentials are set\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user add root:5tHkHhYkjr6cQY');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role add root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user grant-role root root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role list');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user user list');\n# Grant the user access to the specified directory\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user add apisix:abc123');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role add apisix');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user grant-role apisix apisix');\nsystem('etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission apisix --prefix=true readwrite /apisix/');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" auth enable');\n\nrun_tests;\n\n# Authentication is disabled at etcd\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" --user root:5tHkHhYkjr6cQY auth disable');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user delete root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role delete root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user delete apisix');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role delete apisix');\n__DATA__\n\n=== TEST 1: Set and Get a value pass\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local key = \"/test_key\"\n            local val = \"test_value\"\n            local res, err = core.etcd.set(key, val)\n            ngx.say(err)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr /(insufficient credentials code: 401|etcdserver: user name is empty)/\n\n\n\n=== TEST 2: etcd grants permissions with a different prefix than the one used by apisix, etcd will forbidden\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local key = \"/test_key\"\n            local val = \"test_value\"\n            local res, err = core.etcd.set(key, val)\n            ngx.say(err)\n        }\n    }\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    user: apisix\n    password: abc123\n--- request\nGET /t\n--- error_log eval\nqr /etcd forbidden code: 403/\n"
  },
  {
    "path": "t/core/etcd-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{\"ETCD_ENABLE_AUTH\"} = \"true\";\n    delete $ENV{\"FLUSH_ETCD\"};\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\n# Authentication is enabled at etcd and credentials are set\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user add root:5tHkHhYkjr6cQY');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role add root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user grant-role root root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role list');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user user list');\n# Grant the user access to the specified directory\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user add apisix:abc123');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role add apisix');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user grant-role apisix apisix');\nsystem('etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission apisix --prefix=true readwrite /apisix');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" auth enable');\n\nrun_tests;\n\n# Authentication is disabled at etcd\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" --user root:5tHkHhYkjr6cQY auth disable');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user delete root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role delete root');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" user delete apisix');\nsystem('etcdctl --endpoints=\"http://127.0.0.1:2379\" role delete apisix');\n\n\n__DATA__\n\n=== TEST 1: Set and Get a value pass with authentication\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local key = \"/test_key\"\n            local val = \"test_value\"\n            core.etcd.set(key, val)\n            local res, err = core.etcd.get(key)\n            ngx.say(res.body.node.value)\n            core.etcd.delete(val)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntest_value\n\n\n\n=== TEST 2: etcd grants permissions with the same prefix as apisix uses, etcd is normal\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local key = \"/test_key\"\n            local val = \"test_value\"\n            local res, err = core.etcd.set(key, val)\n            ngx.say(err)\n        }\n    }\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    user: apisix\n    password: abc123\n--- request\nGET /t\n"
  },
  {
    "path": "t/core/etcd-mtls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $out = eval { `resty -e \"local s=ngx.socket.tcp();print(s.tlshandshake)\"` };\n\nif ($out !~ m/function:/) {\n    plan(skip_all => \"tlshandshake not patched\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: run etcd in init phase\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n      verify: false\n--- init_by_lua_block\n    local apisix = require(\"apisix\")\n    apisix.http_init()\n    local etcd = require(\"apisix.core.etcd\")\n    assert(etcd.set(\"/a\", \"ab\"))\n\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.body.node.value)\n\n    local res, err = etcd.delete(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.status)\n\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.status)\n--- config\n    location /t {\n        return 200;\n    }\n--- request\nGET /t\n--- error_log eval\nqr/init_by_lua:\\d+: ab/ and qr/init_by_lua:\\d+: 200/ and qr/init_by_lua:\\d+: 404/\n\n\n\n=== TEST 2: run etcd in init phase (stream)\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n      verify: false\n--- stream_init_by_lua_block\n    apisix = require(\"apisix\")\n    apisix.stream_init()\n    local etcd = require(\"apisix.core.etcd\")\n    assert(etcd.set(\"/a\", \"ab\"))\n\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.body.node.value)\n\n    local res, err = etcd.delete(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.status)\n\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.status)\n--- stream_server_config\n    content_by_lua_block {\n        ngx.say(\"ok\")\n    }\n--- stream_enable\n--- error_log eval\nqr/init_by_lua:\\d+: ab/ and qr/init_by_lua:\\d+: 200/ and qr/init_by_lua:\\d+: 404/\n\n\n\n=== TEST 3: sync\n--- extra_yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n      verify: false\n  admin:\n    admin_key_required: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local consumers, _ = core.config.new(\"/consumers\", {\n                automatic = true,\n                item_schema = core.schema.consumer,\n            })\n\n            ngx.sleep(0.6)\n            local idx = consumers.prev_index\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jobs\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jobs\",\n                            \"password\": \"678901\"\n                        }\n                    }\n                }]])\n\n            ngx.sleep(2)\n            local new_idx = consumers.prev_index\n            if new_idx > idx then\n                ngx.say(\"prev_index updated\")\n            else\n                ngx.say(\"prev_index not update\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nprev_index updated\n--- error_log\nwaitdir key\n\n\n\n=== TEST 4: sync (stream)\n--- extra_yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n      verify: false\n--- stream_server_config\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n\n        local sr, _ = core.config.new(\"/stream_routes\", {\n            automatic = true,\n            item_schema = core.schema.stream_routes,\n        })\n\n        ngx.sleep(0.6)\n        local idx = sr.prev_index\n\n        assert(core.etcd.set(\"/stream_routes/1\",\n            {\n                plugins = {\n                }\n            }))\n\n        ngx.sleep(2)\n        local new_idx = sr.prev_index\n        if new_idx > idx then\n            ngx.say(\"prev_index updated\")\n        else\n            ngx.say(\"prev_index not update\")\n        end\n        }\n--- stream_enable\n--- stream_response\nprev_index updated\n--- error_log\nwaitdir key\n\n\n\n=== TEST 5: ssl_trusted_certificate\n--- yaml_config\napisix:\n  ssl:\n    ssl_trusted_certificate: t/certs/mtls_ca.crt\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"https://127.0.0.1:22379\"\n    prefix: \"/apisix\"\n    tls:\n      cert: t/certs/mtls_client.crt\n      key: t/certs/mtls_client.key\n--- init_by_lua_block\n    local apisix = require(\"apisix\")\n    apisix.http_init()\n    local etcd = require(\"apisix.core.etcd\")\n    assert(etcd.set(\"/a\", \"ab\"))\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.body.node.value)\n--- config\n    location /t {\n        return 200;\n    }\n--- request\nGET /t\n--- error_log eval\nqr/init_by_lua:\\d+: ab/\n"
  },
  {
    "path": "t/core/etcd-sync.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: using default timeout\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local consumers, _ = core.config.new(\"/consumers\", {\n                automatic = true,\n                item_schema = core.schema.consumer,\n            })\n\n            ngx.sleep(0.6)\n            local idx = consumers.prev_index\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jobs\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jobs\",\n                            \"password\": \"678901\"\n                        }\n                    }\n                }]])\n\n            ngx.sleep(2)\n            local new_idx = consumers.prev_index\n            core.log.info(\"idx:\", idx, \" new_idx: \", new_idx)\n            if new_idx > idx then\n                ngx.say(\"prev_index updated\")\n            else\n                ngx.say(\"prev_index not update\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nprev_index updated\n--- error_log\nwaitdir key\n\n\n\n=== TEST 2: no update\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local consumers, _ = core.config.new(\"/consumers\", {\n                automatic = true,\n                item_schema = core.schema.consumer\n            })\n\n            ngx.sleep(0.6)\n            local idx = consumers.prev_index\n\n            local key = \"/test_key\"\n            local val = \"test_value\"\n            core.etcd.set(key, val)\n\n            ngx.sleep(2)\n\n            local new_idx = consumers.prev_index\n            core.log.info(\"idx:\", idx, \" new_idx: \", new_idx)\n            if new_idx > idx then\n                ngx.say(\"prev_index updated\")\n            else\n                ngx.say(\"prev_index not update\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nprev_index not update\n\n\n\n=== TEST 3: bad plugin configuration (validated via incremental sync)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            assert(core.etcd.set(\"/global_rules/etcdsync\",\n                {id = 1, plugins = { [\"proxy-rewrite\"] = { uri =  1 }}}\n            ))\n            -- wait for sync\n            ngx.sleep(0.6)\n        }\n    }\n--- request\nGET /t\n--- error_log\nproperty \"uri\" validation failed\n\n\n\n=== TEST 4: bad plugin configuration (validated via full sync)\n--- config\n    location /t {\n        content_by_lua_block {\n        }\n    }\n--- request\nGET /t\n--- error_log\nuse loaded configuration /global_rules\nproperty \"uri\" validation failed\n\n\n\n=== TEST 5: bad plugin configuration (validated without sync during start)\n--- extra_yaml_config\n  disable_sync_configuration_during_start: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            -- wait for full sync finish\n            ngx.sleep(0.6)\n\n            assert(core.etcd.delete(\"/global_rules/etcdsync\"))\n        }\n    }\n--- request\nGET /t\n--- error_log\nproperty \"uri\" validation failed\n--- no_error_log\nuse loaded configuration /global_rules\n"
  },
  {
    "path": "t/core/etcd-write.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add serverless-pre-function with etcd delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:delete(\\\"/test-key\\\") end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 2: should show warn when serverless-pre-function try to write to etcd with cli delete\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 3: add serverless-pre-function with etcd cli grant\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:grant(10) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 4: should show warn when serverless-pre-function try to write to etcd with cli grant\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 5: add serverless-pre-function with etcd cli setnx\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:setnx(\\\"/test-key\\\", {value = \\\"hello from serverless\\\"}) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 6: should show warn when serverless-pre-function try to write to etcd with cli setnx\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 7: add serverless-pre-function with etcd cli set\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:set(\\\"/test-key\\\", {value = \\\"hello from serverless\\\"}) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 8: should show warn when serverless-pre-function try to write to etcd with cli set\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 9: add serverless-pre-function with etcd cli setx\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:setx(\\\"/test-key\\\", {value = \\\"hello from serverless\\\"}) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 10: should show warn when serverless-pre-function try to write to etcd with cli setx\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 11: add serverless-pre-function with etcd cli rmdir\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:rmdir(\\\"/test-key\\\") end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 12: should show warn when serverless-pre-function try to write to etcd with cli rmdir\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 13: add serverless-pre-function with etcd cli revoke\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:revoke(123) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 14: should show warn when serverless-pre-function try to write to etcd with cli revoke\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 15: add serverless-pre-function with etcd cli keepalive\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:keepalive(123) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 16: should show warn when serverless-pre-function try to write to etcd with cli keepalive\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 17: add serverless-pre-function with etcd cli get\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local etcd_cli = require(\\\"apisix.core.etcd\\\").new() etcd_cli:get(\\\"/my-test-key\\\") end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: should not show warn when serverless-pre-function try to read from etcd with cli get\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/hello\")\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- no_error_log\nData plane role should not write to etcd. This operation will be deprecated in future releases.\n\n\n\n=== TEST 19: add serverless-pre-function with etcd function set\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.set(\\\"/my-test-key\\\", {value = \\\"hello from serverless\\\"}) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: should show warn when serverless-pre-function try to write to etcd with function set\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 21: add serverless-pre-function with etcd function atomic_set\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.atomic_set(\\\"/my-test-key\\\", {value = \\\"hello from serverless\\\"}) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: should show warn when serverless-pre-function try to write to etcd with function atomic_set\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 23: add serverless-pre-function with etcd function push\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.push(\\\"/my-test-key\\\", {value = \\\"hello from serverless\\\"}) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: should show warn when serverless-pre-function try to write to etcd with function push\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 25: add serverless-pre-function with etcd function delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.delete(\\\"/my-test-key\\\") end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: should show warn when serverless-pre-function try to write to etcd with function delete\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 27: add serverless-pre-function with etcd function rmdir\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.rmdir(\\\"/my-test-key\\\") end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: should show warn when serverless-pre-function try to write to etcd with function rmdir\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 29: add serverless-pre-function with etcd function keepalive\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.keepalive(123) end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: should show warn when serverless-pre-function try to write to etcd with function keepalive\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/hello')\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/Data plane role should not write to etcd. This operation will be deprecated in future releases./\n\n\n\n=== TEST 31: add serverless-pre-function with etcd function get\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\n                                \"return function() local core = require(\\\"apisix.core\\\") core.etcd.get(\\\"/my-test-key\\\") end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 32: should not show warn when serverless-pre-function try to read from etcd with function get\n--- yaml_config\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/hello\")\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- no_error_log\nData plane role should not write to etcd. This operation will be deprecated in future releases.\n\n\n\n=== TEST 33: should not warn when not data_plane\n--- yaml_config\ndeployment:\n  role: control_plane\n  role_control_plane:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\"\n    prefix: \"/apisix\"\n    tls:\n      verify: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local etcd = require(\"apisix.core.etcd\")\n            etcd.set(\"foo\", \"bar\")\n            etcd.delete(\"foo\")\n        }\n    }\n--- request\nGET /t\n--- no_error_log\nData plane role should not write to etcd. This operation will be deprecated in future releases.\n"
  },
  {
    "path": "t/core/etcd.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: delete test data if exists\n--- config\n    location /delete {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /delete\n--- ignore_response\n\n\n\n=== TEST 2: (add + update + delete) *2 (same uri)\n--- config\n    location /add {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"host\": \"foo.com\",\n                    \"uri\": \"/hello\"\n                }]],\n                nil\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /update {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 2\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"host\": \"foo.com\",\n                    \"uri\": \"/hello\"\n                }]],\n                nil\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /delete {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /add\", \"GET /hello\", \"GET /update\", \"GET /hello\", \"GET /delete\", \"GET /hello\",\n\"GET /add\", \"GET /hello\", \"GET /update\", \"GET /hello\", \"GET /delete\", \"GET /hello\"]\n--- more_headers\nHost: foo.com\n--- error_code eval\n[201, 200, 200, 200, 200, 404, 201, 200, 200, 200, 200, 404]\n--- response_body eval\n[\"passed\\n\", \"hello world\\n\", \"passed\\n\", \"hello world\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\",\n\"passed\\n\", \"hello world\\n\", \"passed\\n\", \"hello world\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\"]\n--- timeout: 5\n\n\n\n=== TEST 3: add + update + delete + add + update + delete (different uris)\n--- config\n    location /add {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"host\": \"foo.com\",\n                    \"uri\": \"/hello\"\n                }]],\n                nil\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /update {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 2\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"host\": \"foo.com\",\n                    \"uri\": \"/status\"\n                }]],\n                nil\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /delete {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /add2 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"host\": \"foo.com\",\n                    \"uri\": \"/hello_chunked\"\n                }]],\n                nil\n                )\n                ngx.sleep(1)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /update2 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 2\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"host\": \"foo.com\",\n                    \"uri\": \"/hello1\"\n                }]],\n                nil\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /delete2 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /add\", \"GET /hello\", \"GET /update\", \"GET /hello\", \"GET /status\", \"GET /delete\", \"GET /status\",\n\"GET /add2\", \"GET /hello_chunked\", \"GET /update2\", \"GET /hello_chunked\", \"GET /hello1\", \"GET /delete\", \"GET /hello1\"]\n--- more_headers\nHost: foo.com\n--- error_code eval\n[201, 200, 200, 404, 200, 200, 404, 201, 200, 200, 404, 200, 200, 404]\n--- response_body eval\n[\"passed\\n\", \"hello world\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\", \"ok\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\",\n\"passed\\n\", \"hello world\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\", \"hello1 world\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\"]\n--- timeout: 5\n\n\n\n=== TEST 4: add*50 + update*50 + delete*50\n--- config\n    location /add {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local path = \"\"\n            local code, body\n            for i = 1, 25 do\n                path = '/apisix/admin/routes/' .. tostring(i)\n                code, body = t(path,\n                    ngx.HTTP_PUT,\n                    string.format('{\"upstream\": {\"nodes\": {\"127.0.0.1:1980\": 1},\"type\": \"roundrobin\"},\"host\": \"foo.com\",\"uri\": \"/print_uri_%s\"}', tostring(i)),\n                    nil\n                )\n            end\n            ngx.sleep(2)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /add2 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local path = \"\"\n            local code, body\n            for i = 26, 50 do\n                path = '/apisix/admin/routes/' .. tostring(i)\n                code, body = t(path,\n                    ngx.HTTP_PUT,\n                    string.format('{\"upstream\": {\"nodes\": {\"127.0.0.1:1980\": 1},\"type\": \"roundrobin\"},\"host\": \"foo.com\",\"uri\": \"/print_uri_%s\"}', tostring(i)),\n                    nil\n                )\n            end\n            ngx.sleep(2)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /update {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local path = \"\"\n            local code, body\n            for i = 1, 25 do\n                path = '/apisix/admin/routes/' .. tostring(i)\n                code, body = t(path,\n                    ngx.HTTP_PUT,\n                    string.format('{\"upstream\": {\"nodes\": {\"127.0.0.1:1980\": 1},\"type\": \"roundrobin\"},\"host\": \"foo.com\",\"uri\": \"/print_uri_%s\"}', tostring(i)),\n                    nil\n                    )\n            end\n            ngx.sleep(2)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /update2 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local path = \"\"\n            local code, body\n            for i = 26, 50 do\n                path = '/apisix/admin/routes/' .. tostring(i)\n                code, body = t(path,\n                    ngx.HTTP_PUT,\n                    string.format('{\"upstream\": {\"nodes\": {\"127.0.0.1:1980\": 1},\"type\": \"roundrobin\"},\"host\": \"foo.com\",\"uri\": \"/print_uri_%s\"}', tostring(i)),\n                    nil\n                    )\n            end\n            ngx.sleep(2)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location /delete {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local path = \"\"\n            local code, body\n            for i = 1, 50 do\n                path = '/apisix/admin/routes/' .. tostring(i)\n                code, body = t(path, ngx.HTTP_DELETE)\n            end\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /add\", \"GET /print_uri_20\", \"GET /add2\", \"GET /print_uri_36\", \"GET /update\", \"GET /print_uri_12\", \"GET /delete\", \"GET /print_uri_12\"]\n--- more_headers\nHost: foo.com\n--- error_code eval\n[201, 200, 201, 200, 200, 200, 200, 404]\n--- response_body eval\n[\"passed\\n\", \"/print_uri_20\\n\", \"passed\\n\", \"/print_uri_36\\n\", \"passed\\n\", \"/print_uri_12\\n\", \"passed\\n\", \"{\\\"error_msg\\\":\\\"404 Route Not Found\\\"}\\n\"]\n--- timeout: 20\n\n\n\n=== TEST 5: get single\n--- config\n    location /t {\n        content_by_lua_block {\n            local etcd = require(\"apisix.core.etcd\")\n            assert(etcd.set(\"/ab\", \"ab\"))\n            local res, err = etcd.get(\"/a\")\n            ngx.status = res.status\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n\n\n\n=== TEST 6: get prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local etcd = require(\"apisix.core.etcd\")\n            assert(etcd.set(\"/ab\", \"ab\"))\n            local res, err = etcd.get(\"/a\", true)\n            assert(err == nil)\n            assert(#res.body.list == 1)\n            ngx.status = res.status\n            ngx.say(res.body.list[1].value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nab\n\n\n\n=== TEST 7: run etcd in init phase\n--- init_by_lua_block\n    local apisix = require(\"apisix\")\n    apisix.http_init()\n    local etcd = require(\"apisix.core.etcd\")\n    assert(etcd.set(\"/a\", \"ab\"))\n\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.body.node.value)\n\n    local res, err = etcd.delete(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.status)\n\n    local res, err = etcd.get(\"/a\")\n    if not res then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, res.status)\n--- config\n    location /t {\n        return 200;\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/init_by_lua.*: \\S+/\n--- grep_error_log_out eval\nqr{init_by_lua.* ab\ninit_by_lua.* 200\ninit_by_lua.* 404}\n\n\n\n=== TEST 8: list multiple kv, get prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local etcd = require(\"apisix.core.etcd\")\n            assert(etcd.set(\"/ab\", \"ab\"))\n            assert(etcd.set(\"/abc\", \"abc\"))\n            -- get prefix\n            local res, err = etcd.get(\"/a\", true)\n            assert(err == nil)\n            assert(#res.body.list == 2)\n            ngx.status = res.status\n            ngx.say(res.body.list[1].value)\n            ngx.say(res.body.list[2].value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nab\nabc\n"
  },
  {
    "path": "t/core/json.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local json_data = core.json.encode({test=\"test\"})\n\n            ngx.say(\"encode: \", json_data)\n\n            local data = core.json.decode(json_data)\n            ngx.say(\"data: \", data.test)\n        }\n    }\n--- response_body\nencode: {\"test\":\"test\"}\ndata: test\n\n\n\n=== TEST 2: delay_encode\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.log(ngx.ERR, \"val: \", core.json.delay_encode({test=\"test1\"}),core.json.delay_encode({test=\"test2\"}))\n        }\n    }\n--- error_log\nval: {\"test\":\"test1\"}{\"test\":\"test2\"}\n\n\n\n=== TEST 3: encode with force argument\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local data = core.json.encode({test=\"test\", fun = function() end}, true)\n\n            ngx.say(\"encode: \", data)\n        }\n    }\n--- response_body_like eval\nqr/\\{(\"test\":\"test\",\"fun\":\"function: 0x[0-9a-f]+\"|\"fun\":\"function: 0x[0-9a-f]+\",\"test\":\"test\")}/\n\n\n\n=== TEST 4: encode, include `cdata` type\n--- config\n    location /t {\n        content_by_lua_block {\n            local ffi = require \"ffi\"\n            local charpp = ffi.new(\"char *[1]\")\n\n            local core = require(\"apisix.core\")\n            local json_data = core.json.encode({test=charpp}, true)\n            ngx.say(\"encode: \", json_data)\n        }\n    }\n--- response_body_like eval\nqr/encode: \\{\"test\":\"cdata\\<char \\*\\[1\\]>: 0x[0-9a-f]+\"\\}/\n\n\n\n=== TEST 5: excessive nesting\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local a = {}\n            local b = {}\n            a.b = b\n            b.a = a\n\n            local json_data = core.json.encode(a, true)\n            ngx.say(\"encode: \", json_data)\n        }\n    }\n--- response_body eval\nqr/\\{\"b\":\\{\"a\":\\{\"b\":\"table: 0x[\\w]+\"\\}\\}\\}/\n\n\n\n=== TEST 6: decode/encode empty array\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local data = core.json.decode('{\"arr\":[]}')\n            ngx.say(core.json.encode(data))\n            local data = { arr = setmetatable({}, core.json.array_mt)}\n            ngx.say(core.json.encode(data))\n            local data = core.json.decode('{\"obj\":{}}')\n            ngx.say(core.json.encode(data))\n        }\n    }\n--- response_body\n{\"arr\":[]}\n{\"arr\":[]}\n{\"obj\":{}}\n\n\n\n=== TEST 7: encode slash without escape\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local json_data = core.json.encode({test=\"/test\"})\n\n            ngx.say(\"encode: \", json_data)\n\n            local data = core.json.decode(json_data)\n            ngx.say(\"data: \", data.test)\n        }\n    }\n--- response_body\nencode: {\"test\":\"/test\"}\ndata: /test\n"
  },
  {
    "path": "t/core/log.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: error log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"error log\")\n            core.log.warn(\"warn log\")\n            core.log.notice(\"notice log\")\n            core.log.info(\"info log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: error\n--- request\nGET /t\n--- error_log\nerror log\n--- no_error_log\nwarn log\nnotice log\ninfo log\n\n\n\n=== TEST 2: warn log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"error log\")\n            core.log.warn(\"warn log\")\n            core.log.notice(\"notice log\")\n            core.log.info(\"info log\")\n            core.log.debug(\"debug log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: warn\n--- request\nGET /t\n--- error_log\nerror log\nwarn log\n--- no_error_log\nnotice log\ninfo log\ndebug log\n\n\n\n=== TEST 3: notice log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"error log\")\n            core.log.warn(\"warn log\")\n            core.log.notice(\"notice log\")\n            core.log.info(\"info log\")\n            core.log.debug(\"debug log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: notice\n--- request\nGET /t\n--- error_log\nerror log\nwarn log\nnotice log\n--- no_error_log\ninfo log\ndebug log\n\n\n\n=== TEST 4: info log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"error log\")\n            core.log.warn(\"warn log\")\n            core.log.notice(\"notice log\")\n            core.log.info(\"info log\")\n            core.log.debug(\"debug log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: info\n--- request\nGET /t\n--- error_log\nerror log\nwarn log\nnotice log\ninfo log\n--- no_error_log\ndebug log\n\n\n\n=== TEST 5: debug log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"error log\")\n            core.log.warn(\"warn log\")\n            core.log.notice(\"notice log\")\n            core.log.info(\"info log\")\n            core.log.debug(\"debug log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: debug\n--- request\nGET /t\n--- error_log\nerror log\nwarn log\nnotice log\ninfo log\ndebug log\n\n\n\n=== TEST 6: print error log with prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local log_prefix = require(\"apisix.core\").log.new(\"prefix: \")\n            log_prefix.error(\"error log\")\n            log_prefix.warn(\"warn log\")\n            log_prefix.notice(\"notice log\")\n            log_prefix.info(\"info log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: error\n--- request\nGET /t\n--- error_log eval\nqr/[error].+prefix: error log/\n--- no_error_log\n[qr/[warn].+warn log/, qr/[notice].+notice log/, qr/[info].+info log/]\n\n\n\n=== TEST 7: print both prefixed error logs and normal logs\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local log_prefix = core.log.new(\"prefix: \")\n            core.log.error(\"raw error log\")\n            core.log.warn(\"raw warn log\")\n            core.log.notice(\"raw notice log\")\n            core.log.info(\"raw info log\")\n\n            log_prefix.error(\"error log\")\n            log_prefix.warn(\"warn log\")\n            log_prefix.notice(\"notice log\")\n            log_prefix.info(\"info log\")\n            ngx.say(\"done\")\n        }\n    }\n--- log_level: error\n--- request\nGET /t\n--- error_log eval\n[qr/[error].+raw error log/, qr/[error].+prefix: error log/]\n--- no_error_log\n[qr/[warn].+warn log/, qr/[notice].+notice log/, qr/[info].+info log/]\n"
  },
  {
    "path": "t/core/lrucache.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local idx = 0\n            local function create_obj()\n                idx = idx + 1\n                return {idx = idx}\n            end\n\n            local obj = core.lrucache.global(\"key\", nil, create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = core.lrucache.global(\"key\", nil, create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = core.lrucache.global(\"key\", \"1\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n        }\n    }\n--- request\nGET /t\n--- response_body\nobj: {\"idx\":1}\nobj: {\"idx\":1}\nobj: {\"idx\":2}\n\n\n\n=== TEST 2: new\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local idx = 0\n            local function create_obj()\n                idx = idx + 1\n                return {idx = idx}\n            end\n\n            local lru_get = core.lrucache.new()\n\n            local obj = lru_get(\"key\", nil, create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = lru_get(\"key\", nil, create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = lru_get(\"key\", \"1\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = lru_get(\"key\", \"1\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = lru_get(\"key-different\", \"1\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n        }\n    }\n--- request\nGET /t\n--- response_body\nobj: {\"idx\":1}\nobj: {\"idx\":1}\nobj: {\"idx\":2}\nobj: {\"idx\":2}\nobj: {\"idx\":3}\n\n\n\n=== TEST 3: cache the non-table object, eg: number or string\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local idx = 0\n            local function create_num()\n                idx = idx + 1\n                return idx\n            end\n\n            local obj = core.lrucache.global(\"key\", nil, create_num)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = core.lrucache.global(\"key\", nil, create_num)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            obj = core.lrucache.global(\"key\", \"1\", create_num)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n        }\n    }\n--- request\nGET /t\n--- response_body\nobj: 1\nobj: 1\nobj: 2\n\n\n\n=== TEST 4: invalid_stale = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local idx = 0\n            local function create_obj()\n                idx = idx + 1\n                return {idx = idx}\n            end\n\n            local lru_get = core.lrucache.new({\n                ttl = 0.1, count = 256, invalid_stale = true,\n            })\n\n            local obj = lru_get(\"key\", \"ver\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n            local obj = lru_get(\"key\", \"ver\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n\n            ngx.sleep(0.15)\n            local obj = lru_get(\"key\", \"ver\", create_obj)\n            ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n        }\n    }\n--- request\nGET /t\n--- response_body\nobj: {\"idx\":1}\nobj: {\"idx\":1}\nobj: {\"idx\":2}\n\n\n\n=== TEST 5: when creating cached objects, use resty-lock to avoid repeated creation.\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local idx = 0\n            local function create_obj()\n                idx = idx + 1\n                ngx.sleep(0.1)\n                return {idx = idx}\n            end\n\n            local lru_get = core.lrucache.new({\n                ttl = 1, count = 256, invalid_stale = true, serial_creating = true,\n            })\n\n            local function f()\n                local obj = lru_get(\"key\", \"ver\", create_obj)\n                ngx.say(\"obj: \", require(\"toolkit.json\").encode(obj))\n            end\n\n            ngx.thread.spawn(f)\n            ngx.thread.spawn(f)\n\n            ngx.sleep(0.3)\n        }\n    }\n--- request\nGET /t\n--- response_body\nobj: {\"idx\":1}\nobj: {\"idx\":1}\n\n\n\n=== TEST 6: different `key` and `ver`, cached same one table\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local item = {}\n            local idx = 0\n            local function create_obj()\n                idx = idx + 1\n                ngx.say(\"create obj \", idx, \" time\")\n                return item\n            end\n\n            local lru_get = core.lrucache.new({\n                ttl = 10, count = 256\n            })\n\n            local obj = lru_get(\"key\", \"ver\", create_obj)\n            ngx.say(\"fetch obj: \", obj == item)\n\n            obj = lru_get(\"key2\", \"ver2\", create_obj)\n            ngx.say(\"fetch obj: \", obj == item)\n\n            obj = lru_get(\"key\", \"ver\", create_obj)\n            ngx.say(\"fetch obj: \", obj == item)\n        }\n    }\n--- request\nGET /t\n--- response_body\ncreate obj 1 time\nfetch obj: true\ncreate obj 2 time\nfetch obj: true\nfetch obj: true\n"
  },
  {
    "path": "t/core/lrucache2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: negative cache basic functionality\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local call_count = 0\n            local function create_obj_fail()\n                call_count = call_count + 1\n                return nil, \"simulated failure\"\n            end\n\n            -- create LRU cache with negative caching\n            local lru_get = core.lrucache.new({\n                ttl = 1,\n                count = 256,\n                neg_ttl = 0.5,  -- shorter TTL for failures\n                neg_count = 128\n            })\n\n            -- First call should execute the function and cache the failure\n            local obj, err = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"call_count after first call: \", call_count)\n            ngx.say(\"first call result: obj=\", tostring(obj), \", err=\", tostring(err))\n\n            -- Second call should return from negative cache without calling create_obj_fail\n            obj, err = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"call_count after second call: \", call_count)\n            ngx.say(\"second call result: obj=\", tostring(obj), \", err=\", tostring(err))\n\n            -- Different version should bypass negative cache\n            obj, err = lru_get(\"fail_key\", \"v2\", create_obj_fail)\n            ngx.say(\"call_count after different version: \", call_count)\n            ngx.say(\"different version result: obj=\", tostring(obj), \", err=\", tostring(err))\n        }\n    }\n--- request\nGET /t\n--- response_body\ncall_count after first call: 1\nfirst call result: obj=nil, err=simulated failure\ncall_count after second call: 1\nsecond call result: obj=nil, err=simulated failure\ncall_count after different version: 2\ndifferent version result: obj=nil, err=simulated failure\n\n\n\n=== TEST 2: negative cache TTL expiration\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local call_count = 0\n            local function create_obj_fail()\n                call_count = call_count + 1\n                return nil, \"simulated failure\"\n            end\n\n            -- Create LRU cache with very short negative TTL\n            local lru_get = core.lrucache.new({\n                ttl = 10,\n                count = 256,\n                neg_ttl = 0.1,  -- very short TTL for failures\n                neg_count = 128\n            })\n\n            -- First call\n            local obj, err = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"call_count after first call: \", call_count)\n\n            -- Immediate second call - should use negative cache\n            obj, err = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"call_count after immediate call: \", call_count)\n\n            -- Wait for negative cache to expire\n            ngx.sleep(0.15)\n\n            -- This should call create_obj_fail again\n            obj, err = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"call_count after TTL expiration: \", call_count)\n        }\n    }\n--- request\nGET /t\n--- response_body\ncall_count after first call: 1\ncall_count after immediate call: 1\ncall_count after TTL expiration: 2\n\n\n\n=== TEST 3: mixed success and failure caching\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local success_count = 0\n            local fail_count = 0\n\n            local function create_obj_success()\n                success_count = success_count + 1\n                return {value = \"success_\" .. success_count}\n            end\n\n            local function create_obj_fail()\n                fail_count = fail_count + 1\n                return nil, \"failure_\" .. fail_count\n            end\n\n            local lru_get = core.lrucache.new({\n                ttl = 1,\n                count = 256,\n                neg_ttl = 0.5,\n                neg_count = 128\n            })\n\n            -- Test success caching\n            local obj1 = lru_get(\"success_key\", \"v1\", create_obj_success)\n            ngx.say(\"success_count after first success: \", success_count)\n            ngx.say(\"success value: \", obj1.value)\n\n            local obj2 = lru_get(\"success_key\", \"v1\", create_obj_success)\n            ngx.say(\"success_count after cached success: \", success_count)\n            ngx.say(\"cached success value: \", obj2.value)\n\n            -- Test failure caching\n            local obj3, err3 = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"fail_count after first failure: \", fail_count)\n            ngx.say(\"failure error: \", err3)\n\n            local obj4, err4 = lru_get(\"fail_key\", \"v1\", create_obj_fail)\n            ngx.say(\"fail_count after cached failure: \", fail_count)\n            ngx.say(\"cached failure error: \", err4)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess_count after first success: 1\nsuccess value: success_1\nsuccess_count after cached success: 1\ncached success value: success_1\nfail_count after first failure: 1\nfailure error: failure_1\nfail_count after cached failure: 1\ncached failure error: failure_1\n\n\n\n=== TEST 4: negative cache with different keys\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local call_count = 0\n            local function create_obj_fail(key)\n                call_count = call_count + 1\n                return nil, \"failed for \" .. key\n            end\n\n            local lru_get = core.lrucache.new({\n                ttl = 1,\n                count = 256,\n                neg_ttl = 0.5,\n                neg_count = 128\n            })\n\n            -- First key\n            local obj1, err1 = lru_get(\"key1\", \"v1\", create_obj_fail, \"key1\")\n            ngx.say(\"call_count after key1: \", call_count)\n\n            -- Second key\n            local obj2, err2 = lru_get(\"key2\", \"v1\", create_obj_fail, \"key2\")\n            ngx.say(\"call_count after key2: \", call_count)\n\n            -- Repeat key1 - should use negative cache\n            local obj3, err3 = lru_get(\"key1\", \"v1\", create_obj_fail, \"key1\")\n            ngx.say(\"call_count after key1 repeat: \", call_count)\n            ngx.say(\"key1 error: \", err3)\n\n            -- Repeat key2 - should use negative cache\n            local obj4, err4 = lru_get(\"key2\", \"v1\", create_obj_fail, \"key2\")\n            ngx.say(\"call_count after key2 repeat: \", call_count)\n            ngx.say(\"key2 error: \", err4)\n        }\n    }\n--- request\nGET /t\n--- response_body\ncall_count after key1: 1\ncall_count after key2: 2\ncall_count after key1 repeat: 2\nkey1 error: failed for key1\ncall_count after key2 repeat: 2\nkey2 error: failed for key2\n\n\n\n=== TEST 5: negative cache respects version changes\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            local call_count = 0\n            local function create_obj_fail(version)\n                call_count = call_count + 1\n                return nil, \"failed for version \" .. version\n            end\n\n            local lru_get = core.lrucache.new({\n                ttl = 10,\n                count = 256,\n                neg_ttl = 10,\n                neg_count = 128\n            })\n\n            -- Call with version 1\n            local obj1, err1 = lru_get(\"version_key\", \"v1\", create_obj_fail, \"v1\")\n            ngx.say(\"call_count after v1: \", call_count)\n\n            -- Call with version 1 again - should use negative cache\n            local obj2, err2 = lru_get(\"version_key\", \"v1\", create_obj_fail, \"v1\")\n            ngx.say(\"call_count after v1 repeat: \", call_count)\n\n            -- Call with version 2 - should bypass negative cache\n            local obj3, err3 = lru_get(\"version_key\", \"v2\", create_obj_fail, \"v2\")\n            ngx.say(\"call_count after v2: \", call_count)\n\n            -- Call with version 2 again - should use negative cache\n            local obj4, err4 = lru_get(\"version_key\", \"v2\", create_obj_fail, \"v2\")\n            ngx.say(\"call_count after v2 repeat: \", call_count)\n        }\n    }\n--- request\nGET /t\n--- response_body\ncall_count after v1: 1\ncall_count after v1 repeat: 1\ncall_count after v2: 2\ncall_count after v2 repeat: 2\n"
  },
  {
    "path": "t/core/os.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: setenv\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            core.os.setenv(\"TEST\", \"A\")\n            ngx.say(os.getenv(\"TEST\"))\n            core.os.setenv(\"TEST\", 1)\n            ngx.say(os.getenv(\"TEST\"))\n        }\n    }\n--- response_body\nA\n1\n\n\n\n=== TEST 2: setenv, bad arguments\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            for _, c in ipairs({\n                {name = \"A\"},\n                {value = \"A\"},\n                {name = 1, value = \"A\"},\n            }) do\n                local ok = core.os.setenv(c.name, c.value)\n                ngx.say(ok)\n            end\n        }\n    }\n--- response_body\nfalse\nfalse\nfalse\n\n\n\n=== TEST 3: usleep, bad arguments\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            for _, c in ipairs({\n                {us = 0.1},\n            }) do\n                local ok = pcall(core.os.usleep, c.us)\n                ngx.say(ok)\n            end\n        }\n    }\n--- response_body\nfalse\n"
  },
  {
    "path": "t/core/profile.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{APISIX_PROFILE} = \"dev\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set env \"APISIX_PROFILE\"\n--- request\nGET /t\n--- error_code: 404\n\n\n\n=== TEST 2: set env \"APISIX_PROFILE\" to Empty String\n--- config\n    location /t {\n        content_by_lua_block {\n            local profile = require(\"apisix.core.profile\")\n            profile.apisix_home = \"./test/\"\n            profile.profile = \"\"\n            local local_conf_path = profile:yaml_path(\"config\")\n            ngx.say(local_conf_path)\n        }\n    }\n--- request\nGET /t\n--- response_body\n./test/conf/config.yaml\n"
  },
  {
    "path": "t/core/random.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nworkers(4);\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: generate different random number in different worker process\n--- config\n    location /t {\n        content_by_lua_block {\n            local log_file = ngx.config.prefix() .. \"logs/error.log\"\n            local file = io.open(log_file, \"r\")\n            local log = file:read(\"*a\")\n\n            local it, err = ngx.re.gmatch(log, [[random test in \\[1, 10000\\]: (\\d+)]], \"jom\")\n            if not it then\n                ngx.log(ngx.ERR, \"failed to gmatch: \", err)\n                return\n            end\n\n            local random_nums = {}\n            while true do\n                local m, err = it()\n                if err then\n                    ngx.log(ngx.ERR, \"error: \", err)\n                    return\n                end\n\n                if not m then\n                    break\n                end\n\n                -- found a match\n                table.insert(random_nums, m[1])\n            end\n\n            for i = 2, #random_nums do\n                local pre = random_nums[i - 1]\n                local cur = random_nums[i]\n                ngx.say(\"random[\", i - 1, \"] == random[\", i, \"]: \", pre == cur)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nrandom[1] == random[2]: false\nrandom[2] == random[3]: false\nrandom[3] == random[4]: false\nrandom[4] == random[5]: false\n"
  },
  {
    "path": "t/core/request.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: get_ip\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ip = core.request.get_ip(ngx.ctx.api_ctx)\n            ngx.say(ip)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\n127.0.0.1\n\n\n\n=== TEST 2: get_ip\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ip = core.request.get_ip(ngx.ctx.api_ctx)\n            ngx.say(ip)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\n127.0.0.1\n\n\n\n=== TEST 3: get_ip and X-Forwarded-For\n--- config\n    location /t {\n        real_ip_header X-Forwarded-For;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ip = core.request.get_ip(ngx.ctx.api_ctx)\n            ngx.say(ip)\n        }\n    }\n--- more_headers\nX-Forwarded-For: 10.0.0.1\n--- response_body\n127.0.0.1\n\n\n\n=== TEST 4: get_remote_client_ip\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ip = core.request.get_remote_client_ip(ngx.ctx.api_ctx)\n            ngx.say(ip)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\n10.0.0.1\n\n\n\n=== TEST 5: get_remote_client_ip and X-Forwarded-For\n--- config\n    location /t {\n        real_ip_header X-Forwarded-For;\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ip = core.request.get_remote_client_ip(ngx.ctx.api_ctx)\n            ngx.say(ip)\n        }\n    }\n--- more_headers\nX-Forwarded-For: 10.0.0.1\n--- response_body\n10.0.0.1\n\n\n\n=== TEST 6: get_host\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local host = core.request.get_host(ngx.ctx.api_ctx)\n            ngx.say(host)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\nlocalhost\n\n\n\n=== TEST 7: get_scheme\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local scheme = core.request.get_scheme(ngx.ctx.api_ctx)\n            ngx.say(scheme)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\nhttp\n\n\n\n=== TEST 8: get_port\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local port = core.request.get_port(ngx.ctx.api_ctx)\n            ngx.say(port)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\n1984\n\n\n\n=== TEST 9: get_http_version\n--- config\n    location /t {\n        real_ip_header X-Real-IP;\n\n        set_real_ip_from 0.0.0.0/0;\n        set_real_ip_from ::/0;\n        set_real_ip_from unix:;\n\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n        }\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local http_version = core.request.get_http_version()\n            ngx.say(http_version)\n        }\n    }\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- response_body\n1.1\n\n\n\n=== TEST 10: set header\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.ctx.api_ctx = {}\n            local h = core.request.header(nil, \"Test\")\n            local ctx = ngx.ctx.api_ctx\n            core.request.set_header(ctx, \"Test\", \"t\")\n            local h2 = core.request.header(ctx, \"Test\")\n            ngx.say(h)\n            ngx.say(h2)\n        }\n    }\n--- response_body\nnil\nt\n\n\n\n=== TEST 11: get_post_args\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n\n            local args = core.request.get_post_args(ngx.ctx.api_ctx)\n            ngx.say(args[\"c\"])\n            ngx.say(args[\"v\"])\n        }\n    }\n--- request\nPOST /t\nc=z_z&v=x%20x\n--- response_body\nz_z\nx x\n\n\n\n=== TEST 12: get_post_args when the body is stored in temp file\n--- config\n    location /t {\n        client_body_in_file_only clean;\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local ngx_ctx = ngx.ctx\n            local api_ctx = ngx_ctx.api_ctx\n            if api_ctx == nil then\n                api_ctx = core.tablepool.fetch(\"api_ctx\", 0, 32)\n                ngx_ctx.api_ctx = api_ctx\n            end\n\n            core.ctx.set_vars_meta(api_ctx)\n\n            local args = core.request.get_post_args(ngx.ctx.api_ctx)\n            ngx.say(args[\"c\"])\n        }\n    }\n--- request\nPOST /t\nc=z_z&v=x%20x\n--- response_body\nnil\n--- error_log\nthe post form is too large: request body in temp file not supported\n\n\n\n=== TEST 13: get_method\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.say(core.request.get_method())\n        }\n    }\n--- request\nPOST /t\n--- response_body\nPOST\n\n\n\n=== TEST 14: add header\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.ctx.api_ctx = {}\n            local ctx = ngx.ctx.api_ctx\n            local json = require(\"toolkit.json\")\n            core.request.add_header(ctx, \"test_header\", \"test\")\n            local h = core.request.header(ctx, \"test_header\")\n            ngx.say(h)\n            core.request.add_header(ctx, \"test_header\", \"t2\")\n            local h2 = core.request.headers(ctx)[\"test_header\"]\n            ngx.say(json.encode(h2))\n            core.request.add_header(ctx, \"test_header\", \"t3\")\n            local h3 = core.request.headers(ctx)[\"test_header\"]\n            ngx.say(json.encode(h3))\n        }\n    }\n--- response_body\ntest\n[\"test\",\"t2\"]\n[\"test\",\"t2\",\"t3\"]\n\n\n\n=== TEST 15: call add_header with deprecated way\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.ctx.api_ctx = {}\n            local ctx = ngx.ctx.api_ctx\n            core.request.add_header(\"test_header\", \"test\")\n            local h = core.request.header(ctx, \"test_header\")\n            ngx.say(h)\n        }\n    }\n--- response_body\ntest\n--- error_log\nDEPRECATED: use add_header(ctx, header_name, header_value) instead\n\n\n\n=== TEST 16: after setting the header, ctx.var can still access the correct value\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.ctx.api_ctx = {}\n            local ctx = ngx.ctx.api_ctx\n            core.ctx.set_vars_meta(ctx)\n\n            ctx.var.http_server = \"ngx\"\n            ngx.say(ctx.var.http_server)\n\n            core.request.set_header(ctx, \"server\",  \"test\")\n            ngx.say(ctx.var.http_server)\n\n            -- case-insensitive\n            core.request.set_header(ctx, \"Server\",  \"apisix\")\n            ngx.say(ctx.var.http_server)\n        }\n    }\n--- response_body\nngx\ntest\napisix\n"
  },
  {
    "path": "t/core/resolver.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: resolve host from /etc/hosts\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local resolver = require(\"apisix.core.resolver\")\n            local domain = \"localhost\"\n            local ip_info, err = resolver.parse_domain(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- response_body\nip_info: \"127.0.0.1\"\n\n\n\n=== TEST 2: resolve host from dns\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local resolver = require(\"apisix.core.resolver\")\n            local domain = \"apisix.apache.org\"\n            resolver.parse_domain = function(domain) -- mock: resolver parser\n\n                if domain == \"apisix.apache.org\" then\n                    return {address = \"127.0.0.2\" }\n                end\n                error(\"unknown domain: \" .. domain)\n            end\n            local ip_info, err = resolver.parse_domain(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- response_body\nip_info: {\"address\":\"127.0.0.2\"}\n\n\n\n=== TEST 3: there is no mapping in /etc/hosts and dns\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local resolver = require(\"apisix.core.resolver\")\n            local domain = \"abc1.test\"\n            resolver.parse_domain(domain)\n        }\n    }\n--- error_log\nfailed to parse domain\n\n\n\n=== TEST 4: test dns config with ipv6 enable\n--- yaml_config\napisix:\n  enable_ipv6: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local resolver = require(\"apisix.core.resolver\")\n            local domain = \"localhost6\"\n            resolver.parse_domain = function(domain)  -- mock: resolver parse_domain\n                 if domain == \"localhost6\" then\n                    return {address = \"::1\" }\n                 end\n                 error(\"unknown domain: \" .. domain)\n\n            end\n            local ip_info, err = resolver.parse_domain(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- response_body\nip_info: {\"address\":\"::1\"}\n\n\n\n=== TEST 5: test dns config with ipv6 disable\n--- yaml_config\napisix:\n  enable_ipv6: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local resolver = require(\"apisix.core.resolver\")\n            local domain = \"localhost6\"\n            local ip_info, err = resolver.parse_domain(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- error_log\nfailed to parse domain\n"
  },
  {
    "path": "t/core/response.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: exit with string\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.exit(201, \"done\\n\")\n        }\n    }\n--- request\nGET /t\n--- error_code: 201\n--- response_body\ndone\n\n\n\n=== TEST 2: exit with table\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.exit(201, {a = \"a\"})\n        }\n    }\n--- request\nGET /t\n--- error_code: 201\n--- response_body\n{\"a\":\"a\"}\n\n\n\n=== TEST 3: multiple response headers\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.set_header(\"aaa\", \"bbb\", \"ccc\", \"ddd\")\n            core.response.exit(200, \"done\\n\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- response_headers\naaa: bbb\nccc: ddd\n\n\n\n=== TEST 4: multiple response headers by table\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.set_header({aaa = \"bbb\", ccc = \"ddd\"})\n            core.response.exit(200, \"done\\n\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- response_headers\naaa: bbb\nccc: ddd\n\n\n\n=== TEST 5: multiple response headers (add)\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.add_header(\"aaa\", \"bbb\", \"aaa\", \"bbb\")\n            core.response.exit(200, \"done\\n\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- response_headers\naaa: bbb, bbb\n\n\n\n=== TEST 6: multiple response headers by table (add)\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.set_header({aaa = \"bbb\"})\n            core.response.add_header({aaa = \"bbb\", ccc = \"ddd\"})\n            core.response.exit(200, \"done\\n\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- response_headers\naaa: bbb, bbb\nccc: ddd\n\n\n\n=== TEST 7: delete header\n--- config\n    location = /t {\n        access_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.response.set_header(\"aaa\", \"bbb\")\n            core.response.set_header(\"aaa\", nil)\n            core.response.exit(200, \"done\\n\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- response_headers\naaa:\n\n\n\n=== TEST 8: hold_body_chunk (ngx.arg[2] == true and ngx.arg[1] ~= \"\")\n--- config\n    location = /t {\n        content_by_lua_block {\n            -- Nginx uses a separate buf to mark the end of the stream,\n            -- hence when ngx.arg[2] == true, ngx.arg[1] will be equal to \"\".\n            -- To avoid something unexpected, here we add a test to verify\n            -- this situation via mock.\n            local t = ngx.arg\n            local metatable = getmetatable(t)\n            local count = 0\n            setmetatable(t, {__index = function(t, idx)\n                if count == 0 then\n                    if idx == 1 then\n                        return \"hello \"\n                    end\n                    count = count + 1\n                    return false\n                end\n                if count == 1 then\n                    if idx == 1 then\n                        return \"world\\n\"\n                    end\n                    count = count + 1\n                    return true\n                end\n\n                return metatable.__index(t, idx)\n            end,\n            __newindex = metatable.__newindex})\n\n            -- trigger body_filter_by_lua_block\n            ngx.print(\"A\")\n        }\n        body_filter_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.ctx._plugin_name = \"test\"\n            local final_body = core.response.hold_body_chunk(ngx.ctx)\n            if not final_body then\n                return\n            end\n            ngx.arg[1] = final_body\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n"
  },
  {
    "path": "t/core/schema.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local schema = {\n                type = \"object\",\n                properties = {\n                    i = {type = \"number\", minimum = 0},\n                    s = {type = \"string\"},\n                    t = {type = \"array\", minItems = 1},\n                }\n            }\n\n            for i = 1, 10 do\n                local ok, err = core.schema.check(schema,\n                                    {i = i, s = \"s\" .. i, t = {i}})\n                assert(ok)\n                assert(err == nil)\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: same schema in different timer\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local schema = {\n                type = \"object\",\n                properties = {\n                    i = {type = \"number\", minimum = 0},\n                    s = {type = \"string\"},\n                    t = {type = \"array\", minItems = 1},\n                }\n            }\n\n            local count = 0\n            local function test()\n                for i = 1, 10 do\n                    local ok, err = core.schema.check(schema,\n                                        {i = i, s = \"s\" .. i, t = {i}})\n                    assert(ok)\n                    assert(err == nil)\n                    count = count + 1\n                end\n            end\n\n            ngx.timer.at(0, test)\n            ngx.timer.at(0, test)\n            ngx.timer.at(0, test)\n\n            ngx.sleep(1)\n            ngx.say(\"passed: \", count)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed: 30\n\n\n\n=== TEST 3: collectgarbage\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local schema = {\n                type = \"object\",\n                properties = {\n                    i = {type = \"number\", minimum = 0},\n                    s = {type = \"string\"},\n                    t = {type = \"array\", minItems = 1},\n                }\n            }\n\n            for i = 1, 1000 do\n                collectgarbage()\n                local ok, err = core.schema.check(schema,\n                                    {i = i, s = \"s\" .. i, t = {i}})\n                assert(ok)\n                assert(err == nil)\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 15\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: invalid schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local schema = {\n                type = \"invalid type\"\n            }\n\n            local ok, err = core.schema.check(schema, 11)\n            ngx.say(\"ok: \", ok, \" err: \", err)\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/ok: false err: .* invalid JSON type: invalid type/\n"
  },
  {
    "path": "t/core/schema_def.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: ip_def\n--- config\n    location /t {\n        content_by_lua_block {\n            local schema_def = require(\"apisix.schema_def\")\n            local core = require(\"apisix.core\")\n            local schema = {\n                type = \"object\",\n                properties = {\n                    ip = {\n                        type = \"string\",\n                        anyOf = schema_def.ip_def,\n                    }\n                },\n            }\n\n            local cases = {\n                \"127.0.0.1/1\",\n                \"127.0.0.1/10\",\n                \"127.0.0.1/11\",\n                \"127.0.0.1/20\",\n                \"127.0.0.1/21\",\n                \"127.0.0.1/30\",\n                \"127.0.0.1/32\",\n            }\n            for _, c in ipairs(cases) do\n                local ok, err = core.schema.check(schema, {ip = c})\n                assert(ok, c)\n                assert(err == nil, c)\n            end\n\n            local cases = {\n                \"127.0.0.1/33\",\n            }\n            for _, c in ipairs(cases) do\n                local ok, err = core.schema.check(schema, {ip = c})\n                assert(not ok, c)\n                assert(err ~= nil, c)\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Missing required fields of global_rule.\n--- config\n    location /t {\n        content_by_lua_block {\n            local schema_def = require(\"apisix.schema_def\")\n            local core = require(\"apisix.core\")\n\n            local cases = {\n                {},\n                { id = \"ADfwefq12D9s\" },\n                { id = 1 },\n                {\n                    plugins = {\n                        foo = \"bar\",\n                    },\n                },\n            }\n            for _, c in ipairs(cases) do\n                local ok, err = core.schema.check(schema_def.global_rule, c)\n                assert(not ok)\n                assert(err ~= nil)\n                ngx.say(\"ok: \", ok, \" err: \", err)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/ok: false err: property \"(id|plugins)\" is required/\n\n\n\n=== TEST 3: Sanity check with minimal valid configuration.\n--- config\n    location /t {\n        content_by_lua_block {\n            local schema_def = require(\"apisix.schema_def\")\n            local core = require(\"apisix.core\")\n\n            local case = {\n                id = 1,\n                plugins = {},\n            }\n\n            local ok, err = core.schema.check(schema_def.global_rule, case)\n            assert(ok)\n            assert(err == nil)\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: sanity check upstream_schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local schema_def = require(\"apisix.schema_def\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert_id = 1,\n                    client_cert = ssl_cert,\n                    client_key = ssl_key\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(not ok)\n            assert(err ~= nil)\n\n            upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert_id = 1\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(ok)\n            assert(err == nil, err)\n\n            upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert = ssl_cert,\n                    client_key = ssl_key\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(ok)\n            assert(err == nil, err)\n\n            upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(ok)\n            assert(err == nil, err)\n\n            upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert = ssl_cert\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(not ok)\n            assert(err ~= nil)\n\n            upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    client_cert_id = 1,\n                    client_key = ssl_key\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(not ok)\n            assert(err ~= nil)\n\n            upstream = {\n                nodes = {\n                    [\"127.0.0.1:8080\"] = 1\n                },\n                type = \"roundrobin\",\n                tls = {\n                    verify = false\n                }\n            }\n            local ok, err = core.schema.check(schema_def.upstream, upstream)\n            assert(ok)\n            assert(err == nil)\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: validate IPv6 address format in upstream nodes\n--- config\nlocation /t {\n    content_by_lua_block {\n        local schema_def = require(\"apisix.schema_def\")\n        local core = require(\"apisix.core\")\n        local upstream = require(\"apisix.upstream\")\n        local t = require(\"lib.test_admin\")\n        -- Test valid IPv6 addresses (enclosed in brackets)\n        local valid_cases = {\n            {\n                nodes = {\n                    {host = \"[::1]\", port = 80, weight = 1},\n                },\n                type = \"roundrobin\"\n            },\n            {\n                nodes = {\n                    {host = \"[2001:db8::1]\", port = 80, weight = 1},\n                },\n                type = \"roundrobin\"\n            }\n        }\n\n        for _, ups in ipairs(valid_cases) do\n            local ok, err = upstream.check_schema(ups)\n            if not ok then\n                ngx.log(ngx.ERR, \"Expected valid case failed: \", err)\n            end\n            assert(ok, \"Valid IPv6 case should pass: \" .. (err or \"\"))\n        end\n\n        -- Test invalid IPv6 addresses (not enclosed in brackets)\n        local invalid_cases = {\n            {\n                nodes = {\n                    {host = \"::1\", port = 80, weight = 1},\n                },\n                type = \"roundrobin\"\n            },\n            {\n                nodes = {\n                    {host = \"2001:db8::1\", port = 80, weight = 1},\n                },\n                type = \"roundrobin\"\n            }\n        }\n\n        for i, ups in ipairs(invalid_cases) do\n            local ok, err = upstream.check_schema(ups)\n            if ok then\n                ngx.log(ngx.ERR, \"Expected invalid case passed: \", i)\n            end\n            assert(not ok, \"Invalid IPv6 case should fail\")\n            assert(string.find(err, \"IPv6 address must be enclosed with '%[' and '%]'\"),\n                   \"Error should mention IPv6 enclosure requirement\")\n        end\n\n        ngx.say(\"passed\")\n    }\n}\n--- response_body\npassed\n"
  },
  {
    "path": "t/core/string.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: find\n--- config\n    location /t {\n        content_by_lua_block {\n            local encode = require \"toolkit.json\".encode\n            local str = require(\"apisix.core.string\")\n            local cases = {\n                {\"xx\", \"\", true},\n                {\"xx\", \"x\", true},\n                {\"\", \"x\", false},\n                {\"\", \"\", true},\n                {\"\", 0, false},\n                {0, \"x\", false},\n                {\"a[\", \"[\", true},\n\n                {\"[a\", \"[\", false, 2},\n                {\"[a\", \"[\", false, 3},\n                {\"[a\", \"[\", true, 1},\n            }\n            for _, case in ipairs(cases) do\n                local ok, idx = pcall(str.find, case[1], case[2], case[4])\n                if not ok then\n                    if case[3] == true then\n                        ngx.log(ngx.ERR, \"unexpected error: \", idx,\n                                \" \", encode(case))\n                    end\n                else\n                    if case[3] ~= (idx ~= nil) then\n                        ngx.log(ngx.ERR, \"unexpected res: \", idx,\n                                \" \", encode(case))\n                    end\n                end\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 2: prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local encode = require \"toolkit.json\".encode\n            local str = require(\"apisix.core.string\")\n            local cases = {\n                {\"xx\", \"\", true},\n                {\"xx\", \"x\", true},\n                {\"\", \"x\", false},\n                {\"\", \"\", true},\n                {\"\", 0, false},\n                {0, \"x\", false},\n                {\"a[\", \"[\", false},\n                {\"[a\", \"[\", true},\n                {\"[a\", \"[b\", false},\n            }\n            for _, case in ipairs(cases) do\n                local ok, res = pcall(str.has_prefix, case[1], case[2])\n                if not ok then\n                    if case[3] == true then\n                        ngx.log(ngx.ERR, \"unexpected error: \", res,\n                                \" \", encode(case))\n                    end\n                else\n                    if case[3] ~= res then\n                        ngx.log(ngx.ERR, \"unexpected res: \", res,\n                                \" \", encode(case))\n                    end\n                end\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 3: suffix\n--- config\n    location /t {\n        content_by_lua_block {\n            local encode = require \"toolkit.json\".encode\n            local str = require(\"apisix.core.string\")\n            local cases = {\n                {\"xx\", \"\", true},\n                {\"xx\", \"x\", true},\n                {\"\", \"x\", false},\n                {\"\", \"\", true},\n                {\"\", 0, false},\n                {0, \"x\", false},\n                {\"a[\", \"[\", true},\n                {\"[a\", \"[\", false},\n                {\"[a\", \"[b\", false},\n            }\n            for _, case in ipairs(cases) do\n                local ok, res = pcall(str.has_suffix, case[1], case[2])\n                if not ok then\n                    if case[3] == true then\n                        ngx.log(ngx.ERR, \"unexpected error: \", res,\n                                \" \", encode(case))\n                    end\n                else\n                    if case[3] ~= res then\n                        ngx.log(ngx.ERR, \"unexpected res: \", res,\n                                \" \", encode(case))\n                    end\n                end\n            end\n        }\n    }\n--- request\nGET /t\n"
  },
  {
    "path": "t/core/table.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = {\"first\"}\n            core.table.insert_tail(t, 'a', 1, true)\n\n            ngx.say(\"encode: \", require(\"toolkit.json\").encode(t))\n\n            core.table.set(t, 'a', 1, true)\n            ngx.say(\"encode: \", require(\"toolkit.json\").encode(t))\n        }\n    }\n--- request\nGET /t\n--- response_body\nencode: [\"first\",\"a\",1,true]\nencode: [\"a\",1,true,true]\n\n\n\n=== TEST 2: deepcopy\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local deepcopy = core.table.deepcopy\n            local cases = {\n                {t = {1, 2, a = {2, 3}}},\n                {t = {{a = b}, 2, true}},\n                {t = {{a = b}, {{a = c}, {}, 1}, true}},\n            }\n            for _, case in ipairs(cases) do\n                local t = case.t\n                local actual = core.json.encode(deepcopy(t))\n                local expect = core.json.encode(t)\n                if actual ~= expect then\n                    ngx.say(\"expect \", expect, \", actual \", actual)\n                    return\n                end\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n\n\n\n=== TEST 3: try_read_attr\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local try_read_attr = core.table.try_read_attr\n\n            local t = {level1 = {level2 = \"value\"}}\n\n            local v = try_read_attr(t, \"level1\", \"level2\")\n            ngx.say(v)\n\n            local v2 = try_read_attr(t, \"level1\", \"level3\")\n            ngx.say(v2)\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\nnil\n\n\n\n=== TEST 4: set_eq\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local cases = {\n                {expect = true, a = {}, b = {}},\n                {expect = true, a = {a = 1}, b = {a = 1}},\n                {expect = true, a = {a = 1}, b = {a = 2}},\n                {expect = false, a = {b = 1}, b = {a = 1}},\n                {expect = false, a = {a = 1, b = 1}, b = {a = 1}},\n                {expect = false, a = {a = 1}, b = {a = 1, b = 2}},\n            }\n            for _, t in ipairs(cases) do\n                local actual = core.table.set_eq(t.a, t.b)\n                local expect = t.expect\n                if actual ~= expect then\n                    ngx.say(\"expect \", expect, \", actual \", actual)\n                    return\n                end\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n--- request\nGET /t\n\n\n\n=== TEST 5: deep_eq\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local cases = {\n                {expect = true, a = {}, b = {}},\n                {expect = true, a = nil, b = nil},\n                {expect = false, a = nil, b = {}},\n                {expect = false, a = {}, b = nil},\n                {expect = true, a = {a = {b = 1}}, b = {a = {b = 1}}},\n                {expect = false, a = {a = {b = 1}}, b = {a = {b = 1, c = 2}}},\n                {expect = false, a = {a = {b = 1}}, b = {a = {b = 2}}},\n                {expect = true, a = {{a = {b = 1}}}, b = {{a = {b = 1}}}},\n            }\n            for _, t in ipairs(cases) do\n                local actual = core.table.deep_eq(t.a, t.b)\n                local expect = t.expect\n                if actual ~= expect then\n                    ngx.say(\"expect \", expect, \", actual \", actual)\n                    return\n                end\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n--- request\nGET /t\n\n\n\n=== TEST 6: pick\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local core = require(\"apisix.core\")\n            local cases = {\n                {expect = {}, a = {}, b = {priority = true}},\n                {expect = {priority = 1}, a = {priority = 1}, b = {priority = true}},\n                {expect = {}, a = {priorities = 1}, b = {priority = true}},\n                {expect = {priority = 1}, a = {priority = 1, ver = \"2\"}, b = {priority = true}},\n                {expect = {priority = 1, ver = \"2\"}, a = {priority = 1, ver = \"2\"}, b = {priority = true, ver = true}},\n            }\n            for _, t in ipairs(cases) do\n                local actual = core.table.pick(t.a, t.b)\n                local expect = t.expect\n                if not core.table.deep_eq(actual, expect) then\n                    ngx.say(\"expect \", json.encode(expect), \", actual \", json.encode(actual))\n                    return\n                end\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n--- request\nGET /t\n\n\n\n=== TEST 7: deepcopy should keep metatable\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local deepcopy = core.table.deepcopy\n            local t = setmetatable({}, core.json.array_mt)\n            local actual = core.json.encode(deepcopy(t))\n            local expect = \"[]\"\n            if actual ~= expect then\n                ngx.say(\"expect \", expect, \", actual \", actual)\n                return\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n\n\n\n=== TEST 8: deepcopy copy same table only once\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local tmp = { name = \"tmp\", priority = 1, enabled = true }\n            local origin = { a = { b = tmp }, c = tmp}\n            local copy = core.table.deepcopy(origin)\n            if not core.table.deep_eq(copy, origin) then\n                ngx.say(\"copy: \", json.encode(expect), \", origin: \", json.encode(actual))\n                return\n            end\n            if copy.a.b ~= copy.c then\n                ngx.say(\"copy.a.b should be the same as copy.c\")\n                return\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n\n\n\n=== TEST 9: reference same table\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local deepcopy = core.table.deepcopy\n            local tab1 = {name = \"tab1\"}\n            local tab2 = {\n                a = tab1,\n                b = tab1\n            }\n            local tab_copied = deepcopy(tab2)\n\n            ngx.say(\"table copied: \", require(\"toolkit.json\").encode(tab_copied))\n\n            ngx.say(\"tab1 == tab2.a: \", tab1 == tab2.a)\n            ngx.say(\"tab2.a == tab2.b: \", tab2.a == tab2.b)\n\n            ngx.say(\"tab_copied.a == tab1: \", tab_copied.a == tab1)\n            ngx.say(\"tab_copied.a == tab_copied.b: \", tab_copied.a == tab_copied.b)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntable copied: {\"a\":{\"name\":\"tab1\"},\"b\":{\"name\":\"tab1\"}}\ntab1 == tab2.a: true\ntab2.a == tab2.b: true\ntab_copied.a == tab1: false\ntab_copied.a == tab_copied.b: true\n\n\n\n=== TEST 10: reference table self(root node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local deepcopy = core.table.deepcopy\n            local tab1 = {name = \"tab1\"}\n            local tab2 = {\n                a = tab1,\n            }\n            tab2.c = tab2\n\n            local tab_copied = deepcopy(tab2)\n\n            ngx.say(\"tab_copied.a == tab1: \", tab_copied.a == tab_copied.b)\n            ngx.say(\"tab_copied == tab_copied.c: \", tab_copied == tab_copied.c)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntab_copied.a == tab1: false\ntab_copied == tab_copied.c: true\n\n\n\n=== TEST 11: reference table self(sub node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local deepcopy = core.table.deepcopy\n            local tab_org = {\n                a = {\n                    a2 = \"a2\"\n                },\n            }\n            tab_org.b = tab_org.a\n\n            local tab_copied = deepcopy(tab_org)\n            ngx.say(\"table copied: \", require(\"toolkit.json\").encode(tab_copied))\n            ngx.say(\"tab_copied.a == tab_copied.b: \", tab_copied.a == tab_copied.b)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntable copied: {\"a\":{\"a2\":\"a2\"},\"b\":{\"a2\":\"a2\"}}\ntab_copied.a == tab_copied.b: true\n\n\n\n=== TEST 12: shallow copy\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local deepcopy = core.table.deepcopy\n            local t1 = {name = \"tab1\"}\n            local t2 = {name = \"tab2\"}\n            local tab = {\n                a = {b = {c = t1}},\n                x = {y = t2},\n            }\n            local tab_copied = deepcopy(tab, { shallows = { \"self.a.b.c\" }})\n\n            ngx.say(\"table copied: \", require(\"toolkit.json\").encode(tab_copied))\n\n            ngx.say(\"tab_copied.a.b.c == tab.a.b.c1: \", tab_copied.a.b.c == tab.a.b.c)\n            ngx.say(\"tab_copied.a.b.c == t1: \", tab_copied.a.b.c == t1)\n            ngx.say(\"tab_copied.x.y == tab.x.y: \", tab_copied.x.y == tab.x.y)\n            ngx.say(\"tab_copied.x.y == t2: \", tab_copied.x.y == t2)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntable copied: {\"a\":{\"b\":{\"c\":{\"name\":\"tab1\"}}},\"x\":{\"y\":{\"name\":\"tab2\"}}}\ntab_copied.a.b.c == tab.a.b.c1: true\ntab_copied.a.b.c == t1: true\ntab_copied.x.y == tab.x.y: false\ntab_copied.x.y == t2: false\n"
  },
  {
    "path": "t/core/timer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local function job()\n                core.log.warn(\"job enter\")\n                ngx.sleep(0.5)\n                core.log.warn(\"job exit\")\n            end\n\n            local ok = core.timer.new(\"test job\", job,\n                {each_ttl = 2, check_interval = 0.1})\n            ngx.say(\"create timer: \", type(ok))\n            ngx.sleep(3)\n        }\n    }\n--- request\nGET /t\n--- response_body\ncreate timer: table\n--- grep_error_log eval\nqr/job (enter|exit)/\n--- grep_error_log_out eval\nqr/(job enter\\njob exit)+/\n--- timeout: 5\n"
  },
  {
    "path": "t/core/trusted-addresses.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: without trusted_addresses configuration, X-Forwarded headers should be overridden\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: localhost\nx-forwarded-port: 1984\nx-forwarded-proto: http\nx-real-ip: 127.0.0.1\n--- error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n\n\n\n=== TEST 2: with IP, X-Forwarded headers should be preserved from trusted client\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"127.0.0.1\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: example.com\nx-forwarded-port: 8443\nx-forwarded-proto: https\nx-real-ip: 127.0.0.1\n--- no_error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n\n\n\n=== TEST 3: with multiple IPs, X-Forwarded headers should be preserved from trusted client\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"127.0.0.1\"\n        - \"127.0.0.2\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: example.com\nx-forwarded-port: 8443\nx-forwarded-proto: https\nx-real-ip: 127.0.0.1\n--- no_error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n\n\n\n=== TEST 4: with CIDR, X-Forwarded headers should be preserved from trusted client\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"127.0.0.0/24\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: example.com\nx-forwarded-port: 8443\nx-forwarded-proto: https\nx-real-ip: 127.0.0.1\n--- no_error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n\n\n\n=== TEST 5: with multiple CIDRs, X-Forwarded headers should be preserved from trusted client\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"127.0.0.0/24\"\n        - \"1.1.1.0/24\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: example.com\nx-forwarded-port: 8443\nx-forwarded-proto: https\nx-real-ip: 127.0.0.1\n--- no_error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n\n\n\n=== TEST 6: with multiple IPs and CIDRs, X-Forwarded headers should be preserved from trusted client\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"127.0.0.0/24\"\n        - \"1.1.1.0/24\"\n        - \"127.0.0.1\"\n        - \"1.1.1.1\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: example.com\nx-forwarded-port: 8443\nx-forwarded-proto: https\nx-real-ip: 127.0.0.1\n--- no_error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n\n\n\n=== TEST 7: with `0.0.0.0/0`, X-Forwarded headers should be preserved from trusted client\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"0.0.0.0/0\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: example.com\nx-forwarded-port: 8443\nx-forwarded-proto: https\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 8: with trusted_addresses configuration, but client not in trusted list, X-Forwarded headers should be overridden\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    trusted_addresses:\n        - \"1.0.0.1\"\n        - \"10.0.0.0/8\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /old_uri\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /old_uri\n--- more_headers\nX-Forwarded-Proto: https\nX-Forwarded-Host: example.com\nX-Forwarded-Port: 8443\n--- response_body\nuri: /old_uri\nhost: localhost\nx-forwarded-for: 127.0.0.1\nx-forwarded-host: localhost\nx-forwarded-port: 1984\nx-forwarded-proto: http\nx-real-ip: 127.0.0.1\n--- no_error_log\ntrusted_addresses is not configured\ntrusted_addresses_matcher is not initialized\n"
  },
  {
    "path": "t/core/uid.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n\n            ngx.say(\"uid: \", core.id.get())\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/uid: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/\n--- error_log\nnot found apisix uid, generate a new one\n"
  },
  {
    "path": "t/core/utils.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local get_seed = require(\"apisix.core.utils\").get_seed_from_urandom\n\n            ngx.say(\"random seed \", get_seed())\n            ngx.say(\"twice: \", get_seed() == get_seed())\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/random seed \\d+(\\.\\d+)?(e\\+\\d+)?\\ntwice: false/\n\n\n\n=== TEST 2: parse_addr\n--- config\n    location /t {\n        content_by_lua_block {\n            local parse_addr = require(\"apisix.core.utils\").parse_addr\n            local cases = {\n                {addr = \"127.0.0.1\", host = \"127.0.0.1\"},\n                {addr = \"127.0.0.1:90\", host = \"127.0.0.1\", port = 90},\n                {addr = \"www.test.com\", host = \"www.test.com\"},\n                {addr = \"www.test.com:90\", host = \"www.test.com\", port = 90},\n                {addr = \"localhost\", host = \"localhost\"},\n                {addr = \"localhost:90\", host = \"localhost\", port = 90},\n                {addr = \"[127.0.0.1:90\", host = \"[127.0.0.1:90\"},\n                {addr = \"[::1]\", host = \"[::1]\"},\n                {addr = \"[::1]:1234\", host = \"[::1]\", port = 1234},\n                {addr = \"[::1234:1234]:12345\", host = \"[::1234:1234]\", port = 12345},\n                {addr = \"::1\", host = \"::1\"},\n            }\n            for _, case in ipairs(cases) do\n                local host, port = parse_addr(case.addr)\n                assert(host == case.host, string.format(\"host %s mismatch %s\", host, case.host))\n                assert(port == case.port, string.format(\"port %s mismatch %s\", port, case.port))\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 3: specify resolvers\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local resolvers = {\"8.8.8.8\"}\n            core.utils.set_resolver(resolvers)\n            local domain = \"github.com\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n            end\n            ngx.say(require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/\"address\":.+,\"name\":\"github.com\"/\n\n\n\n=== TEST 4: default resolvers\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local domain = \"github.com\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n            end\n            core.log.info(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n            ngx.say(\"resolvers: \", require(\"toolkit.json\").encode(core.utils.get_resolver()))\n        }\n    }\n--- request\nGET /t\n--- response_body\nresolvers: [\"8.8.8.8\",\"114.114.114.114\"]\n--- error_log eval\nqr/\"address\":.+,\"name\":\"github.com\"/\n\n\n\n=== TEST 5: enable_server_tokens false\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_server_tokens: false\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n             [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(\"failed\")\n            return\n        end\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"127.0.0.1\", 1984)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: www.test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n    }\n}\n--- request\nGET /t\n--- response_body eval\nqr{connected: 1\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX\nreceived: \\nreceived: hello world\nclose: 1 nil}\n\n\n\n=== TEST 6: resolve_var\n--- config\n    location /t {\n        content_by_lua_block {\n            local resolve_var = require(\"apisix.core.utils\").resolve_var\n            local cases = {\n                \"\",\n                \"xx\",\n                \"$me\",\n                \"$me run\",\n                \"talk with $me\",\n                \"tell $me to\",\n                \"$you and $me\",\n                \"$eva and $me\",\n                \"$you and \\\\$me\",\n                \"${you}_${me}\",\n                \"${you}${me}\",\n                \"${you}$me\",\n                \"${you??}$me\",\n                \"${you??Rose}$me\",\n                \"${she??Rose}$me\",\n                \"${she ??Rose}$me\",\n                \"${she?? Rose}$me\",\n                \"${she ?? Rose}$me\",\n                \"${she   ??     Rose}$me\",\n                \"${ she ?? Rose }$me\",\n                \"${you ?? Rose}$he??\",\n                \"${you ?? Rose}$he??Jack\",\n            }\n            local ctx = {\n                you = \"John\",\n                me = \"David\",\n            }\n            for _, case in ipairs(cases) do\n                local res = resolve_var(case, ctx)\n                ngx.say(\"res:\", res)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nres:\nres:xx\nres:David\nres:David run\nres:talk with David\nres:tell David to\nres:John and David\nres: and David\nres:John and \\$me\nres:John_David\nres:JohnDavid\nres:JohnDavid\nres:JohnDavid\nres:JohnDavid\nres:RoseDavid\nres:RoseDavid\nres:RoseDavid\nres:RoseDavid\nres:RoseDavid\nres:RoseDavid\nres:John??\nres:John??Jack\n\n\n\n=== TEST 7: resolve host from /etc/hosts\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local domain = \"test.com\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- request\nGET /t\n--- response_body\nip_info: {\"address\":\"127.0.0.1\",\"class\":1,\"name\":\"test.com\",\"ttl\":315360000,\"type\":1}\n\n\n\n=== TEST 8: search host with '.org' suffix\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_resolv_search_opt: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local domain = \"apisix\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- request\nGET /t\n--- response_body_like\n.+\"name\":\"apisix\\.apache\\.org\".+\n\n\n\n=== TEST 9: disable search option\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_resolv_search_opt: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local domain = \"apisix\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- request\nGET /t\n--- error_log\nerror: failed to query the DNS server\n--- timeout: 10\n\n\n\n=== TEST 10: test dns config with ipv6 enable\n--- yaml_config\napisix:\n  enable_ipv6: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local domain = \"ipv6.local\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- request\nGET /t\n--- response_body\nip_info: {\"address\":\"[::1]\",\"class\":1,\"name\":\"ipv6.local\",\"ttl\":315360000,\"type\":28}\n\n\n\n=== TEST 11: test dns config with ipv6 disable\n--- yaml_config\napisix:\n  enable_ipv6: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local domain = \"ipv6.local\"\n            local ip_info, err = core.utils.dns_parse(domain)\n            if not ip_info then\n                core.log.error(\"failed to parse domain: \", domain, \", error: \",err)\n                return\n            end\n            ngx.say(\"ip_info: \", require(\"toolkit.json\").encode(ip_info))\n        }\n    }\n--- request\nGET /t\n--- error_log\nfailed to parse domain: ipv6.local\n\n\n\n=== TEST 12: get_last_index\n--- config\n    location /t {\n        content_by_lua_block {\n            local string_rfind = require(\"pl.stringx\").rfind\n            local cases = {\n                {\"you are welcome\", \"co\"},\n                {\"nice to meet you\", \"meet\"},\n                {\"chicken run\", \"cc\"},\n                {\"day day up\", \"day\"},\n                {\"happy new year\", \"e\"},\n                {\"apisix__1928\", \"__\"}\n            }\n\n            for _, case in ipairs(cases) do\n                local res = string_rfind(case[1], case[2])\n                ngx.say(\"res:\", res)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nres:12\nres:9\nres:nil\nres:5\nres:12\nres:7\n\n\n\n=== TEST 13: gethostname\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local hostname = core.utils.gethostname()\n            ngx.say(\"hostname: \", hostname)\n            local hostname2 = core.utils.gethostname()\n            ngx.say(\"hostname cached: \", hostname == hostname2)\n            ngx.say(\"hostname valid: \", hostname ~= \"\")\n\n            local handle = io.popen(\"/bin/hostname\")\n            if handle then\n                local system_hostname = handle:read(\"*a\")\n                handle:close()\n                if system_hostname then\n                    system_hostname = string.gsub(system_hostname, \"\\n$\", \"\")\n                    ngx.say(\"system hostname: \", system_hostname)\n                    ngx.say(\"hostname match: \", hostname == system_hostname)\n                else\n                    ngx.say(\"system hostname: failed to read\")\n                    ngx.say(\"hostname match: unable to verify\")\n                end\n            else\n                ngx.say(\"system hostname: failed to execute /bin/hostname\")\n                ngx.say(\"hostname match: unable to verify\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body_like\nhostname: .+\nhostname cached: true\nhostname valid: true\nsystem hostname: .+\nhostname match: true\n"
  },
  {
    "path": "t/coredns/Corefile",
    "content": "test.local {\n    file db.test.local\n    log\n}\n"
  },
  {
    "path": "t/coredns/db.test.local",
    "content": "$ORIGIN test.local.\n@\t3600 IN\tSOA sns.dns.icann.org. noc.dns.icann.org. (\n\t\t\t\t2017042745 ; serial\n\t\t\t\t7200       ; refresh (2 hours)\n\t\t\t\t3600       ; retry (1 hour)\n\t\t\t\t1209600    ; expire (2 weeks)\n\t\t\t\t3600       ; minimum (1 hour)\n\t\t\t\t)\n\n    3600 IN NS a.iana-servers.net.\n    3600 IN NS b.iana-servers.net.\n\n\nsd          IN A     127.0.0.1\nsd          IN A     127.0.0.2\nipv6.sd     IN AAAA  ::1\nmix.sd      IN A     127.0.0.1\nmix.sd      IN AAAA  ::1\n\nipv6     IN AAAA  ::1\n\nttl 300  IN A     127.0.0.1\nttl.1s 1  IN A     127.0.0.1\n\n; SRV\nA          IN A     127.0.0.1\nB          IN A     127.0.0.2\nC          IN A     127.0.0.3\nC          IN A     127.0.0.4\n; RFC 2782 style\n_sip._tcp.srv   86400 IN    SRV 10       60     1980 A\n_sip._tcp.srv   86400 IN    SRV 10       20     1980 B\n; standard style\nsrv   86400 IN    SRV 10       60     1980 A\nsrv   86400 IN    SRV 10       20     1980 B\n\nport.srv   86400 IN    SRV 10       60     1980 A\nport.srv   86400 IN    SRV 10       20     1981 B\n\nzero-weight.srv   86400 IN    SRV 10       60     1980 A\nzero-weight.srv   86400 IN    SRV 10       0      1980 B\n\nsplit-weight.srv   86400 IN    SRV 10      100   1980 A\nsplit-weight.srv   86400 IN    SRV 10      0     1980 C\n\npriority.srv   86400 IN    SRV 10       60     1979 A\npriority.srv   86400 IN    SRV 20       60     1980 B\n\nzero.srv       86400 IN    SRV 10       60     0    A\n\n; a domain has both SRV & A records\nsrv-a   86400 IN    SRV 10       60     1980 A\nsrv-a         IN    A   127.0.0.1\n"
  },
  {
    "path": "t/debug/debug-mode.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nour $debug_config = t::APISIX::read_file(\"conf/debug.yaml\");\n$debug_config =~ s/basic:\\n  enable: false/basic:\\n  enable: true/;\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: loaded plugin\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nloaded plugin and sort by priority: 23000 name: real-ip\nloaded plugin and sort by priority: 22000 name: client-control\nloaded plugin and sort by priority: 12015 name: request-id\nloaded plugin and sort by priority: 12011 name: zipkin\nloaded plugin and sort by priority: 12000 name: ext-plugin-pre-req\nloaded plugin and sort by priority: 11000 name: fault-injection\nloaded plugin and sort by priority: 10000 name: serverless-pre-function\nloaded plugin and sort by priority: 4000 name: cors\nloaded plugin and sort by priority: 3000 name: ip-restriction\nloaded plugin and sort by priority: 2990 name: referer-restriction\nloaded plugin and sort by priority: 2900 name: uri-blocker\nloaded plugin and sort by priority: 2800 name: request-validation\nloaded plugin and sort by priority: 2600 name: multi-auth\nloaded plugin and sort by priority: 2599 name: openid-connect\nloaded plugin and sort by priority: 2555 name: wolf-rbac\nloaded plugin and sort by priority: 2530 name: hmac-auth\nloaded plugin and sort by priority: 2520 name: basic-auth\nloaded plugin and sort by priority: 2510 name: jwt-auth\nloaded plugin and sort by priority: 2500 name: key-auth\nloaded plugin and sort by priority: 2400 name: consumer-restriction\nloaded plugin and sort by priority: 2000 name: authz-keycloak\nloaded plugin and sort by priority: 1085 name: proxy-cache\nloaded plugin and sort by priority: 1010 name: proxy-mirror\nloaded plugin and sort by priority: 1008 name: proxy-rewrite\nloaded plugin and sort by priority: 1005 name: api-breaker\nloaded plugin and sort by priority: 1003 name: limit-conn\nloaded plugin and sort by priority: 1002 name: limit-count\nloaded plugin and sort by priority: 1001 name: limit-req\nloaded plugin and sort by priority: 995 name: gzip\nloaded plugin and sort by priority: 966 name: traffic-split\nloaded plugin and sort by priority: 900 name: redirect\nloaded plugin and sort by priority: 899 name: response-rewrite\nloaded plugin and sort by priority: 506 name: grpc-transcode\nloaded plugin and sort by priority: 500 name: prometheus\nloaded plugin and sort by priority: 412 name: echo\nloaded plugin and sort by priority: 410 name: http-logger\nloaded plugin and sort by priority: 406 name: sls-logger\nloaded plugin and sort by priority: 405 name: tcp-logger\nloaded plugin and sort by priority: 403 name: kafka-logger\nloaded plugin and sort by priority: 402 name: rocketmq-logger\nloaded plugin and sort by priority: 401 name: syslog\nloaded plugin and sort by priority: 400 name: udp-logger\nloaded plugin and sort by priority: 398 name: clickhouse-logger\nloaded plugin and sort by priority: 0 name: example-plugin\nloaded plugin and sort by priority: -2000 name: serverless-post-function\nloaded plugin and sort by priority: -3000 name: ext-plugin-post-req\n\n\n\n=== TEST 2: set route(no plugin)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: hit routes\n--- debug_config eval: $::debug_config\n--- request\nGET /hello\n--- response_body\nhello world\n--- response_headers\nApisix-Plugins: no plugin\n\n\n\n=== TEST 4: set route(one plugin)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            },\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit routes\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local ngx_re = require(\"ngx.re\")\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            local debug_header = res.headers[\"Apisix-Plugins\"]\n            local arr = ngx_re.split(debug_header, \", \")\n            local hash = {}\n            for i, v in ipairs(arr) do\n                hash[v] = true\n            end\n            ngx.status = res.status\n            ngx.say(json.encode(hash))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"limit-conn\":true,\"limit-count\":true}\n\n\n\n=== TEST 6: global rule, header sent\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"status_code\": 200,\n                            \"body\": \"yes\\n\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit routes\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local ngx_re = require(\"ngx.re\")\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                })\n            local debug_header = res.headers[\"Apisix-Plugins\"]\n            local arr = ngx_re.split(debug_header, \", \")\n            local hash = {}\n            for i, v in ipairs(arr) do\n                hash[v] = true\n            end\n            ngx.status = res.status\n            ngx.say(json.encode(hash))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"limit-conn\":true,\"limit-count\":true,\"response-rewrite\":true}\n--- error_log\nApisix-Plugins: response-rewrite\n\n\n\n=== TEST 8: clear global routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: set stream route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1995,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route\n--- debug_config eval: $::debug_config\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n--- error_log\nmqtt client id: foo while prereading client data\n"
  },
  {
    "path": "t/debug/dynamic-hook.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $debug_config = t::APISIX::read_file(\"conf/debug.yaml\");\n$debug_config =~ s/http_filter:\\n  enable: false/http_filter:\\n  enable: true/;\n$debug_config =~ s/hook_conf:\\n  enable: false/hook_conf:\\n  enable: true/;\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: dynamic enable\n# ai module would conflict with the debug module\n--- extra_yaml_config\nplugins:\n    #- ai\n    - example-plugin\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n\n            local headers = {}\n            headers[\"X-APISIX-Dynamic-Debug\"] = \"\"\n            local code, body = t('/hello',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                headers\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- wait: 2\n--- response_body\npassed\n--- error_log\ncall require(\"apisix\").http_header_filter_phase() args:{}\ncall require(\"apisix\").http_header_filter_phase() return:{}\ncall require(\"apisix\").http_body_filter_phase() args:{}\ncall require(\"apisix\").http_body_filter_phase() return:{}\ncall require(\"apisix\").http_log_phase() args:{}\ncall require(\"apisix\").http_log_phase() return:{}\n--- no_error_log\ncall require(\"apisix\").http_access_phase() return:{}\ncall require(\"apisix\").http_access_phase() args:{}\n\n\n\n=== TEST 2: dynamic enable by per request and disable after handle request\n# ai module would conflict with the debug module\n--- extra_yaml_config\nplugins:\n    #- ai\n    - example-plugin\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uris\": [\"/hello\",\"/hello1\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local headers = {}\n            headers[\"X-APISIX-Dynamic-Debug\"] = \"\"\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello1\"\n            local res, err = httpc:request_uri(uri1, {method = \"GET\", headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                         .. \"/hello\"\n            res, err = httpc:request_uri(uri2)\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- wait: 2\n--- response_body\nhello world\n--- error_log eval\n[qr/call\\srequire\\(\\\"apisix\\\"\\).http_header_filter_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_header_filter_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_body_filter_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_body_filter_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_log_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_log_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/]\n--- no_error_log eval\n[qr/call\\srequire\\(\\\"apisix\\\"\\).http_access_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello1\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_access_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_access_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_header_filter_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_header_filter_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_body_filter_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_body_filter_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_log_phase\\(\\)\\sargs\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/,\nqr/call\\srequire\\(\\\"apisix\\\"\\).http_log_phase\\(\\)\\sreturn\\:\\{\\}.*GET\\s\\/hello\\sHTTP\\/1.1/]\n\n\n\n=== TEST 3: error dynamic enable header\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n\n            local headers = {}\n            headers[\"X-APISIX-Dynamic-Error\"] = \"\"\n            local code, body = t('/hello',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                headers\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- wait: 2\n--- response_body\npassed\n--- no_error_log\ncall require(\"apisix\").http_header_filter_phase() args:{}\ncall require(\"apisix\").http_header_filter_phase() return:{}\ncall require(\"apisix\").http_body_filter_phase() args:{}\ncall require(\"apisix\").http_body_filter_phase() return:{}\ncall require(\"apisix\").http_log_phase() args:{}\ncall require(\"apisix\").http_log_phase() return:{}\n\n\n\n=== TEST 4: plugin filter log\n--- debug_config\nbasic:\n  enable: true\nhttp_filter:\n  enable: true         # enable or disable this feature\n  enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable\nhook_conf:\n  enable: true                  # enable or disable this feature\n  name: hook_test               # the name of module and function list\n  log_level: warn               # log level\n  is_print_input_args: true     # print the input arguments\n  is_print_return_value: true   # print the return value\n\nhook_test:                      # module and function list, name: hook_test\n    apisix.plugin:              # required module name\n    - filter                    # function name\n\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n\n            local headers = {}\n            headers[\"X-APISIX-Dynamic-Debug\"] = \"\"  -- has the header name of dynamic debug\n            local code, body = t('/hello',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                headers\n            )\n\n            ngx.sleep(1.1)\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- wait: 2\n--- response_body\npassed\n--- error_log\nfilter(): call require(\"apisix.plugin\").filter() args:{\nfilter(): call require(\"apisix.plugin\").filter() return:{\n\n\n\n=== TEST 5: multiple requests, only output logs of the request with enable_header_name\n--- debug_config\nbasic:\n  enable: true\nhttp_filter:\n  enable: true\n  enable_header_name: X-APISIX-Dynamic-Debug\nhook_conf:\n  enable: true\n  name: hook_test\n  log_level: warn\n  is_print_input_args: true\n  is_print_return_value: true\nhook_test:\n    apisix.plugin:\n    - filter\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/mysleep*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n\n            local res, err\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            for i = 1, 3 do\n                if i == 1 then\n                    local headers = {}\n                    headers[\"X-APISIX-Dynamic-Debug\"] = \"\"\n                    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                                .. \"/mysleep?seconds=1\"\n                    local res, err = httpc:request_uri(uri, {method = \"GET\", headers = headers})\n                    if not res then\n                        ngx.say(err)\n                        return\n                    end\n                else\n                    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                                .. \"/mysleep?seconds=0.1\"\n                    res, err = httpc:request_uri(uri)\n                    if not res then\n                        ngx.say(err)\n                        return\n                    end\n                end\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- wait: 2\n--- response_body\npassed\n--- error_log eval\nqr/call\\srequire\\(\\\"apisix.plugin\\\"\\).filter\\(\\)\\sreturn.*GET\\s\\/mysleep\\?seconds\\=1\\sHTTP\\/1.1/\n--- no_error_log eval\nqr/call\\srequire\\(\\\"apisix.plugin\\\"\\).filter\\(\\)\\sreturn.*GET\\s\\/mysleep\\?seconds\\=0.1\\sHTTP\\/1.1/\n\n\n\n=== TEST 6: hook function with ctx as param\n# ai module would conflict with the debug module\n--- extra_yaml_config\nplugins:\n    #ai\n    - example-plugin\n--- debug_config\nbasic:\n  enable: true\nhttp_filter:\n  enable: true         # enable or disable this feature\n  enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable\nhook_conf:\n  enable: true                  # enable or disable this feature\n  name: hook_test               # the name of module and function list\n  log_level: warn               # log level\n  is_print_input_args: true     # print the input arguments\n  is_print_return_value: true   # print the return value\n\nhook_test:                      # module and function list, name: hook_test\n    apisix.balancer:              # required module name\n    - pick_server                    # function name\n\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n\n            local headers = {}\n            headers[\"X-APISIX-Dynamic-Debug\"] = \"\"\n            local code, body = t('/hello',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                headers\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- wait: 2\n--- response_body\npassed\n--- error_log\ncall require(\"apisix.balancer\").pick_server() args:{\ncall require(\"apisix.balancer\").pick_server() return:{\n"
  },
  {
    "path": "t/debug/hook.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $debug_config = t::APISIX::read_file(\"conf/debug.yaml\");\n$debug_config =~ s/hook_conf:\\n  enable: false/hook_conf:\\n  enable: true/;\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"hosts\": [\"foo.com\", \"*.bar.com\"],\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: phases log\n--- debug_config eval: $::debug_config\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n--- error_log\ncall require(\"apisix\").http_header_filter_phase() args:{}\ncall require(\"apisix\").http_header_filter_phase() return:{}\ncall require(\"apisix\").http_body_filter_phase() args:{}\ncall require(\"apisix\").http_body_filter_phase() return:{}\ncall require(\"apisix\").http_log_phase() args:{}\ncall require(\"apisix\").http_log_phase() return:{}\n\n\n\n=== TEST 4: plugin filter log\n--- debug_config\nbasic:\n  enable: true\nhttp_filter:\n  enable: true         # enable or disable this feature\n  enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable\nhook_conf:\n  enable: true                  # enable or disable this feature\n  name: hook_test               # the name of module and function list\n  log_level: warn               # log level\n  is_print_input_args: true     # print the input arguments\n  is_print_return_value: true   # print the return value\n\nhook_test:                      # module and function list, name: hook_test\n    apisix.plugin:              # required module name\n    - filter                    # function name\n\n#END\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\nX-APISIX-Dynamic-Debug: true\n--- response_body\nhello world\n--- error_log\nfilter(): call require(\"apisix.plugin\").filter() args:{\nfilter(): call require(\"apisix.plugin\").filter() return:{\n\n\n\n=== TEST 5: missing hook_conf\n--- debug_config\nbasic:\n  enable: true\nhttp_filter:\n  enable: true         # enable or disable this feature\n  enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable\n\nhook_test:                      # module and function list, name: hook_test\n    apisix.plugin:              # required module name\n    - filter                    # function name\n\n#END\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\nX-APISIX-Dynamic-Debug: true\n--- response_body\nhello world\n--- error_log\nread_debug_yaml(): failed to validate debug config property \"hook_conf\" is required\n--- wait: 3\n"
  },
  {
    "path": "t/discovery/consul.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 20999;\n\n        location / {\n            content_by_lua_block {\n                ngx.say(\"missing consul services\")\n            }\n        }\n    }\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n    server {\n        listen 30512;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 2\")\n            }\n        }\n    }\n    server {\n        listen 30513;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 3\")\n            }\n        }\n    }\n    server {\n        listen 30514;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 4\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\n  enable_control: true\n  control:\n    ip: 127.0.0.1\n    port: 9090\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    skip_services:\n      - \"service_c\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\nour $yaml_config_with_acl = <<_EOC_;\napisix:\n  node_listen: 1984\n  enable_control: true\n  control:\n    ip: 127.0.0.1\n    port: 9090\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8502\"\n    token: \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\"\n    skip_services:\n      - \"service_c\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: prepare consul catalog register nodes\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"PUT /consul1/deregister/service_a1\",\n    \"PUT /consul1/deregister/service_b1\",\n    \"PUT /consul1/deregister/service_a2\",\n    \"PUT /consul1/deregister/service_b2\",\n    \"PUT /consul2/deregister/service_a1\",\n    \"PUT /consul2/deregister/service_b1\",\n    \"PUT /consul2/deregister/service_a2\",\n    \"PUT /consul2/deregister/service_b2\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_b1\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30513,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_b2\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30514,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200]\n\n\n\n=== TEST 2: test consul server 1\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n]\n--- no_error_log\n[error, error]\n\n\n\n=== TEST 3: test consul server 2\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_b\n      discovery_type: consul\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\"\n]\n--- response_body_like eval\n[\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n]\n--- no_error_log\n[error, error]\n\n\n\n=== TEST 4: test mini consul config\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:6500\"\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body_like eval\nqr/server [1-2]/\n--- ignore_error_log\n\n\n\n=== TEST 5: test invalid service name sometimes the consul key maybe deleted by mistake\n\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_c\n      discovery_type: consul\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello_api\",\n    \"GET /hello_api\"\n]\n--- response_body eval\n[\n    \"missing consul services\\n\",\n    \"missing consul services\\n\"\n]\n--- ignore_error_log\n\n\n\n=== TEST 6: test skip keys\nskip some services, return default nodes, get response: missing consul services\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8600\"\n    prefix: \"upstreams\"\n    skip_services:\n      - \"service_a\"\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body eval\n\"missing consul services\\n\"\n--- ignore_error_log\n\n\n\n=== TEST 7: test register and unregister nodes\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_a2\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30513,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30514,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_a2\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep?sec=5\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n]\n--- response_body_like eval\n[\n    qr//,\n    qr//,\n    qr//,\n    qr//,\n    qr/ok\\n/,\n\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n\n    qr//,\n    qr//,\n    qr//,\n    qr//,\n    qr/ok\\n/,\n\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/\n]\n--- ignore_error_log\n\n\n\n=== TEST 8: clean nodes\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_a2\",\n]\n--- error_code eval\n[200, 200]\n\n\n\n=== TEST 9: test consul short connect type\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    keepalive: false\n    fetch_interval: 3\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- request eval\n[\n    \"GET /hello\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep?sec=5\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/missing consul services\\n/,\n    qr//,\n    qr/ok\\n/,\n    qr/server 1\\n/\n]\n--- ignore_error_log\n\n\n\n=== TEST 10: retry when Consul can't be reached (long connect type)\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8501\"\n    keepalive: true\n    fetch_interval: 3\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- timeout: 4\n--- config\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- request\nGET /sleep?sec=3\n--- response_body\nok\n--- grep_error_log eval\nqr/retry connecting consul after \\d seconds/\n--- grep_error_log_out\nretry connecting consul after 1 seconds\nretry connecting consul after 4 seconds\n\n\n\n=== TEST 11: prepare healthy and unhealthy nodes\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_a2\",\n    \"PUT /v1/agent/service/deregister/service_b1\",\n    \"PUT /v1/agent/service/deregister/service_b2\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_b1\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30513,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_b2\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30514,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- error_code eval\n[200, 200, 200, 200, 200, 200]\n\n\n\n=== TEST 12: test health checker\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\nupstreams:\n    -\n      service_name: service_b\n      discovery_type: consul\n      type: roundrobin\n      id: 1\n      checks:\n        active:\n            http_path: \"/hello\"\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n--- config\n    location /thc {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(3)\n\n            local code, body, res = t.test('/v1/healthcheck',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            local nodes = res[1].nodes\n            table.sort(nodes, function(a, b)\n                return a.port < b.port\n            end)\n            for _, node in ipairs(nodes) do\n                node.counter = nil\n            end\n            ngx.say(json.encode(nodes))\n\n            local code, body, res = t.test('/v1/healthcheck/upstreams/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            nodes = res.nodes\n            table.sort(nodes, function(a, b)\n                return a.port < b.port\n            end)\n            for _, node in ipairs(nodes) do\n                node.counter = nil\n            end\n            ngx.say(json.encode(nodes))\n        }\n    }\n--- request\nGET /thc\n--- response_body\n[{\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":30513,\"status\":\"healthy\"},{\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":30514,\"status\":\"healthy\"}]\n[{\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":30513,\"status\":\"healthy\"},{\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":30514,\"status\":\"healthy\"}]\n--- ignore_error_log\n\n\n\n=== TEST 13: test consul catalog service change\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    keepalive: false\n    fetch_interval: 3\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"GET /sleep?sec=3\",\n    \"GET /hello\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep?sec=5\",\n    \"GET /hello\",\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"GET /sleep?sec=5\",\n    \"GET /hello\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep?sec=5\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr//,\n    qr/ok\\n/,\n    qr/missing consul services\\n/,\n    qr//,\n    qr/ok\\n/,\n    qr/server 1\\n/,\n    qr//,\n    qr/ok\\n/,\n    qr/missing consul services\\n/,\n    qr//,\n    qr/ok\\n/,\n    qr/server 1\\n/,\n]\n--- ignore_error_log\n\n\n\n=== TEST 14: bootstrap acl\n--- config\nlocation /v1/acl {\n    proxy_pass http://127.0.0.1:8502;\n}\n--- request eval\n\"PUT /v1/acl/bootstrap\\n\" . \"{\\\"BootstrapSecret\\\": \\\"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\\\"}\"\n--- error_code_like: ^(?:200|403)$\n\n\n\n=== TEST 15: test register and unregister nodes with acl\n--- yaml_config eval: $::yaml_config_with_acl\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service-a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8502;\n    proxy_set_header X-Consul-Token \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\";\n}\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- pipelined_requests eval\n[\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service-a1\\\",\\\"Name\\\":\\\"service-a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30513,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service-a2\\\",\\\"Name\\\":\\\"service-a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30514,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n    \"PUT /v1/agent/service/deregister/service-a1\",\n    \"PUT /v1/agent/service/deregister/service-a2\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service-a1\\\",\\\"Name\\\":\\\"service-a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service-a2\\\",\\\"Name\\\":\\\"service-a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"GET /sleep?sec=5\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n    \"PUT /v1/agent/service/deregister/service-a1\",\n    \"PUT /v1/agent/service/deregister/service-a2\",\n]\n--- response_body_like eval\n[\n    qr//,\n    qr//,\n    qr/ok\\n/,\n\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n\n    qr//,\n    qr//,\n    qr//,\n    qr//,\n    qr/ok\\n/,\n\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n\n    qr//,\n    qr//\n]\n--- ignore_error_log\n"
  },
  {
    "path": "t/discovery/consul2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 20999;\n\n        location / {\n            content_by_lua_block {\n                ngx.say(\"missing consul services\")\n            }\n        }\n    }\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n    server {\n        listen 30512;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 2\")\n            }\n        }\n    }\n    server {\n        listen 30513;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 3\")\n            }\n        }\n    }\n    server {\n        listen 30514;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 4\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\n  enable_control: true\n  control:\n    ip: 127.0.0.1\n    port: 9090\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    skip_services:\n      - \"service_c\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:9500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:9501;\n}\nlocation /consul3 {\n    rewrite  ^/consul3/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:9502;\n}\n--- pipelined_requests eval\n[\n    \"PUT /consul1/deregister/service_a1\",\n    \"PUT /consul1/deregister/service_b1\",\n    \"PUT /consul1/deregister/service_a2\",\n    \"PUT /consul1/deregister/service_b2\",\n    \"PUT /consul1/deregister/service_a3\",\n    \"PUT /consul1/deregister/service_a4\",\n    \"PUT /consul1/deregister/service_no_port\",\n    \"PUT /consul2/deregister/service_a1\",\n    \"PUT /consul2/deregister/service_a2\",\n    \"PUT /consul3/deregister/service_a1\",\n    \"PUT /consul3/deregister/service_a2\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a3\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"localhost\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a4\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"localhost\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_no_port\\\",\\\"Name\\\":\\\"service_no_port\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Meta\\\":{\\\"service_version\\\":\\\"1.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul2/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul2/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul3/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul3/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200]\n\n\n\n=== TEST 2: show dump services without duplicates\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:9500\"\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1},{\"host\":\"127.0.0.1\",\"port\":30512,\"weight\":1},{\"host\":\"localhost\",\"port\":30511,\"weight\":1},{\"host\":\"localhost\",\"port\":30512,\"weight\":1}],\"service_no_port\":[{\"host\":\"127.0.0.1\",\"port\":80,\"weight\":1}]}\n\n\n\n=== TEST 3: show dump services with host_sort\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:9500\"\n    sort_type: host_sort\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1},{\"host\":\"127.0.0.1\",\"port\":30512,\"weight\":1},{\"host\":\"localhost\",\"port\":30511,\"weight\":1},{\"host\":\"localhost\",\"port\":30512,\"weight\":1}],\"service_no_port\":[{\"host\":\"127.0.0.1\",\"port\":80,\"weight\":1}]}\n\n\n\n=== TEST 4: show dump services with port sort\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:9500\"\n    sort_type: port_sort\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1},{\"host\":\"localhost\",\"port\":30511,\"weight\":1},{\"host\":\"127.0.0.1\",\"port\":30512,\"weight\":1},{\"host\":\"localhost\",\"port\":30512,\"weight\":1}],\"service_no_port\":[{\"host\":\"127.0.0.1\",\"port\":80,\"weight\":1}]}\n\n\n\n=== TEST 5: show dump services with combine sort\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:9500\"\n    sort_type: combine_sort\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1},{\"host\":\"127.0.0.1\",\"port\":30512,\"weight\":1},{\"host\":\"localhost\",\"port\":30511,\"weight\":1},{\"host\":\"localhost\",\"port\":30512,\"weight\":1}],\"service_no_port\":[{\"host\":\"127.0.0.1\",\"port\":80,\"weight\":1}]}\n\n\n\n=== TEST 6: verify service without port defaults to port 80\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:9500\"\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n\n            -- Check that service_no_port exists and has default port 80\n            local service_no_port = entity.services.service_no_port\n            if service_no_port and #service_no_port > 0 then\n                ngx.say(\"service_no_port found with port: \", service_no_port[1].port)\n            else\n                ngx.say(\"service_no_port not found\")\n            end\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\nservice_no_port found with port: 80\n"
  },
  {
    "path": "t/discovery/consul_dump.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: prepare nodes\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_a2\",\n    \"PUT /v1/agent/service/deregister/service_b1\",\n    \"PUT /v1/agent/service/deregister/service_b2\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"ID\\\":\\\"service_b1\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":8002,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- response_body eval\n--- error_code eval\n[200, 200, 200, 200, 200, 200]\n\n\n\n=== TEST 2: show dump services\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1}],\"service_b\":[{\"host\":\"127.0.0.1\",\"port\":8002,\"weight\":1}]}\n\n\n\n=== TEST 3: prepare dump file for next test\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    dump:\n      path: \"/tmp/consul.dump\"\n      load_on_init: false\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nserver 1\n\n\n\n=== TEST 4: clean registered nodes\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_b1\",\n]\n--- error_code eval\n[200, 200]\n\n\n\n=== TEST 5: test load dump on init\nConfigure the invalid consul server addr, and loading the last test 3 generated /tmp/consul.dump file into memory when initializing\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul.dump\"\n      load_on_init: true\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nserver 1\n--- error_log\nconnect consul\n\n\n\n=== TEST 6: delete dump file\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.cli.util\")\n            local succ, err = util.execute_cmd(\"rm -f /tmp/consul.dump\")\n            ngx.say(succ and \"success\" or err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 7: miss load dump on init\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul.dump\"\n      load_on_init: true\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nconnect consul\nconsul service not found\nfailed to set upstream\n\n\n\n=== TEST 8: prepare expired dump file\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.cli.util\")\n            local json = require(\"toolkit.json\")\n\n            local applications = json.decode('{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1}]}')\n            local entity = {\n                services = applications,\n                last_update = ngx.time(),\n                expire = 10,\n            }\n            local succ, err =  util.write_file(\"/tmp/consul.dump\", json.encode(entity))\n\n            ngx.sleep(2)\n            ngx.say(succ and \"success\" or err)\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 9: unexpired dump\ntest load unexpired /tmp/consul.dump file generated by upper test when initializing\n when initializing\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul.dump\"\n      load_on_init: true\n      expire: 5\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nserver 1\n--- error_log\nconnect consul\n\n\n\n=== TEST 10: expired dump\ntest load expired ( by check: (dump_file.last_update + dump.expire) < ngx.time ) ) /tmp/consul.dump file generated by upper test when initializing\n when initializing\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul.dump\"\n      load_on_init: true\n      expire: 1\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\ndump file: /tmp/consul.dump had expired, ignored it\n\n\n\n=== TEST 11: delete dump file\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.cli.util\")\n            local succ, err = util.execute_cmd(\"rm -f /tmp/consul.dump\")\n            ngx.say(succ and \"success\" or err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 12: dump file inexistence\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul.dump\"\n#END\n--- request\nGET /v1/discovery/consul/show_dump_file\n--- error_code: 503\n--- error_log\nconnect consul\n\n\n\n=== TEST 13: no dump config\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:38500\"\n#END\n--- request\nGET /v1/discovery/consul/show_dump_file\n--- error_code: 503\n--- error_log\nconnect consul\n\n\n\n=== TEST 14: prepare nodes with different consul clusters\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"PUT /consul1/deregister/service_a1\",\n    \"PUT /consul1/deregister/service_b1\",\n    \"PUT /consul1/deregister/service_a2\",\n    \"PUT /consul1/deregister/service_b2\",\n    \"PUT /consul2/deregister/service_a1\",\n    \"PUT /consul2/deregister/service_b1\",\n    \"PUT /consul2/deregister/service_a2\",\n    \"PUT /consul2/deregister/service_b2\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul2/register\\n\" . \"{\\\"ID\\\":\\\"service_b1\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30517,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]\n\n\n\n=== TEST 15: show dump services with different consul clusters\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /bonjour {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /bonjour\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1}],\"service_b\":[{\"host\":\"127.0.0.1\",\"port\":30517,\"weight\":1}]}\n\n\n\n=== TEST 16: prepare nodes with consul health check\n--- config\nlocation /v1/agent {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"PUT /v1/agent/service/deregister/service_a1\",\n    \"PUT /v1/agent/service/deregister/service_a2\",\n    \"PUT /v1/agent/service/deregister/service_b1\",\n    \"PUT /v1/agent/service/deregister/service_b2\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"Checks\\\": [{\\\"http\\\": \\\"https://1.1.1.1\\\",\\\"interval\\\": \\\"1s\\\"}],\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /v1/agent/service/register\\n\" . \"{\\\"Checks\\\": [{\\\"http\\\": \\\"http://127.0.0.1:8002\\\",\\\"interval\\\": \\\"1s\\\"}],\\\"ID\\\":\\\"service_b1\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":8002,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- response_body eval\n--- error_code eval\n[200, 200, 200, 200, 200, 200]\n--- wait: 2\n\n\n\n=== TEST 17: show dump services with consul health check\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    dump:\n      path: \"consul.dump\"\n      load_on_init: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            for i = 1, 3 do\n                ngx.sleep(2)\n                local code, body, res = t.test('/v1/discovery/consul/show_dump_file',\n                    ngx.HTTP_GET)\n                local entity = json.decode(res)\n                if entity.services and entity.services.service_a then\n                    ngx.say(json.encode(entity.services))\n                    return\n                end\n            end\n        }\n    }\n--- timeout: 8\n--- request\nGET /t\n--- response_body\n{\"service_a\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1}]}\n"
  },
  {
    "path": "t/discovery/consul_kv.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 20999;\n\n        location / {\n            content_by_lua_block {\n                ngx.say(\"missing consul_kv services\")\n            }\n        }\n    }\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n    server {\n        listen 30512;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 2\")\n            }\n        }\n    }\n    server {\n        listen 30513;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 3\")\n            }\n        }\n    }\n    server {\n        listen 30514;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 4\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    prefix: \"upstreams\"\n    skip_keys:\n      - \"upstreams/unused_api/\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\nour $yaml_config_with_acl = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8502\"\n    token: \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\"\n    prefix: \"upstreams\"\n    skip_keys:\n      - \"upstreams/unused_api/\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: prepare consul kv register nodes\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"DELETE /consul1/upstreams/webpages/?recurse=true\",\n    \"DELETE /consul2/upstreams/webpages/?recurse=true\",\n    \"PUT /consul1/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul1/upstreams/webpages/127.0.0.1:30512\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul2/upstreams/webpages/127.0.0.1:30513\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul2/upstreams/webpages/127.0.0.1:30514\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n]\n--- response_body eval\n[\"true\", \"true\", \"true\", \"true\", \"true\", \"true\"]\n\n\n\n=== TEST 2: test consul server 1\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n]\n--- no_error_log\n[error, error]\n\n\n\n=== TEST 3: test consul server 2\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8600/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\"\n]\n--- response_body_like eval\n[\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n]\n--- no_error_log\n[error, error]\n\n\n\n=== TEST 4: test mini consul_kv config\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:6500\"\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body_like eval\nqr/server [1-2]/\n--- ignore_error_log\n\n\n\n=== TEST 5: test invalid service name\nsometimes the consul key maybe deleted by mistake\n\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8600/v1/kv/upstreams/deleted_keys/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello_api\",\n    \"GET /hello_api\"\n]\n--- response_body eval\n[\n    \"missing consul_kv services\\n\",\n    \"missing consul_kv services\\n\"\n]\n--- ignore_error_log\n\n\n\n=== TEST 6: test skip keys\nskip some keys, return default nodes, get response: missing consul_kv services\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8600\"\n    prefix: \"upstreams\"\n    skip_keys:\n      - \"upstreams/webpages/\"\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8600/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body eval\n\"missing consul_kv services\\n\"\n--- ignore_error_log\n\n\n\n=== TEST 7: test register and unregister nodes\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8500;\n}\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- request eval\n[\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30511\",\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30512\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30513\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30514\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"GET /sleep\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30513\",\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30514\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"GET /sleep?sec=5\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n]\n--- response_body_like eval\n[\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/ok\\n/,\n\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/ok\\n/,\n\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/\n]\n--- ignore_error_log\n\n\n\n=== TEST 8: prepare healthy and unhealthy nodes\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"DELETE /v1/kv/upstreams/webpages/?recurse=true\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 1, \\\"fail_timeout\\\": 1}\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.2:1988\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 1, \\\"fail_timeout\\\": 1}\",\n]\n--- response_body eval\n[\n    'true',\n    'true',\n    'true',\n]\n\n\n\n=== TEST 9: test health checker\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\nupstreams:\n    -\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n      id: 1\n      checks:\n        active:\n            http_path: \"/hello\"\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n--- config\n    location /thc {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(3)\n\n            local code, body, res = t.test('/v1/healthcheck',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            local nodes = res[1].nodes\n            table.sort(nodes, function(a, b)\n                return a.ip < b.ip\n            end)\n            for _, node in ipairs(nodes) do\n                node.counter = nil\n            end\n            ngx.say(json.encode(nodes))\n\n            local code, body, res = t.test('/v1/healthcheck/upstreams/1',\n                ngx.HTTP_GET)\n            res = json.decode(res)\n            local nodes = res.nodes\n            table.sort(nodes, function(a, b)\n                return a.ip < b.ip\n            end)\n            for _, node in ipairs(nodes) do\n                node.counter = nil\n            end\n            ngx.say(json.encode(nodes))\n        }\n    }\n--- request\nGET /thc\n--- response_body\n[{\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":30511,\"status\":\"healthy\"},{\"hostname\":\"127.0.0.2\",\"ip\":\"127.0.0.2\",\"port\":1988,\"status\":\"unhealthy\"}]\n[{\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":30511,\"status\":\"healthy\"},{\"hostname\":\"127.0.0.2\",\"ip\":\"127.0.0.2\",\"port\":1988,\"status\":\"unhealthy\"}]\n--- ignore_error_log\n\n\n\n=== TEST 10: clean nodes\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"DELETE /v1/kv/upstreams/webpages/?recurse=true\"\n]\n--- response_body eval\n[\n    'true'\n]\n\n\n\n=== TEST 11: test consul_kv short connect type\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    keepalive: false\n    fetch_interval: 3\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8500;\n}\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- request eval\n[\n    \"GET /hello\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"GET /sleep?sec=5\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/missing consul_kv services\\n/,\n    qr/true/,\n    qr/ok\\n/,\n    qr/server 1\\n/\n]\n--- ignore_error_log\n\n\n\n=== TEST 12: retry when Consul can't be reached (long connect type)\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8501\"\n    keepalive: true\n    fetch_interval: 3\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8501/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- timeout: 4\n--- config\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- request\nGET /sleep?sec=3\n--- response_body\nok\n--- grep_error_log eval\nqr/retry connecting consul after \\d seconds/\n--- grep_error_log_out\nretry connecting consul after 1 seconds\nretry connecting consul after 4 seconds\n\n\n\n=== TEST 13: bootstrap acl\n--- config\nlocation /v1/acl {\n    proxy_pass http://127.0.0.1:8502;\n}\n--- request eval\n\"PUT /v1/acl/bootstrap\\n\" . \"{\\\"BootstrapSecret\\\": \\\"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\\\"}\"\n--- error_code_like: ^(?:200|403)$\n\n\n\n=== TEST 14: test register and unregister nodes\n--- yaml_config eval: $::yaml_config_with_acl\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8502/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8502;\n    proxy_set_header X-Consul-Token \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\";\n}\nlocation /sleep {\n    content_by_lua_block {\n        local args = ngx.req.get_uri_args()\n        local sec = args.sec or \"2\"\n        ngx.sleep(tonumber(sec))\n        ngx.say(\"ok\")\n    }\n}\n--- timeout: 6\n--- request eval\n[\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30511\",\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30512\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30513\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30514\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"GET /sleep\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30513\",\n    \"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30514\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"GET /sleep?sec=5\",\n\n    \"GET /hello?random1\",\n    \"GET /hello?random2\",\n    \"GET /hello?random3\",\n    \"GET /hello?random4\",\n\n]\n--- response_body_like eval\n[\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/ok\\n/,\n\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n    qr/server [3-4]\\n/,\n\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/true/,\n    qr/ok\\n/,\n\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/,\n    qr/server [1-2]\\n/\n]\n--- ignore_error_log\n"
  },
  {
    "path": "t/discovery/consul_kv_dump.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: prepare nodes\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"DELETE /v1/kv/upstreams/?recurse=true\",\n    \"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 1, \\\"fail_timeout\\\": 1}\",\n]\n--- response_body eval\n[\n    'true',\n    'true',\n    'true',\n]\n\n\n\n=== TEST 2: show dump services\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    dump:\n      path: \"consul_kv.dump\"\n      load_on_init: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n            ngx.sleep(2)\n\n            local code, body, res = t.test('/v1/discovery/consul_kv/show_dump_file',\n                ngx.HTTP_GET)\n            local entity = json.decode(res)\n            ngx.say(json.encode(entity.services))\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\n{\"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1}]}\n\n\n\n=== TEST 3: prepare dump file for next test\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n    dump:\n      path: \"/tmp/consul_kv.dump\"\n      load_on_init: true\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nserver 1\n\n\n\n=== TEST 4: clean registered nodes\n--- config\nlocation /v1/kv {\n    proxy_pass http://127.0.0.1:8500;\n}\n--- request eval\n[\n    \"DELETE /v1/kv/upstreams/?recurse=true\",\n]\n--- response_body eval\n[\n    'true'\n]\n\n\n\n=== TEST 5: test load dump on init\nConfigure the invalid consul server addr, and loading the last test 3 generated /tmp/consul_kv.dump file into memory when initializing\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul_kv.dump\"\n      load_on_init: true\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nserver 1\n--- error_log\nconnect consul\n\n\n\n=== TEST 6: delete dump file\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.cli.util\")\n            local succ, err = util.execute_cmd(\"rm -f /tmp/consul_kv.dump\")\n            ngx.say(succ and \"success\" or err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 7: miss load dump on init\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul_kv.dump\"\n      load_on_init: true\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nconnect consul\nfetch nodes failed\nfailed to set upstream\n\n\n\n=== TEST 8: prepare expired dump file\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.cli.util\")\n            local json = require(\"toolkit.json\")\n\n            local applications = json.decode('{\"http://127.0.0.1:8500/v1/kv/upstreams/webpages/\":[{\"host\":\"127.0.0.1\",\"port\":30511,\"weight\":1}]}')\n            local entity = {\n                services = applications,\n                last_update = ngx.time(),\n                expire = 10,\n            }\n            local succ, err =  util.write_file(\"/tmp/consul_kv.dump\", json.encode(entity))\n\n            ngx.sleep(2)\n            ngx.say(succ and \"success\" or err)\n        }\n    }\n--- timeout: 3\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 9: unexpired dump\ntest load unexpired /tmp/consul_kv.dump file generated by upper test when initializing\n when initializing\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul_kv.dump\"\n      load_on_init: true\n      expire: 5\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- response_body\nserver 1\n--- error_log\nconnect consul\n\n\n\n=== TEST 10: expired dump\ntest load expired ( by check: (dump_file.last_update + dump.expire) < ngx.time ) ) /tmp/consul_kv.dump file generated by upper test when initializing\n when initializing\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul_kv.dump\"\n      load_on_init: true\n      expire: 1\n#END\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\ndump file: /tmp/consul_kv.dump had expired, ignored it\n\n\n\n=== TEST 11: delete dump file\n--- config\n    location /t {\n        content_by_lua_block {\n            local util = require(\"apisix.cli.util\")\n            local succ, err = util.execute_cmd(\"rm -f /tmp/consul_kv.dump\")\n            ngx.say(succ and \"success\" or err)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 12: dump file inexistence\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:38500\"\n    dump:\n      path: \"/tmp/consul_kv.dump\"\n#END\n--- request\nGET /v1/discovery/consul_kv/show_dump_file\n--- error_code: 503\n--- error_log\nconnect consul\n\n\n\n=== TEST 13: no dump config\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_control: true\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:38500\"\n#END\n--- request\nGET /v1/discovery/consul_kv/show_dump_file\n--- error_code: 503\n--- error_log\nconnect consul\n"
  },
  {
    "path": "t/discovery/dns/mix.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{CUSTOM_DNS_SERVER} = \"127.0.0.1:1053\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nroutes:\n  -\n    id: 1\n    uris:\n        - /hello\n    upstream_id: 1\n  -\n    id: 2\n    uris:\n        - /hello_chunked\n    upstream_id: 2\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: mix cache between discovery & global resolver\n--- log_level: debug\n--- apisix_yaml\nupstreams:\n    -\n        id: 1\n        nodes:\n            ttl.1s.test.local:1980: 1\n        type: roundrobin\n    -\n        id: 2\n        service_name: \"ttl.1s.test.local:1980\"\n        discovery_type: dns\n        type: roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello_chunked\"\n            for i = 1, 2 do\n                for j = 1, 3 do\n                    local httpc = http.new()\n                    local res, err\n                    if j % 2 ~= 0 then\n                        res, err = httpc:request_uri(uri1, {method = \"GET\"})\n                    else\n                        res, err = httpc:request_uri(uri2, {method = \"GET\"})\n                    end\n\n                    if not res or res.body ~= \"hello world\\n\" then\n                        ngx.say(err)\n                        return\n                    end\n                end\n\n                -- It is expected to have 5 DNS queries\n                -- the first turn: one for global resolver & two for discovery (SRV, then A)\n                -- the second turn: each one for both global resolver & discovery\n                if i < 2 then\n                    ngx.sleep(1.1)\n                end\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/connect to 127.0.0.1:1053/\n--- grep_error_log_out\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\n"
  },
  {
    "path": "t/discovery/dns/sanity.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nroutes:\n  -\n    id: 1\n    uris:\n        - /hello\n    upstream_id: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: default port to 53\n--- log_level: debug\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1\"\n--- apisix_yaml\nupstreams:\n    - service_name: sd.test.local\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_code: 503\n--- error_log\nconnect to 127.0.0.1:53\n\n\n\n=== TEST 2: A\n--- apisix_yaml\nupstreams:\n    - service_name: \"sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":1,\"127.0.0.2:1980\":1|\"127.0.0.2:1980\":1,\"127.0.0.1:1980\":1)\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 3: AAAA\n--- listen_ipv6\n--- apisix_yaml\nupstreams:\n    - service_name: \"ipv6.sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- response_body\nhello world\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to [0:0:0:0:0:0:0:1]:1980\n\n\n\n=== TEST 4: prefer A to AAAA\n--- listen_ipv6\n--- apisix_yaml\nupstreams:\n    - service_name: \"mix.sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- response_body\nhello world\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 5: no /etc/hosts\n--- apisix_yaml\nupstreams:\n    - service_name: test.com\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nfailed to query the DNS server\n--- error_code: 503\n\n\n\n=== TEST 6: no /etc/resolv.conf\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    enable_resolv_search_option: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n--- apisix_yaml\nupstreams:\n    - service_name: apisix\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nfailed to query the DNS server\n--- error_code: 503\n\n\n\n=== TEST 7: SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":20|\"127.0.0.2:1980\":20,\"127.0.0.1:1980\":60)\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 8: SRV (RFC 2782 style)\n--- apisix_yaml\nupstreams:\n    - service_name: \"_sip._tcp.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":20|\"127.0.0.2:1980\":20,\"127.0.0.1:1980\":60)\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 9: SRV (different port)\n--- apisix_yaml\nupstreams:\n    - service_name: \"port.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1981\":20|\"127.0.0.2:1981\":20,\"127.0.0.1:1980\":60)\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 10: SRV (zero weight)\n--- apisix_yaml\nupstreams:\n    - service_name: \"zero-weight.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":1|\"127.0.0.2:1980\":1,\"127.0.0.1:1980\":60)\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 11: SRV (split weight)\n--- apisix_yaml\nupstreams:\n    - service_name: \"split-weight.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(,?\"127.0.0.(1:1980\":200|3:1980\":1|4:1980\":1)){3}\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 12: SRV (priority)\n--- apisix_yaml\nupstreams:\n    - service_name: \"priority.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- response_body\nhello world\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1980\n\n\n\n=== TEST 13: prefer SRV than A\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nproxy request to 127.0.0.1:1980\n--- response_body\nhello world\n\n\n\n=== TEST 14: SRV (port is 0)\n--- apisix_yaml\nupstreams:\n    - service_name: \"zero.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nconnect() failed\n--- error_code: 502\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:80\n\n\n\n=== TEST 15: SRV (override port)\n--- apisix_yaml\nupstreams:\n    - service_name: \"port.srv.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":20|\"127.0.0.2:1980\":20,\"127.0.0.1:1980\":60)\\}/\n--- response_body\nhello world\n\n\n\n=== TEST 16: prefer A than SRV when A is ahead of SRV in config.yaml\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n        order:\n            - A\n            - SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_code: 502\n--- error_log\nproxy request to 127.0.0.1:80\n\n\n\n=== TEST 17: Invalid order type in config.yaml\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n        order:\n            - B\n            - SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- must_die\n--- error_log\nmatches none of the enum values\n\n\n\n=== TEST 18: Multiple order type in config.yaml\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n        order:\n            - SRV\n            - SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- must_die\n--- error_log\nexpected unique items but items 1 and 2 are equal\n\n\n\n=== TEST 19: invalid order type in config.yaml\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n        order:\n            - a\n            - SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- must_die\n--- error_log\nmatches none of the enum values\n\n\n\n=== TEST 20: use resolv.conf\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        resolv_conf: build-cache/test_resolve.conf\n--- apisix_yaml\nupstreams:\n    - service_name: \"sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":1,\"127.0.0.2:1980\":1|\"127.0.0.2:1980\":1,\"127.0.0.1:1980\":1)\\}/\n--- response_body\nhello world\n"
  },
  {
    "path": "t/discovery/eureka.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  eureka:\n    host:\n      - \"http://127.0.0.1:8761\"\n    prefix: \"/eureka/\"\n    fetch_interval: 10\n    weight: 80\n    timeout:\n      connect: 1500\n      send: 1500\n      read: 1500\n_EOC_\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: get APISIX-EUREKA info from EUREKA\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /eureka/*\n    upstream:\n      service_name: APISIX-EUREKA\n      discovery_type: eureka\n      type: roundrobin\n\n#END\n--- request\nGET /eureka/apps/APISIX-EUREKA\n--- response_body_like\n.*<name>APISIX-EUREKA</name>.*\n--- error_log\nuse config_provider: yaml\ndefault_weight:80.\nfetch_interval:10.\neureka uri:http://127.0.0.1:8761/eureka/.\nconnect_timeout:1500, send_timeout:1500, read_timeout:1500.\n\n\n\n=== TEST 2: error service_name name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /eureka/*\n    upstream:\n      service_name: APISIX-EUREKA-DEMO\n      discovery_type: eureka\n      type: roundrobin\n\n#END\n--- request\nGET /eureka/apps/APISIX-EUREKA\n--- error_code: 503\n--- error_log eval\nqr/.* no valid upstream node.*/\n\n\n\n=== TEST 3: with proxy-rewrite\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /eureka-test/*\n    plugins:\n      proxy-rewrite:\n        regex_uri: [\"^/eureka-test/(.*)\", \"/${1}\"]\n    upstream:\n      service_name: APISIX-EUREKA\n      discovery_type: eureka\n      type: roundrobin\n\n#END\n--- request\nGET /eureka-test/eureka/apps/APISIX-EUREKA\n--- response_body_like\n.*<name>APISIX-EUREKA</name>.*\n--- error_log\nuse config_provider: yaml\ndefault_weight:80.\nfetch_interval:10.\neureka uri:http://127.0.0.1:8761/eureka/.\nconnect_timeout:1500, send_timeout:1500, read_timeout:1500.\n\n\n\n=== TEST 4: fallback to next eureka host when current host fails\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    eureka:\n        host:\n            - \"http://127.0.0.1:20997\"\n            - \"http://127.0.0.1:8761\"\n        prefix: \"/eureka/\"\n        fetch_interval: 1\n        weight: 80\n        timeout:\n            connect: 1500\n            send: 1500\n            read: 1500\n--- apisix_yaml\nroutes:\n    -\n        uri: /eureka/*\n        upstream:\n            service_name: APISIX-EUREKA\n            discovery_type: eureka\n            type: roundrobin\n#END\n--- http_config\n    server {\n        listen 20997;\n\n        location / {\n            return 502;\n        }\n    }\n--- request\nGET /eureka/apps/APISIX-EUREKA\n--- response_body_like\n.*<name>APISIX-EUREKA</name>.*\n--- error_log\nfailed to fetch registry from http://127.0.0.1:20997/eureka/: status=502\nsuccessfully updated service registry\n--- no_error_log\nfailed to fetch registry from all eureka hosts\nfailed to fetch registry from http://127.0.0.1:8761/eureka/\n\n\n\n=== TEST 5: verify host header with pass_host: node\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /eureka/*\n    upstream:\n      service_name: APISIX-EUREKA\n      discovery_type: eureka\n      type: roundrobin\n      pass_host: node\n    plugins:\n        serverless-post-function:\n            phase: log\n            functions:\n                - \"return function(conf, ctx)\n                    local core = require('apisix.core')\n                    core.log.warn('upstream_host: ', ctx.var.upstream_host)\n                    core.log.warn('upstream_addr: ', ctx.var.upstream_addr)\n                  end\"\n\n#END\n--- request\nGET /eureka/apps/APISIX-EUREKA\n--- response_body_like\n.*<name>APISIX-EUREKA</name>.*\n--- error_log\nupstream_host: localhost\nupstream_addr: 127.0.0.1\n"
  },
  {
    "path": "t/discovery/kubernetes_schema.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: id field validation\n--- config\n    location /t {\n        content_by_lua_block {\n            local k8s_schema = require(\"apisix.discovery.kubernetes.schema\")\n            local core = require(\"apisix.core\")\n\n            -- Valid id: lowercase alphanumeric\n            local config = {\n                {\n                    id = \"cluster1\",\n                    service = {\n                        host = \"k8s.example.com\",\n                        port = \"6443\"\n                    },\n                    client = {\n                        token = \"token123\"\n                    }\n                }\n            }\n            local ok, err = core.schema.check(k8s_schema, config)\n            assert(ok, err)\n            ngx.say(\"valid id: passed\")\n\n            -- Valid id: 64 chars\n            config[1].id = string.rep(\"a\", 64)\n            ok, err = core.schema.check(k8s_schema, config)\n            assert(ok, err)\n            ngx.say(\"valid id 64 chars: passed\")\n\n            -- Invalid id: uppercase letters\n            config[1].id = \"CLUSTER1\"\n            ok, err = core.schema.check(k8s_schema, config)\n            assert(not ok)\n            ngx.say(\"invalid id uppercase: \", err)\n\n            -- Invalid id: special characters\n            config[1].id = \"cluster-1\"\n            ok, err = core.schema.check(k8s_schema, config)\n            assert(not ok)\n            ngx.say(\"invalid id special char: \", err)\n\n            -- Invalid id: too long (over 64 chars)\n            config[1].id = string.rep(\"a\", 65)\n            ok, err = core.schema.check(k8s_schema, config)\n            assert(not ok)\n            ngx.say(\"invalid id too long: \", err)\n        }\n    }\n--- response_body_like\nvalid id: passed\nvalid id 64 chars: passed\ninvalid id uppercase: .*\ninvalid id special char: .*\ninvalid id too long: .*\n"
  },
  {
    "path": "t/discovery/nacos.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\nworkers(4);\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      prefix: \"/nacos/v1/\"\n      fetch_interval: 1\n      weight: 1\n      timeout:\n        connect: 2000\n        send: 2000\n        read: 5000\n\n_EOC_\n\nour $yaml_auth_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  nacos:\n      host:\n        - \"http://nacos:nacos\\@127.0.0.1:8848\"\n      prefix: \"/nacos/v1/\"\n      fetch_interval: 1\n      weight: 1\n      timeout:\n        connect: 2000\n        send: 2000\n        read: 5000\n\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    $block->set_value(\"timeout\", \"10\");\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: get APISIX-NACOS info from NACOS - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n--- no_error_log\n[error, error]\n\n\n\n=== TEST 2: error service_name name - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 3: get APISIX-NACOS info from NACOS - auth\n--- yaml_config eval: $::yaml_auth_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            -- Wait for 2 seconds for APISIX initialization\n            ngx.sleep(2)\n            local httpc = http.new()\n            local valid_responses = 0\n\n            for i = 1, 2 do\n                local res, err = httpc:request_uri(uri .. \"/hello\")\n                if not res then\n                    ngx.log(ngx.ERR, \"Request failed: \", err)\n                else\n                    -- Clean and validate response\n                    local clean_body = res.body:gsub(\"%s+$\", \"\")\n                    if clean_body == \"server 1\" or clean_body == \"server 2\" then\n                        valid_responses = valid_responses + 1\n                    else\n                        ngx.log(ngx.ERR, \"Invalid response: \", clean_body)\n                    end\n                end\n            end\n            -- Final check\n            if valid_responses == 2 then\n                ngx.say(\"PASS\")\n            else\n                ngx.say(\"FAIL: only \", valid_responses, \" valid responses\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nPASS\n\n\n\n=== TEST 4: error service_name name - auth\n--- yaml_config eval: $::yaml_auth_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 5: get APISIX-NACOS info from NACOS - configured in services\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 6: get APISIX-NACOS info from NACOS - configured in upstreams + etcd\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_name\": \"APISIX-NACOS\",\n                    \"discovery_type\": \"nacos\",\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            -- Wait for 2 seconds for APISIX initialization\n            ngx.sleep(2)\n            local httpc = http.new()\n            local valid_responses = 0\n\n            for i = 1, 2 do\n                local res, err = httpc:request_uri(uri .. \"/hello\")\n                if not res then\n                    ngx.log(ngx.ERR, \"Request failed: \", err)\n                else\n                    -- Clean and validate response\n                    local clean_body = res.body:gsub(\"%s+$\", \"\")\n                    if clean_body == \"server 1\" or clean_body == \"server 2\" then\n                        valid_responses = valid_responses + 1\n                    else\n                        ngx.log(ngx.ERR, \"Invalid response: \", clean_body)\n                    end\n                end\n            end\n            -- Final check\n            if valid_responses == 2 then\n                ngx.say(\"PASS\")\n            else\n                ngx.say(\"FAIL: only \", valid_responses, \" valid responses\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nPASS\n\n\n\n=== TEST 8: get APISIX-NACOS info from NACOS - no auth with namespace\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 9: error namespace_id - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: err_ns\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 10: get APISIX-NACOS info from NACOS - configured in services with namespace\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 11: get APISIX-NACOS info from NACOS - configured in upstreams + etcd with namespace\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_name\": \"APISIX-NACOS\",\n                    \"discovery_type\": \"nacos\",\n                    \"type\": \"roundrobin\",\n                    \"discovery_args\": {\n                      \"namespace_id\": \"test_ns\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit with namespace\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            -- Wait for 2 seconds for APISIX initialization\n            ngx.sleep(2)\n            local httpc = http.new()\n            local valid_responses = 0\n\n            for i = 1, 2 do\n                local res, err = httpc:request_uri(uri .. \"/hello\")\n                if not res then\n                    ngx.log(ngx.ERR, \"Request failed: \", err)\n                else\n                    -- Clean and validate response\n                    local clean_body = res.body:gsub(\"%s+$\", \"\")\n                    if clean_body == \"server 1\" or clean_body == \"server 2\" then\n                        valid_responses = valid_responses + 1\n                    else\n                        ngx.log(ngx.ERR, \"Invalid response: \", clean_body)\n                    end\n                end\n            end\n            -- Final check\n            if valid_responses == 2 then\n                ngx.say(\"PASS\")\n            else\n                ngx.say(\"FAIL: only \", valid_responses, \" valid responses\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nPASS\n\n\n\n=== TEST 13: get APISIX-NACOS info from NACOS - no auth with group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 14: error group_name - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: err_group_name\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 15: get APISIX-NACOS info from NACOS - configured in services with group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 16: get APISIX-NACOS info from NACOS - configured in upstreams + etcd with group_name\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_name\": \"APISIX-NACOS\",\n                    \"discovery_type\": \"nacos\",\n                    \"type\": \"roundrobin\",\n                    \"discovery_args\": {\n                      \"group_name\": \"test_group\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: hit with group_name\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            -- Wait for 2 seconds for APISIX initialization\n            ngx.sleep(2)\n            local httpc = http.new()\n            local valid_responses = 0\n\n            for i = 1, 2 do\n                local res, err = httpc:request_uri(uri .. \"/hello\")\n                if not res then\n                    ngx.log(ngx.ERR, \"Request failed: \", err)\n                else\n                    -- Clean and validate response\n                    local clean_body = res.body:gsub(\"%s+$\", \"\")\n                    if clean_body == \"server 1\" or clean_body == \"server 2\" then\n                        valid_responses = valid_responses + 1\n                    else\n                        ngx.log(ngx.ERR, \"Invalid response: \", clean_body)\n                    end\n                end\n            end\n            -- Final check\n            if valid_responses == 2 then\n                ngx.say(\"PASS\")\n            else\n                ngx.say(\"FAIL: only \", valid_responses, \" valid responses\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nPASS\n\n\n\n=== TEST 18: get APISIX-NACOS info from NACOS - no auth with namespace_id and group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 19: error group_name and correct namespace_id - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n        group_name: err_group_name\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 20: error namespace_id and correct group_name - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: err_ns\n        group_name: test_group\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 21: error namespace_id and error group_name - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: err_ns\n        group_name: err_group_name\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 22: get APISIX-NACOS info from NACOS - configured in services with namespace_id and group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 23: get APISIX-NACOS info from NACOS - configured in upstreams + etcd with namespace_id and group_name\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_name\": \"APISIX-NACOS\",\n                    \"discovery_type\": \"nacos\",\n                    \"type\": \"roundrobin\",\n                    \"discovery_args\": {\n                      \"namespace_id\": \"test_ns\",\n                      \"group_name\": \"test_group\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: hit with namespace_id and group_name\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            -- Wait for 2 seconds for APISIX initialization\n            ngx.sleep(2)\n            local httpc = http.new()\n            local valid_responses = 0\n\n            for i = 1, 2 do\n                local res, err = httpc:request_uri(uri .. \"/hello\")\n                if not res then\n                    ngx.log(ngx.ERR, \"Request failed: \", err)\n                else\n                    -- Clean and validate response\n                    local clean_body = res.body:gsub(\"%s+$\", \"\")\n                    if clean_body == \"server 1\" or clean_body == \"server 2\" then\n                        valid_responses = valid_responses + 1\n                    else\n                        ngx.log(ngx.ERR, \"Invalid response: \", clean_body)\n                    end\n                end\n            end\n            -- Final check\n            if valid_responses == 2 then\n                ngx.say(\"PASS\")\n            else\n                ngx.say(\"FAIL: only \", valid_responses, \" valid responses\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nPASS\n\n\n\n=== TEST 25: same namespace_id and service_name, different group_name\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n           -- use nacos-service5\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns\",\n                          \"group_name\": \"test_group\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            -- use nacos-service6\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello1\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns\",\n                          \"group_name\": \"test_group2\"\n                        }\n                    },\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(1.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri1, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            res, err = httpc:request_uri(uri2, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nserver 1\nserver 3\n\n\n\n=== TEST 26: same group_name and service_name, different namespace_id\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n           -- use nacos-service5\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns\",\n                          \"group_name\": \"test_group\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            -- use nacos-service7\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello1\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns2\",\n                          \"group_name\": \"test_group\"\n                        }\n                    },\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(1.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri1, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            res, err = httpc:request_uri(uri2, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nserver 1\nserver 4\n"
  },
  {
    "path": "t/discovery/nacos2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nworkers(3);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: continue to get nacos data after failure in a service\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:20999\"\n      prefix: \"/nacos/v1/\"\n      fetch_interval: 1\n      weight: 1\n      timeout:\n        connect: 2000\n        send: 2000\n        read: 5000\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello_\n    upstream:\n      service_name: NOT-NACOS\n      discovery_type: nacos\n      type: roundrobin\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- http_config\n    server {\n        listen 20999;\n\n        location / {\n            access_by_lua_block {\n                if not package.loaded.hit then\n                    package.loaded.hit = true\n                    ngx.exit(502)\n                end\n            }\n            proxy_pass http://127.0.0.1:8858;\n        }\n    }\n--- request\nGET /hello\n--- response_body_like eval\nqr/server [1-2]/\n--- error_log\nerror: status = 502\n\n\n\n=== TEST 2: change nacos server auth password\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"cjson\")\n            local http = require(\"resty.http\")\n\n            local httpc = http.new()\n            local nacos_host = \"http://127.0.0.1:8848\"\n            local res, err = httpc:request_uri(nacos_host .. \"/nacos/v1/auth/login\", {\n                    method = \"POST\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                    },\n                    body = ngx.encode_args({username = \"nacos\", password = \"nacos\"}),\n                })\n\n            if res.status ~= 200 then\n                ngx.say(\"nacos auth failed\")\n                ngx.exit(401)\n            end\n\n            local res_json = json.decode(res.body)\n            res, err = httpc:request_uri(nacos_host .. \"/nacos/v1/auth/users?accessToken=\" .. res_json[\"accessToken\"], {\n                    method = \"PUT\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                    },\n                    body = ngx.encode_args({username = \"nacos\", newPassword = \"nacos!@#$%^&*()[]\"}),\n                })\n            if res.status ~= 200 then\n                ngx.say(\"nacos token auth failed\")\n                ngx.say(res.body)\n                ngx.exit(401)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 3: test complex host\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://nacos:nacos!@#$%^&*()[]@127.0.0.1:8848\"\n      fetch_interval: 1\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            -- Wait for 2 seconds for APISIX initialization\n            ngx.sleep(2)\n            local httpc = http.new()\n            local valid_responses = 0\n\n            for i = 1, 2 do\n                local res, err = httpc:request_uri(uri .. \"/hello\")\n                if not res then\n                    ngx.log(ngx.ERR, \"Request failed: \", err)\n                else\n                    -- Clean and validate response\n                    local clean_body = res.body:gsub(\"%s+$\", \"\")\n                    if clean_body == \"server 1\" or clean_body == \"server 2\" then\n                        valid_responses = valid_responses + 1\n                    else\n                        ngx.log(ngx.ERR, \"Invalid response: \", clean_body)\n                    end\n                end\n            end\n            -- Final check\n            if valid_responses == 2 then\n                ngx.say(\"PASS\")\n            else\n                ngx.say(\"FAIL: only \", valid_responses, \" valid responses\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nPASS\n\n\n\n=== TEST 4: restore nacos server auth password\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"cjson\")\n            local http = require(\"resty.http\")\n\n            local httpc = http.new()\n            local nacos_host = \"http://127.0.0.1:8848\"\n            local res, err = httpc:request_uri(nacos_host .. \"/nacos/v1/auth/login\", {\n                    method = \"POST\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                    },\n                    body = ngx.encode_args({username = \"nacos\", password = \"nacos!@#$%^&*()[]\"}),\n                })\n\n            if res.status ~= 200 then\n                ngx.say(\"nacos auth failed\")\n                ngx.exit(401)\n            end\n\n            local res_json = json.decode(res.body)\n            res, err = httpc:request_uri(nacos_host .. \"/nacos/v1/auth/users?accessToken=\" .. res_json[\"accessToken\"], {\n                    method = \"PUT\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                    },\n                    body = ngx.encode_args({username = \"nacos\", newPassword = \"nacos\"}),\n                })\n            if res.status ~= 200 then\n                ngx.say(\"nacos token auth failed\")\n                ngx.say(res.body)\n                ngx.exit(401)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 5: same service is registered in route, service and upstream, de-duplicate\n--- yaml_config\napisix:\n  node_listen: 1984\n--- extra_yaml_config\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      fetch_interval: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"scheme\": \"http\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"public\",\n                          \"group_name\": \"DEFAULT_GROUP\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"scheme\": \"http\",\n                        \"discovery_type\": \"nacos\",\n                        \"pass_host\": \"pass\",\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"public\",\n                          \"group_name\": \"DEFAULT_GROUP\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"type\": \"roundrobin\",\n                    \"scheme\": \"http\",\n                    \"discovery_type\": \"nacos\",\n                    \"pass_host\": \"pass\",\n                    \"service_name\": \"APISIX-NACOS\",\n                    \"discovery_args\": {\n                    \"namespace_id\": \"public\",\n                    \"group_name\": \"DEFAULT_GROUP\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(1.5)\n\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local dump_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/v1/discovery/nacos/dump\"\n            local res, err = httpc:request_uri(dump_uri, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n\n            local body = json_decode(res.body)\n            local services = body.services\n            local service = services[\"public.DEFAULT_GROUP.APISIX-NACOS\"]\n            local number = table.getn(service.nodes)\n            ngx.say(number)\n        }\n    }\n--- response_body\n2\n\n\n\n=== TEST 6: fallback to next nacos host when current host fails\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    nacos:\n            host:\n                - \"http://127.0.0.1:20998\"\n                - \"http://127.0.0.1:8858\"\n            prefix: \"/nacos/v1/\"\n            fetch_interval: 1\n            weight: 1\n            timeout:\n                connect: 2000\n                send: 2000\n                read: 5000\n--- apisix_yaml\nroutes:\n    -\n        uri: /hello\n        upstream:\n            service_name: APISIX-NACOS\n            discovery_type: nacos\n            type: roundrobin\n#END\n--- http_config\n        server {\n                listen 20998;\n\n                location / {\n                        return 502;\n                }\n        }\n--- request\nGET /hello\n--- response_body_like eval\nqr/server [1-2]/\n--- error_log\nfetch_from_host: http://127.0.0.1:20998/nacos/v1/ err:all nacos services fetch failed\n"
  },
  {
    "path": "t/discovery/nacos3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# we can't use mse nacos to test, access_key and secret_key won't affect the open source nacos\nuse t::APISIX 'no_plan';\n\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  nacos:\n    host:\n      - \"http://127.0.0.1:8858\"\n    prefix: \"/nacos/v1/\"\n    fetch_interval: 1\n    weight: 1\n    timeout:\n      connect: 2000\n      send: 2000\n      read: 5000\n    access_key: \"my_access_key\"\n    secret_key: \"my_secret_key\"\n\n_EOC_\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: error service_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 2: error namespace_id\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: err_ns\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 3: error group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: err_group_name\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 4: error namespace_id and error group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: err_ns\n        group_name: err_group_name\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 5: error group_name and correct namespace_id\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n        group_name: err_group_name\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 6: error namespace_id and correct group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: err_ns\n        group_name: test_group\n#END\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 7: get APISIX-NACOS info from NACOS - configured in services\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 8: get APISIX-NACOS info from NACOS - configured in services with group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 9: get APISIX-NACOS info from NACOS - configured in services with namespace_id\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 10: get APISIX-NACOS info from NACOS - configured in services with group_name and namespace_id\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    service_id: 1\nservices:\n  -\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: test_group\n        namespace_id: test_ns\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 11: get APISIX-NACOS info from NACOS - configured in upstreams\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n--- no_error_log\n[error, error]\n\n\n\n=== TEST 12: get APISIX-NACOS info from NACOS - configured in upstreams with namespace_id\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 13: get APISIX-NACOS info from NACOS - configured in upstreams with group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 14: get APISIX-NACOS info from NACOS - configured in upstreams with namespace_id and group_name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n      discovery_args:\n        namespace_id: test_ns\n        group_name: test_group\n#END\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n]\n--- response_body_like eval\n[\n    qr/server [1-2]/,\n    qr/server [1-2]/,\n]\n\n\n\n=== TEST 15: get APISIX-NACOS info from NACOS - configured in upstreams + etcd\n--- extra_yaml_config\ndiscovery:\n  nacos:\n    host:\n      - \"http://127.0.0.1:8858\"\n    fetch_interval: 1\n    access_key: \"my_access_key\"\n    secret_key: \"my_secret_key\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_name\": \"APISIX-NACOS\",\n                    \"discovery_type\": \"nacos\",\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: same namespace_id and service_name, different group_name\n--- extra_yaml_config\ndiscovery:\n  nacos:\n    host:\n      - \"http://127.0.0.1:8858\"\n    fetch_interval: 1\n    access_key: \"my_access_key\"\n    secret_key: \"my_secret_key\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n           -- use nacos-service5\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns\",\n                          \"group_name\": \"test_group\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            -- use nacos-service6\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello1\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns\",\n                          \"group_name\": \"test_group2\"\n                        }\n                    },\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(1.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri1, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            res, err = httpc:request_uri(uri2, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nserver 1\nserver 3\n\n\n\n=== TEST 17: same group_name and service_name, different namespace_id\n--- extra_yaml_config\ndiscovery:\n  nacos:\n    host:\n      - \"http://127.0.0.1:8858\"\n    fetch_interval: 1\n    access_key: \"my_access_key\"\n    secret_key: \"my_secret_key\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n           -- use nacos-service5\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns\",\n                          \"group_name\": \"test_group\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            -- use nacos-service7\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello1\",\n                    \"upstream\": {\n                        \"service_name\": \"APISIX-NACOS\",\n                        \"discovery_type\": \"nacos\",\n                        \"type\": \"roundrobin\",\n                        \"discovery_args\": {\n                          \"namespace_id\": \"test_ns2\",\n                          \"group_name\": \"test_group\"\n                        }\n                    },\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(1.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri1, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            res, err = httpc:request_uri(uri2, { method = \"GET\"})\n            if err then\n                ngx.log(ngx.ERR, err)\n                ngx.status = res.status\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nserver 1\nserver 4\n"
  },
  {
    "path": "t/discovery/reset-healthchecker.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworkers(1);\n\n\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nupstreams:\n    - service_name: mock\n      discovery_type: mock\n      type: roundrobin\n      checks:\n        active:\n          http_path: /\n          timeout: 1\n          unhealthy:\n            tcp_failures: 30\n            interval: 1\n          healthy:\n            interval: 1\n      id: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Validate healthchecker recreation on node count reduces to 1\n--- http_config\nserver {\n    listen 3000 ;\n    location / {\n      return 200 'ok';\n    }\n}\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 3000, weight = 50},\n                        {host = \"127.0.0.1\", port = 8000, weight = 50},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(5)\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 3000, weight = 1}\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.body)\n            ngx.sleep(20)\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n--- timeout: 37\n--- no_error_log\nunhealthy TCP increment (20/30)\n\n\n\n=== TEST 2: Validate healthchecker deletion on node count reduces to 0\n--- http_config\nserver {\n    listen 3000 ;\n    location / {\n      return 200 'ok';\n    }\n}\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 3000, weight = 50},\n                        {host = \"127.0.0.1\", port = 8000, weight = 50},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(5)\n            discovery.mock = {\n                nodes = function()\n                    return {\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.status = res.status\n            ngx.sleep(20)\n        }\n    }\n--- request\nGET /t\n--- timeout: 37\n--- no_error_log\nunhealthy TCP increment (20/30)\n--- error_code: 503\n"
  },
  {
    "path": "t/discovery/stream/consul.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 20999;\n\n        location / {\n            content_by_lua_block {\n                ngx.say(\"missing consul services\")\n            }\n        }\n    }\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n    server {\n        listen 30512;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 2\")\n            }\n        }\n    }\n    server {\n        listen 30513;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 3\")\n            }\n        }\n    }\n    server {\n        listen 30514;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 4\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"GET /hello HTTP/1.1\\r\\nHost: 127.0.0.1:1985\\r\\nConnection: close\\r\\n\\r\\n\");\n    }\n});\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\n  enable_control: true\n  control:\n    ip: 127.0.0.1\n    port: 9090\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    skip_services:\n      - \"service_c\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: prepare consul catalog register nodes\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/agent/service/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"PUT /consul1/deregister/service_a1\",\n    \"PUT /consul1/deregister/service_b1\",\n    \"PUT /consul1/deregister/service_a2\",\n    \"PUT /consul1/deregister/service_b2\",\n    \"PUT /consul2/deregister/service_a1\",\n    \"PUT /consul2/deregister/service_b1\",\n    \"PUT /consul2/deregister/service_a2\",\n    \"PUT /consul2/deregister/service_b2\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a1\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30511,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_a2\\\",\\\"Name\\\":\\\"service_a\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30512,\\\"Meta\\\":{\\\"service_a_version\\\":\\\"4.0\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_b1\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30513,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n    \"PUT /consul1/register\\n\" . \"{\\\"ID\\\":\\\"service_b2\\\",\\\"Name\\\":\\\"service_b\\\",\\\"Tags\\\":[\\\"primary\\\",\\\"v1\\\"],\\\"Address\\\":\\\"127.0.0.1\\\",\\\"Port\\\":30514,\\\"Meta\\\":{\\\"service_b_version\\\":\\\"4.1\\\"},\\\"EnableTagOverride\\\":false,\\\"Weights\\\":{\\\"Passing\\\":10,\\\"Warning\\\":1}}\",\n]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200]\n\n\n\n=== TEST 2: test consul server 1\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [1-2]/\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: test consul server 2\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: service_b\n      discovery_type: consul\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [3-4]/\n--- no_error_log\n[error]\n\n\n\n=== TEST 4: test mini consul config\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:6500\"\n#END\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [1-2]/\n--- ignore_error_log\n\n\n\n=== TEST 5: test invalid service name\nsometimes the consul key maybe deleted by mistake\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: service_c\n      discovery_type: consul\n      type: roundrobin\n#END\n--- stream_response_like\nmissing consul services\n--- ignore_error_log\n\n\n\n=== TEST 6: test skip keys\nskip some services, return default nodes, get response: missing consul services\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul:\n    servers:\n      - \"http://127.0.0.1:8600\"\n    prefix: \"upstreams\"\n    skip_services:\n      - \"service_a\"\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n#END\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: service_a\n      discovery_type: consul\n      type: roundrobin\n#END\n--- stream_response_like\nmissing consul services\n--- ignore_error_log\n"
  },
  {
    "path": "t/discovery/stream/consul_kv.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 20999;\n\n        location / {\n            content_by_lua_block {\n                ngx.say(\"missing consul_kv services\")\n            }\n        }\n    }\n\n    server {\n        listen 30511;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 1\")\n            }\n        }\n    }\n    server {\n        listen 30512;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 2\")\n            }\n        }\n    }\n    server {\n        listen 30513;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 3\")\n            }\n        }\n    }\n    server {\n        listen 30514;\n\n        location /hello {\n            content_by_lua_block {\n                ngx.say(\"server 4\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"GET /hello HTTP/1.1\\r\\nHost: 127.0.0.1:1985\\r\\nConnection: close\\r\\n\\r\\n\");\n    }\n});\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:8600\"\n    prefix: \"upstreams\"\n    skip_keys:\n      - \"upstreams/unused_api/\"\n    timeout:\n      connect: 1000\n      read: 1000\n      wait: 60\n    weight: 1\n    fetch_interval: 1\n    keepalive: true\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n_EOC_\n\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: prepare consul kv register nodes\n--- config\nlocation /consul1 {\n    rewrite  ^/consul1/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8500;\n}\n\nlocation /consul2 {\n    rewrite  ^/consul2/(.*) /v1/kv/$1 break;\n    proxy_pass http://127.0.0.1:8600;\n}\n--- pipelined_requests eval\n[\n    \"DELETE /consul1/upstreams/webpages/?recurse=true\",\n    \"DELETE /consul2/upstreams/webpages/?recurse=true\",\n    \"PUT /consul1/upstreams/webpages/127.0.0.1:30511\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul1/upstreams/webpages/127.0.0.1:30512\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul2/upstreams/webpages/127.0.0.1:30513\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n    \"PUT /consul2/upstreams/webpages/127.0.0.1:30514\\n\" . \"{\\\"weight\\\": 1, \\\"max_fails\\\": 2, \\\"fail_timeout\\\": 1}\",\n]\n--- response_body eval\n[\"true\", \"true\", \"true\", \"true\", \"true\", \"true\"]\n\n\n\n=== TEST 2: test consul server 1\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [1-2]/\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: test consul server 2\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: http://127.0.0.1:8600/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [3-4]/\n--- no_error_log\n[error]\n\n\n\n=== TEST 4: test mini consul_kv config\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8500\"\n      - \"http://127.0.0.1:6500\"\n#END\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [1-2]/\n--- ignore_error_log\n\n\n\n=== TEST 5: test invalid service name\nsometimes the consul key maybe deleted by mistake\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: http://127.0.0.1:8600/v1/kv/upstreams/deleted_keys/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- stream_response_like\nmissing consul_kv services\n--- ignore_error_log\n\n\n\n=== TEST 6: test skip keys\nskip some keys, return default nodes, get response: missing consul_kv services\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  consul_kv:\n    servers:\n      - \"http://127.0.0.1:8600\"\n    prefix: \"upstreams\"\n    skip_keys:\n      - \"upstreams/webpages/\"\n    default_service:\n      host: \"127.0.0.1\"\n      port: 20999\n      metadata:\n        fail_timeout: 1\n        weight: 1\n        max_fails: 1\n#END\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: http://127.0.0.1:8600/v1/kv/upstreams/webpages/\n      discovery_type: consul_kv\n      type: roundrobin\n#END\n--- stream_response_like\nmissing consul_kv services\n--- ignore_error_log\n"
  },
  {
    "path": "t/discovery/stream/dns.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nstream_routes:\n  - id: 1\n    server_port: 1985\n    upstream_id: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"GET /hello HTTP/1.0\\r\\nHost: 127.0.0.1:1985\\r\\n\\r\\n\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: default port to 53\n--- log_level: debug\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1\"\n--- apisix_yaml\nupstreams:\n    - service_name: sd.test.local\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nconnect to 127.0.0.1:53\n\n\n\n=== TEST 2: A\n--- apisix_yaml\nupstreams:\n    - service_name: \"sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":1,\"127.0.0.2:1980\":1|\"127.0.0.2:1980\":1,\"127.0.0.1:1980\":1)\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 3: AAAA\n--- listen_ipv6\n--- apisix_yaml\nupstreams:\n    - service_name: \"ipv6.sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- stream_response_like\nhello world\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to [0:0:0:0:0:0:0:1]:1980\n\n\n\n=== TEST 4: prefer A to AAAA\n--- listen_ipv6\n--- apisix_yaml\nupstreams:\n    - service_name: \"mix.sd.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- stream_response_like\nhello world\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 5: no /etc/hosts\n--- apisix_yaml\nupstreams:\n    - service_name: test.com\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nfailed to query the DNS server\n\n\n\n=== TEST 6: no /etc/resolv.conf\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\n    enable_resolv_search_option: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:                        # service discovery center\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n--- apisix_yaml\nupstreams:\n    - service_name: apisix\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nfailed to query the DNS server\n\n\n\n=== TEST 7: SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":20|\"127.0.0.2:1980\":20,\"127.0.0.1:1980\":60)\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 8: SRV (RFC 2782 style)\n--- apisix_yaml\nupstreams:\n    - service_name: \"_sip._tcp.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":20|\"127.0.0.2:1980\":20,\"127.0.0.1:1980\":60)\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 9: SRV (different port)\n--- apisix_yaml\nupstreams:\n    - service_name: \"port.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1981\":20|\"127.0.0.2:1981\":20,\"127.0.0.1:1980\":60)\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 10: SRV (zero weight)\n--- apisix_yaml\nupstreams:\n    - service_name: \"zero-weight.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":1|\"127.0.0.2:1980\":1,\"127.0.0.1:1980\":60)\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 11: SRV (split weight)\n--- apisix_yaml\nupstreams:\n    - service_name: \"split-weight.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(,?\"127.0.0.(1:1980\":200|3:1980\":1|4:1980\":1)){3}\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 12: SRV (priority)\n--- apisix_yaml\nupstreams:\n    - service_name: \"priority.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- stream_response_like\nhello world\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1980\n\n\n\n=== TEST 13: prefer SRV than A\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nproxy request to 127.0.0.1:1980\n--- stream_response_like\nhello world\n\n\n\n=== TEST 14: SRV (port is 0)\n--- apisix_yaml\nupstreams:\n    - service_name: \"zero.srv.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nno valid upstream node\n\n\n\n=== TEST 15: SRV (override port)\n--- apisix_yaml\nupstreams:\n    - service_name: \"port.srv.test.local:1980\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- grep_error_log eval\nqr/upstream nodes: \\{[^}]+\\}/\n--- grep_error_log_out eval\nqr/upstream nodes: \\{(\"127.0.0.1:1980\":60,\"127.0.0.2:1980\":20|\"127.0.0.2:1980\":20,\"127.0.0.1:1980\":60)\\}/\n--- stream_response_like\nhello world\n\n\n\n=== TEST 16: prefer A than SRV when A is ahead of SRV in config.yaml\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n    dns:\n        servers:\n            - \"127.0.0.1:1053\"\n        order:\n            - A\n            - SRV\n--- apisix_yaml\nupstreams:\n    - service_name: \"srv-a.test.local\"\n      discovery_type: dns\n      type: roundrobin\n      id: 1\n--- error_log\nno valid upstream node\n"
  },
  {
    "path": "t/discovery/stream/eureka.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  eureka:\n    host:\n      - \"http://127.0.0.1:8761\"\n    prefix: \"/eureka/\"\n    fetch_interval: 10\n    weight: 80\n    timeout:\n      connect: 1500\n      send: 1500\n      read: 1500\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"GET /eureka/apps/APISIX-EUREKA HTTP/1.1\\r\\nHost: 127.0.0.1:1985\\r\\nConnection: close\\r\\n\\r\\n\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: get APISIX-EUREKA info from EUREKA\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  -\n    id: 1\n    server_port: 1985\n    upstream:\n      service_name: APISIX-EUREKA\n      discovery_type: eureka\n      type: roundrobin\n\n#END\n--- stream_response_like\n.*<name>APISIX-EUREKA</name>.*\n--- error_log\nuse config_provider: yaml\ndefault_weight:80.\nfetch_interval:10.\neureka uri:http://127.0.0.1:8761/eureka/.\nconnect_timeout:1500, send_timeout:1500, read_timeout:1500.\n\n\n\n=== TEST 2: error service_name name\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  -\n    id: 1\n    server_port: 1985\n    upstream:\n      service_name: APISIX-EUREKA-DEMO\n      discovery_type: eureka\n      type: roundrobin\n\n#END\n--- error_log eval\nqr/.* no valid upstream node.*/\n"
  },
  {
    "path": "t/discovery/stream/nacos.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\nworkers(4);\n\nour $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  nacos:\n      host:\n        - \"http://127.0.0.1:8858\"\n      prefix: \"/nacos/v1/\"\n      fetch_interval: 1\n      weight: 1\n      timeout:\n        connect: 2000\n        send: 2000\n        read: 5000\n\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"GET /hello HTTP/1.1\\r\\nHost: 127.0.0.1:1985\\r\\nConnection: close\\r\\n\\r\\n\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: get APISIX-NACOS info from NACOS - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- stream_response eval\nqr/server [1-2]/\n--- no_error_log\n[error]\n\n\n\n=== TEST 2: error service_name name - no auth\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  - server_addr: 127.0.0.1\n    server_port: 1985\n    id: 1\n    upstream:\n      service_name: APISIX-NACOS-DEMO\n      discovery_type: nacos\n      type: roundrobin\n#END\n--- error_log\nno valid upstream node\n"
  },
  {
    "path": "t/error_page/error_page.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\n# We put the error page into apisix-runtime. It is fine since this installation is the default.\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route with serverless-post-function plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function() if ngx.var.http_x_test_status ~= nil then;ngx.exit(tonumber(ngx.var.http_x_test_status));end;end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: test apisix with internal error code 500\n--- request\nGET /hello\n--- more_headers\nX-Test-Status: 500\n--- error_code: 500\n--- response_body_like\n.*apisix.apache.org.*\n\n\n\n=== TEST 3: test apisix with internal error code 502\n--- request\nGET /hello\n--- more_headers\nX-Test-Status: 502\n--- error_code: 502\n--- response_body eval\nqr/502 Bad Gateway/\n\n\n\n=== TEST 4: test apisix with internal error code 503\n--- request\nGET /hello\n--- more_headers\nX-Test-Status: 503\n--- error_code: 503\n--- response_body eval\nqr/503 Service Temporarily Unavailable/\n\n\n\n=== TEST 5: test apisix with internal error code 504\n--- request\nGET /hello\n--- more_headers\nX-Test-Status: 504\n--- error_code: 504\n--- response_body eval\nqr/504 Gateway Time-out/\n\n\n\n=== TEST 6: test apisix with upstream error code 500\n--- request\nGET /specific_status\n--- more_headers\nX-Test-Upstream-Status: 500\n--- error_code: 500\n--- response_body\nupstream status: 500\n\n\n\n=== TEST 7: test apisix with internal error code 500, method isn't GET or HEAD\n--- request\nPOST /hello\n123\n--- more_headers\nX-Test-Status: 500\n--- error_code: 500\n--- response_body_like\n.*apisix.apache.org.*\n\n\n\n=== TEST 8: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 9: set route which upstream is blocking\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: check if the phases after proxy are run when 500 happens before proxy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function() if ngx.var.http_x_test_status ~= nil then;ngx.exit(tonumber(ngx.var.http_x_test_status));end;end\"]\n                        },\n                        \"serverless-pre-function\": {\n                            \"phase\": \"log\",\n                            \"functions\" : [\"return function() ngx.log(ngx.WARN, 'run log phase in error_page') end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit\n--- request\nGET /hello\n--- more_headers\nX-Test-Status: 500\n--- error_code: 500\n--- response_body_like\n.*apisix.apache.org.*\n--- error_log\nrun log phase in error_page\n"
  },
  {
    "path": "t/fake-plugin-exit.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n    }\n}\n\n\nlocal plugin_name = \"uri-blocker\"\n\nlocal _M = {\n    version = 0.1,\n    priority = 2900,\n    name = plugin_name,\n    schema = schema,\n}\n\n\nfunction _M.check_schema(conf)\n    return true\nend\n\n\nfunction _M.rewrite(conf, ctx)\n    core.respond.exit(400)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/fuzzing/client_abort.py",
    "content": "#! /usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport http.client\nimport subprocess\nimport time\nimport threading\nfrom public import check_leak, run_test\nimport yaml\n\ndef get_admin_key_from_yaml(yaml_file_path):\n    with open(yaml_file_path, 'r') as file:\n        yaml_data = yaml.safe_load(file)\n    try:\n        admin_key = yaml_data['deployment']['admin']['admin_key'][0]['key']\n        return admin_key\n    except KeyError:\n        return None\n\ndef create_route():\n    key = get_admin_key_from_yaml('conf/config.yaml')\n    if key is None:\n        print(\"Key not found in the YAML file.\")\n        return\n    command = '''curl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY:{key}\" -X PUT -d '\n{\n    \"uri\": \"/client_abort\",\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:6666\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n    '''\n    subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)\n\ndef req():\n    conn = http.client.HTTPConnection(\"127.0.0.1\", port=9080)\n    conn.request(\"GET\", \"/client_abort?seconds=0.01\")\n    time.sleep(0.001)\n    conn.close()\n\ndef run_in_thread():\n    for i in range(50):\n        req()\n\n@check_leak\ndef run():\n    th = [threading.Thread(target=run_in_thread) for i in range(10)]\n    for t in th:\n        t.start()\n    for t in th:\n        t.join()\n\n\nif __name__ == \"__main__\":\n    run_test(create_route,run)\n"
  },
  {
    "path": "t/fuzzing/http_upstream.py",
    "content": "#! /usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This file provides a fuzzing test with different upstreams\nimport http.client\nimport json\nimport random\nimport threading\nfrom public import check_leak, run_test, connect_admin\nimport yaml\n\nREQ_PER_THREAD = 50\nTHREADS_NUM = 4\nTOTOL_ROUTES = 10\n\ndef get_admin_key_from_yaml(yaml_file_path):\n    with open(yaml_file_path, 'r') as file:\n        yaml_data = yaml.safe_load(file)\n    try:\n        admin_key = yaml_data['deployment']['admin']['admin_key'][0]['key']\n        return admin_key\n    except KeyError:\n        return None\ndef create_route():\n    key = get_admin_key_from_yaml('conf/config.yaml')\n    if key is None:\n        print(\"Key not found in the YAML file.\")\n        return\n    for i in range(TOTOL_ROUTES):\n        conn = connect_admin()\n        scheme = \"http\" if i % 2 == 0 else \"https\"\n        port = \":6666\" if i % 2 == 0 else \":6667\"\n        suffix = str(i + 1)\n        i = str(i)\n        conf = json.dumps({\n            \"uri\": \"/*\",\n            \"host\": \"test\" + i + \".com\",\n            \"plugins\": {\n            },\n            \"upstream\": {\n                \"scheme\": scheme,\n                \"nodes\": {\n                    \"127.0.0.\" + suffix + port: 1\n                },\n                \"type\": \"roundrobin\"\n            },\n        })\n\n        conn.request(\"PUT\", \"/apisix/admin/routes/\" + i, conf,\n                headers={\n                    \"X-API-KEY\":key,\n                })\n        response = conn.getresponse()\n        assert response.status <= 300, response.read()\n\ndef req():\n    route_id = random.randrange(TOTOL_ROUTES)\n    conn = http.client.HTTPConnection(\"127.0.0.1\", port=9080)\n    conn.request(\"GET\", \"/server_addr\",\n            headers={\n                \"Host\":\"test\" + str(route_id) + \".com\",\n            })\n    response = conn.getresponse()\n    assert response.status == 200, response.read()\n    ip = response.read().rstrip().decode()\n    suffix = str(route_id + 1)\n    assert \"127.0.0.\" + suffix == ip, f\"expect: 127.0.0.{suffix}, actual: {ip}\"\n\ndef run_in_thread():\n    for i in range(REQ_PER_THREAD):\n        req()\n\n@check_leak\ndef run():\n    th = [threading.Thread(target=run_in_thread) for i in range(THREADS_NUM)]\n    for t in th:\n        t.start()\n    for t in th:\n        t.join()\n\n\nif __name__ == \"__main__\":\n    run_test(create_route, run)\n\n"
  },
  {
    "path": "t/fuzzing/public.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport http.client\nimport subprocess\nimport os\nfrom functools import wraps\nfrom pathlib import Path\nimport psutil\nfrom boofuzz import FuzzLoggerText, Session, TCPSocketConnection, Target\n\ndef cur_dir():\n    return os.path.split(os.path.realpath(__file__))[0]\n\ndef apisix_pwd():\n    return os.environ.get(\"APISIX_FUZZING_PWD\") or \\\n            (str(Path.home()) + \"/work/apisix/apisix\")\n\ndef connect_admin():\n    conn = http.client.HTTPConnection(\"127.0.0.1\", port=9180)\n    return conn\n\ndef check_log():\n    boofuzz_log = cur_dir() + \"/test.log\"\n    apisix_errorlog = apisix_pwd() + \"/logs/error.log\"\n    apisix_accesslog = apisix_pwd() + \"/logs/access.log\"\n\n    cmds = ['cat %s | grep -a \"error\" | grep -v \"invalid request body\"'%apisix_errorlog, 'cat %s | grep -a \" 500 \"'%apisix_accesslog]\n    if os.path.exists(boofuzz_log):\n        cmds.append('cat %s | grep -a \"fail\"'%boofuzz_log)\n    for cmd in cmds:\n        r = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)\n        err = r.stdout.read().strip()\n        print(\"Error in log: \", err)\n        assert err == b\"\"\n\ndef check_process():\n    with open(apisix_pwd() + \"/logs/nginx.pid\") as f:\n        pid = int(f.read().strip())\n    parent = psutil.Process(pid)\n    children = parent.children(recursive=True)\n    process = {p.pid for p in children if \"cache loader process\" not in p.cmdline()[0]}\n    process.add(parent.pid)\n    return process\n\ndef initfuzz():\n    fw = open(cur_dir() + \"/test.log\",'w')\n    fuzz_loggers = [FuzzLoggerText(file_handle=fw)]\n    session = Session(\n        target=Target(\n            connection=TCPSocketConnection(\"127.0.0.1\", 9080, send_timeout=5.0, recv_timeout=5.0, server=False)\n        ),\n        fuzz_loggers=fuzz_loggers,\n        keep_web_open=False,\n    )\n    return session\n\ndef sum_memory():\n    pmap = {}\n    for p in check_process():\n        proc = psutil.Process(p)\n        pmap[proc] = proc.memory_full_info()\n    return sum(m.rss for m in pmap.values())\n\ndef get_linear_regression_sloped(samples):\n    n = len(samples)\n    avg_x = (n + 1) / 2\n    avg_y = sum(samples) / n\n    avg_xy = sum([(i + 1) * v for i, v in enumerate(samples)]) / n\n    avg_x2 = sum([i * i for i in range(1, n + 1)]) / n\n    denom = avg_x2 - avg_x * avg_x\n    if denom == 0:\n        return None\n    return (avg_xy - avg_x * avg_y) / denom\n\ndef gc():\n    conn = http.client.HTTPConnection(\"127.0.0.1\", port=9090)\n    conn.request(\"POST\", \"/v1/gc\")\n    conn.close()\n\ndef leak_count():\n    return int(os.environ.get(\"APISIX_FUZZING_LEAK_COUNT\") or 100)\n\nLEAK_COUNT = leak_count()\n\ndef check_leak(f):\n    @wraps(f)\n    def wrapper(*args, **kwds):\n        global LEAK_COUNT\n\n        samples = []\n        for i in range(LEAK_COUNT):\n            f(*args, **kwds)\n            gc()\n            samples.append(sum_memory())\n        count = 0\n        for i in range(1, LEAK_COUNT):\n            if samples[i - 1] < samples[i]:\n                count += 1\n        print(samples)\n        sloped = get_linear_regression_sloped(samples)\n        print(sloped)\n        print(count / LEAK_COUNT)\n\n        if os.environ.get(\"CI\"): # CI is not stable\n            return\n\n        # the threshold is chosen so that we can find leaking a table per request\n        if sloped > 10000 and (count / LEAK_COUNT) > 0.2:\n            raise AssertionError(\"memory leak\")\n\n    return wrapper\n\ndef run_test(create_route, run):\n    # before test\n    create_route()\n    r1 = check_process()\n    run()\n    # after test\n    check_log()\n    r2 = check_process()\n    if r2 != r1:\n        print(\"before test, nginx's process list:%s,\\nafter test, nginx's process list:%s\"%(r1,r2))\n        raise AssertionError\n"
  },
  {
    "path": "t/fuzzing/requirements.txt",
    "content": "psutil==5.8.0\ntyping==3.7.4.3\nboofuzz==0.4.0\nPyYAML==5.4.1\n"
  },
  {
    "path": "t/fuzzing/serverless_route_test.py",
    "content": "#! /usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport subprocess\nfrom public import initfuzz, run_test\nfrom boofuzz import s_block, s_delim, s_get, s_group, s_initialize, s_size, s_static, s_string\nimport yaml\n\ndef get_admin_key_from_yaml(yaml_file_path):\n    with open(yaml_file_path, 'r') as file:\n        yaml_data = yaml.safe_load(file)\n    try:\n        admin_key = yaml_data['deployment']['admin']['admin_key'][0]['key']\n        return admin_key\n    except KeyError:\n        return None\n\ndef create_route():\n    key = get_admin_key_from_yaml('conf/config.yaml')\n    if key is None:\n        print(\"Key not found in the YAML file.\")\n        return\n    command = '''curl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: {key}\" -X PUT -d '\n{\n    \"uri\": \"/post*\",\n    \"methods\": [\"POST\"],\n    \"plugins\": {\n        \"serverless-post-function\": {\n            \"functions\": [\"return function()\\n local core = require(\\\"apisix.core\\\")\\n   ngx.req.read_body()\\n    local req_body = ngx.req.get_body_data()\\n    if req_body == \\\"{\\\\\\\"a\\\\\\\":\\\\\\\"b\\\\\\\"}\\\"  then\\n  return\\n else\\n  ngx.exit(ngx.HTTP_BAD_REQUEST)\\n end\\n end\\n\"]\n        }\n    },\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:6666\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n    '''\n    subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)\n\ndef run():\n    session = initfuzz()\n\n    s_initialize(name=\"Request\")\n    with s_block(\"Request-Line\"):\n        s_group(\"Method\", [\"GET\", \"HEAD\", \"POST\", \"PUT\", \"DELETE\", \"CONNECT\", \"OPTIONS\", \"TRACE\", \"PURGE\"])\n        s_delim(\" \", name=\"space-1\")\n        s_string(\"/post\", name=\"Request-URI\")\n        s_delim(\" \", name=\"space-2\")\n        s_string(\"HTTP/1.1\", name=\"HTTP-Version\")\n        s_static(\"\\r\\n\", name=\"Request-Line-CRLF\")\n        s_string(\"Host:\", name=\"Host-Line\")\n        s_delim(\" \", name=\"space-3\")\n        s_string(\"127.0.0.1:9080\", name=\"Host-Line-Value\")\n        s_static(\"\\r\\n\", name=\"Host-Line-CRLF\")\n        s_static('User-Agent', name='User-Agent-Header')\n        s_delim(':', name='User-Agent-Colon-1')\n        s_delim(' ', name='User-Agent-Space-1')\n        s_string('Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3223.8 Safari/537.36', name='User-Agent-Value')\n        s_static('\\r\\n', name='User-Agent-CRLF'),\n        s_static('Accept', name='Accept-Header')\n        s_delim(':', name='Accept-Colon-1')\n        s_delim(' ', name='Accept-Space-1')\n        s_string('text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', name='Accept-Value')\n        s_static('\\r\\n', name='Accept-CRLF')\n        s_static(\"Content-Length:\", name=\"Content-Length-Header\")\n        s_delim(\" \", name=\"space-4\")\n        s_size(\"Body-Content\", output_format=\"ascii\", name=\"Content-Length-Value\")\n        s_static(\"\\r\\n\", \"Content-Length-CRLF\")\n        s_static('Connection', name='Connection-Header')\n        s_delim(':', name='Connection-Colon-1')\n        s_delim(' ', name='Connection-Space-1')\n        s_group('Connection-Type', ['keep-alive', 'close'])\n        s_static('\\r\\n', 'Connection-CRLF')\n        s_static('Content-Type', name='Content-Type-Header')\n        s_delim(':', name='Content-Type-Colon-1')\n        s_delim(' ', name='Content-Type-Space-1')\n        s_string('application/x-www-form-urlencoded', name='Content-Type-Value')\n        s_static('\\r\\n', name='Content-Type-CRLF')\n    s_static(\"\\r\\n\", \"Request-CRLF\")\n\n    with s_block(\"Body-Content\"):\n        s_string('{\"a\":\"b\"}', name=\"Body-Content-Value\")\n\n    session.connect(s_get(\"Request\"))\n    session.fuzz(max_depth=1)\n\nif __name__ == \"__main__\":\n    run_test(create_route,run)\n"
  },
  {
    "path": "t/fuzzing/simple_http.py",
    "content": "#! /usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This file provides a fuzzing test with most common plugins via plain HTTP request\nimport http.client\nimport json\nimport random\nimport threading\nfrom public import check_leak, LEAK_COUNT, run_test, connect_admin\nimport yaml\n\nREQ_PER_THREAD = 50\nTHREADS_NUM = 10\nTOTOL_ROUTES = 50\n\ndef get_admin_key_from_yaml(yaml_file_path):\n    with open(yaml_file_path, 'r') as file:\n        yaml_data = yaml.safe_load(file)\n    try:\n        admin_key = yaml_data['deployment']['admin']['admin_key'][0]['key']\n        return admin_key\n    except KeyError:\n        return None\n\ndef create_route():\n    conf = json.dumps({\n        \"username\": \"jack\",\n        \"plugins\": {\n            \"jwt-auth\": {\n                \"key\": \"user-key\",\n                \"secret\": \"my-secret-key\"\n            }\n        }\n    })\n    conn = connect_admin()\n    key = get_admin_key_from_yaml('conf/config.yaml')\n    if key is None:\n        print(\"Key not found in the YAML file.\")\n        return\n    key = key.replace('\"', '')\n    print(\"the key is\", key)\n    headers = {\n    \"X-API-KEY\": key,\n    }\n    print(\"Request headers:\", headers)\n    conn.request(\"PUT\", \"/apisix/admin/consumers\", conf,\n            headers=headers)\n    response = conn.getresponse()\n    assert response.status <= 300, response.read()\n\n    for i in range(TOTOL_ROUTES):\n        conn = connect_admin()\n        i = str(i)\n        conf = json.dumps({\n            \"uri\": \"/*\",\n            \"host\": \"test\" + i + \".com\",\n            \"plugins\": {\n                \"limit-count\": {\n                    \"count\": LEAK_COUNT * REQ_PER_THREAD * THREADS_NUM,\n                    \"time_window\": 3600,\n                },\n                \"jwt-auth\": {\n                },\n                \"proxy-rewrite\": {\n                    \"uri\": \"/\" + i,\n                    \"headers\": {\n                        \"X-APISIX-Route\": \"apisix-\" + i\n                    }\n                },\n                \"response-rewrite\": {\n                    \"headers\": {\n                        \"X-APISIX-Route\": \"$http_x_apisix_route\"\n                    }\n                },\n            },\n            \"upstream\": {\n                \"nodes\": {\n                    \"127.0.0.1:6666\": 1\n                },\n                \"type\": \"roundrobin\"\n            },\n        })\n        conn.request(\"PUT\", \"/apisix/admin/routes/\" + i, conf,\n                headers=headers)\n        response = conn.getresponse()\n        assert response.status <= 300, response.read()\n\ndef req():\n    jwt_token = (\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\"+\n        \"eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.\"+\n        \"fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\")\n    route_id = str(random.randrange(TOTOL_ROUTES))\n    conn = http.client.HTTPConnection(\"127.0.0.1\", port=9080)\n    conn.request(\"GET\", \"/\",\n            headers={\n                \"Host\":\"test\" + route_id + \".com\",\n                \"Authorization\":jwt_token,\n            })\n    response = conn.getresponse()\n    assert response.status == 200, response.read()\n    hdr = response.headers[\"X-APISIX-Route\"]\n    assert hdr == \"apisix-\" + route_id, hdr\n\ndef run_in_thread():\n    for i in range(REQ_PER_THREAD):\n        req()\n\n@check_leak\ndef run():\n    th = [threading.Thread(target=run_in_thread) for i in range(THREADS_NUM)]\n    for t in th:\n        t.start()\n    for t in th:\n        t.join()\n\n\nif __name__ == \"__main__\":\n    run_test(create_route, run)\n"
  },
  {
    "path": "t/fuzzing/simpleroute_test.py",
    "content": "#! /usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport subprocess\nfrom public import initfuzz, run_test\nfrom boofuzz import s_block, s_delim, s_get, s_group, s_initialize, s_static, s_string\nimport yaml\n\n\ndef get_admin_key_from_yaml(yaml_file_path):\n    with open(yaml_file_path, 'r') as file:\n        yaml_data = yaml.safe_load(file)\n    try:\n        admin_key = yaml_data['deployment']['admin']['admin_key'][0]['key']\n        return admin_key\n    except KeyError:\n        return None\n\n\n\n\ndef create_route():\n    key = get_admin_key_from_yaml('conf/config.yaml')\n    if key is None:\n        print(\"Key not found in the YAML file.\")\n        return\n    # Construct curl command with the extracted key\n    command = f'''curl http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: {key}\" -X PUT -d '\n{{\n    \"uri\": \"/get*\",\n    \"methods\": [\"GET\"],\n    \"upstream\": {{\n        \"type\": \"roundrobin\",\n        \"nodes\": {{\n            \"127.0.0.1:6666\": 1\n        }}\n    }}\n}}'\n    '''\n    subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)\n\ndef run():\n    session = initfuzz()\n\n    s_initialize(name=\"Request\")\n    with s_block(\"Request-Line\"):\n        s_group(\"Method\", ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', \"PURGE\"])\n        s_delim(\" \", name='space-1')\n        s_string(\"/get\", name='Request-URI')\n        s_delim(\" \", name='space-2')\n        s_string('HTTP/1.1', name='HTTP-Version')\n        s_static(\"\\r\\n\", name=\"Request-Line-CRLF\")\n        s_string(\"Host:\", name=\"Host-Line\")\n        s_delim(\" \", name=\"space-3\")\n        s_string(\"example.com\", name=\"Host-Line-Value\")\n        s_static(\"\\r\\n\", name=\"Host-Line-CRLF\")\n        s_string(\"Connection:\", name=\"Connection-Line\")\n        s_delim(\" \", name=\"space-4\")\n        s_string(\"Keep-Alive\", name=\"Connection-Line-Value\")\n        s_static(\"\\r\\n\", name=\"Connection-Line-CRLF\")\n        s_string(\"User-Agent:\", name=\"User-Agent-Line\")\n        s_delim(\" \", name=\"space-5\")\n        s_string(\"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1\", name=\"User-Agent-Line-Value\")\n        s_static(\"\\r\\n\", name=\"User-Agent-Line-CRLF\")\n\n    s_static(\"\\r\\n\", \"Request-CRLF\")\n    session.connect(s_get(\"Request\"))\n    session.fuzz(max_depth=1)\n\nif __name__ == \"__main__\":\n    run_test(create_route,run)\n"
  },
  {
    "path": "t/fuzzing/upstream/nginx.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nmaster_process on;\nworker_processes 1;\nworker_cpu_affinity auto;\nerror_log logs/error.log error;\npid logs/nginx.pid;\nworker_rlimit_nofile 20480;\n\nevents {\n    accept_mutex off;\n    worker_connections 10620;\n}\n\nworker_shutdown_timeout 1;\n\nhttp {\n    lua_socket_log_errors off;\n\n    resolver ipv6=off local=on;\n\n    access_log off;\n    server_tokens off;\n    more_clear_headers Server;\n    keepalive_requests 10000;\n    tcp_nodelay on;\n\n    server {\n        listen 6666 reuseport;\n        location / {\n            content_by_lua_block {\n                ngx.say(\"cur time: \", ngx.time())\n            }\n        }\n\n        location /client_abort {\n            content_by_lua_block {\n                ngx.sleep(tonumber(ngx.var.arg_seconds or 1))\n            }\n        }\n\n        location /server_addr {\n            content_by_lua_block {\n                ngx.say(ngx.var.server_addr)\n            }\n        }\n    }\n\n    server {\n        listen 6667 ssl;\n        ssl_certificate ../../certs/apisix.crt;\n        ssl_certificate_key ../../certs/apisix.key;\n\n        location /server_addr {\n            content_by_lua_block {\n                ngx.say(ngx.var.server_addr)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "t/fuzzing/vars_route_test.py",
    "content": "#! /usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport subprocess\nfrom public import initfuzz, run_test\nfrom boofuzz import s_block, s_delim, s_get, s_group, s_initialize, s_static, s_string\nimport yaml\n\ndef get_admin_key_from_yaml(yaml_file_path):\n    with open(yaml_file_path, 'r') as file:\n        yaml_data = yaml.safe_load(file)\n    try:\n        admin_key = yaml_data['deployment']['admin']['admin_key'][0]['key']\n        return admin_key\n    except KeyError:\n        return None\ndef create_route():\n    key = get_admin_key_from_yaml('conf/config.yaml')\n    if key is None:\n        print(\"Key not found in the YAML file.\")\n        return\n    command = '''curl -i http://127.0.0.1:9180/apisix/admin/routes/1 -H \"X-API-KEY: {key}\" -X PUT -d '\n{\n    \"uri\": \"/parameter*\",\n    \"vars\": [\n        [\"arg_name\",\"==\",\"jack\"],\n        [\"http_token\",\"==\",\"140b543013d988f4767277b6f45ba542\"]\n    ],\n    \"upstream\": {\n        \"nodes\": {\n            \"127.0.0.1:6666\": 1\n        },\n        \"type\": \"roundrobin\"\n    }\n}'\n    '''\n    subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)\n\ndef run():\n    session = initfuzz()\n\n    s_initialize(name=\"Request\")\n    with s_block(\"Request-Line\"):\n        s_group(\"Method\", ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PURGE'])\n        s_delim(\" \", name='space-1')\n        s_string(\"/parameter?name=jack\", name='Request-URI')\n        s_delim(\" \", name='space-2')\n        s_string('HTTP/1.1', name='HTTP-Version')\n        s_static(\"\\r\\n\", name=\"Request-Line-CRLF\")\n        s_string(\"Host:\", name=\"Host-Line\")\n        s_delim(\" \", name=\"space-3\")\n        s_string(\"example.com\", name=\"Host-Line-Value\")\n        s_static(\"\\r\\n\", name=\"Host-Line-CRLF\")\n        s_string(\"Connection:\", name=\"Connection-Line\")\n        s_delim(\" \", name=\"space-4\")\n        s_string(\"Keep-Alive\", name=\"Connection-Line-Value\")\n        s_static(\"\\r\\n\", name=\"Connection-Line-CRLF\")\n        s_string(\"User-Agent:\", name=\"User-Agent-Line\")\n        s_delim(\" \", name=\"space-5\")\n        s_string(\"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1\", name=\"User-Agent-Line-Value\")\n        s_static(\"\\r\\n\", name=\"User-Agent-Line-CRLF\")\n        s_string(\"token:\", name=\"age-Line\")\n        s_delim(\" \", name=\"space-6\")\n        s_string(\"140b543013d988f4767277b6f45ba542\", name=\"age-Line-Value\")\n        s_static(\"\\r\\n\", name=\"age-Line-CRLF\")\n\n    s_static(\"\\r\\n\", \"Request-CRLF\")\n    session.connect(s_get(\"Request\"))\n    session.fuzz(max_depth=1)\n\nif __name__ == \"__main__\":\n    run_test(create_route,run)\n"
  },
  {
    "path": "t/gm/gm.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nBEGIN {\n    $ENV{TEST_ENV_GMSSL_CRT_ENC} = \"-----BEGIN CERTIFICATE-----\nMIIB2DCCAX6gAwIBAgIBAzAKBggqgRzPVQGDdTBFMQswCQYDVQQGEwJBQTELMAkG\nA1UECAwCQkIxCzAJBgNVBAoMAkNDMQswCQYDVQQLDAJERDEPMA0GA1UEAwwGc3Vi\nIGNhMB4XDTIyMTEwMjAzMTkzNloXDTMyMTAzMDAzMTkzNlowSTELMAkGA1UEBhMC\nQUExCzAJBgNVBAgMAkJCMQswCQYDVQQKDAJDQzELMAkGA1UECwwCREQxEzARBgNV\nBAMMCnNlcnZlciBlbmMwWjAUBggqgRzPVQGCLQYIKoEcz1UBgi0DQgAED+MQrLrZ\n9PbMmz/44Kb73Qc7FlMs7u034XImjJREBAn1KzZ7jqcYfCiV/buhmu1sLhMXnB69\nmERtf1tAaXcgIaNaMFgwCQYDVR0TBAIwADALBgNVHQ8EBAMCAzgwHQYDVR0OBBYE\nFBxHDo0gHhMoYkDeHWySTIJy5BZpMB8GA1UdIwQYMBaAFCTrpmbUig3JfveqAIGJ\n6n+vAk2AMAoGCCqBHM9VAYN1A0gAMEUCIHtXgpOxcb3mZv2scRZHZz5YGFr45dfk\nVfLkF9BkrB/xAiEA8EeUg7nCFfgHzrfgB7v0wgN1Hrgj8snTUO6IDfkBKYM=\n-----END CERTIFICATE-----\n\";\n}\n\nuse t::APISIX;\n\nif (-f \"/usr/local/tongsuo/bin/openssl\") {\n    plan 'no_plan';\n} else {\n    plan(skip_all => \"only for GM tests\");\n}\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    # setup default conf.yaml\n    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;\nplugins:\n    - gm\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set ssl\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local f = assert(io.open(\"t/certs/server_enc.crt\"))\n        local cert_enc = f:read(\"*a\")\n        f:close()\n\n        local f = assert(io.open(\"t/certs/server_sign.crt\"))\n        local cert_sign = f:read(\"*a\")\n        f:close()\n\n        local f = assert(io.open(\"t/certs/server_enc.key\"))\n        local pkey_enc = f:read(\"*a\")\n        f:close()\n\n        local f = assert(io.open(\"t/certs/server_sign.key\"))\n        local pkey_sign = f:read(\"*a\")\n        f:close()\n\n        local data = {cert = cert_enc,\n            key = pkey_enc,\n            certs = {cert_sign},\n            keys = {pkey_sign},\n            sni = \"localhost\",\n            gm = true,\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local code, body = t.test('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/echo\"\n            }]]\n        )\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- exec\n/usr/local/tongsuo/bin/openssl s_client -connect localhost:1994 -servername localhost -cipher ECDHE-SM2-WITH-SM4-SM3 -enable_ntls -ntls -verifyCAfile t/certs/gm_ca.crt -sign_cert t/certs/client_sign.crt -sign_key t/certs/client_sign.key -enc_cert t/certs/client_enc.crt -enc_key t/certs/client_enc.key\n--- response_body eval\nqr/^CONNECTED/\n--- no_error_log\nSSL_do_handshake() failed\n[error]\n\n\n\n=== TEST 3: reject bad SSL\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local f = assert(io.open(\"t/certs/server_enc.crt\"))\n        local cert_enc = f:read(\"*a\")\n        f:close()\n\n        local f = assert(io.open(\"t/certs/server_enc.key\"))\n        local pkey_enc = f:read(\"*a\")\n        f:close()\n\n        local data = {\n            cert = cert_enc,\n            key = pkey_enc,\n            sni = \"localhost\",\n            gm = true,\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.print(body)\n            return\n        end\n    }\n}\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"sign cert/key are required\"}\n\n\n\n=== TEST 4: hit with gm disabled\n--- extra_yaml_config\n--- exec\n/usr/local/tongsuo/bin/openssl s_client -connect localhost:1994 -servername localhost -cipher ECDHE-SM2-WITH-SM4-SM3 -enable_ntls -ntls -verifyCAfile t/certs/gm_ca.crt -sign_cert t/certs/client_sign.crt -sign_key t/certs/client_sign.key -enc_cert t/certs/client_enc.crt -enc_key t/certs/client_enc.key\n--- response_body\n--- error_log\nSSL_do_handshake() failed\n\n\n\n=== TEST 5: set ssl: server_enc with secret ref\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local f = assert(io.open(\"t/certs/server_sign.crt\"))\n        local cert_sign = f:read(\"*a\")\n        f:close()\n\n        local f = assert(io.open(\"t/certs/server_enc.key\"))\n        local pkey_enc = f:read(\"*a\")\n        f:close()\n\n        local f = assert(io.open(\"t/certs/server_sign.key\"))\n        local pkey_sign = f:read(\"*a\")\n        f:close()\n\n        local data = {\n            cert = \"$env://TEST_ENV_GMSSL_CRT_ENC\",\n            key = pkey_enc,\n            certs = {cert_sign},\n            keys = {pkey_sign},\n            sni = \"localhost\",\n            gm = true,\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local code, body = t.test('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/echo\"\n            }]]\n        )\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- exec\n/usr/local/tongsuo/bin/openssl s_client -connect localhost:1994 -servername localhost -cipher ECDHE-SM2-WITH-SM4-SM3 -enable_ntls -ntls -verifyCAfile t/certs/gm_ca.crt -sign_cert t/certs/client_sign.crt -sign_key t/certs/client_sign.key -enc_cert t/certs/client_enc.crt -enc_key t/certs/client_enc.key\n--- response_body eval\nqr/^CONNECTED/\n--- no_error_log\nSSL_do_handshake() failed\n[error]\n"
  },
  {
    "path": "t/grpc_server_example/go.mod",
    "content": "module github.com/api7/grpc_server_example\n\ngo 1.11\n\nrequire (\n\tgithub.com/golang/protobuf v1.5.2\n\tgolang.org/x/net v0.7.0\n\tgoogle.golang.org/grpc v1.53.0\n\tgoogle.golang.org/protobuf v1.33.0\n)\n"
  },
  {
    "path": "t/grpc_server_example/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=\ncloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=\ncloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=\ncloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=\ncloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=\ncloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=\ncloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=\ncloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o=\ncloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE=\ncloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=\ncloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=\ncloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg=\ncloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=\ncloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=\ncloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk=\ncloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc=\ncloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc=\ncloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04=\ncloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno=\ncloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak=\ncloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=\ncloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=\ncloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=\ncloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=\ncloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0=\ncloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc=\ncloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=\ncloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=\ncloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=\ncloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ=\ncloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY=\ncloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=\ncloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=\ncloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=\ncloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=\ncloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=\ncloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=\ncloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=\ncloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8=\ncloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM=\ncloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc=\ncloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI=\ncloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE=\ncloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE=\ncloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4=\ncloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=\ncloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw=\ncloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc=\ncloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=\ncloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=\ncloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI=\ncloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y=\ncloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=\ncloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=\ncloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0=\ncloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk=\ncloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg=\ncloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590=\ncloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk=\ncloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk=\ncloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U=\ncloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA=\ncloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM=\ncloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk=\ncloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=\ncloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=\ncloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4=\ncloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=\ncloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=\ncloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=\ncloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=\ncloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=\ncloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=\ncloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=\ncloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=\ncloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=\ncloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=\ncloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=\ncloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=\ncloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=\ncloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=\ncloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=\ncloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg=\ncloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo=\ncloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=\ncloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=\ncloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=\ncloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=\ncloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=\ncloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE=\ncloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM=\ncloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=\ncloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=\ncloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=\ncloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=\ncloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0=\ncloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38=\ncloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w=\ncloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=\ncloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=\ncloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA=\ncloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A=\ncloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s=\ncloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI=\ncloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=\ncloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM=\ncloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=\ncloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=\ncloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g=\ncloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4=\ncloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c=\ncloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s=\ncloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=\ncloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=\ncloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=\ncloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek=\ncloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0=\ncloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM=\ncloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q=\ncloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=\ncloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=\ncloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k=\ncloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4=\ncloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=\ncloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=\ncloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=\ncloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=\ncloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=\ncloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI=\ncloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8=\ncloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc=\ncloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw=\ncloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w=\ncloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI=\ncloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=\ncloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=\ncloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=\ncloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY=\ncloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08=\ncloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=\ncloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=\ncloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w=\ncloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM=\ncloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60=\ncloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo=\ncloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=\ncloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=\ncloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=\ncloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=\ncloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA=\ncloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI=\ncloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=\ncloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM=\ncloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o=\ncloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=\ncloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=\ncloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=\ncloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=\ncloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=\ncloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=\ncloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=\ncloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=\ncloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM=\ncloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY=\ncloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs=\ncloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g=\ncloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=\ncloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg=\ncloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0=\ncloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=\ncloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=\ncloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE=\ncloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8=\ncloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=\ncloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=\ncloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw=\ncloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE=\ncloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=\ncloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE=\ncloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM=\ncloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI=\ncloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=\ncloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=\ncloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=\ncloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=\ncloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA=\ncloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY=\ncloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=\ncloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=\ncloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8=\ncloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI=\ncloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk=\ncloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4=\ncloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=\ncloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=\ncloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM=\ncloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8=\ncloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8=\ncloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4=\ncloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=\ncloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=\ncloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=\ncloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=\ncloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA=\ncloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0=\ncloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4=\ncloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs=\ncloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA=\ncloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk=\ncloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE=\ncloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc=\ncloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=\ncloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=\ncloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo=\ncloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw=\ncloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=\ncloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=\ncloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70=\ncloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo=\ncloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=\ncloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=\ncloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg=\ncloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE=\ncloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=\ncloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI=\ncloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0=\ncloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg=\ncloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=\ncloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=\ncloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=\ncloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=\ncloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE=\ncloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U=\ncloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=\ncloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=\ncloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=\ncloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=\ncloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs=\ncloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70=\ncloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=\ncloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=\ncloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA=\ncloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM=\ncloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA=\ncloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0=\ncloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU=\ncloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg=\ncloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=\ncloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=\ncloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc=\ncloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y=\ncloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do=\ncloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo=\ncloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=\ncloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=\ncloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk=\ncloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44=\ncloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=\ncloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4=\ncloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4=\ncloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=\ncloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=\ncloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=\ncloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q=\ncloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA=\ncloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=\ncloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=\ncloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk=\ncloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk=\ncloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU=\ncloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s=\ncloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=\ncloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=\ncloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4=\ncloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U=\ncloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco=\ncloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo=\ncloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E=\ncloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU=\ncloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4=\ncloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw=\ncloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos=\ncloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=\ncloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=\ncloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0=\ncloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ncloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=\ncloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=\ncloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=\ncloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=\ncloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=\ncloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=\ncloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=\ncloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM=\ncloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA=\ncloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8=\ncloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4=\ncloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ=\ncloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg=\ncloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28=\ncloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y=\ncloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs=\ncloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg=\ncloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk=\ncloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw=\ncloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=\ncloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=\ncloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M=\ncloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU=\ncloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=\ncloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=\ncloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=\ncloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY=\ncloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E=\ncloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE=\ncloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g=\ncloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208=\ncloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w=\ncloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8=\ncloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=\ncloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=\ncloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc=\ncloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A=\ncloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo=\ncloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ=\ncloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=\ncloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=\ncloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M=\ncloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=\ngithub.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=\ngithub.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=\ngithub.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=\ngithub.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=\ngithub.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=\ngithub.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=\ngithub.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=\ngithub.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=\ngolang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=\ngoogle.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=\ngoogle.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=\ngoogle.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=\ngoogle.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=\ngoogle.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08=\ngoogle.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=\ngoogle.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=\ngoogle.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=\ngoogle.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=\ngoogle.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=\ngoogle.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=\ngoogle.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=\ngoogle.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=\ngoogle.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=\ngoogle.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=\ngoogle.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=\ngoogle.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=\ngoogle.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=\ngoogle.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=\ngoogle.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=\ngoogle.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=\ngoogle.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=\ngoogle.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "t/grpc_server_example/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/helloworld.proto\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/import.proto\n//go:generate protoc  --include_imports --descriptor_set_out=proto.pb --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/src.proto\n//go:generate protoc --descriptor_set_out=echo.pb --include_imports --proto_path=$PWD/proto echo.proto\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/echo.proto\n\n// Package main implements a server for Greeter service.\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/reflection\"\n\t\"google.golang.org/grpc/status\"\n\n\tpb \"github.com/api7/grpc_server_example/proto\"\n)\n\nvar (\n\tgrpcAddr      = \":10051\"\n\tgrpcsAddr     = \":10052\"\n\tgrpcsMtlsAddr string\n\tgrpcHTTPAddr  string\n\n\tcrtFilePath = \"../t/cert/apisix.crt\"\n\tkeyFilePath = \"../t/cert/apisix.key\"\n\tcaFilePath  string\n)\n\nfunc init() {\n\tflag.StringVar(&grpcAddr, \"grpc-address\", grpcAddr, \"address for grpc\")\n\tflag.StringVar(&grpcsAddr, \"grpcs-address\", grpcsAddr, \"address for grpcs\")\n\tflag.StringVar(&grpcsMtlsAddr, \"grpcs-mtls-address\", grpcsMtlsAddr, \"address for grpcs in mTLS\")\n\tflag.StringVar(&grpcHTTPAddr, \"grpc-http-address\", grpcHTTPAddr, \"addresses for http and grpc services at the same time\")\n\tflag.StringVar(&crtFilePath, \"crt\", crtFilePath, \"path to certificate\")\n\tflag.StringVar(&keyFilePath, \"key\", keyFilePath, \"path to key\")\n\tflag.StringVar(&caFilePath, \"ca\", caFilePath, \"path to ca\")\n}\n\n// server is used to implement helloworld.GreeterServer.\ntype server struct {\n\t// Embed the unimplemented server\n\tpb.UnimplementedGreeterServer\n\tpb.UnimplementedTestImportServer\n\tpb.UnimplementedEchoServer\n}\n\n// SayHello implements helloworld.GreeterServer\nfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {\n\tlog.Printf(\"Received: %v\", in.Name)\n\tlog.Printf(\"Enum Gender: %v\", in.GetGender())\n\tmsg := \"Hello \" + in.Name\n\n\tperson := in.GetPerson()\n\tif person != nil {\n\t\tif person.GetName() != \"\" {\n\t\t\tmsg += fmt.Sprintf(\", name: %v\", person.GetName())\n\t\t}\n\t\tif person.GetAge() != 0 {\n\t\t\tmsg += fmt.Sprintf(\", age: %v\", person.GetAge())\n\t\t}\n\t}\n\n\treturn &pb.HelloReply{\n\t\tMessage: msg,\n\t\tItems:   in.GetItems(),\n\t\tGender:  in.GetGender(),\n\t}, nil\n}\n\n// GetErrResp implements helloworld.GreeterServer\nfunc (s *server) GetErrResp(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {\n\tst := status.New(codes.Unavailable, \"Out of service\")\n\tst, err := st.WithDetails(&pb.ErrorDetail{\n\t\tCode:    1,\n\t\tMessage: \"The server is out of service\",\n\t\tType:    \"service\",\n\t})\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Unexpected error attaching metadata: %v\", err))\n\t}\n\n\treturn nil, st.Err()\n}\n\nfunc (s *server) SayHelloAfterDelay(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {\n\tselect {\n\tcase <-time.After(1 * time.Second):\n\t\tfmt.Println(\"overslept\")\n\tcase <-ctx.Done():\n\t\terrStr := ctx.Err().Error()\n\t\tif ctx.Err() == context.DeadlineExceeded {\n\t\t\treturn nil, status.Error(codes.DeadlineExceeded, errStr)\n\t\t}\n\t}\n\n\ttime.Sleep(1 * time.Second)\n\n\tlog.Printf(\"Received: %v\", in.Name)\n\n\treturn &pb.HelloReply{Message: \"Hello delay \" + in.Name}, nil\n}\n\nfunc (s *server) Plus(ctx context.Context, in *pb.PlusRequest) (*pb.PlusReply, error) {\n\tlog.Printf(\"Received: %v %v\", in.A, in.B)\n\treturn &pb.PlusReply{Result: in.A + in.B}, nil\n}\n\nfunc (s *server) EchoStruct(ctx context.Context, in *pb.StructRequest) (*pb.StructReply, error) {\n\tlog.Printf(\"Received: %+v\", in)\n\n\treturn &pb.StructReply{\n\t\tData: in.Data,\n\t}, nil\n}\n\n// SayHelloServerStream streams HelloReply back to the client.\nfunc (s *server) SayHelloServerStream(req *pb.HelloRequest, stream pb.Greeter_SayHelloServerStreamServer) error {\n\tlog.Printf(\"Received server side stream req: %v\\n\", req)\n\n\t// Say Hello 5 times.\n\tfor i := 0; i < 5; i++ {\n\t\tif err := stream.Send(&pb.HelloReply{\n\t\t\tMessage: fmt.Sprintf(\"Hello %s\", req.Name),\n\t\t}); err != nil {\n\t\t\treturn status.Errorf(codes.Unavailable, \"Unable to stream request back to client: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// SayHelloClientStream receives a stream of HelloRequest from a client.\nfunc (s *server) SayHelloClientStream(stream pb.Greeter_SayHelloClientStreamServer) error {\n\tlog.Println(\"SayHello client side streaming has been initiated.\")\n\tcache := \"\"\n\tfor {\n\t\treq, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\treturn stream.SendAndClose(&pb.HelloReply{Message: cache})\n\t\t}\n\t\tif err != nil {\n\t\t\treturn status.Errorf(codes.Unavailable, \"Failed to read client stream: %v\", err)\n\t\t}\n\t\tcache = fmt.Sprintf(\"%sHello %s!\", cache, req.Name)\n\t}\n}\n\n// SayHelloBidirectionalStream establishes a bidirectional stream with the client.\nfunc (s *server) SayHelloBidirectionalStream(stream pb.Greeter_SayHelloBidirectionalStreamServer) error {\n\tlog.Println(\"SayHello bidirectional streaming has been initiated.\")\n\n\tfor {\n\t\treq, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\treturn stream.Send(&pb.HelloReply{Message: \"stream ended\"})\n\t\t}\n\t\tif err != nil {\n\t\t\treturn status.Errorf(codes.Unavailable, \"Failed to read client stream: %v\", err)\n\t\t}\n\n\t\t// A small 0.5 sec sleep\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\tif err := stream.Send(&pb.HelloReply{Message: fmt.Sprintf(\"Hello %s\", req.Name)}); err != nil {\n\t\t\treturn status.Errorf(codes.Unknown, \"Failed to stream response back to client: %v\", err)\n\t\t}\n\t}\n}\n\n// SayMultipleHello implements helloworld.GreeterServer\nfunc (s *server) SayMultipleHello(ctx context.Context, in *pb.MultipleHelloRequest) (*pb.MultipleHelloReply, error) {\n\tlog.Printf(\"Received: %v\", in.Name)\n\tlog.Printf(\"Enum Gender: %v\", in.GetGenders())\n\tmsg := \"Hello \" + in.Name\n\n\tpersons := in.GetPersons()\n\tif persons != nil {\n\t\tfor _, person := range persons {\n\t\t\tif person.GetName() != \"\" {\n\t\t\t\tmsg += fmt.Sprintf(\", name: %v\", person.GetName())\n\t\t\t}\n\t\t\tif person.GetAge() != 0 {\n\t\t\t\tmsg += fmt.Sprintf(\", age: %v\", person.GetAge())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &pb.MultipleHelloReply{\n\t\tMessage: msg,\n\t\tItems:   in.GetItems(),\n\t\tGenders: in.GetGenders(),\n\t}, nil\n}\n\nfunc (s *server) Run(ctx context.Context, in *pb.Request) (*pb.Response, error) {\n\treturn &pb.Response{Body: in.User.Name + \" \" + in.Body}, nil\n}\n\nfunc gRPCAndHTTPFunc(grpcServer *grpc.Server) http.Handler {\n\treturn h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tmux := http.NewServeMux()\n\t\tmux.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Write([]byte(\"hello http\"))\n\t\t})\n\n\t\tif r.ProtoMajor == 2 && strings.Contains(r.Header.Get(\"Content-Type\"), \"application/grpc\") {\n\t\t\tgrpcServer.ServeHTTP(w, r)\n\t\t} else {\n\t\t\tmux.ServeHTTP(w, r)\n\t\t}\n\t}), &http2.Server{})\n}\n\nfunc main() {\n\tflag.Parse()\n\n\tgo func() {\n\t\tlis, err := net.Listen(\"tcp\", grpcAddr)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to listen: %v\", err)\n\t\t}\n\t\ts := grpc.NewServer()\n\n\t\treflection.Register(s)\n\t\tpb.RegisterGreeterServer(s, &server{})\n\t\tpb.RegisterTestImportServer(s, &server{})\n\t\tpb.RegisterEchoServer(s, &server{})\n\n\t\tif err := s.Serve(lis); err != nil {\n\t\t\tlog.Fatalf(\"failed to serve: %v\", err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tlis, err := net.Listen(\"tcp\", grpcsAddr)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to listen: %v\", err)\n\t\t}\n\n\t\tc, err := credentials.NewServerTLSFromFile(crtFilePath, keyFilePath)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"credentials.NewServerTLSFromFile err: %v\", err)\n\t\t}\n\t\ts := grpc.NewServer(grpc.Creds(c))\n\t\treflection.Register(s)\n\t\tpb.RegisterGreeterServer(s, &server{})\n\t\tif err := s.Serve(lis); err != nil {\n\t\t\tlog.Fatalf(\"failed to serve: %v\", err)\n\t\t}\n\t}()\n\n\tif grpcHTTPAddr != \"\" {\n\t\tgo func() {\n\t\t\tlis, err := net.Listen(\"tcp\", grpcHTTPAddr)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"failed to listen: %v\", err)\n\t\t\t}\n\t\t\ts := grpc.NewServer()\n\n\t\t\treflection.Register(s)\n\t\t\tpb.RegisterGreeterServer(s, &server{})\n\t\t\tpb.RegisterTestImportServer(s, &server{})\n\n\t\t\tif err := http.Serve(lis, gRPCAndHTTPFunc(s)); err != nil {\n\t\t\t\tlog.Fatalf(\"failed to serve grpc: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tif grpcsMtlsAddr != \"\" {\n\t\tgo func() {\n\t\t\tlis, err := net.Listen(\"tcp\", grpcsMtlsAddr)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"failed to listen: %v\", err)\n\t\t\t}\n\n\t\t\tcertificate, err := tls.LoadX509KeyPair(crtFilePath, keyFilePath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"could not load server key pair: %s\", err)\n\t\t\t}\n\n\t\t\tcertPool := x509.NewCertPool()\n\t\t\tca, err := os.ReadFile(caFilePath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"could not read ca certificate: %s\", err)\n\t\t\t}\n\n\t\t\tif ok := certPool.AppendCertsFromPEM(ca); !ok {\n\t\t\t\tlog.Fatalf(\"failed to append client certs\")\n\t\t\t}\n\n\t\t\tc := credentials.NewTLS(&tls.Config{\n\t\t\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\t\t\tCertificates: []tls.Certificate{certificate},\n\t\t\t\tClientCAs:    certPool,\n\t\t\t})\n\t\t\ts := grpc.NewServer(grpc.Creds(c))\n\t\t\treflection.Register(s)\n\t\t\tpb.RegisterGreeterServer(s, &server{})\n\t\t\tif err := s.Serve(lis); err != nil {\n\t\t\t\tlog.Fatalf(\"failed to serve: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tsignals := make(chan os.Signal)\n\tsignal.Notify(signals, os.Interrupt, syscall.SIGTERM)\n\tsig := <-signals\n\tlog.Printf(\"get signal %s, exit\\n\", sig.String())\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/echo.pb.go",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.28.1\n// \tprotoc        v3.6.1\n// source: proto/echo.proto\n\npackage proto\n\nimport (\n\t_struct \"github.com/golang/protobuf/ptypes/struct\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype StructRequest struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tData *_struct.Struct `protobuf:\"bytes,1,opt,name=data,proto3\" json:\"data,omitempty\"`\n}\n\nfunc (x *StructRequest) Reset() {\n\t*x = StructRequest{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_echo_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *StructRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StructRequest) ProtoMessage() {}\n\nfunc (x *StructRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_echo_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StructRequest.ProtoReflect.Descriptor instead.\nfunc (*StructRequest) Descriptor() ([]byte, []int) {\n\treturn file_proto_echo_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *StructRequest) GetData() *_struct.Struct {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype StructReply struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tData *_struct.Struct `protobuf:\"bytes,1,opt,name=data,proto3\" json:\"data,omitempty\"`\n}\n\nfunc (x *StructReply) Reset() {\n\t*x = StructReply{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_echo_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *StructReply) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StructReply) ProtoMessage() {}\n\nfunc (x *StructReply) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_echo_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StructReply.ProtoReflect.Descriptor instead.\nfunc (*StructReply) Descriptor() ([]byte, []int) {\n\treturn file_proto_echo_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *StructReply) GetData() *_struct.Struct {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nvar File_proto_echo_proto protoreflect.FileDescriptor\n\nvar file_proto_echo_proto_rawDesc = []byte{\n\t0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x70, 0x72, 0x6f,\n\t0x74, 0x6f, 0x12, 0x04, 0x65, 0x63, 0x68, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,\n\t0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74,\n\t0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3c, 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74,\n\t0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18,\n\t0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x04,\n\t0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x65,\n\t0x70, 0x6c, 0x79, 0x12, 0x2b, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,\n\t0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,\n\t0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61,\n\t0x32, 0x3e, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x36, 0x0a, 0x0a, 0x45, 0x63, 0x68, 0x6f,\n\t0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x12, 0x13, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x53, 0x74,\n\t0x72, 0x75, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x65, 0x63,\n\t0x68, 0x6f, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00,\n\t0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,\n\t0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_proto_echo_proto_rawDescOnce sync.Once\n\tfile_proto_echo_proto_rawDescData = file_proto_echo_proto_rawDesc\n)\n\nfunc file_proto_echo_proto_rawDescGZIP() []byte {\n\tfile_proto_echo_proto_rawDescOnce.Do(func() {\n\t\tfile_proto_echo_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_echo_proto_rawDescData)\n\t})\n\treturn file_proto_echo_proto_rawDescData\n}\n\nvar file_proto_echo_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_proto_echo_proto_goTypes = []interface{}{\n\t(*StructRequest)(nil),  // 0: echo.StructRequest\n\t(*StructReply)(nil),    // 1: echo.StructReply\n\t(*_struct.Struct)(nil), // 2: google.protobuf.Struct\n}\nvar file_proto_echo_proto_depIdxs = []int32{\n\t2, // 0: echo.StructRequest.data:type_name -> google.protobuf.Struct\n\t2, // 1: echo.StructReply.data:type_name -> google.protobuf.Struct\n\t0, // 2: echo.Echo.EchoStruct:input_type -> echo.StructRequest\n\t1, // 3: echo.Echo.EchoStruct:output_type -> echo.StructReply\n\t3, // [3:4] is the sub-list for method output_type\n\t2, // [2:3] is the sub-list for method input_type\n\t2, // [2:2] is the sub-list for extension type_name\n\t2, // [2:2] is the sub-list for extension extendee\n\t0, // [0:2] is the sub-list for field type_name\n}\n\nfunc init() { file_proto_echo_proto_init() }\nfunc file_proto_echo_proto_init() {\n\tif File_proto_echo_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_proto_echo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*StructRequest); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_echo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*StructReply); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_proto_echo_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_proto_echo_proto_goTypes,\n\t\tDependencyIndexes: file_proto_echo_proto_depIdxs,\n\t\tMessageInfos:      file_proto_echo_proto_msgTypes,\n\t}.Build()\n\tFile_proto_echo_proto = out.File\n\tfile_proto_echo_proto_rawDesc = nil\n\tfile_proto_echo_proto_goTypes = nil\n\tfile_proto_echo_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/echo.proto",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nsyntax = \"proto3\";\n\npackage echo;\noption go_package = \"./proto\";\n\nimport \"google/protobuf/struct.proto\";\n\nservice Echo {\n  rpc EchoStruct (StructRequest) returns (StructReply) {}\n}\n\nmessage StructRequest {\n  google.protobuf.Struct data = 1;\n}\n\nmessage StructReply {\n  google.protobuf.Struct data = 1;\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/echo_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.2.0\n// - protoc             v3.6.1\n// source: proto/echo.proto\n\npackage proto\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.32.0 or later.\nconst _ = grpc.SupportPackageIsVersion7\n\n// EchoClient is the client API for Echo service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype EchoClient interface {\n\tEchoStruct(ctx context.Context, in *StructRequest, opts ...grpc.CallOption) (*StructReply, error)\n}\n\ntype echoClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewEchoClient(cc grpc.ClientConnInterface) EchoClient {\n\treturn &echoClient{cc}\n}\n\nfunc (c *echoClient) EchoStruct(ctx context.Context, in *StructRequest, opts ...grpc.CallOption) (*StructReply, error) {\n\tout := new(StructReply)\n\terr := c.cc.Invoke(ctx, \"/echo.Echo/EchoStruct\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// EchoServer is the server API for Echo service.\n// All implementations must embed UnimplementedEchoServer\n// for forward compatibility\ntype EchoServer interface {\n\tEchoStruct(context.Context, *StructRequest) (*StructReply, error)\n\tmustEmbedUnimplementedEchoServer()\n}\n\n// UnimplementedEchoServer must be embedded to have forward compatible implementations.\ntype UnimplementedEchoServer struct {\n}\n\nfunc (UnimplementedEchoServer) EchoStruct(context.Context, *StructRequest) (*StructReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method EchoStruct not implemented\")\n}\nfunc (UnimplementedEchoServer) mustEmbedUnimplementedEchoServer() {}\n\n// UnsafeEchoServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to EchoServer will\n// result in compilation errors.\ntype UnsafeEchoServer interface {\n\tmustEmbedUnimplementedEchoServer()\n}\n\nfunc RegisterEchoServer(s grpc.ServiceRegistrar, srv EchoServer) {\n\ts.RegisterService(&Echo_ServiceDesc, srv)\n}\n\nfunc _Echo_EchoStruct_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(StructRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(EchoServer).EchoStruct(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/echo.Echo/EchoStruct\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(EchoServer).EchoStruct(ctx, req.(*StructRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Echo_ServiceDesc is the grpc.ServiceDesc for Echo service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Echo_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"echo.Echo\",\n\tHandlerType: (*EchoServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"EchoStruct\",\n\t\t\tHandler:    _Echo_EchoStruct_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"proto/echo.proto\",\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/helloworld.pb.go",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.27.1\n// \tprotoc        v3.12.4\n// source: proto/helloworld.proto\n\npackage proto\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Gender int32\n\nconst (\n\tGender_GENDER_UNKNOWN Gender = 0\n\tGender_GENDER_MALE    Gender = 1\n\tGender_GENDER_FEMALE  Gender = 2\n)\n\n// Enum value maps for Gender.\nvar (\n\tGender_name = map[int32]string{\n\t\t0: \"GENDER_UNKNOWN\",\n\t\t1: \"GENDER_MALE\",\n\t\t2: \"GENDER_FEMALE\",\n\t}\n\tGender_value = map[string]int32{\n\t\t\"GENDER_UNKNOWN\": 0,\n\t\t\"GENDER_MALE\":    1,\n\t\t\"GENDER_FEMALE\":  2,\n\t}\n)\n\nfunc (x Gender) Enum() *Gender {\n\tp := new(Gender)\n\t*p = x\n\treturn p\n}\n\nfunc (x Gender) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Gender) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_proto_helloworld_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Gender) Type() protoreflect.EnumType {\n\treturn &file_proto_helloworld_proto_enumTypes[0]\n}\n\nfunc (x Gender) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Gender.Descriptor instead.\nfunc (Gender) EnumDescriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{0}\n}\n\ntype Person struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tAge  int32  `protobuf:\"varint,2,opt,name=age,proto3\" json:\"age,omitempty\"`\n}\n\nfunc (x *Person) Reset() {\n\t*x = Person{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Person) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Person) ProtoMessage() {}\n\nfunc (x *Person) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Person.ProtoReflect.Descriptor instead.\nfunc (*Person) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Person) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Person) GetAge() int32 {\n\tif x != nil {\n\t\treturn x.Age\n\t}\n\treturn 0\n}\n\ntype HelloRequest struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tName   string   `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tItems  []string `protobuf:\"bytes,2,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tGender Gender   `protobuf:\"varint,3,opt,name=gender,proto3,enum=helloworld.Gender\" json:\"gender,omitempty\"`\n\tPerson *Person  `protobuf:\"bytes,4,opt,name=person,proto3\" json:\"person,omitempty\"`\n}\n\nfunc (x *HelloRequest) Reset() {\n\t*x = HelloRequest{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *HelloRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HelloRequest) ProtoMessage() {}\n\nfunc (x *HelloRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.\nfunc (*HelloRequest) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *HelloRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *HelloRequest) GetItems() []string {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\nfunc (x *HelloRequest) GetGender() Gender {\n\tif x != nil {\n\t\treturn x.Gender\n\t}\n\treturn Gender_GENDER_UNKNOWN\n}\n\nfunc (x *HelloRequest) GetPerson() *Person {\n\tif x != nil {\n\t\treturn x.Person\n\t}\n\treturn nil\n}\n\ntype HelloReply struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMessage string   `protobuf:\"bytes,1,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tItems   []string `protobuf:\"bytes,2,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tGender  Gender   `protobuf:\"varint,3,opt,name=gender,proto3,enum=helloworld.Gender\" json:\"gender,omitempty\"`\n}\n\nfunc (x *HelloReply) Reset() {\n\t*x = HelloReply{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[2]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *HelloReply) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HelloReply) ProtoMessage() {}\n\nfunc (x *HelloReply) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[2]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.\nfunc (*HelloReply) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *HelloReply) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *HelloReply) GetItems() []string {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\nfunc (x *HelloReply) GetGender() Gender {\n\tif x != nil {\n\t\treturn x.Gender\n\t}\n\treturn Gender_GENDER_UNKNOWN\n}\n\ntype PlusRequest struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tA int64 `protobuf:\"varint,1,opt,name=a,proto3\" json:\"a,omitempty\"`\n\tB int64 `protobuf:\"varint,2,opt,name=b,proto3\" json:\"b,omitempty\"`\n}\n\nfunc (x *PlusRequest) Reset() {\n\t*x = PlusRequest{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[3]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *PlusRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PlusRequest) ProtoMessage() {}\n\nfunc (x *PlusRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[3]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PlusRequest.ProtoReflect.Descriptor instead.\nfunc (*PlusRequest) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *PlusRequest) GetA() int64 {\n\tif x != nil {\n\t\treturn x.A\n\t}\n\treturn 0\n}\n\nfunc (x *PlusRequest) GetB() int64 {\n\tif x != nil {\n\t\treturn x.B\n\t}\n\treturn 0\n}\n\ntype PlusReply struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tResult int64 `protobuf:\"varint,1,opt,name=result,proto3\" json:\"result,omitempty\"`\n}\n\nfunc (x *PlusReply) Reset() {\n\t*x = PlusReply{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[4]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *PlusReply) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PlusReply) ProtoMessage() {}\n\nfunc (x *PlusReply) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[4]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PlusReply.ProtoReflect.Descriptor instead.\nfunc (*PlusReply) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *PlusReply) GetResult() int64 {\n\tif x != nil {\n\t\treturn x.Result\n\t}\n\treturn 0\n}\n\ntype MultipleHelloRequest struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tName    string    `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tItems   []string  `protobuf:\"bytes,2,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tGenders []Gender  `protobuf:\"varint,3,rep,packed,name=genders,proto3,enum=helloworld.Gender\" json:\"genders,omitempty\"`\n\tPersons []*Person `protobuf:\"bytes,4,rep,name=persons,proto3\" json:\"persons,omitempty\"`\n}\n\nfunc (x *MultipleHelloRequest) Reset() {\n\t*x = MultipleHelloRequest{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[5]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *MultipleHelloRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MultipleHelloRequest) ProtoMessage() {}\n\nfunc (x *MultipleHelloRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[5]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MultipleHelloRequest.ProtoReflect.Descriptor instead.\nfunc (*MultipleHelloRequest) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *MultipleHelloRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *MultipleHelloRequest) GetItems() []string {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\nfunc (x *MultipleHelloRequest) GetGenders() []Gender {\n\tif x != nil {\n\t\treturn x.Genders\n\t}\n\treturn nil\n}\n\nfunc (x *MultipleHelloRequest) GetPersons() []*Person {\n\tif x != nil {\n\t\treturn x.Persons\n\t}\n\treturn nil\n}\n\ntype MultipleHelloReply struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tMessage string   `protobuf:\"bytes,1,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tItems   []string `protobuf:\"bytes,2,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tGenders []Gender `protobuf:\"varint,3,rep,packed,name=genders,proto3,enum=helloworld.Gender\" json:\"genders,omitempty\"`\n}\n\nfunc (x *MultipleHelloReply) Reset() {\n\t*x = MultipleHelloReply{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[6]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *MultipleHelloReply) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MultipleHelloReply) ProtoMessage() {}\n\nfunc (x *MultipleHelloReply) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[6]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MultipleHelloReply.ProtoReflect.Descriptor instead.\nfunc (*MultipleHelloReply) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *MultipleHelloReply) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *MultipleHelloReply) GetItems() []string {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\nfunc (x *MultipleHelloReply) GetGenders() []Gender {\n\tif x != nil {\n\t\treturn x.Genders\n\t}\n\treturn nil\n}\n\ntype ErrorDetail struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tCode    int64  `protobuf:\"varint,1,opt,name=code,proto3\" json:\"code,omitempty\"`\n\tMessage string `protobuf:\"bytes,2,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tType    string `protobuf:\"bytes,3,opt,name=type,proto3\" json:\"type,omitempty\"`\n}\n\nfunc (x *ErrorDetail) Reset() {\n\t*x = ErrorDetail{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_helloworld_proto_msgTypes[7]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *ErrorDetail) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ErrorDetail) ProtoMessage() {}\n\nfunc (x *ErrorDetail) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_helloworld_proto_msgTypes[7]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ErrorDetail.ProtoReflect.Descriptor instead.\nfunc (*ErrorDetail) Descriptor() ([]byte, []int) {\n\treturn file_proto_helloworld_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *ErrorDetail) GetCode() int64 {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn 0\n}\n\nfunc (x *ErrorDetail) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *ErrorDetail) GetType() string {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn \"\"\n}\n\nvar File_proto_helloworld_proto protoreflect.FileDescriptor\n\nvar file_proto_helloworld_proto_rawDesc = []byte{\n\t0x0a, 0x16, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72,\n\t0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77,\n\t0x6f, 0x72, 0x6c, 0x64, 0x22, 0x2e, 0x0a, 0x06, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x12, 0x12,\n\t0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,\n\t0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,\n\t0x03, 0x61, 0x67, 0x65, 0x22, 0x90, 0x01, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65,\n\t0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,\n\t0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x74, 0x65,\n\t0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12,\n\t0x2a, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32,\n\t0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x47, 0x65, 0x6e,\n\t0x64, 0x65, 0x72, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x06, 0x70,\n\t0x65, 0x72, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65,\n\t0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x52,\n\t0x06, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x22, 0x68, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f,\n\t0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,\n\t0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,\n\t0x14, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05,\n\t0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18,\n\t0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72,\n\t0x6c, 0x64, 0x2e, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65,\n\t0x72, 0x22, 0x29, 0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,\n\t0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x61, 0x12, 0x0c,\n\t0x0a, 0x01, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x62, 0x22, 0x23, 0x0a, 0x09,\n\t0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73,\n\t0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c,\n\t0x74, 0x22, 0x9c, 0x01, 0x0a, 0x14, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65,\n\t0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,\n\t0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14,\n\t0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x69,\n\t0x74, 0x65, 0x6d, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x18,\n\t0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72,\n\t0x6c, 0x64, 0x2e, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x07, 0x67, 0x65, 0x6e, 0x64, 0x65,\n\t0x72, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20,\n\t0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64,\n\t0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x52, 0x07, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x73,\n\t0x22, 0x72, 0x0a, 0x12, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c,\n\t0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,\n\t0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,\n\t0x12, 0x14, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,\n\t0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72,\n\t0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77,\n\t0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x07, 0x67, 0x65, 0x6e,\n\t0x64, 0x65, 0x72, 0x73, 0x22, 0x4f, 0x0a, 0x0b, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x44, 0x65, 0x74,\n\t0x61, 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,\n\t0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,\n\t0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,\n\t0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,\n\t0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x40, 0x0a, 0x06, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12,\n\t0x12, 0x0a, 0x0e, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,\n\t0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x4d, 0x41,\n\t0x4c, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x46,\n\t0x45, 0x4d, 0x41, 0x4c, 0x45, 0x10, 0x02, 0x32, 0xda, 0x04, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65,\n\t0x74, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12,\n\t0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c,\n\t0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c,\n\t0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c,\n\t0x79, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x45, 0x72, 0x72, 0x52, 0x65, 0x73,\n\t0x70, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48,\n\t0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65,\n\t0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65,\n\t0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x04, 0x50, 0x6c, 0x75, 0x73, 0x12, 0x17, 0x2e,\n\t0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x50, 0x6c, 0x75, 0x73, 0x52,\n\t0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f,\n\t0x72, 0x6c, 0x64, 0x2e, 0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12,\n\t0x48, 0x0a, 0x12, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x41, 0x66, 0x74, 0x65, 0x72,\n\t0x44, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72,\n\t0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,\n\t0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c,\n\t0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x10, 0x53, 0x61, 0x79,\n\t0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x20, 0x2e,\n\t0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69,\n\t0x70, 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,\n\t0x1e, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x4d, 0x75, 0x6c,\n\t0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22,\n\t0x00, 0x12, 0x4c, 0x0a, 0x14, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x65, 0x72,\n\t0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c,\n\t0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75,\n\t0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64,\n\t0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12,\n\t0x4c, 0x0a, 0x14, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x43, 0x6c, 0x69, 0x65, 0x6e,\n\t0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77,\n\t0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,\n\t0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48,\n\t0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x12, 0x55, 0x0a,\n\t0x1b, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x42, 0x69, 0x64, 0x69, 0x72, 0x65, 0x63,\n\t0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68,\n\t0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52,\n\t0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f,\n\t0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00,\n\t0x28, 0x01, 0x30, 0x01, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,\n\t0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_proto_helloworld_proto_rawDescOnce sync.Once\n\tfile_proto_helloworld_proto_rawDescData = file_proto_helloworld_proto_rawDesc\n)\n\nfunc file_proto_helloworld_proto_rawDescGZIP() []byte {\n\tfile_proto_helloworld_proto_rawDescOnce.Do(func() {\n\t\tfile_proto_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_helloworld_proto_rawDescData)\n\t})\n\treturn file_proto_helloworld_proto_rawDescData\n}\n\nvar file_proto_helloworld_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_proto_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 8)\nvar file_proto_helloworld_proto_goTypes = []interface{}{\n\t(Gender)(0),                  // 0: helloworld.Gender\n\t(*Person)(nil),               // 1: helloworld.Person\n\t(*HelloRequest)(nil),         // 2: helloworld.HelloRequest\n\t(*HelloReply)(nil),           // 3: helloworld.HelloReply\n\t(*PlusRequest)(nil),          // 4: helloworld.PlusRequest\n\t(*PlusReply)(nil),            // 5: helloworld.PlusReply\n\t(*MultipleHelloRequest)(nil), // 6: helloworld.MultipleHelloRequest\n\t(*MultipleHelloReply)(nil),   // 7: helloworld.MultipleHelloReply\n\t(*ErrorDetail)(nil),          // 8: helloworld.ErrorDetail\n}\nvar file_proto_helloworld_proto_depIdxs = []int32{\n\t0,  // 0: helloworld.HelloRequest.gender:type_name -> helloworld.Gender\n\t1,  // 1: helloworld.HelloRequest.person:type_name -> helloworld.Person\n\t0,  // 2: helloworld.HelloReply.gender:type_name -> helloworld.Gender\n\t0,  // 3: helloworld.MultipleHelloRequest.genders:type_name -> helloworld.Gender\n\t1,  // 4: helloworld.MultipleHelloRequest.persons:type_name -> helloworld.Person\n\t0,  // 5: helloworld.MultipleHelloReply.genders:type_name -> helloworld.Gender\n\t2,  // 6: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest\n\t2,  // 7: helloworld.Greeter.GetErrResp:input_type -> helloworld.HelloRequest\n\t4,  // 8: helloworld.Greeter.Plus:input_type -> helloworld.PlusRequest\n\t2,  // 9: helloworld.Greeter.SayHelloAfterDelay:input_type -> helloworld.HelloRequest\n\t6,  // 10: helloworld.Greeter.SayMultipleHello:input_type -> helloworld.MultipleHelloRequest\n\t2,  // 11: helloworld.Greeter.SayHelloServerStream:input_type -> helloworld.HelloRequest\n\t2,  // 12: helloworld.Greeter.SayHelloClientStream:input_type -> helloworld.HelloRequest\n\t2,  // 13: helloworld.Greeter.SayHelloBidirectionalStream:input_type -> helloworld.HelloRequest\n\t3,  // 14: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply\n\t3,  // 15: helloworld.Greeter.GetErrResp:output_type -> helloworld.HelloReply\n\t5,  // 16: helloworld.Greeter.Plus:output_type -> helloworld.PlusReply\n\t3,  // 17: helloworld.Greeter.SayHelloAfterDelay:output_type -> helloworld.HelloReply\n\t7,  // 18: helloworld.Greeter.SayMultipleHello:output_type -> helloworld.MultipleHelloReply\n\t3,  // 19: helloworld.Greeter.SayHelloServerStream:output_type -> helloworld.HelloReply\n\t3,  // 20: helloworld.Greeter.SayHelloClientStream:output_type -> helloworld.HelloReply\n\t3,  // 21: helloworld.Greeter.SayHelloBidirectionalStream:output_type -> helloworld.HelloReply\n\t14, // [14:22] is the sub-list for method output_type\n\t6,  // [6:14] is the sub-list for method input_type\n\t6,  // [6:6] is the sub-list for extension type_name\n\t6,  // [6:6] is the sub-list for extension extendee\n\t0,  // [0:6] is the sub-list for field type_name\n}\n\nfunc init() { file_proto_helloworld_proto_init() }\nfunc file_proto_helloworld_proto_init() {\n\tif File_proto_helloworld_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_proto_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Person); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*HelloRequest); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*HelloReply); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*PlusRequest); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*PlusReply); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*MultipleHelloRequest); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*MultipleHelloReply); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_helloworld_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*ErrorDetail); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_proto_helloworld_proto_rawDesc,\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   8,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_proto_helloworld_proto_goTypes,\n\t\tDependencyIndexes: file_proto_helloworld_proto_depIdxs,\n\t\tEnumInfos:         file_proto_helloworld_proto_enumTypes,\n\t\tMessageInfos:      file_proto_helloworld_proto_msgTypes,\n\t}.Build()\n\tFile_proto_helloworld_proto = out.File\n\tfile_proto_helloworld_proto_rawDesc = nil\n\tfile_proto_helloworld_proto_goTypes = nil\n\tfile_proto_helloworld_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/helloworld.proto",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nsyntax = \"proto3\";\n\npackage helloworld;\noption go_package = \"./proto\";\n\nservice Greeter {\n  // Unary RPC.\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n  rpc GetErrResp (HelloRequest) returns (HelloReply) {}\n  rpc Plus (PlusRequest) returns (PlusReply) {}\n  rpc SayHelloAfterDelay (HelloRequest) returns (HelloReply) {}\n  rpc SayMultipleHello(MultipleHelloRequest) returns (MultipleHelloReply) {}\n\n  // Server side streaming.\n  rpc SayHelloServerStream (HelloRequest) returns (stream HelloReply) {}\n\n  // Client side streaming.\n  rpc SayHelloClientStream (stream HelloRequest) returns (HelloReply) {}\n\n  // Bidirectional streaming.\n  rpc SayHelloBidirectionalStream (stream HelloRequest) returns (stream HelloReply) {}\n\n}\n\nenum Gender {\n    GENDER_UNKNOWN = 0;\n    GENDER_MALE = 1;\n    GENDER_FEMALE = 2;\n}\n\nmessage Person {\n    string name = 1;\n    int32 age = 2;\n}\n\nmessage HelloRequest {\n  string name = 1;\n  repeated string items = 2;\n  Gender gender = 3;\n  Person person = 4;\n}\n\nmessage HelloReply {\n  string message = 1;\n  repeated string items = 2;\n  Gender gender = 3;\n}\n\nmessage PlusRequest {\n  int64 a = 1;\n  int64 b = 2;\n}\n\nmessage PlusReply {\n  int64 result = 1;\n}\n\nmessage MultipleHelloRequest {\n  string name = 1;\n  repeated string items = 2;\n  repeated Gender genders = 3;\n  repeated Person persons = 4;\n}\n\nmessage MultipleHelloReply{\n  string message = 1;\n  repeated string items = 2;\n  repeated Gender genders = 3;\n}\n\nmessage ErrorDetail {\n    int64 code = 1;\n    string message = 2;\n    string type = 3;\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/helloworld_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.2.0\n// - protoc             v3.12.4\n// source: proto/helloworld.proto\n\npackage proto\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.32.0 or later.\nconst _ = grpc.SupportPackageIsVersion7\n\n// GreeterClient is the client API for Greeter service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype GreeterClient interface {\n\t// Unary RPC.\n\tSayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)\n\tGetErrResp(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)\n\tPlus(ctx context.Context, in *PlusRequest, opts ...grpc.CallOption) (*PlusReply, error)\n\tSayHelloAfterDelay(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)\n\tSayMultipleHello(ctx context.Context, in *MultipleHelloRequest, opts ...grpc.CallOption) (*MultipleHelloReply, error)\n\t// Server side streaming.\n\tSayHelloServerStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (Greeter_SayHelloServerStreamClient, error)\n\t// Client side streaming.\n\tSayHelloClientStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloClientStreamClient, error)\n\t// Bidirectional streaming.\n\tSayHelloBidirectionalStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloBidirectionalStreamClient, error)\n}\n\ntype greeterClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {\n\treturn &greeterClient{cc}\n}\n\nfunc (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {\n\tout := new(HelloReply)\n\terr := c.cc.Invoke(ctx, \"/helloworld.Greeter/SayHello\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *greeterClient) GetErrResp(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {\n\tout := new(HelloReply)\n\terr := c.cc.Invoke(ctx, \"/helloworld.Greeter/GetErrResp\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *greeterClient) Plus(ctx context.Context, in *PlusRequest, opts ...grpc.CallOption) (*PlusReply, error) {\n\tout := new(PlusReply)\n\terr := c.cc.Invoke(ctx, \"/helloworld.Greeter/Plus\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *greeterClient) SayHelloAfterDelay(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {\n\tout := new(HelloReply)\n\terr := c.cc.Invoke(ctx, \"/helloworld.Greeter/SayHelloAfterDelay\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *greeterClient) SayMultipleHello(ctx context.Context, in *MultipleHelloRequest, opts ...grpc.CallOption) (*MultipleHelloReply, error) {\n\tout := new(MultipleHelloReply)\n\terr := c.cc.Invoke(ctx, \"/helloworld.Greeter/SayMultipleHello\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *greeterClient) SayHelloServerStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (Greeter_SayHelloServerStreamClient, error) {\n\tstream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[0], \"/helloworld.Greeter/SayHelloServerStream\", opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &greeterSayHelloServerStreamClient{stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\ntype Greeter_SayHelloServerStreamClient interface {\n\tRecv() (*HelloReply, error)\n\tgrpc.ClientStream\n}\n\ntype greeterSayHelloServerStreamClient struct {\n\tgrpc.ClientStream\n}\n\nfunc (x *greeterSayHelloServerStreamClient) Recv() (*HelloReply, error) {\n\tm := new(HelloReply)\n\tif err := x.ClientStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\nfunc (c *greeterClient) SayHelloClientStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloClientStreamClient, error) {\n\tstream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[1], \"/helloworld.Greeter/SayHelloClientStream\", opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &greeterSayHelloClientStreamClient{stream}\n\treturn x, nil\n}\n\ntype Greeter_SayHelloClientStreamClient interface {\n\tSend(*HelloRequest) error\n\tCloseAndRecv() (*HelloReply, error)\n\tgrpc.ClientStream\n}\n\ntype greeterSayHelloClientStreamClient struct {\n\tgrpc.ClientStream\n}\n\nfunc (x *greeterSayHelloClientStreamClient) Send(m *HelloRequest) error {\n\treturn x.ClientStream.SendMsg(m)\n}\n\nfunc (x *greeterSayHelloClientStreamClient) CloseAndRecv() (*HelloReply, error) {\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\tm := new(HelloReply)\n\tif err := x.ClientStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\nfunc (c *greeterClient) SayHelloBidirectionalStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloBidirectionalStreamClient, error) {\n\tstream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[2], \"/helloworld.Greeter/SayHelloBidirectionalStream\", opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &greeterSayHelloBidirectionalStreamClient{stream}\n\treturn x, nil\n}\n\ntype Greeter_SayHelloBidirectionalStreamClient interface {\n\tSend(*HelloRequest) error\n\tRecv() (*HelloReply, error)\n\tgrpc.ClientStream\n}\n\ntype greeterSayHelloBidirectionalStreamClient struct {\n\tgrpc.ClientStream\n}\n\nfunc (x *greeterSayHelloBidirectionalStreamClient) Send(m *HelloRequest) error {\n\treturn x.ClientStream.SendMsg(m)\n}\n\nfunc (x *greeterSayHelloBidirectionalStreamClient) Recv() (*HelloReply, error) {\n\tm := new(HelloReply)\n\tif err := x.ClientStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\n// GreeterServer is the server API for Greeter service.\n// All implementations must embed UnimplementedGreeterServer\n// for forward compatibility\ntype GreeterServer interface {\n\t// Unary RPC.\n\tSayHello(context.Context, *HelloRequest) (*HelloReply, error)\n\tGetErrResp(context.Context, *HelloRequest) (*HelloReply, error)\n\tPlus(context.Context, *PlusRequest) (*PlusReply, error)\n\tSayHelloAfterDelay(context.Context, *HelloRequest) (*HelloReply, error)\n\tSayMultipleHello(context.Context, *MultipleHelloRequest) (*MultipleHelloReply, error)\n\t// Server side streaming.\n\tSayHelloServerStream(*HelloRequest, Greeter_SayHelloServerStreamServer) error\n\t// Client side streaming.\n\tSayHelloClientStream(Greeter_SayHelloClientStreamServer) error\n\t// Bidirectional streaming.\n\tSayHelloBidirectionalStream(Greeter_SayHelloBidirectionalStreamServer) error\n\tmustEmbedUnimplementedGreeterServer()\n}\n\n// UnimplementedGreeterServer must be embedded to have forward compatible implementations.\ntype UnimplementedGreeterServer struct {\n}\n\nfunc (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SayHello not implemented\")\n}\nfunc (UnimplementedGreeterServer) GetErrResp(context.Context, *HelloRequest) (*HelloReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method GetErrResp not implemented\")\n}\nfunc (UnimplementedGreeterServer) Plus(context.Context, *PlusRequest) (*PlusReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Plus not implemented\")\n}\nfunc (UnimplementedGreeterServer) SayHelloAfterDelay(context.Context, *HelloRequest) (*HelloReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SayHelloAfterDelay not implemented\")\n}\nfunc (UnimplementedGreeterServer) SayMultipleHello(context.Context, *MultipleHelloRequest) (*MultipleHelloReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SayMultipleHello not implemented\")\n}\nfunc (UnimplementedGreeterServer) SayHelloServerStream(*HelloRequest, Greeter_SayHelloServerStreamServer) error {\n\treturn status.Errorf(codes.Unimplemented, \"method SayHelloServerStream not implemented\")\n}\nfunc (UnimplementedGreeterServer) SayHelloClientStream(Greeter_SayHelloClientStreamServer) error {\n\treturn status.Errorf(codes.Unimplemented, \"method SayHelloClientStream not implemented\")\n}\nfunc (UnimplementedGreeterServer) SayHelloBidirectionalStream(Greeter_SayHelloBidirectionalStreamServer) error {\n\treturn status.Errorf(codes.Unimplemented, \"method SayHelloBidirectionalStream not implemented\")\n}\nfunc (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}\n\n// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to GreeterServer will\n// result in compilation errors.\ntype UnsafeGreeterServer interface {\n\tmustEmbedUnimplementedGreeterServer()\n}\n\nfunc RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {\n\ts.RegisterService(&Greeter_ServiceDesc, srv)\n}\n\nfunc _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(HelloRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GreeterServer).SayHello(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/helloworld.Greeter/SayHello\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Greeter_GetErrResp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(HelloRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GreeterServer).GetErrResp(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/helloworld.Greeter/GetErrResp\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GreeterServer).GetErrResp(ctx, req.(*HelloRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Greeter_Plus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PlusRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GreeterServer).Plus(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/helloworld.Greeter/Plus\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GreeterServer).Plus(ctx, req.(*PlusRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Greeter_SayHelloAfterDelay_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(HelloRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GreeterServer).SayHelloAfterDelay(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/helloworld.Greeter/SayHelloAfterDelay\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GreeterServer).SayHelloAfterDelay(ctx, req.(*HelloRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Greeter_SayMultipleHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(MultipleHelloRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GreeterServer).SayMultipleHello(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/helloworld.Greeter/SayMultipleHello\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GreeterServer).SayMultipleHello(ctx, req.(*MultipleHelloRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Greeter_SayHelloServerStream_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(HelloRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(GreeterServer).SayHelloServerStream(m, &greeterSayHelloServerStreamServer{stream})\n}\n\ntype Greeter_SayHelloServerStreamServer interface {\n\tSend(*HelloReply) error\n\tgrpc.ServerStream\n}\n\ntype greeterSayHelloServerStreamServer struct {\n\tgrpc.ServerStream\n}\n\nfunc (x *greeterSayHelloServerStreamServer) Send(m *HelloReply) error {\n\treturn x.ServerStream.SendMsg(m)\n}\n\nfunc _Greeter_SayHelloClientStream_Handler(srv interface{}, stream grpc.ServerStream) error {\n\treturn srv.(GreeterServer).SayHelloClientStream(&greeterSayHelloClientStreamServer{stream})\n}\n\ntype Greeter_SayHelloClientStreamServer interface {\n\tSendAndClose(*HelloReply) error\n\tRecv() (*HelloRequest, error)\n\tgrpc.ServerStream\n}\n\ntype greeterSayHelloClientStreamServer struct {\n\tgrpc.ServerStream\n}\n\nfunc (x *greeterSayHelloClientStreamServer) SendAndClose(m *HelloReply) error {\n\treturn x.ServerStream.SendMsg(m)\n}\n\nfunc (x *greeterSayHelloClientStreamServer) Recv() (*HelloRequest, error) {\n\tm := new(HelloRequest)\n\tif err := x.ServerStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\nfunc _Greeter_SayHelloBidirectionalStream_Handler(srv interface{}, stream grpc.ServerStream) error {\n\treturn srv.(GreeterServer).SayHelloBidirectionalStream(&greeterSayHelloBidirectionalStreamServer{stream})\n}\n\ntype Greeter_SayHelloBidirectionalStreamServer interface {\n\tSend(*HelloReply) error\n\tRecv() (*HelloRequest, error)\n\tgrpc.ServerStream\n}\n\ntype greeterSayHelloBidirectionalStreamServer struct {\n\tgrpc.ServerStream\n}\n\nfunc (x *greeterSayHelloBidirectionalStreamServer) Send(m *HelloReply) error {\n\treturn x.ServerStream.SendMsg(m)\n}\n\nfunc (x *greeterSayHelloBidirectionalStreamServer) Recv() (*HelloRequest, error) {\n\tm := new(HelloRequest)\n\tif err := x.ServerStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\n// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Greeter_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"helloworld.Greeter\",\n\tHandlerType: (*GreeterServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"SayHello\",\n\t\t\tHandler:    _Greeter_SayHello_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetErrResp\",\n\t\t\tHandler:    _Greeter_GetErrResp_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Plus\",\n\t\t\tHandler:    _Greeter_Plus_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SayHelloAfterDelay\",\n\t\t\tHandler:    _Greeter_SayHelloAfterDelay_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SayMultipleHello\",\n\t\t\tHandler:    _Greeter_SayMultipleHello_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"SayHelloServerStream\",\n\t\t\tHandler:       _Greeter_SayHelloServerStream_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"SayHelloClientStream\",\n\t\t\tHandler:       _Greeter_SayHelloClientStream_Handler,\n\t\t\tClientStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"SayHelloBidirectionalStream\",\n\t\t\tHandler:       _Greeter_SayHelloBidirectionalStream_Handler,\n\t\t\tServerStreams: true,\n\t\t\tClientStreams: true,\n\t\t},\n\t},\n\tMetadata: \"proto/helloworld.proto\",\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/import.pb.go",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.27.1\n// \tprotoc        v3.12.4\n// source: proto/import.proto\n\npackage proto\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype User struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n}\n\nfunc (x *User) Reset() {\n\t*x = User{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_import_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *User) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*User) ProtoMessage() {}\n\nfunc (x *User) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_import_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use User.ProtoReflect.Descriptor instead.\nfunc (*User) Descriptor() ([]byte, []int) {\n\treturn file_proto_import_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *User) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype Response struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tBody string `protobuf:\"bytes,1,opt,name=body,proto3\" json:\"body,omitempty\"`\n}\n\nfunc (x *Response) Reset() {\n\t*x = Response{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_import_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Response) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Response) ProtoMessage() {}\n\nfunc (x *Response) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_import_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Response.ProtoReflect.Descriptor instead.\nfunc (*Response) Descriptor() ([]byte, []int) {\n\treturn file_proto_import_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Response) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nvar File_proto_import_proto protoreflect.FileDescriptor\n\nvar file_proto_import_proto_rawDesc = []byte{\n\t0x0a, 0x12, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x70, 0x6b, 0x67, 0x22, 0x1a, 0x0a, 0x04, 0x55, 0x73, 0x65,\n\t0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,\n\t0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x1e, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,\n\t0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,\n\t0x04, 0x62, 0x6f, 0x64, 0x79, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,\n\t0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_proto_import_proto_rawDescOnce sync.Once\n\tfile_proto_import_proto_rawDescData = file_proto_import_proto_rawDesc\n)\n\nfunc file_proto_import_proto_rawDescGZIP() []byte {\n\tfile_proto_import_proto_rawDescOnce.Do(func() {\n\t\tfile_proto_import_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_import_proto_rawDescData)\n\t})\n\treturn file_proto_import_proto_rawDescData\n}\n\nvar file_proto_import_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_proto_import_proto_goTypes = []interface{}{\n\t(*User)(nil),     // 0: pkg.User\n\t(*Response)(nil), // 1: pkg.Response\n}\nvar file_proto_import_proto_depIdxs = []int32{\n\t0, // [0:0] is the sub-list for method output_type\n\t0, // [0:0] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_proto_import_proto_init() }\nfunc file_proto_import_proto_init() {\n\tif File_proto_import_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_proto_import_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*User); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_proto_import_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Response); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_proto_import_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_proto_import_proto_goTypes,\n\t\tDependencyIndexes: file_proto_import_proto_depIdxs,\n\t\tMessageInfos:      file_proto_import_proto_msgTypes,\n\t}.Build()\n\tFile_proto_import_proto = out.File\n\tfile_proto_import_proto_rawDesc = nil\n\tfile_proto_import_proto_goTypes = nil\n\tfile_proto_import_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/import.proto",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nsyntax = \"proto3\";\n\npackage pkg;\noption go_package = \"./proto\";\n\nmessage User {\n    string name = 1;\n}\n\nmessage Response {\n  string body = 1;\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/src.pb.go",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.27.1\n// \tprotoc        v3.12.4\n// source: proto/src.proto\n\npackage proto\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Request struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tUser *User  `protobuf:\"bytes,1,opt,name=user,proto3\" json:\"user,omitempty\"`\n\tBody string `protobuf:\"bytes,2,opt,name=body,proto3\" json:\"body,omitempty\"`\n}\n\nfunc (x *Request) Reset() {\n\t*x = Request{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_proto_src_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Request) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Request) ProtoMessage() {}\n\nfunc (x *Request) ProtoReflect() protoreflect.Message {\n\tmi := &file_proto_src_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Request.ProtoReflect.Descriptor instead.\nfunc (*Request) Descriptor() ([]byte, []int) {\n\treturn file_proto_src_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Request) GetUser() *User {\n\tif x != nil {\n\t\treturn x.User\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nvar File_proto_src_proto protoreflect.FileDescriptor\n\nvar file_proto_src_proto_rawDesc = []byte{\n\t0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x72, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74,\n\t0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x12, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,\n\t0x6f, 0x22, 0x3c, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x04,\n\t0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x70, 0x6b, 0x67,\n\t0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x62,\n\t0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x32,\n\t0x39, 0x0a, 0x0a, 0x54, 0x65, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2b, 0x0a,\n\t0x03, 0x52, 0x75, 0x6e, 0x12, 0x13, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c,\n\t0x64, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x70, 0x6b, 0x67, 0x2e,\n\t0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f,\n\t0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_proto_src_proto_rawDescOnce sync.Once\n\tfile_proto_src_proto_rawDescData = file_proto_src_proto_rawDesc\n)\n\nfunc file_proto_src_proto_rawDescGZIP() []byte {\n\tfile_proto_src_proto_rawDescOnce.Do(func() {\n\t\tfile_proto_src_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_src_proto_rawDescData)\n\t})\n\treturn file_proto_src_proto_rawDescData\n}\n\nvar file_proto_src_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_proto_src_proto_goTypes = []interface{}{\n\t(*Request)(nil),  // 0: helloworld.Request\n\t(*User)(nil),     // 1: pkg.User\n\t(*Response)(nil), // 2: pkg.Response\n}\nvar file_proto_src_proto_depIdxs = []int32{\n\t1, // 0: helloworld.Request.user:type_name -> pkg.User\n\t0, // 1: helloworld.TestImport.Run:input_type -> helloworld.Request\n\t2, // 2: helloworld.TestImport.Run:output_type -> pkg.Response\n\t2, // [2:3] is the sub-list for method output_type\n\t1, // [1:2] is the sub-list for method input_type\n\t1, // [1:1] is the sub-list for extension type_name\n\t1, // [1:1] is the sub-list for extension extendee\n\t0, // [0:1] is the sub-list for field type_name\n}\n\nfunc init() { file_proto_src_proto_init() }\nfunc file_proto_src_proto_init() {\n\tif File_proto_src_proto != nil {\n\t\treturn\n\t}\n\tfile_proto_import_proto_init()\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_proto_src_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Request); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_proto_src_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_proto_src_proto_goTypes,\n\t\tDependencyIndexes: file_proto_src_proto_depIdxs,\n\t\tMessageInfos:      file_proto_src_proto_msgTypes,\n\t}.Build()\n\tFile_proto_src_proto = out.File\n\tfile_proto_src_proto_rawDesc = nil\n\tfile_proto_src_proto_goTypes = nil\n\tfile_proto_src_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/src.proto",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nsyntax = \"proto3\";\n\npackage helloworld;\noption go_package = \"./proto\";\n\nimport \"proto/import.proto\";\n\nservice TestImport {\n  rpc Run (Request) returns (pkg.Response) {}\n}\n\nmessage Request {\n  pkg.User user = 1;\n  string body = 2;\n}\n"
  },
  {
    "path": "t/grpc_server_example/proto/src_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.2.0\n// - protoc             v3.12.4\n// source: proto/src.proto\n\npackage proto\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.32.0 or later.\nconst _ = grpc.SupportPackageIsVersion7\n\n// TestImportClient is the client API for TestImport service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype TestImportClient interface {\n\tRun(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)\n}\n\ntype testImportClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewTestImportClient(cc grpc.ClientConnInterface) TestImportClient {\n\treturn &testImportClient{cc}\n}\n\nfunc (c *testImportClient) Run(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {\n\tout := new(Response)\n\terr := c.cc.Invoke(ctx, \"/helloworld.TestImport/Run\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// TestImportServer is the server API for TestImport service.\n// All implementations must embed UnimplementedTestImportServer\n// for forward compatibility\ntype TestImportServer interface {\n\tRun(context.Context, *Request) (*Response, error)\n\tmustEmbedUnimplementedTestImportServer()\n}\n\n// UnimplementedTestImportServer must be embedded to have forward compatible implementations.\ntype UnimplementedTestImportServer struct {\n}\n\nfunc (UnimplementedTestImportServer) Run(context.Context, *Request) (*Response, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Run not implemented\")\n}\nfunc (UnimplementedTestImportServer) mustEmbedUnimplementedTestImportServer() {}\n\n// UnsafeTestImportServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to TestImportServer will\n// result in compilation errors.\ntype UnsafeTestImportServer interface {\n\tmustEmbedUnimplementedTestImportServer()\n}\n\nfunc RegisterTestImportServer(s grpc.ServiceRegistrar, srv TestImportServer) {\n\ts.RegisterService(&TestImport_ServiceDesc, srv)\n}\n\nfunc _TestImport_Run_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(Request)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(TestImportServer).Run(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: \"/helloworld.TestImport/Run\",\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(TestImportServer).Run(ctx, req.(*Request))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// TestImport_ServiceDesc is the grpc.ServiceDesc for TestImport service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar TestImport_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"helloworld.TestImport\",\n\tHandlerType: (*TestImportServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Run\",\n\t\t\tHandler:    _TestImport_Run_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"proto/src.proto\",\n}\n"
  },
  {
    "path": "t/http3/admin/basic.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nrun_tests();\n\n__DATA__\n\n=== TEST 1:  create ssl for test.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\"\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: Successfully access test.com with QUIC\n--- config\n    location /echo {\n        echo world;\n    }\n--- exec\ncurl -k -v -H \"Host: test.com\" -H \"content-length: 0\" --http3-only --resolve \"test.com:1994:127.0.0.1\" https://test.com:1994/echo 2>&1 | cat\n--- response_body eval\nqr/world/\n\n\n\n=== TEST 3: set route\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n        )\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(message)\n            return\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: Successfully access route with QUIC\n--- exec\ncurl -k -v -H \"Host: test.com:1994\" -H \"content-length: 0\" --http3-only --resolve \"test.com:1994:127.0.0.1\" https://test.com:1994/hello 2>&1 | cat\n--- response_body_like\nhello world\n"
  },
  {
    "path": "t/jest.config.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { Config } from 'jest';\n\nconst config: Config = {\n  coverageProvider: 'v8',\n  testEnvironment: 'node',\n  testRegex: '(/__tests__/.*|(\\\\.|/)(spec|test))\\\\.(ts|mts)$',\n  transform: {\n    '^.+\\\\.(ts|mts)$': ['ts-jest', { useESM: true }],\n  },\n  extensionsToTreatAsEsm: ['.mts'],\n  moduleFileExtensions: ['ts', 'mts', 'js'],\n};\n\nexport default config;\n"
  },
  {
    "path": "t/kubernetes/configs/account.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nkind: ServiceAccount\napiVersion: v1\nmetadata:\n  name: apisix-test\n  namespace: default\n---\nkind: ClusterRole\napiVersion: rbac.authorization.k8s.io/v1\nmetadata:\n  name: apisix-test\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ endpoints]\n    verbs: [ get,list,watch ]\n  - apiGroups: [ \"discovery.k8s.io\" ]\n    resources: [ endpointslices ]\n    verbs: [ get,list,watch ]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: apisix-test\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: apisix-test\nsubjects:\n  - kind: ServiceAccount\n    name: apisix-test\n    namespace: default\n"
  },
  {
    "path": "t/kubernetes/configs/endpoint.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nkind: Namespace\napiVersion: v1\nmetadata:\n  name: ns-a\n---\n\nkind: Endpoints\napiVersion: v1\nmetadata:\n  name: ep\n  namespace: ns-a\nsubsets: [ ]\n---\n\nkind: Namespace\napiVersion: v1\nmetadata:\n  name: ns-b\n---\n\nkind: Endpoints\napiVersion: v1\nmetadata:\n  name: ep\n  namespace: ns-b\nsubsets: [ ]\n---\n\nkind: Namespace\napiVersion: v1\nmetadata:\n  name: ns-c\n---\n\nkind: Endpoints\napiVersion: v1\nmetadata:\n  name: ep\n  namespace: ns-c\nsubsets: [ ]\n---\n"
  },
  {
    "path": "t/kubernetes/configs/endpointslices.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nkind: Namespace\napiVersion: v1\nmetadata:\n  name: ns-a\n---\n\nkind: EndpointSlice\napiVersion: discovery.k8s.io/v1\nmetadata:\n  name: service-a-epslice1\n  namespace: ns-a\n  labels:\n    \"kubernetes.io/service-name\": service-a\naddressType: IPv4\nendpoints: [ ]\n---\n\nkind: Namespace\napiVersion: v1\nmetadata:\n  name: ns-b\n---\n\nkind: EndpointSlice\napiVersion: discovery.k8s.io/v1\nmetadata:\n  name: service-a-epslice1\n  namespace: ns-b\n  labels:\n    \"kubernetes.io/service-name\": service-a\naddressType: IPv4\nendpoints: [ ]\n---\n\nkind: Namespace\napiVersion: v1\nmetadata:\n  name: ns-c\n---\n\nkind: EndpointSlice\napiVersion: discovery.k8s.io/v1\nmetadata:\n  name: service-a-epslice1\n  namespace: ns-c\n  labels:\n    \"kubernetes.io/service-name\": service-a\naddressType: IPv4\nendpoints: [ ]\n---\n"
  },
  {
    "path": "t/kubernetes/configs/kind.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nkind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnetworking:\n  apiServerAddress: 127.0.0.1\n  apiServerPort: 6443\n"
  },
  {
    "path": "t/kubernetes/discovery/kubernetes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nour $token_file = \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\";\nour $token_value = eval {`cat $token_file 2>/dev/null`};\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $main_config = $block->main_config // <<_EOC_;\nenv MyPort=6443;\nenv KUBERNETES_SERVICE_HOST=127.0.0.1;\nenv KUBERNETES_SERVICE_PORT=6443;\nenv KUBERNETES_CLIENT_TOKEN=$::token_value;\nenv KUBERNETES_CLIENT_TOKEN_FILE=$::token_file;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $config = $block->config // <<_EOC_;\n\n        location /compare {\n            content_by_lua_block {\n                local http = require(\"resty.http\")\n                local core = require(\"apisix.core\")\n                local local_conf = require(\"apisix.core.config_local\").local_conf()\n\n                local function deep_compare(tbl1, tbl2)\n                    if tbl1 == tbl2 then\n                        return true\n                    elseif type(tbl1) == \"table\" and type(tbl2) == \"table\" then\n                        for key1, value1 in pairs(tbl1) do\n                            local value2 = tbl2[key1]\n                            if value2 == nil then\n                                -- avoid the type call for missing keys in tbl2 by directly comparing with nil\n                                return false\n                            elseif value1 ~= value2 then\n                                if type(value1) == \"table\" and type(value2) == \"table\" then\n                                    if not deep_compare(value1, value2) then\n                                        return false\n                                    end\n                                else\n                                    return false\n                                end\n                            end\n                        end\n                        for key2, _ in pairs(tbl2) do\n                            if tbl1[key2] == nil then\n                                return false\n                            end\n                        end\n                        return true\n                    end\n\n                    return false\n                end\n\n                ngx.req.read_body()\n                local request_body = ngx.req.get_body_data()\n                local expect = core.json.decode(request_body)\n                local current = local_conf.discovery.kubernetes\n                if deep_compare(expect,current) then\n                  ngx.say(\"true\")\n                else\n                  ngx.say(\"false, current is \",core.json.encode(current,true))\n                end\n            }\n        }\n\n        location /update_token {\n            content_by_lua_block {\n                local token_file = \"$::token_file\"\n                local file = io.open(token_file, \"w\")\n                file:write(\"invalid_token_value\")\n                file:close()\n                ngx.sleep(3)\n                file = io.open(token_file, \"w\")\n                local token_value = [[$::token_value]]\n                file:write(token_value)\n                file:close()\n            }\n        }\n\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: default value with minimal configuration\n--- yaml_config\napisix:\n  node_listen: 1984\n  config_center: yaml\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /compare\n{\n  \"service\": {\n    \"schema\": \"https\",\n    \"host\": \"${KUBERNETES_SERVICE_HOST}\",\n    \"port\": \"${KUBERNETES_SERVICE_PORT}\"\n  },\n  \"client\": {\n    \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n  },\n  \"watch_endpoint_slices\": false,\n  \"shared_size\": \"1m\",\n  \"default_weight\": 50\n}\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n\n\n\n=== TEST 2: default value with minimal service and client configuration\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    service: {}\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /compare\n{\n  \"service\": {\n    \"schema\": \"https\",\n    \"host\": \"${KUBERNETES_SERVICE_HOST}\",\n    \"port\": \"${KUBERNETES_SERVICE_PORT}\"\n  },\n  \"client\": {\n    \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n  },\n  \"watch_endpoint_slices\": false,\n  \"shared_size\": \"1m\",\n  \"default_weight\": 50\n}\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n\n\n\n=== TEST 3: mixing set custom and default values\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    service:\n        host: \"sample.com\"\n    shared_size: \"2m\"\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /compare\n{\n  \"service\": {\n    \"schema\": \"https\",\n    \"host\": \"sample.com\",\n    \"port\": \"${KUBERNETES_SERVICE_PORT}\"\n  },\n  \"client\": {\n    \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n  },\n  \"watch_endpoint_slices\": false,\n  \"shared_size\": \"2m\",\n  \"default_weight\": 50\n}\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n\n\n\n=== TEST 4: mixing set custom and default values\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n    default_weight: 33\n--- request\nGET /compare\n{\n  \"service\": {\n    \"schema\": \"https\",\n    \"host\": \"${KUBERNETES_SERVICE_HOST}\",\n    \"port\": \"${KUBERNETES_SERVICE_PORT}\"\n  },\n  \"client\": {\n    \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n  },\n  \"watch_endpoint_slices\": false,\n  \"shared_size\": \"1m\",\n  \"default_weight\": 33\n}\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n\n\n\n=== TEST 5: multi cluster mode configuration\n--- http_config\nlua_shared_dict kubernetes-debug 1m;\nlua_shared_dict kubernetes-release 1m;\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n  - id: \"debug\"\n    service:\n        host: \"1.cluster.com\"\n        port: \"6445\"\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n  - id: \"release\"\n    service:\n        schema: \"http\"\n        host: \"2.cluster.com\"\n        port: \"${MyPort}\"\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n    default_weight: 33\n    shared_size: \"2m\"\n--- request\nGET /compare\n[\n  {\n    \"id\": \"debug\",\n    \"service\": {\n      \"schema\": \"https\",\n      \"host\": \"1.cluster.com\",\n      \"port\": \"6445\"\n    },\n    \"client\": {\n      \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n    },\n    \"watch_endpoint_slices\": false,\n    \"default_weight\": 50,\n    \"shared_size\": \"1m\"\n  },\n  {\n    \"id\": \"release\",\n    \"service\": {\n      \"schema\": \"http\",\n      \"host\": \"2.cluster.com\",\n      \"port\": \"${MyPort}\"\n    },\n    \"client\": {\n      \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n    },\n    \"watch_endpoint_slices\": false,\n    \"default_weight\": 33,\n    \"shared_size\": \"2m\"\n  }\n]\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n\n\n\n=== TEST 6: set watch_endpoint_slices true and use kubernetes endpointslices api\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n    default_weight: 33\n    watch_endpoint_slices: true\n--- request\nGET /compare\n{\n  \"service\": {\n    \"schema\": \"https\",\n    \"host\": \"${KUBERNETES_SERVICE_HOST}\",\n    \"port\": \"${KUBERNETES_SERVICE_PORT}\"\n  },\n  \"client\": {\n    \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n  },\n  \"watch_endpoint_slices\": true,\n  \"shared_size\": \"1m\",\n  \"default_weight\": 33\n}\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n\n\n\n=== TEST 7: auto read token file before get token value\n--- yaml_config\napisix:\n  node_listen: 1984\n  config_center: yaml\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    client:\n        token_file: \"${KUBERNETES_CLIENT_TOKEN_FILE}\"\n--- request\nGET /update_token\n--- log_level: debug\n--- grep_error_log eval\nqr/re-read the token value/\n--- grep_error_log_out\nre-read the token value\nre-read the token value\n\n\n\n=== TEST 8: default value with minimal configuration and large shared_size\n--- yaml_config\napisix:\n  node_listen: 1984\n  config_center: yaml\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n    shared_size: \"1000m\"\n--- request\nGET /compare\n{\n  \"service\": {\n    \"schema\": \"https\",\n    \"host\": \"${KUBERNETES_SERVICE_HOST}\",\n    \"port\": \"${KUBERNETES_SERVICE_PORT}\"\n  },\n  \"client\": {\n    \"token\": \"${KUBERNETES_CLIENT_TOKEN}\"\n  },\n  \"watch_endpoint_slices\": false,\n  \"shared_size\": \"1000m\",\n  \"default_weight\": 50\n}\n--- more_headers\nContent-type: application/json\n--- response_body\ntrue\n"
  },
  {
    "path": "t/kubernetes/discovery/kubernetes2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    our $token_file = \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\";\n    our $token_value = eval {`cat $token_file 2>/dev/null`};\n\n    our $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n\n_EOC_\n\n    our $scale_ns_c = <<_EOC_;\n[\n  {\n    \"op\": \"replace_subsets\",\n    \"name\": \"ep\",\n    \"namespace\": \"ns-c\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p1\",\n            \"port\": 5001\n          }\n        ]\n      }\n    ]\n  }\n]\n_EOC_\n\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $main_config = $block->main_config // <<_EOC_;\nenv KUBERNETES_SERVICE_HOST=127.0.0.1;\nenv KUBERNETES_SERVICE_PORT=6443;\nenv KUBERNETES_CLIENT_TOKEN=$::token_value;\nenv KUBERNETES_CLIENT_TOKEN_FILE=$::token_file;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $config = $block->config // <<_EOC_;\n        location /queries {\n            content_by_lua_block {\n              local core = require(\"apisix.core\")\n              local d = require(\"apisix.discovery.kubernetes\")\n\n              ngx.sleep(1)\n\n              ngx.req.read_body()\n              local request_body = ngx.req.get_body_data()\n              local queries = core.json.decode(request_body)\n              local response_body = \"{\"\n              for _,query in ipairs(queries) do\n                local nodes = d.nodes(query)\n                if nodes==nil or #nodes==0 then\n                    response_body=response_body..\" \"..0\n                else\n                    response_body=response_body..\" \"..#nodes\n                end\n              end\n              ngx.say(response_body..\" }\")\n            }\n        }\n\n        location /operators {\n            content_by_lua_block {\n                local http = require(\"resty.http\")\n                local core = require(\"apisix.core\")\n                local ipairs = ipairs\n\n                ngx.req.read_body()\n                local request_body = ngx.req.get_body_data()\n                local operators = core.json.decode(request_body)\n\n                core.log.info(\"get body \", request_body)\n                core.log.info(\"get operators \", #operators)\n                for _, op in ipairs(operators) do\n                    local method, path, body\n                    local headers = {\n                        [\"Host\"] = \"127.0.0.1:6445\"\n                    }\n\n                    if op.op == \"replace_subsets\" then\n                        method = \"PATCH\"\n                        path = \"/api/v1/namespaces/\" .. op.namespace .. \"/endpoints/\" .. op.name\n                        if #op.subsets == 0 then\n                            body = '[{\"path\":\"/subsets\",\"op\":\"replace\",\"value\":[]}]'\n                        else\n                            local t = { { op = \"replace\", path = \"/subsets\", value = op.subsets } }\n                            body = core.json.encode(t, true)\n                        end\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n                    end\n\n                    if op.op == \"replace_labels\" then\n                        method = \"PATCH\"\n                        path = \"/api/v1/namespaces/\" .. op.namespace .. \"/endpoints/\" .. op.name\n                        local t = { { op = \"replace\", path = \"/metadata/labels\", value = op.labels } }\n                        body = core.json.encode(t, true)\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n                    end\n\n                    local httpc = http.new()\n                    core.log.info(\"begin to connect \", \"127.0.0.1:6445\")\n                    local ok, message = httpc:connect({\n                        scheme = \"http\",\n                        host = \"127.0.0.1\",\n                        port = 6445,\n                    })\n                    if not ok then\n                        core.log.error(\"connect 127.0.0.1:6445 failed, message : \", message)\n                        ngx.say(\"FAILED\")\n                    end\n                    local res, err = httpc:request({\n                        method = method,\n                        path = path,\n                        headers = headers,\n                        body = body,\n                    })\n                    if err ~= nil then\n                        core.log.err(\"operator k8s cluster error: \", err)\n                        return 500\n                    end\n                    if res.status ~= 200 and res.status ~= 201 and res.status ~= 409 then\n                        return res.status\n                    end\n                end\n                ngx.say(\"DONE\")\n            }\n        }\n\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create namespace and endpoints\n--- yaml_config eval: $::yaml_config\n--- request\nPOST /operators\n[\n  {\n    \"op\": \"replace_subsets\",\n    \"namespace\": \"ns-a\",\n    \"name\": \"ep\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          },\n          {\n            \"ip\": \"10.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p1\",\n            \"port\": 5001\n          }\n        ]\n      },\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"20.0.0.1\"\n          },\n          {\n            \"ip\": \"20.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p2\",\n            \"port\": 5002\n          }\n        ]\n      }\n    ]\n  },\n  {\n    \"op\": \"create_namespace\",\n    \"name\": \"ns-b\"\n  },\n  {\n    \"op\": \"replace_subsets\",\n    \"namespace\": \"ns-b\",\n    \"name\": \"ep\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          },\n          {\n            \"ip\": \"10.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p1\",\n            \"port\": 5001\n          }\n        ]\n      },\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"20.0.0.1\"\n          },\n          {\n            \"ip\": \"20.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p2\",\n            \"port\": 5002\n          }\n        ]\n      }\n    ]\n  },\n  {\n    \"op\": \"create_namespace\",\n    \"name\": \"ns-c\"\n  },\n  {\n    \"op\": \"replace_subsets\",\n    \"namespace\": \"ns-c\",\n    \"name\": \"ep\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          },\n          {\n            \"ip\": \"10.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"port\": 5001\n          }\n        ]\n      },\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"20.0.0.1\"\n          },\n          {\n            \"ip\": \"20.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"port\": 5002\n          }\n        ]\n      }\n    ]\n  }\n]\n--- more_headers\nContent-type: application/json\n\n\n\n=== TEST 2: use default parameters\n--- yaml_config eval: $::yaml_config\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 2 2 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 3: use specify environment parameters\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 2 2 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 4: use namespace selector equal\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      namespace_selector:\n        equal: ns-a\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 0 0 0 0 2 2 2 2 2 2 }\n\n\n\n=== TEST 5: use namespace selector not_equal\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      namespace_selector:\n        not_equal: ns-a\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 0 0 2 2 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 6: use namespace selector match\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      namespace_selector:\n        match: [ns-a,ns-b]\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 2 2 0 0 2 2 2 2 2 2 }\n\n\n\n=== TEST 7: use namespace selector match with regex\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      namespace_selector:\n        match: [\"ns-[ab]\"]\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 2 2 0 0 2 2 2 2 2 2 }\n\n\n\n=== TEST 8: use namespace selector not_match\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      namespace_selector:\n        not_match: [\"ns-a\"]\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 0 0 2 2 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 9: use namespace selector not_match with regex\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      namespace_selector:\n        not_match: [\"ns-[ab]\"]\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"first/ns-b/ep:p2\",\"first/ns-c/ep:5001\",\"first/ns-c/ep:5002\",\n  \"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\",\"second/ns-b/ep:p2\",\"second/ns-c/ep:5001\",\"second/ns-c/ep:5002\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 0 0 0 0 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 10: use label selector\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      label_selector: |-\n        first=1,second\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request eval\n[\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-a\\\",\\\"labels\\\":{}}]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-b\\\",\\\"labels\\\":{}}]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-c\\\",\\\"labels\\\":{}}]\",\n\n\"GET /queries\n[\\\"first/ns-a/ep:p1\\\",\\\"first/ns-b/ep:p1\\\",\\\"first/ns-c/ep:5001\\\"]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-a\\\",\\\"labels\\\":{\\\"first\\\":\\\"1\\\" }}]\",\n\n\"GET /queries\n[\\\"first/ns-a/ep:p1\\\",\\\"first/ns-b/ep:p1\\\",\\\"first/ns-c/ep:5001\\\"]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-b\\\",\\\"labels\\\":{\\\"first\\\":\\\"1\\\",\\\"second\\\":\\\"o\\\" }}]\",\n\n\"GET /queries\n[\\\"first/ns-a/ep:p1\\\",\\\"first/ns-b/ep:p1\\\",\\\"first/ns-c/ep:5001\\\"]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-c\\\",\\\"labels\\\":{\\\"first\\\":\\\"2\\\",\\\"second\\\":\\\"o\\\" }}]\",\n\n\"GET /queries\n[\\\"first/ns-a/ep:p1\\\",\\\"first/ns-b/ep:p1\\\",\\\"first/ns-c/ep:5001\\\"]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-c\\\",\\\"labels\\\":{\\\"first\\\":\\\"1\\\" }}]\",\n\n\"GET /queries\n[\\\"first/ns-a/ep:p1\\\",\\\"first/ns-b/ep:p1\\\",\\\"first/ns-c/ep:5001\\\"]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_labels\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-c\\\",\\\"labels\\\":{\\\"first\\\":\\\"1\\\",\\\"second\\\":\\\"o\\\" }}]\",\n\n\"GET /queries\n[\\\"first/ns-a/ep:p1\\\",\\\"first/ns-b/ep:p1\\\",\\\"first/ns-c/ep:5001\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"{ 0 0 0 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 0 0 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 2 0 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 2 0 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 2 0 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 2 2 }\\n\",\n]\n\n\n\n=== TEST 11: scale endpoints\n--- yaml_config eval: $::yaml_config\n--- request eval\n[\n\n\"GET /queries\n[\n  \\\"first/ns-a/ep:p1\\\",\\\"first/ns-a/ep:p2\\\",\n  \\\"second/ns-a/ep:p1\\\",\\\"second/ns-a/ep:p2\\\"\n]\",\n\n\"POST /operators\n[{\\\"op\\\":\\\"replace_subsets\\\",\\\"name\\\":\\\"ep\\\",\\\"namespace\\\":\\\"ns-a\\\",\\\"subsets\\\":[]}]\",\n\n\"GET /queries\n[\n  \\\"first/ns-a/ep:p1\\\",\\\"first/ns-a/ep:p2\\\",\n  \\\"second/ns-a/ep:p1\\\",\\\"second/ns-a/ep:p2\\\"\n]\",\n\n\"GET /queries\n[\n  \\\"first/ns-c/ep:5001\\\",\\\"first/ns-c/ep:5002\\\",\\\"first/ns-c/ep:p1\\\",\n  \\\"second/ns-c/ep:5001\\\",\\\"second/ns-c/ep:5002\\\",\\\"second/ns-c/ep:p1\\\"\n]\",\n\n\"POST /operators\n$::scale_ns_c\",\n\n\"GET /queries\n[\n  \\\"first/ns-c/ep:5001\\\",\\\"first/ns-c/ep:5002\\\",\\\"first/ns-c/ep:p1\\\",\n  \\\"second/ns-c/ep:5001\\\",\\\"second/ns-c/ep:5002\\\",\\\"second/ns-c/ep:p1\\\"\n]\"\n\n]\n--- response_body eval\n[\n    \"{ 2 2 2 2 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 0 0 0 }\\n\",\n    \"{ 2 2 0 2 2 0 }\\n\",\n    \"DONE\\n\",\n    \"{ 0 0 1 0 0 1 }\\n\",\n]\n"
  },
  {
    "path": "t/kubernetes/discovery/kubernetes3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    our $token_file = \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\";\n    our $token_value = eval {`cat $token_file 2>/dev/null`};\n\n    our $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: true\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: true\n\n_EOC_\n\n    our $single_yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    service:\n      host: \"127.0.0.1\"\n      port: \"6443\"\n    client:\n      token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n    watch_endpoint_slices: true\n_EOC_\n\n    our $scale_ns_c = <<_EOC_;\n[\n  {\n    \"op\": \"replace_endpointslices\",\n    \"name\": \"ep\",\n    \"namespace\": \"ns-c\",\n    \"endpoints\": [\n      {\n        \"addresses\": [\n            \"10.0.0.1\"\n        ],\n        \"conditions\": {\n           \"ready\": true,\n           \"serving\": true,\n           \"terminating\": false\n        },\n        \"nodeName\": \"kind-control-plane\"\n      }\n    ]\n    \"ports\": [\n      {\n        \"name\": \"p1\",\n        \"port\": 5001\n      }\n    ]\n  }\n]\n_EOC_\n\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $main_config = $block->main_config // <<_EOC_;\nenv KUBERNETES_SERVICE_HOST=127.0.0.1;\nenv KUBERNETES_SERVICE_PORT=6443;\nenv KUBERNETES_CLIENT_TOKEN=$::token_value;\nenv KUBERNETES_CLIENT_TOKEN_FILE=$::token_file;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $config = $block->config // <<_EOC_;\n        location /queries {\n            content_by_lua_block {\n              local core = require(\"apisix.core\")\n              local d = require(\"apisix.discovery.kubernetes\")\n\n              ngx.sleep(1)\n\n              ngx.req.read_body()\n              local request_body = ngx.req.get_body_data()\n              local queries = core.json.decode(request_body)\n              local response_body = \"{\"\n              for _,query in ipairs(queries) do\n                local nodes = d.nodes(query)\n                if nodes==nil or #nodes==0 then\n                    response_body=response_body..\" \"..0\n                else\n                    response_body=response_body..\" \"..#nodes\n                end\n              end\n              ngx.say(response_body..\" }\")\n            }\n        }\n\n        location /operators {\n            content_by_lua_block {\n                local http = require(\"resty.http\")\n                local core = require(\"apisix.core\")\n                local ipairs = ipairs\n\n                ngx.req.read_body()\n                local request_body = ngx.req.get_body_data()\n                local operators = core.json.decode(request_body)\n\n                core.log.info(\"get body \", request_body)\n                core.log.info(\"get operators \", #operators)\n                for _, op in ipairs(operators) do\n                    local method, path, body\n                    local headers = {\n                        [\"Host\"] = \"127.0.0.1:6445\"\n                    }\n\n                    if op.op == \"replace_endpointslices\" then\n                        method = \"PATCH\"\n                        path = \"/apis/discovery.k8s.io/v1/namespaces/\" .. op.namespace .. \"/endpointslices/\" .. op.name\n                        if #op.endpoints == 0 then\n                            body = '[{\"path\":\"/endpoints\",\"op\":\"replace\",\"value\":[]}]'\n                        else\n                            local t = { { op = \"replace\", path = \"/endpoints\", value = op.endpoints }, { op = \"replace\", path = \"/ports\", value = op.ports } }\n                            body = core.json.encode(t, true)\n                        end\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n                    end\n\n                    if op.op == \"replace_labels\" then\n                        method = \"PATCH\"\n                        path = \"/apis/discovery.k8s.io/v1/namespaces/\" .. op.namespace .. \"/endpointslices/\" .. op.name\n                        local t = { { op = \"replace\", path = \"/metadata/labels\", value = op.labels } }\n                        body = core.json.encode(t, true)\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n                    end\n\n                    local httpc = http.new()\n                    core.log.info(\"begin to connect \", \"127.0.0.1:6445\")\n                    local ok, message = httpc:connect({\n                        scheme = \"http\",\n                        host = \"127.0.0.1\",\n                        port = 6445,\n                    })\n                    if not ok then\n                        core.log.error(\"connect 127.0.0.1:6445 failed, message : \", message)\n                        ngx.say(\"FAILED\")\n                    end\n                    local res, err = httpc:request({\n                        method = method,\n                        path = path,\n                        headers = headers,\n                        body = body,\n                    })\n                    if err ~= nil then\n                        core.log.err(\"operator k8s cluster error: \", err)\n                        return 500\n                    end\n\n                    ngx.sleep(1)\n\n                    local k8s = require(\"apisix.discovery.kubernetes\")\n                    local data = k8s.dump_data()\n                    ngx.say(core.json.encode(data,true))\n\n                    if res.status ~= 200 and res.status ~= 201 and res.status ~= 409 then\n                        return res.status\n                    end\n                end\n                ngx.say(\"DONE\")\n            }\n        }\n\n        location /dump {\n            content_by_lua_block {\n                local json_decode = require(\"toolkit.json\").decode\n                local core = require(\"apisix.core\")\n                local http = require \"resty.http\"\n                local httpc = http.new()\n\n                ngx.sleep(1)\n\n                local dump_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/v1/discovery/kubernetes/dump\"\n                local res, err = httpc:request_uri(dump_uri, { method = \"GET\"})\n                if err then\n                    ngx.log(ngx.ERR, err)\n                    ngx.status = res.status\n                    return\n                end\n\n                local body = json_decode(res.body)\n                local endpoints = body.endpoints\n                ngx.say(core.json.encode(endpoints,true))\n            }\n        }\n\n        location /ready_check {\n            content_by_lua_block {\n                local http = require(\"resty.http\")\n                local healthcheck_uri = \"http://127.0.0.1:7085\" .. \"/status/ready\"\n                for i = 1, 4 do\n                    local httpc = http.new()\n                    local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n                    if res.status == 200 then\n                        ngx.status = res.status\n                        return\n                    end\n                    ngx.sleep(1)\n                end\n                local httpc = http.new()\n                local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n                ngx.status = res.status\n            }\n        }\n\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create namespace and endpoints\n--- yaml_config eval: $::yaml_config\n--- request\nPOST /operators\n[\n    {\n        \"op\": \"replace_endpointslices\",\n        \"namespace\": \"ns-a\",\n        \"name\": \"service-a-epslice1\",\n        \"metadata\": {\n            \"labels\": {\n                \"kubernetes.io/service-name\": \"service-a\"\n            }\n        },\n        \"endpoints\": [\n            {\n                \"addresses\": [\n                    \"10.0.0.1\",\n                    \"10.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"kind-control-plane\"\n            },\n            {\n                \"addresses\": [\n                    \"20.0.0.1\",\n                    \"20.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": false,\n                    \"serving\": false,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"kind-control-plane\"\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"p1\",\n                \"port\": 5001\n            }\n        ]\n    },\n    {\n        \"op\": \"create_namespace\",\n        \"name\": \"ns-b\"\n    },\n    {\n        \"op\": \"replace_endpointslices\",\n        \"namespace\": \"ns-b\",\n        \"name\": \"service-a-epslice1\",\n        \"metadata\": {\n            \"labels\": {\n                \"kubernetes.io/service-name\": \"service-a\"\n            }\n        },\n        \"endpoints\": [\n            {\n                \"addresses\": [\n                    \"10.0.0.1\",\n                    \"10.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"kind-control-plane\"\n            },\n            {\n                \"addresses\": [\n                    \"20.0.0.1\",\n                    \"20.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": false,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"kind-control-plane\"\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"p2\",\n                \"port\": 5002\n            }\n        ]\n    },\n    {\n        \"op\": \"create_namespace\",\n        \"name\": \"ns-c\"\n    },\n    {\n        \"op\": \"replace_endpointslices\",\n        \"namespace\": \"ns-c\",\n        \"name\": \"service-a-epslice1\",\n        \"metadata\": {\n            \"labels\": {\n                \"kubernetes.io/service-name\": \"service-a\"\n            }\n        },\n        \"endpoints\": [\n            {\n                \"addresses\": [\n                    \"10.0.0.1\",\n                    \"10.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"kind-control-plane\"\n            },\n            {\n                \"addresses\": [\n                    \"20.0.0.1\",\n                    \"20.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": false,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"kind-control-plane\"\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"p3\",\n                \"port\": 5003\n            }\n        ]\n    }\n]\n--- more_headers\nContent-type: application/json\n--- response_body_like\n.*\"name\":\"default/kubernetes\".*\n\n\n\n=== TEST 2: use default parameters\n--- yaml_config eval: $::yaml_config\n--- request\nGET /queries\n[\n  \"first/ns-a/service-a:p1\",\"first/ns-a/service-a:p1\",\"first/ns-b/service-a:p2\",\"first/ns-b/service-a:p2\",\"first/ns-c/service-a:p3\",\"first/ns-c/service-a:p3\",\n  \"second/ns-a/service-a:p1\",\"second/ns-a/service-a:p1\",\"second/ns-b/service-a:p2\",\"second/ns-b/service-a:p2\",\"second/ns-c/service-a:p3\",\"second/ns-c/service-a:p3\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 2 2 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 3: use specify environment parameters\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n      watch_endpoint_slices: true\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n      watch_endpoint_slices: true\n\n--- request\nGET /queries\n[\n  \"first/ns-a/service-a:p1\",\"first/ns-a/service-a:p1\",\"first/ns-b/service-a:p2\",\"first/ns-b/service-a:p2\",\"first/ns-c/service-a:p3\",\"first/ns-c/service-a:p3\",\n  \"second/ns-a/service-a:p1\",\"second/ns-a/service-a:p1\",\"second/ns-b/service-a:p2\",\"second/ns-b/service-a:p2\",\"second/ns-c/service-a:p3\",\"second/ns-c/service-a:p3\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 2 2 2 2 2 2 2 2 2 2 }\n\n\n\n=== TEST 4: use namespace selector equal\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: ${KUBERNETES_SERVICE_HOST}\n        port: ${KUBERNETES_SERVICE_PORT}\n      client:\n        token_file: ${KUBERNETES_CLIENT_TOKEN_FILE}\n      watch_endpoint_slices: true\n      namespace_selector:\n        equal: ns-a\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      watch_endpoint_slices: true\n      client:\n        token: ${KUBERNETES_CLIENT_TOKEN}\n--- request\nGET /queries\n[\n  \"first/ns-a/service-a:p1\",\"first/ns-a/service-a:p1\",\"first/ns-b/service-a:p2\",\"first/ns-b/service-a:p2\",\"first/ns-c/service-a:p3\",\"first/ns-c/service-a:p3\",\n  \"second/ns-a/service-a:p1\",\"second/ns-a/service-a:p1\",\"second/ns-b/service-a:p2\",\"second/ns-b/service-a:p2\",\"second/ns-c/service-a:p3\",\"second/ns-c/service-a:p3\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 2 2 0 0 0 0 2 2 2 2 2 2 }\n\n\n\n=== TEST 5: test dump\n--- yaml_config eval: $::yaml_config\n--- request\nGET /dump\n--- response_body_like\n.*\"name\":\"default/kubernetes\".*\n\n\n\n=== TEST 6: test single mode dump\n--- yaml_config eval: $::single_yaml_config\n--- request\nGET /dump\n--- response_body_like\n.*\"name\":\"default/kubernetes\".*\n\n\n\n=== TEST 7: test pre_list and post_list work  for single-k8s with endpoint_slices\n--- log_level: info\n--- yaml_config eval: $::single_yaml_config\n--- extra_init_by_lua\n    local ngx = ngx\n    local core = require(\"apisix.core\")\n\n    local dict = ngx.shared[\"kubernetes\"]\n    local ok,err = dict:set(\"dirty_key\", true)\n    if not ok then\n        core.log.error(\"set dirty_key to dict fail, err: \", err)\n    end\n--- request\nGET /ready_check\n--- no_error_log\n[error]\n--- grep_error_log eval\nqr/kubernetes discovery module found dirty data in shared dict, key: dirty_key/\n--- grep_error_log_out\nkubernetes discovery module found dirty data in shared dict, key: dirty_key\n\n\n\n=== TEST 8: test pre_list and post_list work for multi-k8s with endpoint_slices\n--- log_level: info\n--- yaml_config eval: $::yaml_config\n--- extra_init_by_lua\n    local ngx = ngx\n    local core = require(\"apisix.core\")\n\n    local dict = ngx.shared[\"kubernetes-first\"]\n    local ok,err = dict:set(\"dirty_key\", true)\n    if not ok then\n        core.log.error(\"set dirty_key to dict fail, err: \", err)\n    end\n--- request\nGET /ready_check\n--- no_error_log\n[error]\n--- grep_error_log eval\nqr/kubernetes discovery module found dirty data in shared dict, key: dirty_key/\n--- grep_error_log_out\nkubernetes discovery module found dirty data in shared dict, key: dirty_key\n\n\n\n=== TEST 9: test pre_list and post_list work  for single-k8s with endpoints\n--- log_level: info\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    service:\n      host: \"127.0.0.1\"\n      port: \"6443\"\n    client:\n      token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n    watch_endpoint_slices: false\n--- extra_init_by_lua\n    local ngx = ngx\n    local core = require(\"apisix.core\")\n\n    local dict = ngx.shared[\"kubernetes\"]\n    local ok,err = dict:set(\"dirty_key\", true)\n    if not ok then\n        core.log.error(\"set dirty_key to dict fail, err: \", err)\n    end\n--- request\nGET /ready_check\n--- no_error_log\n[error]\n--- grep_error_log eval\nqr/kubernetes discovery module found dirty data in shared dict, key: dirty_key/\n--- grep_error_log_out\nkubernetes discovery module found dirty data in shared dict, key: dirty_key\n\n\n\n=== TEST 10: test pre_list and post_list work for multi-k8s with endpoints\n--- log_level: info\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: false\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: false\n--- extra_init_by_lua\n    local ngx = ngx\n    local core = require(\"apisix.core\")\n\n    local dict = ngx.shared[\"kubernetes-first\"]\n    local ok,err = dict:set(\"dirty_key\", true)\n    if not ok then\n        core.log.error(\"set dirty_key to dict fail, err: \", err)\n    end\n--- request\nGET /ready_check\n--- no_error_log\n[error]\n--- grep_error_log eval\nqr/kubernetes discovery module found dirty data in shared dict, key: dirty_key/\n--- grep_error_log_out\nkubernetes discovery module found dirty data in shared dict, key: dirty_key\n\n\n\n=== TEST 11: test healthcheck unready\n--- log_level: warn\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: false\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6446\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: false\n--- request\nGET /ready_check\n--- error_code: 503\n--- grep_error_log eval\nqr/connect apiserver failed/\n--- grep_error_log_out\nconnect apiserver failed\n\n\n\n=== TEST 12: test healthcheck ready\n--- log_level: warn\n--- yaml_config\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: false\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: false\n--- request\nGET /ready_check\n--- error_code: 200\n"
  },
  {
    "path": "t/kubernetes/discovery/kubernetes4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    our $token_file = \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\";\n    our $token_value = eval {`cat $token_file 2>/dev/null`};\n\n    our $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: true\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n      watch_endpoint_slices: true\n\n_EOC_\n\n    our $single_yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    service:\n      host: \"127.0.0.1\"\n      port: \"6443\"\n    client:\n      token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n    watch_endpoint_slices: true\n_EOC_\n\n    our $create_ns_a_epslice2 = <<_EOC_;\n[\n    {\n        \"op\": \"create_endpointslices\",\n        \"namespace\": \"ns-a\",\n        \"apiVersion\": \"discovery.k8s.io/v1\",\n        \"kind\": \"EndpointSlice\",\n        \"metadata\": {\n            \"name\": \"service-a-epslice2\",\n            \"labels\": {\n                \"kubernetes.io/service-name\": \"service-a\"\n            }\n        },\n        \"addressType\": \"IPv4\",\n        \"endpoints\": [\n            {\n                \"addresses\": [\n                    \"10.0.0.4\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node4\"\n            },\n            {\n                \"addresses\": [\n                    \"10.0.0.5\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node5\"\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"p1\",\n                \"port\": 5001\n            }\n        ]\n    }\n]\n_EOC_\n\n    our $scale_in_ns_a_epslice1 = <<_EOC_;\n[\n    {\n        \"op\": \"replace_endpointslices\",\n        \"name\": \"service-a-epslice1\",\n        \"namespace\": \"ns-a\",\n        \"apiVersion\": \"discovery.k8s.io/v1\",\n        \"kind\": \"EndpointSlice\",\n        \"metadata\": {\n            \"name\": \"service-a-epslice1\",\n            \"labels\": {\n                \"kubernetes.io/service-name\": \"service-a\"\n            }\n        },\n        \"addressType\": \"IPv4\",\n        \"endpoints\": [\n            {\n                \"addresses\": [\n                    \"10.0.0.1\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node1\"\n            },\n            {\n                \"addresses\": [\n                    \"10.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node2\"\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"p1\",\n                \"port\": 5001\n            }\n        ]\n    }\n]\n_EOC_\n\n    our $scale_up_ns_a_epslice1 = <<_EOC_;\n[\n    {\n        \"op\": \"replace_endpointslices\",\n        \"name\": \"service-a-epslice1\",\n        \"namespace\": \"ns-a\",\n        \"apiVersion\": \"discovery.k8s.io/v1\",\n        \"kind\": \"EndpointSlice\",\n        \"metadata\": {\n            \"name\": \"service-a-epslice1\",\n            \"labels\": {\n                \"kubernetes.io/service-name\": \"service-a\"\n            }\n        },\n        \"addressType\": \"IPv4\",\n        \"endpoints\": [\n            {\n                \"addresses\": [\n                    \"10.0.0.1\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node1\"\n            },\n            {\n                \"addresses\": [\n                    \"10.0.0.2\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node2\"\n            },\n            {\n                \"addresses\": [\n                    \"10.0.0.3\"\n                ],\n                \"conditions\": {\n                    \"ready\": true,\n                    \"serving\": true,\n                    \"terminating\": false\n                },\n                \"nodeName\": \"service-a-node3\"\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"p1\",\n                \"port\": 5001\n            }\n        ]\n    }\n]\n_EOC_\n\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $main_config = $block->main_config // <<_EOC_;\nenv KUBERNETES_SERVICE_HOST=127.0.0.1;\nenv KUBERNETES_SERVICE_PORT=6443;\nenv KUBERNETES_CLIENT_TOKEN=$::token_value;\nenv KUBERNETES_CLIENT_TOKEN_FILE=$::token_file;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $config = $block->config // <<_EOC_;\n        location /queries {\n            content_by_lua_block {\n              local core = require(\"apisix.core\")\n              local d = require(\"apisix.discovery.kubernetes\")\n\n              ngx.sleep(1)\n\n              ngx.req.read_body()\n              local request_body = ngx.req.get_body_data()\n              local queries = core.json.decode(request_body)\n              local response_body = \"{\"\n              for _,query in ipairs(queries) do\n                local nodes = d.nodes(query)\n                if nodes==nil or #nodes==0 then\n                    response_body=response_body..\" \"..0\n                else\n                    response_body=response_body..\" \"..#nodes\n                end\n              end\n              ngx.say(response_body..\" }\")\n            }\n        }\n\n        location /operators {\n            content_by_lua_block {\n                local http = require(\"resty.http\")\n                local core = require(\"apisix.core\")\n                local ipairs = ipairs\n\n                ngx.req.read_body()\n                local request_body = ngx.req.get_body_data()\n                local operators = core.json.decode(request_body)\n\n                core.log.info(\"get body \", request_body)\n                core.log.info(\"get operators \", #operators)\n                for _, op in ipairs(operators) do\n                    local method, path, body\n                    local headers = {\n                        [\"Host\"] = \"127.0.0.1:6445\"\n                    }\n\n                    if op.op == \"replace_endpointslices\" then\n                        method = \"PATCH\"\n                        path = \"/apis/discovery.k8s.io/v1/namespaces/\" .. op.namespace .. \"/endpointslices/\" .. op.name\n                        if #op.endpoints == 0 then\n                            body = '[{\"path\":\"/endpoints\",\"op\":\"replace\",\"value\":[]}]'\n                        else\n                            local t = { { op = \"replace\", path = \"/endpoints\", value = op.endpoints }, { op = \"replace\", path = \"/ports\", value = op.ports } }\n                            body = core.json.encode(t, true)\n                        end\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n\n                    elseif op.op == \"create_endpointslices\" then\n                        method = \"POST\"\n                        path = \"/apis/discovery.k8s.io/v1/namespaces/\" .. op.namespace .. \"/endpointslices\"\n                        op.op = nil\n                        op.namespace = nil\n                        body = core.json.encode(op, true)\n\n                    elseif op.op == \"delete_endpointslices\" then\n                        method = \"DELETE\"\n                        path = \"/apis/discovery.k8s.io/v1/namespaces/\" .. op.namespace .. \"/endpointslices/\" .. op.name\n                    end\n\n\n                    local httpc = http.new()\n                    core.log.info(\"begin to connect \", \"127.0.0.1:6445\")\n                    local ok, message = httpc:connect({\n                        scheme = \"http\",\n                        host = \"127.0.0.1\",\n                        port = 6445,\n                    })\n                    if not ok then\n                        core.log.error(\"connect 127.0.0.1:6445 failed, message : \", message)\n                        ngx.say(\"FAILED\")\n                    end\n                    local res, err = httpc:request({\n                        method = method,\n                        path = path,\n                        headers = headers,\n                        body = body,\n                    })\n                    if err ~= nil then\n                        core.log.err(\"operator k8s cluster error: \", err)\n                        return 500\n                    end\n\n                    ngx.sleep(1)\n\n                    if res.status ~= 200 and res.status ~= 201 and res.status ~= 409 then\n                        return res.status\n                    end\n                end\n                ngx.say(\"DONE\")\n            }\n        }\n\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: endpointSlice1 update\n--- yaml_config eval: $::yaml_config\n--- request eval\n[\n\"POST /operators\n$::scale_up_ns_a_epslice1\",\n]\n--- more_headers\nContent-type: application/json\n--- response_body\nDONE\n\n\n\n=== TEST 2: test multi-k8s watching endpointSlices\n--- yaml_config eval: $::yaml_config\n--- request\nGET /queries\n[\n  \"first/ns-a/service-a:p1\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 3 }\n\n\n\n=== TEST 3: test single-k8s watching endpointSlices\n--- yaml_config eval: $::single_yaml_config\n--- request\nGET /queries\n[\n  \"ns-a/service-a:p1\"\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\nqr{ 3 }\n\n\n\n=== TEST 4: endpointSlice2 create and delete for multi-k8s mode\n--- yaml_config eval: $::yaml_config\n--- request eval\n[\n\n\"POST /operators\n$::create_ns_a_epslice2\",\n\n\"GET /queries\n[\n  \\\"first/ns-a/service-a:p1\\\"\n]\",\n\n\"POST /operators\n[\n    {\n        \\\"op\\\": \\\"delete_endpointslices\\\",\n        \\\"namespace\\\": \\\"ns-a\\\",\n        \\\"name\\\": \\\"service-a-epslice2\\\"\n    }\n]\",\n\n\"GET /queries\n[\n  \\\"first/ns-a/service-a:p1\\\"\n]\",\n\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 5 }\\n\",\n    \"DONE\\n\",\n    \"{ 3 }\\n\",\n]\n\n\n\n=== TEST 5: endpointSlice2 create and delete for single-k8s mode\n--- yaml_config eval: $::single_yaml_config\n--- request eval\n[\n\n\"POST /operators\n$::create_ns_a_epslice2\",\n\n\"GET /queries\n[\n  \\\"ns-a/service-a:p1\\\"\n]\",\n\n\"POST /operators\n[\n    {\n        \\\"op\\\": \\\"delete_endpointslices\\\",\n        \\\"namespace\\\": \\\"ns-a\\\",\n        \\\"name\\\": \\\"service-a-epslice2\\\"\n    }\n]\",\n\n\"GET /queries\n[\n  \\\"ns-a/service-a:p1\\\"\n]\",\n\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 5 }\\n\",\n    \"DONE\\n\",\n    \"{ 3 }\\n\",\n]\n\n\n\n=== TEST 6: endpointSlice scale for multi-k8s mode\n--- yaml_config eval: $::yaml_config\n--- request eval\n[\n\n\"POST /operators\n$::scale_in_ns_a_epslice1\",\n\n\"GET /queries\n[\n  \\\"first/ns-a/service-a:p1\\\"\n]\",\n\n\"POST /operators\n$::scale_up_ns_a_epslice1\",\n\n\"GET /queries\n[\n  \\\"first/ns-a/service-a:p1\\\"\n]\",\n\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 2 }\\n\",\n    \"DONE\\n\",\n    \"{ 3 }\\n\",\n]\n\n\n\n=== TEST 7: endpointSlice scale for single-k8s mode\n--- yaml_config eval: $::single_yaml_config\n--- request eval\n[\n\n\"POST /operators\n$::scale_in_ns_a_epslice1\",\n\n\"GET /queries\n[\n  \\\"ns-a/service-a:p1\\\"\n]\",\n\n\"POST /operators\n$::scale_up_ns_a_epslice1\",\n\n\"GET /queries\n[\n  \\\"ns-a/service-a:p1\\\"\n]\",\n\n]\n--- more_headers\nContent-type: application/json\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 2 }\\n\",\n    \"DONE\\n\",\n    \"{ 3 }\\n\",\n]\n"
  },
  {
    "path": "t/kubernetes/discovery/stream/kubernetes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    our $token_file = \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\";\n    our $token_value = eval {`cat $token_file 2>/dev/null`};\n\n    our $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\ndeployment:\n  role: data_plane\n  role_data_plane:\n    config_provider: yaml\ndiscovery:\n  kubernetes:\n    - id: first\n      service:\n        host: \"127.0.0.1\"\n        port: \"6443\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n    - id: second\n      service:\n        schema: \"http\"\n        host: \"127.0.0.1\"\n        port: \"6445\"\n      client:\n        token_file: \"/tmp/var/run/secrets/kubernetes.io/serviceaccount/token\"\n\n_EOC_\n\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nplan('no_plan');\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $main_config = $block->main_config // <<_EOC_;\nenv KUBERNETES_SERVICE_HOST=127.0.0.1;\nenv KUBERNETES_SERVICE_PORT=6443;\nenv KUBERNETES_CLIENT_TOKEN=$::token_value;\nenv KUBERNETES_CLIENT_TOKEN_FILE=$::token_file;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $config = $block->config // <<_EOC_;\n        location /operators {\n            content_by_lua_block {\n                local http = require(\"resty.http\")\n                local core = require(\"apisix.core\")\n                local ipairs = ipairs\n\n                ngx.req.read_body()\n                local request_body = ngx.req.get_body_data()\n                local operators = core.json.decode(request_body)\n\n                core.log.info(\"get body \", request_body)\n                core.log.info(\"get operators \", #operators)\n                for _, op in ipairs(operators) do\n                    local method, path, body\n                    local headers = {\n                        [\"Host\"] = \"127.0.0.1:6445\"\n                    }\n\n                    if op.op == \"replace_subsets\" then\n                        method = \"PATCH\"\n                        path = \"/api/v1/namespaces/\" .. op.namespace .. \"/endpoints/\" .. op.name\n                        if #op.subsets == 0 then\n                            body = '[{\"path\":\"/subsets\",\"op\":\"replace\",\"value\":[]}]'\n                        else\n                            local t = { { op = \"replace\", path = \"/subsets\", value = op.subsets } }\n                            body = core.json.encode(t, true)\n                        end\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n                    end\n\n                    if op.op == \"replace_labels\" then\n                        method = \"PATCH\"\n                        path = \"/api/v1/namespaces/\" .. op.namespace .. \"/endpoints/\" .. op.name\n                        local t = { { op = \"replace\", path = \"/metadata/labels\", value = op.labels } }\n                        body = core.json.encode(t, true)\n                        headers[\"Content-Type\"] = \"application/json-patch+json\"\n                    end\n\n                    local httpc = http.new()\n                    core.log.info(\"begin to connect \", \"127.0.0.1:6445\")\n                    local ok, message = httpc:connect({\n                        scheme = \"http\",\n                        host = \"127.0.0.1\",\n                        port = 6445,\n                    })\n                    if not ok then\n                        core.log.error(\"connect 127.0.0.1:6445 failed, message : \", message)\n                        ngx.say(\"FAILED\")\n                    end\n                    local res, err = httpc:request({\n                        method = method,\n                        path = path,\n                        headers = headers,\n                        body = body,\n                    })\n                    if err ~= nil then\n                        core.log.err(\"operator k8s cluster error: \", err)\n                        return 500\n                    end\n                    if res.status ~= 200 and res.status ~= 201 and res.status ~= 409 then\n                        return res.status\n                    end\n                end\n                ngx.say(\"DONE\")\n            }\n        }\n\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_config = $block->stream_config // <<_EOC_;\n        server {\n            listen 8125;\n            content_by_lua_block {\n                local core = require(\"apisix.core\")\n                local d = require(\"apisix.discovery.kubernetes\")\n\n                ngx.sleep(1)\n\n                local sock = ngx.req.socket()\n                local request_body = sock:receive()\n\n                core.log.info(\"get body \", request_body)\n\n                local response_body = \"{\"\n                local queries = core.json.decode(request_body)\n                for _,query in ipairs(queries) do\n                  local nodes = d.nodes(query)\n                  if nodes==nil or #nodes==0 then\n                      response_body=response_body..\" \"..0\n                  else\n                      response_body=response_body..\" \"..#nodes\n                  end\n                end\n                ngx.say(response_body..\" }\")\n            }\n        }\n\n_EOC_\n\n  $block->set_value(\"extra_stream_config\", $stream_config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create namespace and endpoints\n--- yaml_config eval: $::yaml_config\n--- request\nPOST /operators\n[\n  {\n    \"op\": \"replace_subsets\",\n    \"namespace\": \"ns-a\",\n    \"name\": \"ep\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          },\n          {\n            \"ip\": \"10.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p1\",\n            \"port\": 5001\n          }\n        ]\n      },\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"20.0.0.1\"\n          },\n          {\n            \"ip\": \"20.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p2\",\n            \"port\": 5002\n          }\n        ]\n      }\n    ]\n  },\n  {\n    \"op\": \"create_namespace\",\n    \"name\": \"ns-b\"\n  },\n  {\n    \"op\": \"replace_subsets\",\n    \"namespace\": \"ns-b\",\n    \"name\": \"ep\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          },\n          {\n            \"ip\": \"10.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p1\",\n            \"port\": 5001\n          }\n        ]\n      },\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"20.0.0.1\"\n          },\n          {\n            \"ip\": \"20.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"name\": \"p2\",\n            \"port\": 5002\n          }\n        ]\n      }\n    ]\n  },\n  {\n    \"op\": \"create_namespace\",\n    \"name\": \"ns-c\"\n  },\n  {\n    \"op\": \"replace_subsets\",\n    \"namespace\": \"ns-c\",\n    \"name\": \"ep\",\n    \"subsets\": [\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"10.0.0.1\"\n          },\n          {\n            \"ip\": \"10.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"port\": 5001\n          }\n        ]\n      },\n      {\n        \"addresses\": [\n          {\n            \"ip\": \"20.0.0.1\"\n          },\n          {\n            \"ip\": \"20.0.0.2\"\n          }\n        ],\n        \"ports\": [\n          {\n            \"port\": 5002\n          }\n        ]\n      }\n    ]\n  }\n]\n--- more_headers\nContent-type: application/json\n\n\n\n=== TEST 2: use default parameters\n--- yaml_config eval: $::yaml_config\n--- apisix_yaml\nstream_routes:\n  -\n    id: 1\n    server_port: 1985\n    upstream_id: 1\n\nupstreams:\n  - nodes:\n      \"127.0.0.1:8125\": 1\n    type: roundrobin\n    id: 1\n\n#END\n--- stream_request\n[\"first/ns-a/ep:p1\",\"first/ns-a/ep:p2\",\"first/ns-b/ep:p1\",\"second/ns-a/ep:p1\",\"second/ns-a/ep:p2\",\"second/ns-b/ep:p1\"]\n--- stream_response eval\nqr{ 2 2 2 2 2 2 }\n"
  },
  {
    "path": "t/lib/apisix/plugins/jwt-auth.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal jwt = require(\"resty.jwt\")\n\nlocal ngx_time = ngx.time\nlocal ngx_decode_base64 = ngx.decode_base64\nlocal pcall = pcall\n\n\nlocal _M = {}\n\n\nlocal function get_secret(conf)\n    local secret = conf.secret\n\n    if conf.base64_secret then\n        return ngx_decode_base64(secret)\n    end\n\n    return secret\nend\n\nlocal function get_real_payload(key, exp, payload)\n    local real_payload = {\n        key = key,\n        exp = ngx_time() + exp\n    }\n    if payload then\n        local extra_payload = core.json.decode(payload)\n        core.table.merge(extra_payload, real_payload)\n        return extra_payload\n    end\n    return real_payload\nend\n\nlocal function sign_jwt_with_HS(key, auth_conf, payload)\n    local auth_secret, err = get_secret(auth_conf)\n    if not auth_secret then\n        core.log.error(\"failed to sign jwt, err: \", err)\n        return nil, \"failed to sign jwt: failed to get auth_secret\"\n    end\n    local ok, jwt_token = pcall(jwt.sign, _M,\n            auth_secret,\n            {\n                header = {\n                    typ = \"JWT\",\n                    alg = auth_conf.algorithm\n                },\n                payload = get_real_payload(key, auth_conf.exp, payload)\n            }\n    )\n    if not ok then\n        core.log.error(\"failed to sign jwt, err: \", jwt_token.reason)\n        return nil, \"failed to sign jwt\"\n    end\n    return jwt_token\nend\n\nlocal function sign_jwt_with_RS256_ES256(key, auth_conf, payload)\n    local ok, jwt_token = pcall(jwt.sign, _M,\n            auth_conf.private_key,\n            {\n                header = {\n                    typ = \"JWT\",\n                    alg = auth_conf.algorithm,\n                    x5c = {\n                        auth_conf.public_key,\n                    }\n                },\n                payload = get_real_payload(key, auth_conf.exp, payload)\n            }\n    )\n    if not ok then\n        core.log.error(\"failed to sign jwt, err: \", jwt_token.reason)\n        return nil, \"failed to sign jwt\"\n    end\n    return jwt_token\nend\n\nlocal function get_sign_handler(algorithm)\n    if not algorithm or algorithm == \"HS256\" or algorithm == \"HS512\" then\n        return sign_jwt_with_HS\n    elseif algorithm == \"RS256\" or algorithm == \"ES256\" then\n        return sign_jwt_with_RS256_ES256\n    end\nend\n\nlocal function gen_token(auth_conf, payload)\n    if not auth_conf.exp then\n        auth_conf.exp = 86400\n    end\n    if not auth_conf.lifetime_grace_period then\n        auth_conf.lifetime_grace_period = 0\n    end\n    if not auth_conf.algorithm then\n        auth_conf.algorithm = \"HS256\"\n    end\n    local sign_handler = get_sign_handler(auth_conf.algorithm)\n    local jwt_token, err = sign_handler(auth_conf.key, auth_conf, payload)\n    return jwt_token, err\nend\n\n\n_M.gen_token = gen_token\n\nreturn _M\n"
  },
  {
    "path": "t/lib/apisix/plugins/prometheus/exporter.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal core = require(\"apisix.core\")\nlocal _M = {}\n\n\nfunction _M.http_init()\n    return true\nend\n\n\nfunction _M.stream_init()\n    return true\nend\n\n\nfunction _M.export_metrics()\n    local process_type = require(\"ngx.process\").type()\n    core.log.info(\"process type: \", process_type)\n    return core.response.exit(200)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/chaitin_waf_server.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal _M = {}\n\nlocal function get_socket()\n    ngx.flush(true)\n    local sock, err = ngx.req.socket(true)\n    if not sock then\n        ngx.log(ngx.ERR, \"failed to get the request socket: \" .. tostring(err))\n        return nil\n    end\n    return sock\nend\n\nfunction _M.pass()\n    local sock = get_socket()\n    sock:send({ string.char(65), string.char(1), string.char(0), string.char(0), string.char(0) })\n    sock:send(\".\")\n    sock:send({ string.char(165), string.char(77), string.char(0), string.char(0), string.char(0) })\n    sock:send(\"{\\\"event_id\\\":\\\"1e902e84bf5a4ead8f7760a0fe2c7719\\\",\\\"request_hit_whitelist\\\":false}\")\n\n    ngx.exit(200)\nend\n\nfunction _M.reject()\n    local sock = get_socket()\n    sock:send({ string.char(65), string.char(1), string.char(0), string.char(0), string.char(0) })\n    sock:send(\"?\")\n    sock:send({ string.char(2), string.char(3), string.char(0), string.char(0), string.char(0) })\n    sock:send(\"403\")\n    sock:send({ string.char(37), string.char(77), string.char(0), string.char(0), string.char(0) })\n    sock:send(\"{\\\"event_id\\\":\\\"b3c6ce574dc24f09a01f634a39dca83b\\\",\\\"request_hit_whitelist\\\":false}\")\n    sock:send({ string.char(35), string.char(79), string.char(0), string.char(0), string.char(0) })\n    sock:send(\"Set-Cookie:sl-session=ulgbPfMSuWRNsi/u7Aj9aA==; Domain=; Path=/; Max-Age=86400\\n\")\n    sock:send({ string.char(164), string.char(51), string.char(0), string.char(0), string.char(0) })\n    sock:send(\"<!-- event_id: b3c6ce574dc24f09a01f634a39dca83b -->\")\n\n    ngx.exit(200)\nend\n\nfunction _M.timeout()\n    ngx.sleep(100)\n    _M.pass()\nend\n\nreturn _M\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-interface/pom.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.apache.dubbo.backend</groupId>\n        <artifactId>dubbo-backend</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <groupId>org.apache.dubbo.backend</groupId>\n    <artifactId>dubbo-backend-interface</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n    <description></description>\n    <properties>\n        <skip_maven_deploy>true</skip_maven_deploy>\n    </properties>\n    <build>\n    <plugins>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <configuration>\n                <source>1.8</source>\n                <target>1.8</target>\n            </configuration>\n        </plugin>\n    </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-interface/src/main/java/org/apache/dubbo/backend/DemoService.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend;\n\nimport java.util.Map;\n\npublic interface DemoService {;\n\n    /**\n     * standard samples tengine dubbo infterace demo\n     * @param context pass http infos\n     * @return Map<String, Object></> pass to response http\n     **/\n    Map<String, Object> hello(Map<String, Object> context);\n\n    /**\n     * test for dubbo non-200 response\n     * @param context pass http infos\n     * @return Map<String, Object></> pass to response http\n     **/\n    Map<String, Object> fail(Map<String, Object> context);\n\n    /**\n     * test for dubbo response timeout\n     * @param context pass http infos\n     * @return Map<String, Object></> pass to response http\n     **/\n    Map<String, Object> timeout(Map<String, Object> context);\n\n    /**\n     * test for non-string status code\n     * @param context pass http infos\n     * @return Map<String, Object></> pass to response http\n     **/\n    Map<String, Object> badStatus(Map<String, Object> context);\n}\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-provider/pom.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.apache.dubbo.backend</groupId>\n        <artifactId>dubbo-backend</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <groupId>org.apache.dubbo.backend</groupId>\n    <artifactId>dubbo-backend-provider</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n    <description></description>\n    <properties>\n        <skip_maven_deploy>true</skip_maven_deploy>\n        <slf4j-log4j12.version>1.7.25</slf4j-log4j12.version>\n        <curator.version>2.12.0</curator.version>\n        <dubbo.version>2.7.21</dubbo.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.apache.dubbo.backend</groupId>\n            <artifactId>dubbo-backend-interface</artifactId>\n            <version>1.0.0-SNAPSHOT</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.dubbo</groupId>\n            <artifactId>dubbo</artifactId>\n            <version>${dubbo.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>4.5.13</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>dubbo-demo-provider</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <source>1.8</source>\n                    <target>1.8</target>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-jar-plugin</artifactId>\n                <configuration>\n                    <archive>\n                        <manifest>\n                            <mainClass>org.apache.dubbo.backend.provider.Provider</mainClass>\n                        </manifest>\n                    </archive>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>com.jolira</groupId>\n                <artifactId>onejar-maven-plugin</artifactId>\n                <version>1.4.4</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>one-jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-provider/src/main/java/org/apache/dubbo/backend/provider/DemoServiceImpl.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend.provider;\n\nimport org.apache.dubbo.backend.DemoService;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\nimport java.lang.InterruptedException;\n\npublic class DemoServiceImpl implements DemoService {\n    @Override\n    public Map<String, Object> hello(Map<String, Object> context) {\n        Map<String, Object> ret = new HashMap<String, Object>();\n        ret.put(\"body\", \"dubbo success\\n\");\n        ret.put(\"status\", \"200\");\n\n        for (Map.Entry<String, Object> entry : context.entrySet()) {\n            System.out.println(\"Key = \" + entry.getKey() + \", Value = \" + entry.getValue());\n            if (entry.getKey().startsWith(\"extra-arg\")) {\n                ret.put(\"Got-\" + entry.getKey(), entry.getValue());\n            }\n        }\n\n        return ret;\n    }\n\n    @Override\n    public Map<String, Object> fail(Map<String, Object> context) {\n        Map<String, Object> ret = new HashMap<String, Object>();\n        ret.put(\"body\", \"dubbo fail\\n\");\n        ret.put(\"status\", \"503\");\n        return ret;\n    }\n\n    @Override\n    public Map<String, Object> timeout(Map<String, Object> context) {\n        Map<String, Object> ret = new HashMap<String, Object>();\n        try {\n            TimeUnit.MILLISECONDS.sleep(500);\n        } catch (InterruptedException ex) {}\n        ret.put(\"body\", \"dubbo fail\\n\");\n        ret.put(\"status\", \"503\");\n        return ret;\n    }\n\n    @Override\n    public Map<String, Object> badStatus(Map<String, Object> context) {\n        Map<String, Object> ret = new HashMap<String, Object>();\n        ret.put(\"body\", \"ok\\n\");\n        ret.put(\"status\", 200);\n        return ret;\n    }\n}\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-provider/src/main/java/org/apache/dubbo/backend/provider/Provider.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend.provider;\n\nimport org.springframework.context.support.ClassPathXmlApplicationContext;\n\nimport java.util.concurrent.TimeUnit;\nimport java.lang.InterruptedException;\n\npublic class Provider {\n\n    /**\n     * To get ipv6 address to work, add\n     * System.setProperty(\"java.net.preferIPv6Addresses\", \"true\");\n     * before running your application.\n     */\n    public static void main(String[] args) throws Exception {\n        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{\"META-INF/spring/dubbo-demo-provider.xml\"});\n        context.start();\n        while (true) {\n            try {\n                TimeUnit.MINUTES.sleep(1);\n            } catch (InterruptedException ex) {}\n        }\n    }\n}\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-provider/src/main/resources/META-INF/spring/dubbo-demo-provider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  Licensed to the Apache Software Foundation (ASF) under one or more\n  contributor license agreements.  See the NOTICE file distributed with\n  this work for additional information regarding copyright ownership.\n  The ASF licenses this file to You under the Apache License, Version 2.0\n  (the \"License\"); you may not use this file except in compliance with\n  the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n  -->\n<beans xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xmlns:dubbo=\"http://dubbo.apache.org/schema/dubbo\"\n       xmlns=\"http://www.springframework.org/schema/beans\"\n       xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd\n       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd\">\n\n    <!-- provider's application name, used for tracing dependency relationship -->\n    <dubbo:application name=\"demo-provider\"/>\n\n    <!-- use multicast registry center to export service -->\n    <dubbo:registry address=\"multicast://224.5.6.7:1234\"/>\n\n    <!-- use dubbo protocol to export service on port 20880 -->\n    <dubbo:protocol name=\"dubbo\" port=\"20880\" threads=\"1024\"/>\n\n    <!-- service implementation, as same as regular local bean -->\n    <bean id=\"demoService\" class=\"org.apache.dubbo.backend.provider.DemoServiceImpl\"/>\n\n    <!-- declare the service interface to be exported -->\n    <dubbo:service interface=\"org.apache.dubbo.backend.DemoService\" ref=\"demoService\" version=\"1.0.0\"/>\n\n</beans>"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-provider/src/main/resources/dubbo.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\ndubbo.application.qos.enable=false\n"
  },
  {
    "path": "t/lib/dubbo-backend/dubbo-backend-provider/src/main/resources/log4j.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n###set log levels###\nlog4j.rootLogger=info, stdout\n###output to the console###\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.Target=System.out\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=[%d{dd/MM/yy HH:mm:ss:SSS z}] %t %5p %c{2}: %m%n\n"
  },
  {
    "path": "t/lib/dubbo-backend/pom.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>org.apache.dubbo.backend</groupId>\n    <artifactId>dubbo-backend</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>pom</packaging>\n    <name>${project.artifactId}</name>\n    <description>A dubbo backend for test based on dubbo-samples-tengine</description>\n    <properties>\n        <skip_maven_deploy>true</skip_maven_deploy>\n        <dubbo.version>2.7.21</dubbo.version>\n    </properties>\n    <modules>\n        <module>dubbo-backend-interface</module>\n        <module>dubbo-backend-provider</module>\n    </modules>\n\n    <dependencyManagement>\n        <dependencies>\n            <!-- Spring Boot -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>2.1.5.RELEASE</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.dubbo</groupId>\n                <artifactId>dubbo-dependencies-bom</artifactId>\n                <version>${dubbo.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.dubbo</groupId>\n                <artifactId>dubbo</artifactId>\n                <version>${dubbo.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>org.springframework</groupId>\n                        <artifactId>spring</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>javax.servlet</groupId>\n                        <artifactId>servlet-api</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>log4j</groupId>\n                        <artifactId>log4j</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n    <dependencies>\n        <!-- Spring Boot dependencies -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n            <version>2.1.5.RELEASE</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.dubbo</groupId>\n            <artifactId>dubbo-spring-boot-starter</artifactId>\n            <version>2.7.1</version>\n        </dependency>\n    </dependencies>\n    <build>\n        <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-compiler-plugin</artifactId>\n                    <configuration>\n                        <source>1.8</source>\n                        <target>1.8</target>\n                    </configuration>\n                </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-interface/pom.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.apache.dubbo.backend</groupId>\n        <artifactId>dubbo-serialization-backend</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <groupId>org.apache.dubbo.backend</groupId>\n    <artifactId>dubbo-serialization-backend-interface</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n    <description></description>\n    <properties>\n        <skip_maven_deploy>true</skip_maven_deploy>\n    </properties>\n    <build>\n    <plugins>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <configuration>\n                <source>1.8</source>\n                <target>1.8</target>\n            </configuration>\n        </plugin>\n    </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-interface/src/main/java/org/apache/dubbo/backend/DubboSerializationTestService.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend;\n\npublic interface DubboSerializationTestService {\n\n    PoJo testPoJo(PoJo input);\n\n    PoJo[] testPoJos(PoJo[] input);\n\n    void testVoid();\n\n    void testFailure();\n\n    void testTimeout();\n}\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-interface/src/main/java/org/apache/dubbo/backend/PoJo.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class PoJo {\n    private String aString;\n    private Boolean aBoolean;\n    private Byte aByte;\n    private Character acharacter;\n    private Integer aInt;\n    private Float aFloat;\n    private Double aDouble;\n    private Long aLong;\n    private Short aShort;\n    private String[] strings;\n    private Map<String, String> stringMap;\n\n    public String getaString() {\n        return aString;\n    }\n\n    public void setaString(String aString) {\n        this.aString = aString;\n    }\n\n    public Boolean getaBoolean() {\n        return aBoolean;\n    }\n\n    public void setaBoolean(Boolean aBoolean) {\n        this.aBoolean = aBoolean;\n    }\n\n    public Byte getaByte() {\n        return aByte;\n    }\n\n    public void setaByte(Byte aByte) {\n        this.aByte = aByte;\n    }\n\n    public Character getAcharacter() {\n        return acharacter;\n    }\n\n    public void setAcharacter(Character acharacter) {\n        this.acharacter = acharacter;\n    }\n\n    public Integer getaInt() {\n        return aInt;\n    }\n\n    public void setaInt(Integer aInt) {\n        this.aInt = aInt;\n    }\n\n    public Float getaFloat() {\n        return aFloat;\n    }\n\n    public void setaFloat(Float aFloat) {\n        this.aFloat = aFloat;\n    }\n\n    public Double getaDouble() {\n        return aDouble;\n    }\n\n    public void setaDouble(Double aDouble) {\n        this.aDouble = aDouble;\n    }\n\n    public Long getaLong() {\n        return aLong;\n    }\n\n    public void setaLong(Long aLong) {\n        this.aLong = aLong;\n    }\n\n    public Short getaShort() {\n        return aShort;\n    }\n\n    public void setaShort(Short aShort) {\n        this.aShort = aShort;\n    }\n\n    public Map<String, String> getStringMap() {\n        return stringMap;\n    }\n\n    public void setStringMap(Map<String, String> stringMap) {\n        this.stringMap = stringMap;\n    }\n\n    public String[] getStrings() {\n        return strings;\n    }\n\n    public void setStrings(String[] strings) {\n        this.strings = strings;\n    }\n\n    public static PoJo getTestInstance(){\n        PoJo poJo = new PoJo();\n        poJo.aBoolean =true;\n        poJo.aByte =1;\n        poJo.acharacter ='a';\n        poJo.aInt =2;\n        poJo.aDouble = 1.1;\n        poJo.aFloat =1.2f;\n        poJo.aLong = 3L;\n        poJo.aShort = 4;\n        poJo.aString =\"aa\";\n        HashMap<String, String> poJoMap = new HashMap<>();\n        poJoMap.put(\"key\",\"value\");\n        poJo.stringMap = poJoMap;\n        poJo.strings = new String[]{\"aa\",\"bb\"};\n        return poJo;\n    }\n}\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-provider/pom.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.apache.dubbo.backend</groupId>\n        <artifactId>dubbo-serialization-backend</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <groupId>org.apache.dubbo.backend</groupId>\n    <artifactId>dubbo-serialization-backend-provider</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n    <description></description>\n    <properties>\n        <skip_maven_deploy>true</skip_maven_deploy>\n        <slf4j-log4j12.version>1.7.25</slf4j-log4j12.version>\n        <curator.version>2.12.0</curator.version>\n        <dubbo.version>2.7.21</dubbo.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.apache.dubbo.backend</groupId>\n            <artifactId>dubbo-serialization-backend-interface</artifactId>\n            <version>1.0.0-SNAPSHOT</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.dubbo</groupId>\n            <artifactId>dubbo</artifactId>\n            <version>${dubbo.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>4.5.13</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>dubbo-demo-provider</finalName>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <source>1.8</source>\n                    <target>1.8</target>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-jar-plugin</artifactId>\n                <configuration>\n                    <archive>\n                        <manifest>\n                            <mainClass>org.apache.dubbo.backend.provider.Provider</mainClass>\n                        </manifest>\n                    </archive>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>com.jolira</groupId>\n                <artifactId>onejar-maven-plugin</artifactId>\n                <version>1.4.4</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>one-jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-provider/src/main/java/org/apache/dubbo/backend/provider/DubboSerializationTestServiceImpl.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend.provider;\n\nimport org.apache.dubbo.backend.DubboSerializationTestService;\nimport org.apache.dubbo.backend.PoJo;\nimport org.apache.dubbo.common.utils.ReflectUtils;\n\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\nimport java.util.concurrent.TimeUnit;\n\npublic class DubboSerializationTestServiceImpl implements DubboSerializationTestService {\n\n    @Override\n    public PoJo testPoJo(PoJo input) {\n        return input;\n    }\n\n    @Override\n    public PoJo[] testPoJos(PoJo[] input) {\n        return input;\n    }\n\n    @Override\n    public void testVoid() {\n    }\n\n    @Override\n    public void testFailure() {\n        throw new RuntimeException(\"testFailure\");\n    }\n\n    @Override\n    public void testTimeout() {\n        try {\n            TimeUnit.SECONDS.sleep(10);\n        } catch (InterruptedException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-provider/src/main/java/org/apache/dubbo/backend/provider/Provider.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.dubbo.backend.provider;\n\nimport com.alibaba.fastjson.JSONObject;\nimport org.apache.dubbo.backend.DubboSerializationTestService;\nimport org.apache.dubbo.backend.PoJo;\nimport org.apache.dubbo.common.utils.ReflectUtils;\nimport org.springframework.context.support.ClassPathXmlApplicationContext;\n\nimport java.lang.reflect.Method;\nimport java.util.concurrent.TimeUnit;\nimport java.lang.InterruptedException;\n\npublic class Provider {\n\n    /**\n     * To get ipv6 address to work, add\n     * System.setProperty(\"java.net.preferIPv6Addresses\", \"true\");\n     * before running your application.\n     */\n    public static void main(String[] args) throws Exception {\n\n        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{\"META-INF/spring/dubbo-demo-provider.xml\"});\n        String jsonString = JSONObject.toJSONString(PoJo.getTestInstance());\n        System.out.println(jsonString);\n        context.start();\n        while (true) {\n            try {\n                TimeUnit.MINUTES.sleep(1);\n            } catch (InterruptedException ex) {}\n        }\n    }\n}\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-provider/src/main/resources/META-INF/spring/dubbo-demo-provider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  Licensed to the Apache Software Foundation (ASF) under one or more\n  contributor license agreements.  See the NOTICE file distributed with\n  this work for additional information regarding copyright ownership.\n  The ASF licenses this file to You under the Apache License, Version 2.0\n  (the \"License\"); you may not use this file except in compliance with\n  the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n  -->\n<beans xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xmlns:dubbo=\"http://dubbo.apache.org/schema/dubbo\"\n       xmlns=\"http://www.springframework.org/schema/beans\"\n       xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd\n       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd\">\n\n    <!-- provider's application name, used for tracing dependency relationship -->\n    <dubbo:application name=\"demo-provider2\"/>\n\n    <!-- use multicast registry center to export service -->\n    <dubbo:registry address=\"multicast://224.5.6.7:1234\"/>\n\n    <dubbo:protocol name=\"dubbo\" port=\"30880\" threads=\"1024\" serialization=\"fastjson\"/>\n\n    <!-- service implementation, as same as regular local bean -->\n    <bean id=\"demoService\" class=\"org.apache.dubbo.backend.provider.DubboSerializationTestServiceImpl\"/>\n\n    <!-- declare the service interface to be exported -->\n    <dubbo:service interface=\"org.apache.dubbo.backend.DubboSerializationTestService\" ref=\"demoService\" version=\"1.0.0\"/>\n\n</beans>\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-provider/src/main/resources/dubbo.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\ndubbo.application.qos.enable=false\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/dubbo-serialization-backend-provider/src/main/resources/log4j.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n###set log levels###\nlog4j.rootLogger=info, stdout\n###output to the console###\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.Target=System.out\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=[%d{dd/MM/yy HH:mm:ss:SSS z}] %t %5p %c{2}: %m%n\n"
  },
  {
    "path": "t/lib/dubbo-serialization-backend/pom.xml",
    "content": "<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>org.apache.dubbo.backend</groupId>\n    <artifactId>dubbo-serialization-backend</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>pom</packaging>\n    <name>${project.artifactId}</name>\n    <description>A dubbo backend for test based on dubbo-samples-tengine</description>\n    <properties>\n        <skip_maven_deploy>true</skip_maven_deploy>\n        <dubbo.version>2.7.21</dubbo.version>\n    </properties>\n    <modules>\n        <module>dubbo-serialization-backend-interface</module>\n        <module>dubbo-serialization-backend-provider</module>\n    </modules>\n\n    <dependencyManagement>\n        <dependencies>\n            <!-- Spring Boot -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>2.1.5.RELEASE</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.dubbo</groupId>\n                <artifactId>dubbo-dependencies-bom</artifactId>\n                <version>${dubbo.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.dubbo</groupId>\n                <artifactId>dubbo</artifactId>\n                <version>${dubbo.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>org.springframework</groupId>\n                        <artifactId>spring</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>javax.servlet</groupId>\n                        <artifactId>servlet-api</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>log4j</groupId>\n                        <artifactId>log4j</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n    <dependencies>\n        <!-- Spring Boot dependencies -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n            <version>2.1.5.RELEASE</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.dubbo</groupId>\n            <artifactId>dubbo-spring-boot-starter</artifactId>\n            <version>2.7.1</version>\n        </dependency>\n    </dependencies>\n    <build>\n        <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-compiler-plugin</artifactId>\n                    <configuration>\n                        <source>1.8</source>\n                        <target>1.8</target>\n                    </configuration>\n                </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "t/lib/etcd.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = \"proto3\";\npackage etcdserverpb;\n\nmessage StatusRequest {\n}\n\nmessage StatusResponse {\n  // version is the cluster protocol version used by the responding member.\n  string version = 2;\n}\n\nservice Maintenance {\n  // Status gets the status of the member.\n  rpc Status(StatusRequest) returns (StatusResponse) {}\n}\n"
  },
  {
    "path": "t/lib/ext-plugin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal json = require(\"toolkit.json\")\nlocal ext = require(\"apisix.plugins.ext-plugin.init\")\nlocal constants = require(\"apisix.constants\")\nlocal flatbuffers = require(\"flatbuffers\")\nlocal err_code = require(\"A6.Err.Code\")\nlocal err_resp = require(\"A6.Err.Resp\")\nlocal prepare_conf_req = require(\"A6.PrepareConf.Req\")\nlocal prepare_conf_resp = require(\"A6.PrepareConf.Resp\")\nlocal a6_method = require(\"A6.Method\")\nlocal text_entry = require(\"A6.TextEntry\")\nlocal http_req_call_req = require(\"A6.HTTPReqCall.Req\")\nlocal http_req_call_resp = require(\"A6.HTTPReqCall.Resp\")\nlocal http_req_call_action = require(\"A6.HTTPReqCall.Action\")\nlocal http_req_call_stop = require(\"A6.HTTPReqCall.Stop\")\nlocal http_req_call_rewrite = require(\"A6.HTTPReqCall.Rewrite\")\nlocal http_resp_call_req = require(\"A6.HTTPRespCall.Req\")\nlocal http_resp_call_resp = require(\"A6.HTTPRespCall.Resp\")\nlocal extra_info = require(\"A6.ExtraInfo.Info\")\nlocal extra_info_req = require(\"A6.ExtraInfo.Req\")\nlocal extra_info_var = require(\"A6.ExtraInfo.Var\")\nlocal extra_info_resp = require(\"A6.ExtraInfo.Resp\")\nlocal extra_info_reqbody = require(\"A6.ExtraInfo.ReqBody\")\nlocal extra_info_respbody = require(\"A6.ExtraInfo.RespBody\")\n\nlocal _M = {}\nlocal builder = flatbuffers.Builder(0)\n\n\nlocal function build_extra_info(info, ty)\n    extra_info_req.Start(builder)\n    extra_info_req.AddInfoType(builder, ty)\n    extra_info_req.AddInfo(builder, info)\nend\n\n\nlocal function build_action(action, ty)\n    http_req_call_resp.Start(builder)\n    http_req_call_resp.AddActionType(builder, ty)\n    http_req_call_resp.AddAction(builder, action)\nend\n\n\nlocal function ask_extra_info(sock, case_extra_info)\n    local data\n    for _, action in ipairs(case_extra_info) do\n        if action.type == \"closed\" then\n            ngx.exit(-1)\n            return\n        end\n\n        if action.type == \"var\" then\n            local name = builder:CreateString(action.name)\n            extra_info_var.Start(builder)\n            extra_info_var.AddName(builder, name)\n            local var_req = extra_info_var.End(builder)\n            build_extra_info(var_req, extra_info.Var)\n            local req = extra_info_req.End(builder)\n            builder:Finish(req)\n            data = builder:Output()\n            local ok, err = ext.send(sock, constants.RPC_EXTRA_INFO, data)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.log(ngx.WARN, \"send extra info req successfully\")\n\n            local ty, data = ext.receive(sock)\n            if not ty then\n                ngx.log(ngx.ERR, data)\n                return\n            end\n\n            assert(ty == constants.RPC_EXTRA_INFO, ty)\n            local buf = flatbuffers.binaryArray.New(data)\n            local resp = extra_info_resp.GetRootAsResp(buf, 0)\n            local res = resp:ResultAsString()\n            assert(res == action.result, res)\n        end\n\n        if action.type == \"reqbody\" then\n            extra_info_reqbody.Start(builder)\n            local reqbody_req = extra_info_reqbody.End(builder)\n            build_extra_info(reqbody_req, extra_info.ReqBody)\n            local req = extra_info_req.End(builder)\n            builder:Finish(req)\n            data = builder:Output()\n            local ok, err = ext.send(sock, constants.RPC_EXTRA_INFO, data)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.log(ngx.WARN, \"send extra info req successfully\")\n\n            local ty, data = ext.receive(sock)\n            if not ty then\n                ngx.log(ngx.ERR, data)\n                return\n            end\n\n            assert(ty == constants.RPC_EXTRA_INFO, ty)\n            local buf = flatbuffers.binaryArray.New(data)\n            local resp = extra_info_resp.GetRootAsResp(buf, 0)\n            local res = resp:ResultAsString()\n            assert(res == action.result, res)\n        end\n\n        if action.type == \"respbody\" then\n            extra_info_respbody.Start(builder)\n            local respbody_req = extra_info_respbody.End(builder)\n            build_extra_info(respbody_req, extra_info.RespBody)\n            local req = extra_info_req.End(builder)\n            builder:Finish(req)\n            data = builder:Output()\n            local ok, err = ext.send(sock, constants.RPC_EXTRA_INFO, data)\n            if not ok then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.log(ngx.WARN, \"send extra info req successfully\")\n\n            local ty, data = ext.receive(sock)\n            if not ty then\n                ngx.log(ngx.ERR, data)\n                return\n            end\n\n            assert(ty == constants.RPC_EXTRA_INFO, ty)\n            local buf = flatbuffers.binaryArray.New(data)\n            local resp = extra_info_resp.GetRootAsResp(buf, 0)\n            local res = resp:ResultAsString()\n            assert(res == action.result, res)\n        end\n    end\n\nend\n\n\nfunction _M.go(case)\n    local sock = ngx.req.socket(true)\n    local ty, data = ext.receive(sock)\n    if not ty then\n        ngx.log(ngx.ERR, data)\n        return\n    end\n    ngx.log(ngx.WARN, \"receive rpc call successfully\")\n\n    if ty == constants.RPC_PREPARE_CONF then\n        if case.inject_error then\n            ty = constants.RPC_ERROR\n            err_resp.Start(builder)\n            err_resp.AddCode(builder, err_code.BAD_REQUEST)\n            local req = prepare_conf_req.End(builder)\n            builder:Finish(req)\n            data = builder:Output()\n\n        else\n            local buf = flatbuffers.binaryArray.New(data)\n            local pc = prepare_conf_req.GetRootAsReq(buf, 0)\n\n            if case.with_conf then\n                local conf = pc:Conf(1)\n                assert(conf:Name(), \"foo\")\n                assert(conf:Value(), \"bar\")\n                local conf = pc:Conf(2)\n                assert(conf:Name(), \"cat\")\n                assert(conf:Value(), \"dog\")\n            else\n                assert(pc:ConfLength() == 0)\n            end\n\n            if case.expect_key_pattern then\n                local m = ngx.re.find(pc:Key(), case.expect_key_pattern, \"jo\")\n                assert(m ~= nil, pc:Key())\n            else\n                assert(pc:Key() ~= \"\")\n            end\n\n            prepare_conf_resp.Start(builder)\n            prepare_conf_resp.AddConfToken(builder, 233)\n            local req = prepare_conf_req.End(builder)\n            builder:Finish(req)\n            data = builder:Output()\n        end\n\n    elseif case.no_token then\n        ty = constants.RPC_ERROR\n        err_resp.Start(builder)\n        err_resp.AddCode(builder, err_code.CONF_TOKEN_NOT_FOUND)\n        local req = prepare_conf_req.End(builder)\n        builder:Finish(req)\n        data = builder:Output()\n\n    elseif ty == constants.RPC_HTTP_REQ_CALL then\n        local buf = flatbuffers.binaryArray.New(data)\n        local call_req = http_req_call_req.GetRootAsReq(buf, 0)\n        if case.check_input then\n            assert(call_req:Id() == 0)\n            assert(call_req:ConfToken() == 233)\n            assert(call_req:SrcIpLength() == 4)\n            assert(call_req:SrcIp(1) == 127)\n            assert(call_req:SrcIp(2) == 0)\n            assert(call_req:SrcIp(3) == 0)\n            assert(call_req:SrcIp(4) == 1)\n            assert(call_req:Method() == a6_method.PUT)\n            assert(call_req:Path() == \"/hello\")\n\n            assert(call_req:ArgsLength() == 4)\n            local res = {}\n            for i = 1, call_req:ArgsLength() do\n                local entry = call_req:Args(i)\n                local r = res[entry:Name()]\n                if r then\n                    res[entry:Name()] = {r, entry:Value()}\n                else\n                    res[entry:Name()] = entry:Value() or true\n                end\n            end\n            assert(json.encode(res) == '{\\\"xx\\\":[\\\"y\\\",\\\"z\\\"],\\\"y\\\":\\\"\\\",\\\"z\\\":true}')\n\n            assert(call_req:HeadersLength() == 5)\n            local res = {}\n            for i = 1, call_req:HeadersLength() do\n                local entry = call_req:Headers(i)\n                local r = res[entry:Name()]\n                if r then\n                    res[entry:Name()] = {r, entry:Value()}\n                else\n                    res[entry:Name()] = entry:Value() or true\n                end\n            end\n            assert(json.encode(res) == '{\\\"connection\\\":\\\"close\\\",\\\"host\\\":\\\"localhost\\\",' ..\n                   '\\\"x-req\\\":[\\\"foo\\\",\\\"bar\\\"],\\\"x-resp\\\":\\\"cat\\\"}')\n        elseif case.check_input_ipv6 then\n            assert(call_req:SrcIpLength() == 16)\n            for i = 1, 15 do\n                assert(call_req:SrcIp(i) == 0)\n            end\n            assert(call_req:SrcIp(16) == 1)\n        elseif case.check_input_rewrite_host then\n            for i = 1, call_req:HeadersLength() do\n                local entry = call_req:Headers(i)\n                if entry:Name() == \"host\" then\n                    assert(entry:Value() == \"test.com\")\n                end\n            end\n        elseif case.check_input_rewrite_path then\n            assert(call_req:Path() == \"/xxx\")\n        elseif case.check_input_rewrite_args then\n            assert(call_req:Path() == \"/xxx\")\n            assert(call_req:ArgsLength() == 1)\n            local entry = call_req:Args(1)\n            assert(entry:Name() == \"x\")\n            assert(entry:Value() == \"z\")\n        elseif case.get_request_body then\n            assert(call_req:Method() == a6_method.POST)\n        else\n            assert(call_req:Method() == a6_method.GET)\n        end\n\n        if case.extra_info then\n            ask_extra_info(sock, case.extra_info)\n        end\n\n        if case.stop == true then\n            local len = 3\n            http_req_call_stop.StartBodyVector(builder, len)\n            builder:PrependByte(string.byte(\"t\"))\n            builder:PrependByte(string.byte(\"a\"))\n            builder:PrependByte(string.byte(\"c\"))\n            local b = builder:EndVector(len)\n\n            local hdrs = {\n                {\"X-Resp\", \"foo\"},\n                {\"X-Req\", \"bar\"},\n                {\"X-Same\", \"one\"},\n                {\"X-Same\", \"two\"},\n            }\n            local len = #hdrs\n            local textEntries = {}\n            for i = 1, len do\n                local name = builder:CreateString(hdrs[i][1])\n                local value = builder:CreateString(hdrs[i][2])\n                text_entry.Start(builder)\n                text_entry.AddName(builder, name)\n                text_entry.AddValue(builder, value)\n                local c = text_entry.End(builder)\n                textEntries[i] = c\n            end\n            http_req_call_stop.StartHeadersVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            local vec = builder:EndVector(len)\n\n            http_req_call_stop.Start(builder)\n            if case.check_default_status ~= true then\n                http_req_call_stop.AddStatus(builder, 405)\n            end\n            http_req_call_stop.AddBody(builder, b)\n            http_req_call_stop.AddHeaders(builder, vec)\n            local action = http_req_call_stop.End(builder)\n            build_action(action, http_req_call_action.Stop)\n\n        elseif case.rewrite == true or case.rewrite_host == true then\n            local hdrs\n            if case.rewrite_host then\n                hdrs = {{\"host\", \"127.0.0.1\"}}\n            else\n                hdrs = {\n                    {\"X-Delete\", nil},\n                    {\"X-Change\", \"bar\"},\n                    {\"X-Add\", \"bar\"},\n                }\n            end\n\n            local len = #hdrs\n            local textEntries = {}\n            for i = 1, len do\n                local name = builder:CreateString(hdrs[i][1])\n                local value\n                if hdrs[i][2] then\n                    value = builder:CreateString(hdrs[i][2])\n                end\n                text_entry.Start(builder)\n                text_entry.AddName(builder, name)\n                if value then\n                    text_entry.AddValue(builder, value)\n                end\n                local c = text_entry.End(builder)\n                textEntries[i] = c\n            end\n            http_req_call_rewrite.StartHeadersVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            local vec = builder:EndVector(len)\n\n            local path = builder:CreateString(\"/uri\")\n\n            http_req_call_rewrite.Start(builder)\n            http_req_call_rewrite.AddPath(builder, path)\n            http_req_call_rewrite.AddHeaders(builder, vec)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        elseif case.rewrite_args == true or case.rewrite_args_only == true then\n            local path = builder:CreateString(\"/plugin_proxy_rewrite_args\")\n\n            local args = {\n                {\"a\", \"foo\"},\n                {\"d\", nil},\n                {\"c\", \"bar\"},\n                {\"a\", \"bar\"},\n            }\n\n            local len = #args\n            local textEntries = {}\n            for i = 1, len do\n                local name = builder:CreateString(args[i][1])\n                local value\n                if args[i][2] then\n                    value = builder:CreateString(args[i][2])\n                end\n                text_entry.Start(builder)\n                text_entry.AddName(builder, name)\n                if value then\n                    text_entry.AddValue(builder, value)\n                end\n                local c = text_entry.End(builder)\n                textEntries[i] = c\n            end\n            http_req_call_rewrite.StartHeadersVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            local vec = builder:EndVector(len)\n\n            http_req_call_rewrite.Start(builder)\n            if not case.rewrite_args_only then\n                http_req_call_rewrite.AddPath(builder, path)\n            end\n            http_req_call_rewrite.AddArgs(builder, vec)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        elseif case.rewrite_path_only then\n            local path = builder:CreateString(\"/plugin_proxy_rewrite_args\")\n            http_req_call_rewrite.Start(builder)\n            http_req_call_rewrite.AddPath(builder, path)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        elseif case.rewrite_bad_path == true then\n            local path = builder:CreateString(\"/plugin_proxy_rewrite_args?a=2\")\n            http_req_call_rewrite.Start(builder)\n            http_req_call_rewrite.AddPath(builder, path)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        elseif case.rewrite_resp_header == true or case.rewrite_vital_resp_header == true then\n            local hdrs = {\n                {\"X-Resp\", \"foo\"},\n                {\"X-Req\", \"bar\"},\n                {\"Content-Type\", \"application/json\"},\n                {\"Content-Encoding\", \"deflate\"},\n            }\n            local len = #hdrs\n            local textEntries = {}\n            for i = 1, len do\n                local name = builder:CreateString(hdrs[i][1])\n                local value = builder:CreateString(hdrs[i][2])\n                text_entry.Start(builder)\n                text_entry.AddName(builder, name)\n                text_entry.AddValue(builder, value)\n                local c = text_entry.End(builder)\n                textEntries[i] = c\n            end\n            http_req_call_rewrite.StartRespHeadersVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            local vec = builder:EndVector(len)\n\n            local path = builder:CreateString(\"/plugin_proxy_rewrite_resp_header\")\n\n            http_req_call_rewrite.Start(builder)\n            http_req_call_rewrite.AddRespHeaders(builder, vec)\n            http_req_call_rewrite.AddPath(builder, path)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        elseif case.rewrite_same_resp_header == true then\n            local hdrs = {\n                {\"X-Resp\", \"foo\"},\n                {\"X-Req\", \"bar\"},\n                {\"X-Same\", \"one\"},\n                {\"X-Same\", \"two\"},\n            }\n            local len = #hdrs\n            local textEntries = {}\n            for i = 1, len do\n                local name = builder:CreateString(hdrs[i][1])\n                local value = builder:CreateString(hdrs[i][2])\n                text_entry.Start(builder)\n                text_entry.AddName(builder, name)\n                text_entry.AddValue(builder, value)\n                local c = text_entry.End(builder)\n                textEntries[i] = c\n            end\n            http_req_call_rewrite.StartRespHeadersVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            local vec = builder:EndVector(len)\n\n            local path = builder:CreateString(\"/plugin_proxy_rewrite_resp_header\")\n\n            http_req_call_rewrite.Start(builder)\n            http_req_call_rewrite.AddRespHeaders(builder, vec)\n            http_req_call_rewrite.AddPath(builder, path)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        elseif case.rewrite_request_body == true then\n            local len = 4\n            http_req_call_rewrite.StartBodyVector(builder, len)\n            builder:PrependByte(string.byte(\"\\n\"))\n            builder:PrependByte(string.byte(\"c\"))\n            builder:PrependByte(string.byte(\"b\"))\n            builder:PrependByte(string.byte(\"a\"))\n            local b = builder:EndVector(len)\n            http_req_call_rewrite.Start(builder)\n            http_req_call_rewrite.AddBody(builder, b)\n            local action = http_req_call_rewrite.End(builder)\n            build_action(action, http_req_call_action.Rewrite)\n\n        else\n            http_req_call_resp.Start(builder)\n        end\n\n        local req = http_req_call_resp.End(builder)\n        builder:Finish(req)\n        data = builder:Output()\n\n    elseif ty == constants.RPC_HTTP_RESP_CALL  then\n        local buf = flatbuffers.binaryArray.New(data)\n        local call_req = http_resp_call_req.GetRootAsReq(buf, 0)\n        if case.check_input then\n            assert(call_req:Id() == 0)\n            assert(call_req:ConfToken() == 233)\n            assert(call_req:Status() == 200)\n            local len = call_req:HeadersLength()\n\n            local headers = {}\n            for i = 1, len do\n                local entry = call_req:Headers(i)\n                local r = headers[entry:Name()]\n                if r then\n                    headers[entry:Name()] = {r, entry:Value()}\n                else\n                    headers[entry:Name()] = entry:Value() or true\n                end\n            end\n            assert(json.encode(headers), '{\"Connection\":\"close\",\"Content-Length\":\"12\",' ..\n                                    '\"Content-Type\":\"text/plain\",\"Server\":\"openresty\"}')\n            http_resp_call_resp.Start(builder)\n\n        elseif case.modify_body then\n            local len = 3\n            http_resp_call_resp.StartBodyVector(builder, len)\n            builder:PrependByte(string.byte(\"t\"))\n            builder:PrependByte(string.byte(\"a\"))\n            builder:PrependByte(string.byte(\"c\"))\n            local b = builder:EndVector(len)\n\n\n            http_resp_call_resp.Start(builder)\n            http_resp_call_resp.AddBody(builder, b)\n\n        elseif case.modify_header then\n            local len = call_req:HeadersLength()\n\n            local headers = {}\n            for i = 1, len do\n                local entry = call_req:Headers(i)\n                local r = headers[entry:Name()]\n                if r then\n                    headers[entry:Name()] = {r, entry:Value()}\n                else\n                    headers[entry:Name()] = entry:Value() or true\n                end\n            end\n\n            if case.same_header then\n                headers[\"x-same\"] = {\"one\", \"two\"}\n            else\n                local runner = headers[\"x-runner\"]\n                if runner and runner == \"Go-runner\" then\n                    headers[\"x-runner\"] = \"Test-Runner\"\n                end\n\n                headers[\"Content-Type\"] = \"application/json\"\n            end\n\n            local i = 1\n            local textEntries = {}\n            for k, v in pairs(headers) do\n                local name = builder:CreateString(k)\n                if type(v) == \"table\" then\n                    for j = 1, #v do\n                        local value = builder:CreateString(v[j])\n                        text_entry.Start(builder)\n                        text_entry.AddName(builder, name)\n                        text_entry.AddValue(builder, value)\n                        local c = text_entry.End(builder)\n                        textEntries[i] = c\n                        i = i + 1\n                    end\n                else\n                    local value = builder:CreateString(v)\n                    text_entry.Start(builder)\n                    text_entry.AddName(builder, name)\n                    text_entry.AddValue(builder, value)\n                    local c = text_entry.End(builder)\n                    textEntries[i] = c\n                    i = i + 1\n                end\n            end\n\n            len = #textEntries\n            http_resp_call_resp.StartHeadersVector(builder, len)\n            for i = len, 1, -1 do\n                builder:PrependUOffsetTRelative(textEntries[i])\n            end\n            local vec = builder:EndVector(len)\n\n            http_resp_call_resp.Start(builder)\n            http_resp_call_resp.AddHeaders(builder, vec)\n\n        elseif case.modify_status then\n            local status = call_req:Status()\n            if status == 200 then\n                status = 304\n            end\n            http_resp_call_resp.Start(builder)\n            http_resp_call_resp.AddStatus(builder, status)\n\n        elseif case.extra_info then\n            ask_extra_info(sock, case.extra_info)\n            http_resp_call_resp.Start(builder)\n        else\n            http_resp_call_resp.Start(builder)\n        end\n\n        local resp = http_resp_call_resp.End(builder)\n        builder:Finish(resp)\n        data = builder:Output()\n    end\n\n    local ok, err = ext.send(sock, ty, data)\n    if not ok then\n        ngx.log(ngx.ERR, err)\n        return\n    end\n    ngx.log(ngx.WARN, \"send rpc call response successfully\")\nend\n\n\nfunction _M.header_too_short()\n    local sock = ngx.req.socket()\n    local ty, data = ext.receive(sock)\n    if not ty then\n        ngx.log(ngx.ERR, data)\n        return\n    end\n    ngx.log(ngx.WARN, \"receive rpc call successfully\")\n\n    local ok, err = sock:send({string.char(2), string.char(1)})\n    if not ok then\n        ngx.log(ngx.ERR, err)\n        return\n    end\nend\n\n\nfunction _M.data_too_short()\n    local sock = ngx.req.socket()\n    local ty, data = ext.receive(sock)\n    if not ty then\n        ngx.log(ngx.ERR, data)\n        return\n    end\n    ngx.log(ngx.WARN, \"receive rpc call successfully\")\n\n    local ok, err = sock:send({string.char(2), string.char(1), string.rep(string.char(0), 3)})\n    if not ok then\n        ngx.log(ngx.ERR, err)\n        return\n    end\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/grafana_loki.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal cjson = require(\"cjson\")\nlocal http = require(\"resty.http\")\n\nlocal _M = {}\n\n\nfunction _M.fetch_logs_from_loki(from, to, options)\n    options = options or {}\n\n    local direction = options.direction or \"backward\"\n    local limit = options.limit or \"10\"\n    local query = options.query or [[{job=\"apisix\"} | json]]\n    local url = options.url or \"http://127.0.0.1:3100/loki/api/v1/query_range\"\n    local headers = options.headers or {\n        [\"X-Scope-OrgID\"] = \"tenant_1\"\n    }\n\n    local httpc = http.new()\n    local res, err = httpc:request_uri(url, {\n        query = {\n            start = from,\n            [\"end\"] = to,\n            direction = direction,\n            limit = limit,\n            query = query,\n        },\n        headers = headers\n    })\n\n    if not res or err then\n        return nil, err\n    end\n\n    if res.status > 300 then\n        return nil, \"HTTP status code: \" .. res.status .. \", body: \" .. res.body\n    end\n\n    local data = cjson.decode(res.body)\n    if not data then\n        return nil, \"failed to decode response body: \" .. res.body\n    end\n    return data, nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/keycloak.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal http = require \"resty.http\"\n\nlocal _M = {}\n\n\n-- Request APISIX and redirect to keycloak,\n-- Login keycloak and return the res of APISIX\nfunction _M.login_keycloak(uri, username, password)\n    local httpc = http.new()\n\n    local res, err = httpc:request_uri(uri, {method = \"GET\"})\n    if not res then\n        return nil, err\n    elseif res.status ~= 302 then\n        -- Not a redirect which we expect.\n        -- Use 500 to indicate error.\n        return nil, \"Initial request was not redirected to ID provider authorization endpoint.\"\n    else\n        -- Extract cookies. Important since OIDC module tracks state with a session cookie.\n        local cookies = res.headers['Set-Cookie']\n\n        -- Concatenate cookies into one string as expected when sent in request header.\n        local cookie_str = _M.concatenate_cookies(cookies)\n\n        -- Call authorization endpoint we were redirected to.\n        -- Note: This typically returns a login form which is the case here for Keycloak as well.\n        -- However, how we process the form to perform the login is specific to Keycloak and\n        -- possibly even the version used.\n        res, err = httpc:request_uri(res.headers['Location'], {method = \"GET\"})\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 200 then\n            -- Unexpected response.\n            return nil, res.body\n        end\n\n        -- Check if response code was ok.\n        if res.status ~= 200 then\n            return nil, \"unexpected status \" .. res.status\n        end\n\n        -- From the returned form, extract the submit URI and parameters.\n        local uri, params = res.body:match('.*action=\"(.*)%?(.*)\" method=\"post\">')\n\n        -- Substitute escaped ampersand in parameters.\n        params = params:gsub(\"&amp;\", \"&\")\n\n        -- Get all cookies returned. Probably not so important since not part of OIDC specification.\n        local auth_cookies = res.headers['Set-Cookie']\n\n        -- Concatenate cookies into one string as expected when sent in request header.\n        local auth_cookie_str = _M.concatenate_cookies(auth_cookies)\n\n        -- Invoke the submit URI with parameters and cookies, adding username\n        -- and password in the body.\n        -- Note: Username and password are specific to the Keycloak Docker image used.\n        res, err = httpc:request_uri(uri .. \"?\" .. params, {\n                method = \"POST\",\n                body = \"username=\" .. username .. \"&password=\" .. password,\n                headers = {\n                    [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                    [\"Cookie\"] = auth_cookie_str\n                }\n            })\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 302 then\n            -- Not a redirect which we expect.\n            return nil, \"Login form submission did not return redirect to redirect URI.\"\n        end\n\n        -- Extract the redirect URI from the response header.\n        -- TODO: Consider validating this against the plugin configuration.\n        local redirect_uri = res.headers['Location']\n\n        -- Invoke the redirect URI (which contains the authorization code as an URL parameter).\n        res, err = httpc:request_uri(redirect_uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = cookie_str\n                }\n            })\n\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 302 then\n            -- Not a redirect which we expect.\n            return nil, \"Invoking redirect URI with authorization code\" ..\n                \"did not return redirect to original URI.\"\n        end\n\n        return res, nil\n    end\nend\n\n\n-- Concatenate cookies into one string as expected when sent in request header.\nfunction _M.concatenate_cookies(cookies)\n    local cookie_str = \"\"\n    if type(cookies) == 'string' then\n        cookie_str = cookies:match('([^;]*); .*')\n    else\n        -- Must be a table.\n        local len = #cookies\n        if len > 0 then\n            cookie_str = cookies[1]:match('([^;]*); .*')\n            for i = 2, len do\n                cookie_str = cookie_str .. \"; \" .. cookies[i]:match('([^;]*); .*')\n            end\n        end\n    end\n\n    return cookie_str, nil\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/keycloak_cas.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal http = require \"resty.http\"\n\nlocal _M = {}\n\nlocal default_opts = {\n    idp_uri = \"http://127.0.0.1:8080/realms/test/protocol/cas\",\n    cas_callback_uri = \"/cas_callback\",\n    logout_uri = \"/logout\",\n}\n\nfunction _M.get_default_opts()\n    return default_opts\nend\n\n-- Login keycloak and return the login original uri\nfunction _M.login_keycloak(uri, username, password)\n    local httpc = http.new()\n\n    local res, err = httpc:request_uri(uri, {method = \"GET\"})\n    if not res then\n        return nil, err\n    elseif res.status ~= 302 then\n        return nil, \"login was not redirected to keycloak.\"\n    else\n        local cookies = res.headers['Set-Cookie']\n        local cookie_str = _M.concatenate_cookies(cookies)\n\n        res, err = httpc:request_uri(res.headers['Location'], {method = \"GET\"})\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 200 then\n            -- Unexpected response.\n            return nil, res.body\n        end\n\n        -- From the returned form, extract the submit URI and parameters.\n        local uri, params = res.body:match('.*action=\"(.*)%?(.*)\" method=\"post\">')\n\n        -- Substitute escaped ampersand in parameters.\n        params = params:gsub(\"&amp;\", \"&\")\n\n        local auth_cookies = res.headers['Set-Cookie']\n\n        -- Concatenate cookies into one string as expected when sent in request header.\n        local auth_cookie_str = _M.concatenate_cookies(auth_cookies)\n\n        -- Invoke the submit URI with parameters and cookies, adding username\n        -- and password in the body.\n        -- Note: Username and password are specific to the Keycloak Docker image used.\n        res, err = httpc:request_uri(uri .. \"?\" .. params, {\n                method = \"POST\",\n                body = \"username=\" .. username .. \"&password=\" .. password,\n                headers = {\n                    [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                    [\"Cookie\"] = auth_cookie_str\n                }\n            })\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 302 then\n            -- Not a redirect which we expect.\n            return nil, \"Login form submission did not return redirect to redirect URI.\"\n        end\n\n        local keycloak_cookie_str = _M.concatenate_cookies(res.headers['Set-Cookie'])\n\n        -- login callback\n        local redirect_uri = res.headers['Location']\n        res, err = httpc:request_uri(redirect_uri, {\n            method = \"GET\",\n            headers = {\n                [\"Cookie\"] = cookie_str\n            }\n        })\n\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 302 then\n            -- Not a redirect which we expect.\n            return nil, \"login callback: \" ..\n                \"did not return redirect to original URI.\"\n        end\n\n        cookies = res.headers['Set-Cookie']\n        cookie_str = _M.concatenate_cookies(cookies)\n\n        return res, nil, cookie_str, keycloak_cookie_str\n    end\nend\n\n-- Login keycloak and return the login original uri\nfunction _M.login_keycloak_for_second_sp(uri, keycloak_cookie_str)\n    local httpc = http.new()\n\n    local res, err = httpc:request_uri(uri, {method = \"GET\"})\n    if not res then\n        return nil, err\n    elseif res.status ~= 302 then\n        return nil, \"login was not redirected to keycloak.\"\n    end\n\n    local cookies = res.headers['Set-Cookie']\n    local cookie_str = _M.concatenate_cookies(cookies)\n\n    res, err = httpc:request_uri(res.headers['Location'], {\n        method = \"GET\",\n        headers = {\n            [\"Cookie\"] = keycloak_cookie_str\n        }\n    })\n    ngx.log(ngx.INFO, keycloak_cookie_str)\n\n    if not res then\n        -- No response, must be an error.\n        return nil, err\n    elseif res.status ~= 302 then\n        -- Not a redirect which we expect.\n        return nil, res.body\n    end\n\n    -- login callback\n    res, err = httpc:request_uri(res.headers['Location'], {\n        method = \"GET\",\n        headers = {\n            [\"Cookie\"] = cookie_str\n        }\n    })\n\n    if not res then\n        -- No response, must be an error.\n        return nil, err\n    elseif res.status ~= 302 then\n        -- Not a redirect which we expect.\n        return nil, \"login callback: \" ..\n            \"did not return redirect to original URI.\"\n    end\n\n    cookies = res.headers['Set-Cookie']\n    cookie_str = _M.concatenate_cookies(cookies)\n\n    return res, nil, cookie_str\nend\n\nfunction _M.logout_keycloak(uri, cookie_str, keycloak_cookie_str)\n    local httpc = http.new()\n\n    local res, err = httpc:request_uri(uri, {\n        method = \"GET\",\n        headers = {\n            [\"Cookie\"] = cookie_str\n        }\n    })\n\n    if not res then\n        return nil, err\n    elseif res.status ~= 302 then\n        return nil, \"logout was not redirected to keycloak.\"\n    else\n        -- keycloak logout\n        res, err = httpc:request_uri(res.headers['Location'], {\n            method = \"GET\",\n            headers = {\n                [\"Cookie\"] = keycloak_cookie_str\n            }\n        })\n        if not res then\n            -- No response, must be an error.\n            return nil, err\n        elseif res.status ~= 200 then\n            return nil, \"Logout did not return 200.\"\n        end\n\n        return res, nil\n    end\nend\n\n-- Concatenate cookies into one string as expected when sent in request header.\nfunction _M.concatenate_cookies(cookies)\n    local cookie_str = \"\"\n    if type(cookies) == 'string' then\n        cookie_str = cookies:match('([^;]*); .*')\n    else\n        -- Must be a table.\n        local len = #cookies\n        if len > 0 then\n            cookie_str = cookies[1]:match('([^;]*); .*')\n            for i = 2, len do\n                cookie_str = cookie_str .. \"; \" .. cookies[i]:match('([^;]*); .*')\n            end\n        end\n    end\n\n    return cookie_str, nil\nend\n\nreturn _M\n"
  },
  {
    "path": "t/lib/mock_layer4.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal json_decode = require(\"toolkit.json\").decode\nlocal json_encode = require(\"toolkit.json\").encode\nlocal ngx = ngx\nlocal socket = ngx.req.socket\n\nlocal _M = {}\n\nfunction _M.dogstatsd()\n    local sock, err = socket()\n    if not sock then\n        core.log.error(\"failed to get the request socket: \", err)\n        return\n     end\n\n    while true do\n        local data, err = sock:receive()\n\n        if not data then\n            if err and err ~= \"no more data\" then\n                core.log.error(\"socket error, returning: \", err)\n            end\n            return\n        end\n        core.log.warn(\"message received: \", data)\n    end\nend\n\n\nfunction _M.loggly()\n    local sock, err = socket()\n    if not sock then\n        core.log.error(\"failed to get the request socket: \", err)\n        return\n     end\n\n    while true do\n        local data, err = sock:receive()\n\n        if not data then\n            if err and err ~= \"no more data\" then\n                core.log.error(\"socket error, returning: \", err)\n            end\n            return\n        end\n        local m, err = ngx.re.match(data, \"(^[ -~]*] )([ -~]*)\")\n        if not m then\n            core.log.error(\"unknown data received, failed to extract: \", err)\n            return\n        end\n        if #m ~= 2 then\n            core.log.error(\"failed to match two (header, log body) subgroups\", #m)\n        end\n        -- m[1] contains syslog header header <14>1 .... & m[2] contains actual log body\n        local logbody = json_decode(m[2])\n        -- order keys\n        logbody = json_encode(logbody)\n        core.log.warn(\"message received: \", m[1] .. logbody)\n    end\nend\n\nreturn _M\n"
  },
  {
    "path": "t/lib/pubsub.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ws_client = require \"resty.websocket.client\"\nlocal protoc    = require(\"protoc\")\nlocal pb        = require(\"pb\")\n\nlocal _M = {}\nlocal mt = { __index = _M }\n\n\nlocal pb_state\nlocal function load_proto()\n    pb.state(nil)\n    protoc.reload()\n    pb.option(\"int64_as_string\")\n    local pubsub_protoc = protoc.new()\n    pubsub_protoc:addpath(\"apisix/include/apisix/model\")\n    local ok, err = pcall(pubsub_protoc.loadfile, pubsub_protoc, \"pubsub.proto\")\n    if not ok then\n        ngx.log(ngx.ERR, \"failed to load protocol: \"..err)\n        return err\n    end\n    pb_state = pb.state(nil)\nend\n\n\nlocal function init_websocket_client(endpoint)\n    local ws, err = ws_client:new()\n    if not ws then\n        ngx.log(ngx.ERR, \"failed to create websocket client: \"..err)\n        return nil, err\n    end\n    local ok, err = ws:connect(endpoint)\n    if not ok then\n        ngx.log(ngx.ERR, \"failed to connect: \"..err)\n        return nil, err\n    end\n    return ws\nend\n\n\nfunction _M.new_ws(server)\n    local err = load_proto()\n    if err then\n        return nil, err\n    end\n    local ws, err = init_websocket_client(server)\n    if not ws then\n        return nil, err\n    end\n\n    local obj = setmetatable({\n        type = \"ws\",\n        ws_client = ws,\n    }, mt)\n\n    return obj\nend\n\n\nfunction _M.send_recv_ws_binary(self, data, is_raw)\n    pb.state(pb_state)\n    local ws = self.ws_client\n    if not is_raw then\n        data = pb.encode(\"PubSubReq\", data)\n    end\n    local _, err = ws:send_binary(data)\n    if err then\n        return nil, err\n    end\n    local raw_data, _, err = ws:recv_frame()\n    if not raw_data then\n        ngx.log(ngx.ERR, \"failed to receive the frame: \", err)\n        return nil, err\n    end\n    local data, err = pb.decode(\"PubSubResp\", raw_data)\n    if not data then\n        ngx.log(ngx.ERR, \"failed to decode the frame: \", err)\n        return nil, err\n    end\n\n    return data\nend\n\n\nfunction _M.send_recv_ws_text(self, text)\n    pb.state(pb_state)\n    local ws = self.ws_client\n    local _, err = ws:send_text(text)\n    if err then\n        return nil, err\n    end\n    local raw_data, _, err = ws:recv_frame()\n    if not raw_data then\n        ngx.log(ngx.ERR, \"failed to receive the frame: \", err)\n        return nil, err\n    end\n    local data, err = pb.decode(\"PubSubResp\", raw_data)\n    if not data then\n        ngx.log(ngx.ERR, \"failed to decode the frame: \", err)\n        return nil, err\n    end\n\n    return data\nend\n\n\nfunction _M.close_ws(self)\n    self.ws_client:send_close()\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/server.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal json_decode = require(\"toolkit.json\").decode\nlocal json_encode = require(\"toolkit.json\").encode\n\nlocal rsa_public_key = [[\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\n94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\nz3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\nsYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\noU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\nG70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\nzQIDAQAB\n-----END PUBLIC KEY-----]]\n\nlocal rsa_private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]]\n\nlocal _M = {}\n\n\nlocal function inject_headers()\n    local hdrs = ngx.req.get_headers()\n    for k, v in pairs(hdrs) do\n        if k:sub(1, 5) == \"resp-\" then\n            ngx.header[k:sub(6)] = v\n        end\n    end\nend\n\n\nfunction _M.hello()\n    ngx.req.read_body()\n    local s = \"hello world\"\n    ngx.header['Content-Length'] = #s + 1\n    ngx.say(s)\nend\n\n\nfunction _M.hello_chunked()\n    ngx.print(\"hell\")\n    ngx.flush(true)\n    ngx.print(\"o w\")\n    ngx.flush(true)\n    ngx.say(\"orld\")\nend\n\n\nfunction _M.hello1()\n    ngx.say(\"hello1 world\")\nend\n\n\n-- Fake endpoint, needed for testing authz-keycloak plugin.\nfunction _M.course_foo()\n    ngx.say(\"course foo\")\nend\n\n\nfunction _M.server_port()\n    ngx.print(ngx.var.server_port)\nend\n_M.server_port_route2 = _M.server_port\n_M.server_port_hello = _M.server_port\n_M.server_port_aa = _M.server_port\n\n\nfunction _M.limit_conn()\n    ngx.sleep(0.3)\n    ngx.say(\"hello world\")\nend\n\n\nfunction _M.plugin_proxy_rewrite()\n    ngx.say(\"uri: \", ngx.var.uri)\n    ngx.say(\"host: \", ngx.var.host)\n    ngx.say(\"scheme: \", ngx.var.scheme)\n    ngx.log(ngx.WARN, \"plugin_proxy_rewrite get method: \", ngx.req.get_method())\nend\n\n\nfunction _M.plugin_proxy_rewrite_args()\n    ngx.say(\"uri: \", ngx.var.uri)\n    local args = ngx.req.get_uri_args()\n\n    local keys = {}\n    for k, _ in pairs(args) do\n        table.insert(keys, k)\n    end\n    table.sort(keys)\n\n    for _, key in ipairs(keys) do\n        if type(args[key]) == \"table\" then\n            ngx.say(key, \": \", table.concat(args[key], ','))\n        else\n            ngx.say(key, \": \", args[key])\n        end\n    end\nend\n\n\nfunction _M.specific_status()\n    local status = ngx.var.http_x_test_upstream_status\n    if status ~= nil then\n        ngx.status = status\n        ngx.say(\"upstream status: \", status)\n    end\nend\n\n\nfunction _M.status()\n    ngx.log(ngx.WARN, \"client request host: \", ngx.var.http_host)\n    ngx.say(\"ok\")\nend\n\n\nfunction _M.ewma()\n    if ngx.var.server_port == \"1981\"\n       or ngx.var.server_port == \"1982\" then\n        ngx.sleep(0.2)\n    else\n        ngx.sleep(0.1)\n    end\n    ngx.print(ngx.var.server_port)\nend\n\n\nlocal builtin_hdr_ignore_list = {\n    [\"x-forwarded-for\"] = true,\n    [\"x-forwarded-proto\"] = true,\n    [\"x-forwarded-host\"] = true,\n    [\"x-forwarded-port\"] = true,\n}\n\nfunction _M.uri()\n    ngx.say(\"uri: \", ngx.var.uri)\n    local headers = ngx.req.get_headers()\n\n    local keys = {}\n    for k in pairs(headers) do\n        if not builtin_hdr_ignore_list[k] then\n            table.insert(keys, k)\n        end\n    end\n    table.sort(keys)\n\n    for _, key in ipairs(keys) do\n        ngx.say(key, \": \", headers[key])\n    end\nend\n_M.uri_plugin_proxy_rewrite = _M.uri\n_M.uri_plugin_proxy_rewrite_args = _M.uri\n\n\nfunction _M.old_uri()\n    ngx.say(\"uri: \", ngx.var.uri)\n    local headers = ngx.req.get_headers()\n\n    local keys = {}\n    for k in pairs(headers) do\n        table.insert(keys, k)\n    end\n    table.sort(keys)\n\n    for _, key in ipairs(keys) do\n        ngx.say(key, \": \", headers[key])\n    end\nend\n\n\nfunction _M.opentracing()\n    ngx.say(\"opentracing\")\nend\n\n\nfunction _M.with_header()\n    --split into multiple chunk\n    ngx.say(\"hello\")\n    ngx.say(\"world\")\n    ngx.say(\"!\")\nend\n\n\nfunction _M.mock_zipkin()\n    ngx.req.read_body()\n    local data = ngx.req.get_body_data()\n    ngx.log(ngx.NOTICE, data)\n\n    local spans = json_decode(data)\n    local ver = ngx.req.get_uri_args()['span_version']\n    if ver == \"1\" then\n        if #spans ~= 5 then\n            ngx.log(ngx.ERR, \"wrong number of spans: \", #spans)\n            ngx.exit(400)\n        end\n    else\n        if #spans ~= 3 then\n            -- request/proxy/response\n            ngx.log(ngx.ERR, \"wrong number of spans: \", #spans)\n            ngx.exit(400)\n        end\n    end\n\n    for _, span in pairs(spans) do\n        local prefix = string.sub(span.name, 1, 6)\n        if prefix ~= 'apisix' then\n            ngx.log(ngx.ERR, \"wrong prefix of name\", prefix)\n            ngx.exit(400)\n        end\n        if not span.traceId then\n            ngx.log(ngx.ERR, \"missing trace id\")\n            ngx.exit(400)\n        end\n\n        if not span.localEndpoint then\n            ngx.log(ngx.ERR, \"missing local endpoint\")\n            ngx.exit(400)\n        end\n\n        if span.localEndpoint.serviceName ~= 'APISIX'\n          and span.localEndpoint.serviceName ~= 'apisix' then\n            ngx.log(ngx.ERR, \"wrong serviceName: \", span.localEndpoint.serviceName)\n            ngx.exit(400)\n        end\n\n        if span.localEndpoint.port ~= 1984 then\n            ngx.log(ngx.ERR, \"wrong port: \", span.localEndpoint.port)\n            ngx.exit(400)\n        end\n\n        local server_addr = ngx.req.get_uri_args()['server_addr']\n        if server_addr then\n            if span.localEndpoint.ipv4 ~= server_addr then\n                ngx.log(ngx.ERR, \"server_addr mismatched\")\n                ngx.exit(400)\n            end\n        end\n\n    end\nend\n\n\nfunction _M.wolf_rbac_login_rest()\n    ngx.req.read_body()\n    local data = ngx.req.get_body_data()\n    local args = json_decode(data)\n    if not args.username then\n        ngx.say(json_encode({ok=false, reason=\"ERR_USERNAME_MISSING\"}))\n        ngx.exit(0)\n    end\n    if not args.password then\n        ngx.say(json_encode({ok=false, reason=\"ERR_PASSWORD_MISSING\"}))\n        ngx.exit(0)\n    end\n    if args.username ~= \"admin\" then\n        ngx.say(json_encode({ok=false, reason=\"ERR_USER_NOT_FOUND\"}))\n        ngx.exit(0)\n    end\n    if args.password ~= \"123456\" then\n        ngx.say(json_encode({ok=false, reason=\"ERR_PASSWORD_ERROR\"}))\n        ngx.exit(0)\n    end\n\n    ngx.say(json_encode({ok=true, data={token=\"wolf-rbac-token\",\n        userInfo={nickname=\"administrator\",username=\"admin\", id=\"100\"}}}))\nend\n\n\nfunction _M.wolf_rbac_access_check()\n    local headers = ngx.req.get_headers()\n    local token = headers['x-rbac-token']\n    if token ~= 'wolf-rbac-token' then\n        ngx.say(json_encode({ok=false, reason=\"ERR_TOKEN_INVALID\"}))\n        ngx.exit(0)\n    end\n\n    local args = ngx.req.get_uri_args()\n    local resName = args.resName\n    if resName == '/hello' or resName == '/wolf/rbac/custom/headers' then\n        ngx.say(json_encode({ok=true,\n                            data={ userInfo={nickname=\"administrator\",\n                                username=\"admin\", id=\"100\"} }}))\n    elseif resName == '/hello/500' then\n        ngx.status = 500\n        ngx.say(json_encode({ok=false, reason=\"ERR_SERVER_ERROR\"}))\n    elseif resName == '/hello/401' then\n        ngx.status = 401\n        ngx.say(json_encode({ok=false, reason=\"ERR_TOKEN_INVALID\"}))\n    else\n        ngx.status = 403\n        ngx.say(json_encode({ok=false, reason=\"ERR_ACCESS_DENIED\"}))\n    end\nend\n\n\nfunction _M.wolf_rbac_user_info()\n    local headers = ngx.req.get_headers()\n    local token = headers['x-rbac-token']\n    if token ~= 'wolf-rbac-token' then\n        ngx.say(json_encode({ok=false, reason=\"ERR_TOKEN_INVALID\"}))\n        ngx.exit(0)\n    end\n\n    ngx.say(json_encode({ok=true,\n                        data={ userInfo={nickname=\"administrator\", username=\"admin\", id=\"100\"} }}))\nend\n\n\nfunction _M.wolf_rbac_change_pwd()\n    ngx.req.read_body()\n    local data = ngx.req.get_body_data()\n    local args = json_decode(data)\n    if args.oldPassword ~= \"123456\" then\n        ngx.say(json_encode({ok=false, reason=\"ERR_OLD_PASSWORD_INCORRECT\"}))\n        ngx.exit(0)\n    end\n\n    ngx.say(json_encode({ok=true, data={ }}))\nend\n\n\nfunction _M.wolf_rbac_custom_headers()\n    local headers = ngx.req.get_headers()\n    ngx.say('id:' .. headers['X-UserId'] .. ',username:' .. headers['X-Username']\n            .. ',nickname:' .. headers['X-Nickname'])\nend\n\n\nfunction _M.websocket_handshake()\n    local websocket = require \"resty.websocket.server\"\n    local wb, err = websocket:new()\n    if not wb then\n        ngx.log(ngx.ERR, \"failed to new websocket: \", err)\n        return ngx.exit(400)\n    end\n\n    local bytes, err = wb:send_text(\"hello\")\n    if not bytes then\n        ngx.log(ngx.ERR, \"failed to send text: \", err)\n        return ngx.exit(444)\n    end\nend\n_M.websocket_handshake_route = _M.websocket_handshake\n\n\nfunction _M.api_breaker()\n    ngx.exit(tonumber(ngx.var.arg_code))\nend\n\n\nfunction _M.mysleep()\n    ngx.sleep(tonumber(ngx.var.arg_seconds))\n    if ngx.var.arg_abort then\n        ngx.exit(ngx.ERROR)\n    else\n        ngx.say(ngx.var.arg_seconds)\n    end\nend\n\n\nlocal function print_uri()\n    ngx.say(ngx.var.uri)\nend\nfor i = 1, 100 do\n    _M[\"print_uri_\" .. i] = print_uri\nend\n\nfunction _M.print_uri_detailed()\n    ngx.say(\"ngx.var.uri: \", ngx.var.uri)\n    ngx.say(\"ngx.var.request_uri: \", ngx.var.request_uri)\nend\n\nfunction _M.headers()\n    local args = ngx.req.get_uri_args()\n    for name, val in pairs(args) do\n        ngx.header[name] = nil\n        ngx.header[name] = val\n    end\n\n    ngx.say(\"/headers\")\nend\n\n\nfunction _M.echo()\n    ngx.req.read_body()\n    local hdrs = ngx.req.get_headers()\n    for k, v in pairs(hdrs) do\n        ngx.header[k] = v\n    end\n    ngx.print(ngx.req.get_body_data() or \"\")\nend\n\n\nfunction _M.log()\n    ngx.req.read_body()\n    local body = ngx.req.get_body_data()\n    local ct = ngx.var.content_type\n    if ct ~= \"text/plain\" then\n        body = json_decode(body)\n        body = json_encode(body)\n    end\n    ngx.log(ngx.WARN, \"request log: \", body or \"nil\")\nend\n\n\nfunction _M.server_error()\n    error(\"500 Internal Server Error\")\nend\n\n\nfunction _M.log_request()\n    ngx.log(ngx.WARN, \"uri: \", ngx.var.uri)\n    local headers = ngx.req.get_headers()\n\n    local keys = {}\n    for k in pairs(headers) do\n        table.insert(keys, k)\n    end\n    table.sort(keys)\n\n    for _, key in ipairs(keys) do\n        ngx.log(ngx.WARN, key, \": \", headers[key])\n    end\nend\n\n\nfunction _M.v3_auth_authenticate()\n    ngx.log(ngx.WARN, \"etcd auth failed!\")\nend\n\n\nfunction _M._well_known_openid_configuration()\n    local t = require(\"lib.test_admin\")\n    local openid_data = t.read_file(\"t/plugin/openid-connect/configuration.json\")\n    ngx.say(openid_data)\nend\n\nfunction _M.google_logging_token()\n    local args = ngx.req.get_uri_args()\n    local args_token_type = args.token_type or \"Bearer\"\n    ngx.req.read_body()\n    local data = ngx.decode_args(ngx.req.get_body_data())\n    local jwt = require(\"resty.jwt\")\n    local access_scopes = \"https://apisix.apache.org/logs:admin\"\n    local verify = jwt:verify(rsa_public_key, data[\"assertion\"])\n    if not verify.verified then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"identity authentication failed\" }))\n        return\n    end\n\n    local scopes_valid = type(verify.payload.scope) == \"string\" and\n            verify.payload.scope:find(access_scopes)\n    if not scopes_valid then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"no access to this scopes\" }))\n        return\n    end\n\n    local expire_time = (verify.payload.exp or ngx.time()) - ngx.time()\n    if expire_time <= 0 then\n        expire_time = 0\n    end\n\n    local jwt_token = jwt:sign(rsa_private_key, {\n        header = { typ = \"JWT\", alg = \"RS256\" },\n        payload = { exp = verify.payload.exp, scope = access_scopes }\n    })\n\n    ngx.say(json_encode({\n        access_token = jwt_token,\n        expires_in = expire_time,\n        token_type = args_token_type\n    }))\nend\n\nfunction _M.google_logging_entries()\n    local args = ngx.req.get_uri_args()\n    local args_token_type = args.token_type or \"Bearer\"\n    ngx.req.read_body()\n    local data = ngx.req.get_body_data()\n    local jwt = require(\"resty.jwt\")\n    local access_scopes = \"https://apisix.apache.org/logs:admin\"\n\n    local headers = ngx.req.get_headers()\n    local token = headers[\"Authorization\"]\n    if not token then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"authentication header not exists\" }))\n        return\n    end\n\n    token = string.sub(token, #args_token_type + 2)\n    local verify = jwt:verify(rsa_public_key, token)\n    if not verify.verified then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"identity authentication failed\" }))\n        return\n    end\n\n    local scopes_valid = type(verify.payload.scope) == \"string\" and\n            verify.payload.scope:find(access_scopes)\n    if not scopes_valid then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"no access to this scopes\" }))\n        return\n    end\n\n    local expire_time = (verify.payload.exp or ngx.time()) - ngx.time()\n    if expire_time <= 0 then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"token has expired\" }))\n        return\n    end\n\n    ngx.say(data)\nend\n\nfunction _M.google_secret_token()\n    local args = ngx.req.get_uri_args()\n    local args_token_type = args.token_type or \"Bearer\"\n    ngx.req.read_body()\n    local data = ngx.decode_args(ngx.req.get_body_data())\n    local jwt = require(\"resty.jwt\")\n    local access_scopes = \"https://www.googleapis.com/auth/cloud\"\n    local verify = jwt:verify(rsa_public_key, data[\"assertion\"])\n    if not verify.verified then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"identity authentication failed\" }))\n        return\n    end\n\n    local scopes_valid = type(verify.payload.scope) == \"string\" and\n            verify.payload.scope:find(access_scopes)\n    if not scopes_valid then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"no access to this scope\" }))\n        return\n    end\n\n    local expire_time = (verify.payload.exp or ngx.time()) - ngx.time()\n    if expire_time <= 0 then\n        expire_time = 0\n    end\n\n    local jwt_token = jwt:sign(rsa_private_key, {\n        header = { typ = \"JWT\", alg = \"RS256\" },\n        payload = { exp = verify.payload.exp, scope = access_scopes }\n    })\n\n    ngx.say(json_encode({\n        access_token = jwt_token,\n        expires_in = expire_time,\n        token_type = args_token_type\n    }))\nend\n\nfunction _M.google_secret_apisix_jack()\n    local args = ngx.req.get_uri_args()\n    local args_token_type = args.token_type or \"Bearer\"\n    local jwt = require(\"resty.jwt\")\n    local access_scopes = \"https://www.googleapis.com/auth/cloud\"\n\n    local headers = ngx.req.get_headers()\n    local token = headers[\"Authorization\"]\n    if not token then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"authentication header not exists\" }))\n        return\n    end\n\n    token = string.sub(token, #args_token_type + 2)\n    local verify = jwt:verify(rsa_public_key, token)\n    if not verify.verified then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"identity authentication failed\" }))\n        return\n    end\n\n    local scopes_valid = type(verify.payload.scope) == \"string\" and\n            verify.payload.scope:find(access_scopes)\n    if not scopes_valid then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"no access to this scope\" }))\n        return\n    end\n\n    local expire_time = (verify.payload.exp or ngx.time()) - ngx.time()\n    if expire_time <= 0 then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"token has expired\" }))\n        return\n    end\n\n    local response = {\n        name = \"projects/647037004838/secrets/apisix/versions/1\",\n        payload = {\n          data = \"eyJrZXkiOiJ2YWx1ZSJ9\",\n          dataCrc32c = \"2296192492\"\n        }\n      }\n\n    ngx.status = 200\n    ngx.say(json_encode(response))\nend\n\nfunction _M.google_secret_apisix_error_jack()\n    local args = ngx.req.get_uri_args()\n    local args_token_type = args.token_type or \"Bearer\"\n    local jwt = require(\"resty.jwt\")\n    local access_scopes = \"https://www.googleapis.com/auth/root/cloud\"\n\n    local headers = ngx.req.get_headers()\n    local token = headers[\"Authorization\"]\n    if not token then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"authentication header not exists\" }))\n        return\n    end\n\n    token = string.sub(token, #args_token_type + 2)\n    local verify = jwt:verify(rsa_public_key, token)\n    if not verify.verified then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"identity authentication failed\" }))\n        return\n    end\n\n    local scopes_valid = type(verify.payload.scope) == \"string\" and\n            verify.payload.scope:find(access_scopes)\n    if not scopes_valid then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"no access to this scope\" }))\n        return\n    end\n\n    local expire_time = (verify.payload.exp or ngx.time()) - ngx.time()\n    if expire_time <= 0 then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"token has expired\" }))\n        return\n    end\n\n    local response = {\n        name = \"projects/647037004838/secrets/apisix_error/versions/1\",\n        payload = {\n          data = \"eyJrZXkiOiJ2YWx1ZSJ9\",\n          dataCrc32c = \"2296192492\"\n        }\n      }\n\n    ngx.status = 200\n    ngx.say(json_encode(response))\nend\n\nfunction _M.google_secret_apisix_mysql()\n    local args = ngx.req.get_uri_args()\n    local args_token_type = args.token_type or \"Bearer\"\n    local jwt = require(\"resty.jwt\")\n    local access_scopes = \"https://www.googleapis.com/auth/cloud\"\n\n    local headers = ngx.req.get_headers()\n    local token = headers[\"Authorization\"]\n    if not token then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"authentication header not exists\" }))\n        return\n    end\n\n    token = string.sub(token, #args_token_type + 2)\n    local verify = jwt:verify(rsa_public_key, token)\n    if not verify.verified then\n        ngx.status = 401\n        ngx.say(json_encode({ error = \"identity authentication failed\" }))\n        return\n    end\n\n    local scopes_valid = type(verify.payload.scope) == \"string\" and\n            verify.payload.scope:find(access_scopes)\n    if not scopes_valid then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"no access to this scope\" }))\n        return\n    end\n\n    local expire_time = (verify.payload.exp or ngx.time()) - ngx.time()\n    if expire_time <= 0 then\n        ngx.status = 403\n        ngx.say(json_encode({ error = \"token has expired\" }))\n        return\n    end\n\n    local response = {\n        name = \"projects/647037004838/secrets/apisix/versions/1\",\n        payload = {\n          data = \"c2VjcmV0\",\n          dataCrc32c = \"0xB03C4D4D\"\n        }\n      }\n\n    ngx.status = 200\n    ngx.say(json_encode(response))\nend\n\nfunction _M.plugin_proxy_rewrite_resp_header()\n    ngx.req.read_body()\n    local s = \"plugin_proxy_rewrite_resp_header\"\n    ngx.header['Content-Length'] = #s + 1\n    ngx.say(s)\nend\n\n-- Please add your fake upstream above\nfunction _M.go()\n    local action = string.sub(ngx.var.uri, 2)\n    action = string.gsub(action, \"[/\\\\.-]\", \"_\")\n    if not action or not _M[action] then\n        ngx.log(ngx.WARN, \"undefined path in test server, uri: \", ngx.var.request_uri)\n        return ngx.exit(404)\n    end\n\n    inject_headers()\n    return _M[action]()\nend\n\n\nfunction _M.clickhouse_logger_server()\n    ngx.req.read_body()\n    local data = ngx.req.get_body_data()\n    local headers = ngx.req.get_headers()\n    ngx.log(ngx.WARN, \"clickhouse body: \", data)\n    for k, v in pairs(headers) do\n        ngx.log(ngx.WARN, \"clickhouse headers: \" .. k .. \":\" .. v)\n    end\n    ngx.say(\"ok\")\nend\n\n\nfunction _M.mock_compressed_upstream_response()\n    local s = \"compressed_response\"\n    ngx.header['Content-Encoding'] = 'gzip'\n    ngx.say(s)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/test_admin.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal http              = require(\"resty.http\")\nlocal json              = require(\"toolkit.json\")\nlocal core              = require(\"apisix.core\")\nlocal aes               = require \"resty.aes\"\nlocal ngx_encode_base64 = ngx.encode_base64\nlocal str_find          = core.string.find\nlocal dir_names         = {}\n\n\nlocal _M = {}\n\n\nlocal function com_tab(pattern, data, deep)\n    deep = deep or 1\n\n    for k, v in pairs(pattern) do\n        dir_names[deep] = k\n\n        if v == ngx.null then\n            v = nil\n        end\n\n        if type(v) == \"table\" and data[k] then\n            local ok, err = com_tab(v, data[k], deep + 1)\n            if not ok then\n                return false, err\n            end\n\n        elseif v ~= data[k] then\n            return false, \"path: \" .. table.concat(dir_names, \"->\", 1, deep)\n                          .. \" expect: \" .. tostring(v) .. \" got: \"\n                          .. tostring(data[k])\n        end\n    end\n\n    return true\nend\n\n\nlocal methods = {\n    [ngx.HTTP_GET    ] = \"GET\",\n    [ngx.HTTP_HEAD   ] = \"HEAD\",\n    [ngx.HTTP_PUT    ] = \"PUT\",\n    [ngx.HTTP_POST   ] = \"POST\",\n    [ngx.HTTP_DELETE ] = \"DELETE\",\n    [ngx.HTTP_OPTIONS] = \"OPTIONS\",\n    [ngx.HTTP_PATCH]   = \"PATCH\",\n    [ngx.HTTP_TRACE] = \"TRACE\",\n}\n\n\nfunction _M.test_ipv6(uri)\n    local sock = ngx.socket.tcp()\n    local ok, err = sock:connect(\"[::1]\", 1984)\n    if not ok then\n        ngx.say(\"failed to connect: \", err)\n        return\n    end\n\n    ngx.say(\"connected: \", ok)\n\n    local req = \"GET \" .. uri .. \" HTTP/1.0\\r\\nHost: localhost\\r\\n\"\n                .. \"Connection: close\\r\\n\\r\\n\"\n    -- req = \"OK\"\n    -- ngx.log(ngx.WARN, \"req: \", req)\n\n    local bytes, err = sock:send(req)\n    if not bytes then\n        ngx.say(\"failed to send request: \", err)\n        return\n    end\n\n    ngx.say(\"request sent: \", bytes)\n\n    while true do\n        local line, err, part = sock:receive()\n        if line then\n            if line ~= \"\" then\n                ngx.say(\"received: \", line)\n            else\n                ngx.say(\"received:\")\n            end\n\n        else\n            ngx.say(\"failed to receive a line: \", err, \" [\", part, \"]\")\n            break\n        end\n    end\n\n    ok, err = sock:close()\n    ngx.say(\"close: \", ok, \" \", err)\nend\n\n\nfunction _M.comp_tab(left_tab, right_tab)\n    local err\n    dir_names = {}\n\n    local _\n    if type(left_tab) == \"string\" then\n        left_tab, _, err = json.decode(left_tab)\n        if not left_tab then\n            return false, \"failed to decode expected data: \" .. err\n        end\n    end\n    if type(right_tab) == \"string\" then\n        right_tab, _, err  = json.decode(right_tab)\n        if not right_tab then\n            return false, \"failed to decode expected data: \" .. err\n        end\n    end\n\n    local ok, err = com_tab(left_tab, right_tab)\n    if not ok then\n        return false, err\n    end\n\n    return true\nend\n\n\nlocal function set_yaml(fn, data)\n    local profile = os.getenv(\"APISIX_PROFILE\")\n    if profile then\n        fn = fn .. \"-\" .. profile .. \".yaml\"\n    else\n        fn = fn .. \".yaml\"\n    end\n\n    local f = assert(io.open(os.getenv(\"TEST_NGINX_HTML_DIR\") .. \"/../conf/\" .. fn, 'w'))\n    assert(f:write(data))\n    f:close()\nend\n\n\nfunction _M.set_config_yaml(data)\n    set_yaml(\"config\", data)\nend\n\n\nfunction _M.set_apisix_yaml(data)\n    set_yaml(\"apisix\", data)\nend\n\n\nfunction _M.test(uri, method, body, pattern, headers)\n    if not headers then\n        headers = {}\n    end\n    if not headers[\"Content-Type\"] then\n        headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n    end\n\n    if type(body) == \"table\" then\n        -- {} will be encoded as '[]' whether decode_array_with_array_mt or not\n        body = json.encode(body)\n    end\n\n    if type(pattern) == \"table\" then\n        pattern = json.encode(pattern)\n    end\n\n    if type(method) == \"number\" then\n        method = methods[method]\n    end\n\n    local httpc = http.new()\n    -- https://github.com/ledgetech/lua-resty-http\n    uri = ngx.var.scheme .. \"://\" .. ngx.var.server_addr\n          .. \":\" .. ngx.var.server_port .. uri\n    local res, err = httpc:request_uri(uri,\n        {\n            method = method,\n            body = body,\n            keepalive = false,\n            headers = headers,\n        }\n    )\n    if not res then\n        ngx.log(ngx.ERR, \"failed http: \", err)\n        return nil, err\n    end\n\n    if res.status >= 300 then\n        return res.status, res.body, res.headers\n    end\n\n    if pattern == nil then\n        return res.status, \"passed\", res.body, res.headers\n    end\n\n    local res_data = json.decode(res.body)\n    local ok, err = _M.comp_tab(pattern, res_data)\n    if not ok then\n        return 500, \"failed, \" .. err, res_data\n    end\n\n    return 200, \"passed\", res_data, res.headers\nend\n\n\nfunction _M.read_file(path)\n    local f = assert(io.open(path, \"rb\"))\n    local cert = f:read(\"*all\")\n    f:close()\n    return cert\nend\n\n\nfunction _M.req_self_with_http(uri, method, body, headers)\n    if type(body) == \"table\" then\n        body = json.encode(body)\n    end\n\n    if type(method) == \"number\" then\n        method = methods[method]\n    end\n    headers = headers or {}\n\n    local httpc = http.new()\n    -- https://github.com/ledgetech/lua-resty-http\n    uri = ngx.var.scheme .. \"://\" .. ngx.var.server_addr\n          .. \":\" .. ngx.var.server_port .. uri\n    headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n    local res, err = httpc:request_uri(uri,\n        {\n            method = method,\n            body = body,\n            keepalive = false,\n            headers = headers,\n        }\n    )\n\n    return res, err\nend\n\n\nfunction _M.aes_encrypt(origin)\n    local iv = \"1234567890123456\"\n    local aes_128_cbc_with_iv = assert(aes:new(iv, nil, aes.cipher(128, \"cbc\"), {iv=iv}))\n\n    if aes_128_cbc_with_iv ~= nil and str_find(origin, \"---\") then\n        local encrypted = aes_128_cbc_with_iv:encrypt(origin)\n        if encrypted == nil then\n            core.log.error(\"failed to encrypt key[\", origin, \"] \")\n            return origin\n        end\n\n        return ngx_encode_base64(encrypted)\n    end\n\n    return origin\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/test_inspect.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n--\n-- Don't edit existing code, because the hooks are identified by line number.\n-- Instead, append new code to this file.\n--\nlocal _M = {}\n\nfunction _M.run1()\n    local var1 = \"hello\"\n    local var2 = \"world\"\n    return var1 .. var2\nend\n\nlocal upvar1 = 2\nlocal upvar2 = \"yes\"\nfunction _M.run2()\n    return upvar1\nend\n\nfunction _M.run3()\n    return upvar1 .. upvar2\nend\n\nlocal str = string.rep(\"a\", 8192) .. \"llzz\"\n\nlocal sk = require(\"socket\")\n\nfunction _M.hot1()\n    local t1 = sk.gettime()\n    for i=1,100000 do\n        string.find(str, \"ll\", 1, true)\n    end\n    local t2 = sk.gettime()\n    return t2 - t1\nend\n\nfunction _M.hot2()\n    local t1 = sk.gettime()\n    for i=1,100000 do\n        string.find(str, \"ll\", 1, true)\n    end\n    local t2 = sk.gettime()\n    return t2 - t1\nend\n\nreturn _M\n"
  },
  {
    "path": "t/lib/test_otel.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal cjson = require(\"cjson\")\n\nlocal _M = {}\n\n\n-- Parse a data-otlp.json file (one JSON object per line) into a spans_by_id table.\nlocal function parse_spans(filepath)\n    local file = io.open(filepath, \"rb\")\n    if not file then\n        return nil, \"cannot open \" .. filepath\n    end\n\n    local spans_by_id = {}\n    for line in file:lines() do\n        if line and #line > 0 then\n            local ok, data = pcall(cjson.decode, line)\n            if ok and data.resourceSpans then\n                for _, rs in ipairs(data.resourceSpans) do\n                    for _, ss in ipairs(rs.scopeSpans or {}) do\n                        for _, span in ipairs(ss.spans or {}) do\n                            spans_by_id[span.spanId] = span\n                        end\n                    end\n                end\n            end\n        end\n    end\n    file:close()\n\n    return spans_by_id\nend\n\n\n-- Find a child span of the given parent by name.\nlocal function find_child(spans_by_id, parent_id, child_name)\n    for _, span in pairs(spans_by_id) do\n        if span.parentSpanId == parent_id and span.name == child_name then\n            return span\n        end\n    end\n    return nil\nend\n\n\n-- Convert span.attributes array into a key -> value map.\nlocal function get_attr_map(span)\n    local map = {}\n    for _, attr in ipairs(span.attributes or {}) do\n        local v = attr.value\n        map[attr.key] = v.stringValue or v.intValue or v.boolValue\n    end\n    return map\nend\n\n\n-- Recursively verify a span tree node against the expected structure.\nlocal function verify(spans_by_id, expected, actual, path, errors)\n    if not actual then\n        table.insert(errors, path .. \": span not found\")\n        return\n    end\n\n    if expected.kind and actual.kind ~= expected.kind then\n        table.insert(errors, string.format(\n            \"%s: expected kind=%d, got=%s\",\n            path, expected.kind, tostring(actual.kind)))\n    end\n\n    if expected.attributes then\n        local attr_map = get_attr_map(actual)\n        for key, val in pairs(expected.attributes) do\n            if tostring(attr_map[key]) ~= tostring(val) then\n                table.insert(errors, string.format(\n                    \"%s: attr '%s' expected '%s', got '%s'\",\n                    path, key, tostring(val), tostring(attr_map[key])))\n            end\n        end\n    end\n\n    if expected.children then\n        for _, child_exp in ipairs(expected.children) do\n            local child = find_child(spans_by_id, actual.spanId, child_exp.name)\n            verify(spans_by_id, child_exp, child,\n                   path .. \" > \" .. child_exp.name, errors)\n        end\n    end\nend\n\n\n-- Main entry point: verify a span tree from a data-otlp.json file.\n-- Returns true on success, or (false, error_string) on failure.\nfunction _M.verify_tree(filepath, expected_tree)\n    local spans_by_id, err = parse_spans(filepath)\n    if not spans_by_id then\n        return false, err\n    end\n\n    -- find root span (no parentSpanId)\n    local root\n    for _, span in pairs(spans_by_id) do\n        if span.name == expected_tree.name\n           and (not span.parentSpanId or span.parentSpanId == \"\")\n        then\n            root = span\n            break\n        end\n    end\n\n    local errors = {}\n    verify(spans_by_id, expected_tree, root, expected_tree.name, errors)\n\n    if #errors > 0 then\n        return false, table.concat(errors, \"\\n\")\n    end\n    return true\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/lib/test_redis.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\nlocal ngx = require(\"ngx\")\nlocal redis = require \"resty.redis\"\n\nlocal ipairs = ipairs\nlocal tonumber = tonumber\n\nlocal _M = {}\n\nlocal DEFAULT_PORTS = {6379, 5000, 5001, 5002, 5003, 5004, 5005, 5006}\nlocal DEFAULT_HOST = \"127.0.0.1\"\n\nlocal function log_warn(...)\n    if ngx then\n        ngx.log(ngx.WARN, ...)\n    end\nend\n\nlocal function add_port(target, visited, port)\n    port = tonumber(port)\n    if port and not visited[port] then\n        visited[port] = true\n        target[#target + 1] = port\n    end\nend\n\nlocal function auth_if_needed(red, opts)\n    opts = opts or {}\n    local username = opts.username\n    local password = opts.password\n    if not password or password == \"\" then\n        return true\n    end\n\n    local ok, err\n    if username and username ~= \"\" then\n        ok, err = red:auth(username, password)\n    else\n        ok, err = red:auth(password)\n    end\n\n    if ok then\n        return true\n    end\n\n    if err and (err:find(\"no password is set\", 1, true)\n        or err:find(\"without any password configured\", 1, true)) then\n        return true\n    end\n\n    log_warn(\"failed to auth redis: \", err)\n    return nil, err\nend\n\nlocal function flush_single(host, port, opts)\n    local red = redis:new()\n    local connect_timeout = opts.connect_timeout or 1000\n    local send_timeout = opts.send_timeout or connect_timeout\n    local read_timeout = opts.read_timeout or connect_timeout\n    red:set_timeouts(connect_timeout, send_timeout, read_timeout)\n\n    local ok, err = red:connect(host, port)\n    if not ok then\n        log_warn(\"failed to connect to redis \", host, \":\", port, \": \", err)\n        return nil, err\n    end\n\n    local ok_auth, auth_err = auth_if_needed(red, opts)\n    if not ok_auth then\n        local ok_close, close_err = red:close()\n        if not ok_close then\n            log_warn(\"failed to close redis connection \", host, \":\", port, \": \", close_err)\n        end\n        return nil, auth_err\n    end\n\n    local _, flush_err = red:flushall()\n    if flush_err then\n        log_warn(\"failed to flush redis \", host, \":\", port, \": \", flush_err)\n    end\n\n    local keepalive_pool = opts.keepalive_pool\n    if keepalive_pool == nil then\n        keepalive_pool = 0\n    end\n    if keepalive_pool == 0 then\n        local ok_close, close_err = red:close()\n        if not ok_close then\n            log_warn(\"failed to close redis connection \", host, \":\", port, \": \", close_err)\n        end\n    else\n        local keepalive_timeout = opts.keepalive_timeout or 10000\n        keepalive_pool = keepalive_pool or 100\n        local ok_keepalive, keepalive_err = red:set_keepalive(keepalive_timeout, keepalive_pool)\n        if not ok_keepalive then\n            log_warn(\"failed to set keepalive for redis \", host, \":\", port, \": \", keepalive_err)\n        end\n    end\n\n    return true\nend\n\nfunction _M.flush_all(opts)\n    opts = opts or {}\n    local host = opts.host or DEFAULT_HOST\n\n    local visited = {}\n    local ports = {}\n\n    local source_ports = opts.ports or DEFAULT_PORTS\n    for _, port in ipairs(source_ports) do\n        add_port(ports, visited, port)\n    end\n\n    add_port(ports, visited, os.getenv(\"TEST_NGINX_REDIS_PORT\"))\n\n    if opts.extra_ports then\n        for _, port in ipairs(opts.extra_ports) do\n            add_port(ports, visited, port)\n        end\n    end\n\n    for _, port in ipairs(ports) do\n        flush_single(host, port, opts)\n    end\nend\n\nfunction _M.flush_port(host, port, opts)\n    if type(host) == \"table\" then\n        opts = host\n        host = opts.host or DEFAULT_HOST\n        port = opts.port\n    end\n\n    opts = opts or {}\n    host = host or opts.host or DEFAULT_HOST\n    port = port or opts.port\n    if not port then\n        return nil, \"port is required\"\n    end\n\n    return flush_single(host, port, opts)\nend\n\n_M.default_ports = DEFAULT_PORTS\n\nreturn _M\n"
  },
  {
    "path": "t/misc/patch.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: flatten send args\n--- extra_init_by_lua\nlocal sock = ngx.socket.tcp()\ngetmetatable(sock.sock).__index.send = function (_, data)\n    ngx.log(ngx.WARN, data)\n    return #data\nend\nsock:send({1, \"a\", {1, \"b\", true}})\nsock:send(1, \"a\", {1, \"b\", false})\n--- config\n    location /t {\n        return 200;\n    }\n--- grep_error_log eval\nqr/send\\(\\): \\S+/\n--- grep_error_log_out\nsend(): 1a1btrue\nsend(): 1a1bfalse\n\n\n\n=== TEST 2: sslhandshake options\n--- extra_init_by_lua\nlocal sock = ngx.socket.tcp()\nsock:settimeout(1)\nlocal ok, err = sock:connect(\"0.0.0.0\", 12379)\nif not ok then\n    ngx.log(ngx.ERR, \"failed to connect: \", err)\n    return\nend\n\nlocal sess, err = sock:sslhandshake(true, \"test.com\", true, true)\nif not sess then\n    ngx.log(ngx.ERR, \"failed to do SSL handshake: \", err)\nend\n\nlocal sock = ngx.socket.tcp()\nlocal ok, err = sock:connect(\"0.0.0.0\", 12379)\nif not ok then\n    ngx.log(ngx.ERR, \"failed to connect: \", err)\n    return\nend\nlocal sess, err = sock:sslhandshake(true, \"test.com\", nil, true)\nif not sess then\n    ngx.log(ngx.ERR, \"failed to do SSL handshake: \", err)\nend\n\nsock:setkeepalive()\n--- config\n    location /t {\n        return 200;\n    }\n--- grep_error_log eval\nqr/failed to do SSL handshake/\n--- grep_error_log_out\nfailed to do SSL handshake\n--- error_log\nreused_session is not supported yet\nsend_status_req is not supported yet\n\n\n\n=== TEST 3: unix socket\n--- http_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n    }\n--- extra_init_worker_by_lua\nlocal sock = ngx.socket.tcp()\nsock:settimeout(1)\nlocal ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\nif not ok then\n    ngx.log(ngx.ERR, \"failed to connect: \", err)\n    return\nend\n\nlocal ok, err = sock:receive()\nif not ok then\n    ngx.log(ngx.ERR, \"failed to read: \", err)\n    return\nend\n--- config\n    location /t {\n        return 200;\n    }\n--- error_log\nfailed to read: timeout\n\n\n\n=== TEST 4: resolve host by ourselves\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_resolv_search_opt: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://apisix\", {headers={Host=\"apisix.apache.org\"}})\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n\n\n\n=== TEST 5: resolve host by ourselves (in stream sub-system)\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_resolv_search_opt: true\n--- stream_enable\n--- stream_server_config\n    content_by_lua_block {\n        local sock = ngx.req.socket(true)\n        -- drain the buffer\n        local _, err = sock:receive(1)\n        if err ~= nil then\n          ngx.log(ngx.ERR, err)\n          return ngx.exit(-1)\n        end\n        local http = require(\"resty.http\")\n        local httpc = http.new()\n        local res, err = httpc:request_uri(\"http://apisix\", {headers={Host=\"apisix.apache.org\"}})\n        if not res then\n            ngx.log(ngx.ERR, err)\n            return ngx.exit(-1)\n        end\n        sock:send(\"ok\")\n    }\n--- stream_request eval\nm\n--- stream_response: ok\n\n\n\n=== TEST 6: resolve host by ourselves (UDP)\n--- yaml_config\napisix:\n  node_listen: 1984\n  enable_resolv_search_opt: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local sock = ngx.socket.udp()\n            local res, err = sock:setpeername(\"apisix\", 80)\n            if not res then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n    }\n\n\n\n=== TEST 7: ensure our patch works with unix socket\n--- stream_server_config\n    content_by_lua_block {\n    }\n--- stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n        content_by_lua_block {\n        }\n    }\n--- config\n    location /t {\n        content_by_lua_block {\n            local sock = ngx.socket.udp()\n            local res, err = sock:setpeername(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not res then\n                ngx.log(ngx.ERR, err)\n            end\n        }\n    }\n"
  },
  {
    "path": "t/misc/pre-function.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: invalid pre_function\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"_meta\": {\n                                    \"pre_function\": \"not a function\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to load _meta.pre_function in plugin limit-count: [string \\\"meta pre_function\\\"]:1: unexpected symbol near 'not'\"}\n\n\n\n=== TEST 2: attempt setting pre_function in _meta with a typo in `pre_function`\n# this is to test the case where user (or CP) would attempt configuring pre_function\n# using incorrect field name, this validation is achieved by setting `additionalProperties = false`\n# in schema_def.lua\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"_meta\": {\n                                    \"prefunction\": \"not a function\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"_meta\\\" validation failed: additional properties forbidden, found prefunction\"}\n\n\n\n=== TEST 3: pre_function with error in code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"_meta\": {\n                                    \"pre_function\": \"return function() print(invalid.index) end\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 4: sending request will execute erroneous code and print error log\n--- request\nGET /hello\n--- error_log\npre_function execution for plugin limit-count failed: [string \"meta pre_function\"]:1: attempt to index global 'invalid' (a nil value),\n\n\n\n=== TEST 5: test pre_function sanity: correct function\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"_meta\": {\n                                    \"pre_function\": \"return function(conf, ctx) ngx.log(ngx.WARN, 'hello ', ngx.req.get_headers()[\\\"User-Agent\\\"]) end\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 6: request\n--- request\nGET /hello\n--- more_headers\nUser-Agent: test-nginx\n--- error_log\nhello test-nginx\n\n\n\n=== TEST 7: pre_function is executed in all phases\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"example-plugin\": {\n                            \"i\": 11,\n                            \"_meta\": {\n                                \"pre_function\": \"return function(conf, ctx) ngx.log(ngx.WARN, 'hello: ', ngx.get_phase()) end\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 8: request\n--- request\nGET /hello\n--- error_log\nhello: access\nhello: header_filter\nhello: body_filter\nhello: log\n\n\n\n=== TEST 9: test pre-function with proxy-rewrite, (rewrite phase)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/uri\",\n                            \"headers\": {\n                                \"x-api\": \"$example_var_name\"\n                            },\n                            \"_meta\": {\n                                \"pre_function\": \"return function(conf, ctx) local core = require \\\"apisix.core\\\" core.ctx.register_var(\\\"example_var_name\\\", function(ctx) return \\\"example_var_value\\\" end) end\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route(header supports nginx variables)\n--- request\nGET /hello\n--- response_body\nuri: /uri\nhost: localhost\nx-api: example_var_value\nx-real-ip: 127.0.0.1\n"
  },
  {
    "path": "t/misc/timers.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: always start a new timer even the previous one is blocked\n--- config\n    location /t {\n        content_by_lua_block {\n            local timers = require(\"apisix.timers\")\n            timers.register_timer(\"t\", function()\n                ngx.log(ngx.WARN, \"fire\")\n            end)\n            timers.register_timer(\"c\", function()\n                ngx.sleep(5)\n            end)\n\n            ngx.sleep(2.1)\n        }\n    }\n--- grep_error_log eval\nqr/fire/\n--- grep_error_log_out\nfire\nfire\n"
  },
  {
    "path": "t/node/chash-balance.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two upstream node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"remote_addr\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n\n\n\n=== TEST 3: set route(three upstream node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"remote_addr\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1,\n                            \"127.0.0.1:1982\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1982\"}]\n\n\n\n=== TEST 5: set route(three upstream node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"remote_addr\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 0,\n                            \"127.0.0.1:1982\": 0\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n\n\n\n=== TEST 7 set route(three upstream node with querystring)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"query_string\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1,\n                            \"127.0.0.1:1982\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes with querystring\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?var=2&var2=\"\n\n            local t = {}\n            local ports_count = {}\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri..i, {method = \"GET\"})\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    ports_count[res.body] = (ports_count[res.body] or 0) + 1\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.count > b.count\n            end\n            table.sort(ports_arr, cmd)\n\n            if (ports_arr[1].count - ports_arr[3].count) / ports_arr[2].count > 0.2 then\n                ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            else\n                ngx.say('ok')\n            end\n        }\n    }\n--- request\nGET /t\n--- wait: 5\n--- response_body\nok\n\n\n\n=== TEST 9: set route(three upstream node with arg_xxx)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"arg_device_id\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1,\n                            \"127.0.0.1:1982\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit routes with args\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?device_id=\"\n\n            local t = {}\n            local ports_count = {}\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri..i, {method = \"GET\"})\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    ports_count[res.body] = (ports_count[res.body] or 0) + 1\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.count > b.count\n            end\n            table.sort(ports_arr, cmd)\n\n            if (ports_arr[1].count - ports_arr[3].count) / ports_arr[2].count > 0.2 then\n                ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            else\n                ngx.say('ok')\n            end\n        }\n    }\n--- request\nGET /t\n--- wait: 5\n--- response_body\nok\n\n\n\n=== TEST 11: set route(weight 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"arg_device_id\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 0,\n                            \"127.0.0.1:1981\": 0\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?device_id=1\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_code_like: ^(?:50\\d)$\n--- error_log\nfailed to find valid upstream server, no valid upstream node\n\n\n\n=== TEST 13: set route(ensure retry can try every node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"arg_device_id\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1979\": 1000,\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?device_id=1\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200\n--- error_log\nConnection refused\n\n\n\n=== TEST 15: set routes with very big weights\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"arg_device_id\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1000000000,\n                            \"127.0.0.1:1981\": 2000000000,\n                            \"127.0.0.1:1982\": 1000000000\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?device_id=1\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- a `size too large` error will be thrown if we don't reduce the weight\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200\n\n\n\n=== TEST 17: set routes with very big weights, some nodes have zero weight\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"arg_device_id\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1000000000,\n                            \"127.0.0.1:1981\": 0,\n                            \"127.0.0.1:1982\": 4000000000\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port?device_id=1\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- a `size too large` error will be thrown if we don't reduce the weight\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200\n"
  },
  {
    "path": "t/node/chash-hashon.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: add two consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"jack\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"re62sf0vRJqOBjvJJ6RUcA==\"\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.say(\"create consumer jack failed\")\n                return\n            end\n            ngx.say(code .. \" \" ..body)\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"tom\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-tom\"\n                        }\n                    }\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"tom\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"RAL/niDfEUpx+ynsoqWDuA==\"\n                            }\n                        }\n                    }\n                }]]\n                )\n            ngx.say(code .. \" \" ..body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200 passed\n200 passed\n\n\n\n=== TEST 2: add key auth plugin, chash hash_on consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"chash\",\n                        \"hash_on\": \"consumer\"\n                    },\n                    \"uri\": \"/server_port\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: hit routes, hash_on one consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            request_headers[\"apikey\"] = \"auth-jack\"\n\n            local ports_count = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1981\"}]\n--- grep_error_log eval\nqr/hash_on: consumer|chash_key: \"jack\"|chash_key: \"tom\"/\n--- grep_error_log_out\nhash_on: consumer\nchash_key: \"jack\"\nhash_on: consumer\nchash_key: \"jack\"\nhash_on: consumer\nchash_key: \"jack\"\nhash_on: consumer\nchash_key: \"jack\"\n\n\n\n=== TEST 4: hit routes, hash_on two consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            local ports_count = {}\n            for i = 1, 4 do\n                if i%2 == 0 then\n                    request_headers[\"apikey\"] = \"auth-tom\"\n                else\n                    request_headers[\"apikey\"] = \"auth-jack\"\n                end\n\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":2,\"port\":\"1981\"},{\"count\":2,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/hash_on: consumer|chash_key: \"jack\"|chash_key: \"tom\"/\n--- grep_error_log_out\nhash_on: consumer\nchash_key: \"jack\"\nhash_on: consumer\nchash_key: \"tom\"\nhash_on: consumer\nchash_key: \"jack\"\nhash_on: consumer\nchash_key: \"tom\"\n\n\n\n=== TEST 5: set route(two upstream node, type chash), hash_on header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"custom_header\",\n                        \"type\": \"chash\",\n                        \"hash_on\": \"header\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes, hash_on custom header\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            request_headers[\"custom_header\"] = \"custom-one\"\n\n            local ports_count = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/hash_on: header|chash_key: \"custom-one\"/\n--- grep_error_log_out\nhash_on: header\nchash_key: \"custom-one\"\nhash_on: header\nchash_key: \"custom-one\"\nhash_on: header\nchash_key: \"custom-one\"\nhash_on: header\nchash_key: \"custom-one\"\n\n\n\n=== TEST 7: hit routes, hash_on custom header miss, use default\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            request_headers[\"miss-custom-header\"] = \"custom-one\"\n\n            local ports_count = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/chash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1/\n--- grep_error_log_out\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\n\n\n\n=== TEST 8: set route(two upstream node, type chash), hash_on cookie\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"custom-cookie\",\n                        \"type\": \"chash\",\n                        \"hash_on\": \"cookie\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit routes, hash_on custom cookie\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            request_headers[\"Cookie\"] = \"custom-cookie=cuscookie\"\n\n            local ports_count = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1981\"}]\n--- grep_error_log eval\nqr/hash_on: cookie|chash_key: \"cuscookie\"/\n--- grep_error_log_out\nhash_on: cookie\nchash_key: \"cuscookie\"\nhash_on: cookie\nchash_key: \"cuscookie\"\nhash_on: cookie\nchash_key: \"cuscookie\"\nhash_on: cookie\nchash_key: \"cuscookie\"\n\n\n\n=== TEST 10: hit routes, hash_on custom cookie miss, use default\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            request_headers[\"Cookie\"] = \"miss-custom-cookie=cuscookie\"\n\n            local ports_count = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/chash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1/\n--- grep_error_log_out\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\n\n\n\n=== TEST 11: set route(key contains uppercase letters and hyphen)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"X-Sessionid\",\n                        \"type\": \"chash\",\n                        \"hash_on\": \"header\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit routes with header\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 6 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"X-Sessionid\"] = \"chash_val_\" .. i\n                    }\n                })\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":3,\"port\":\"1981\"},{\"count\":3,\"port\":\"1980\"}]\n--- error_log\nchash_key: \"chash_val_1\"\nchash_key: \"chash_val_2\"\nchash_key: \"chash_val_3\"\nchash_key: \"chash_val_4\"\nchash_key: \"chash_val_5\"\nchash_key: \"chash_val_6\"\n\n\n\n=== TEST 13: set route(two upstream nodes, type chash), hash_on vars_combinations\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"$http_custom_header-$http_custom_header_second\",\n                        \"type\": \"chash\",\n                        \"hash_on\": \"vars_combinations\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: hit routes, hash_on custom header combinations\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local request_headers = {}\n            request_headers[\"custom_header\"] = \"custom-one\"\n            request_headers[\"custom_header_second\"] = \"custom-two\"\n\n            local ports_count = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", headers = request_headers})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/hash_on: vars_combinations|chash_key: \"custom-one-custom-two\"/\n--- grep_error_log_out\nhash_on: vars_combinations\nchash_key: \"custom-one-custom-two\"\nhash_on: vars_combinations\nchash_key: \"custom-one-custom-two\"\nhash_on: vars_combinations\nchash_key: \"custom-one-custom-two\"\nhash_on: vars_combinations\nchash_key: \"custom-one-custom-two\"\n\n\n\n=== TEST 15: hit routes, hash_on custom header combinations\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":2,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/chash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1/\n--- grep_error_log_out\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\nchash_key fetch is nil, use default chash_key remote_addr: 127.0.0.1\n"
  },
  {
    "path": "t/node/client-mtls-openresty.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan('no_plan');\n} else {\n    plan(skip_all => \"for vanilla OpenResty only\");\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set verification\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 2: hit\n--- exec\ncurl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello\n--- response_body\nhello world\n\n\n\n=== TEST 3: no client certificate\n--- exec\ncurl -k https://localhost:1994/hello\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate was not present\n\n\n\n=== TEST 4: wrong client certificate\n--- exec\ncurl --cert t/certs/apisix.crt --key t/certs/apisix.key -k https://localhost:1994/hello\n--- response_body eval\nqr/400 Bad Request/\n--- error_log eval\nqr/client certificate verification is not passed: FAILED:self[- ]signed certificate/\n\n\n\n=== TEST 5: hit with different host which doesn't require mTLS\n--- exec\ncurl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H \"Host: test.com\"\n--- response_body\nhello world\n\n\n\n=== TEST 6: set verification (2 ssl objects)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 7: hit without mTLS verify, with Host requires mTLS verification\n--- exec\ncurl -k https://localhost:1994/hello -H \"Host: test.com\"\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate was not present\n\n\n\n=== TEST 8: set verification (2 ssl objects, both have mTLS)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_ca_cert2 = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = ssl_ca_cert2,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 9: hit with mTLS verify, with Host requires different mTLS verification\n--- exec\ncurl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H \"Host: test.com\"\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate verified with SNI localhost, but the host is test.com\n"
  },
  {
    "path": "t/node/client-mtls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: bad client certificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = (\"test.com\"):rep(128),\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to validate client_cert: failed to parse cert: PEM_read_bio_X509_AUX() failed\"}\n\n\n\n=== TEST 2: missing client certificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"client\\\" validation failed: property \\\"ca\\\" is required\"}\n\n\n\n=== TEST 3: set verification\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1994\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                plugins = {\n                    [\"proxy-rewrite\"] = {\n                        uri = \"/hello\"\n                    }\n                },\n                uri = \"/mtls\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 4: hit\n--- request\nGET /mtls\n--- more_headers\nHost: localhost\n--- response_body\nhello world\n\n\n\n=== TEST 5: no client certificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1994\"] = 1,\n                    },\n                },\n                plugins = {\n                    [\"proxy-rewrite\"] = {\n                        uri = \"/hello\"\n                    }\n                },\n                uri = \"/mtls2\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 6: hit\n--- request\nGET /mtls2\n--- more_headers\nHost: localhost\n--- error_code: 502\n--- error_log\npeer did not return a certificate\n\n\n\n=== TEST 7: wrong client certificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key = t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1994\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                plugins = {\n                    [\"proxy-rewrite\"] = {\n                        uri = \"/hello\"\n                    }\n                },\n                uri = \"/mtls3\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 8: hit\n--- request\nGET /mtls3\n--- more_headers\nHost: localhost\n--- error_code: 502\n--- error_log\ncertificate verify failed\n\n\n\n=== TEST 9: set verification\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 10: hit with different host which doesn't require mTLS\n--- exec\ncurl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H \"Host: x.com\"\n--- response_body\nhello world\n\n\n\n=== TEST 11: set verification (2 ssl objects)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 12: hit without mTLS verify, with Host requires mTLS verification\n--- exec\ncurl -k https://localhost:1994/hello -H \"Host: test.com\"\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate verified with SNI localhost, but the host is test.com\n\n\n\n=== TEST 13: set verification (2 ssl objects, both have mTLS)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_ca_cert2 = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/hello\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = ssl_ca_cert2,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 14: hit with mTLS verify, with Host requires different mTLS verification\n--- exec\ncurl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H \"Host: test.com\"\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate verified with SNI localhost, but the host is test.com\n\n\n\n=== TEST 15: request localhost and save tls session to reuse\n--- max_size: 1048576\n--- exec\necho \"GET /hello HTTP/1.1\\r\\nHost: localhost\\r\\n\" | \\\n    timeout 1 openssl s_client -ign_eof -connect 127.0.0.1:1994 \\\n    -servername localhost -cert t/certs/mtls_client.crt -key t/certs/mtls_client.key \\\n    -sess_out session.dat || true\n--- response_body eval\nqr/200 OK/\n\n\n\n=== TEST 16: request test.com with saved tls session\n--- max_size: 1048576\n--- exec\necho \"GET /hello HTTP/1.1\\r\\nHost: test.com\\r\\n\" | \\\n    openssl s_client -ign_eof -connect 127.0.0.1:1994 -servername test.com \\\n    -sess_in session.dat\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nsni in client hello mismatch hostname of ssl session, sni: test.com, hostname: localhost\n\n\n\n=== TEST 17: set verification (2 ssl objects, both have mTLS)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_ca_cert2 = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    },\n                },\n                uri = \"/*\"\n            }\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                    skip_mtls_uri_regex = {\n                        \"/hello[0-9]+\",\n                    }\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = ssl_ca_cert2,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 18: skip the mtls, although no client cert provided\n--- exec\ncurl -k https://localhost:1994/hello1\n--- response_body eval\nqr/hello1 world/\n\n\n\n=== TEST 19: skip the mtls, although with wrong client cert\n--- exec\ncurl -k --cert t/certs/test2.crt --key t/certs/test2.key -k https://localhost:1994/hello1\n--- response_body eval\nqr/hello1 world/\n\n\n\n=== TEST 20: mtls failed, returns 400\n--- exec\ncurl -k https://localhost:1994/hello\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate was not present\n\n\n\n=== TEST 21: mtls failed, wrong client cert\n--- exec\ncurl --cert t/certs/test2.crt --key t/certs/test2.key -k https://localhost:1994/hello\n--- response_body eval\nqr/400 Bad Request/\n--- error_log\nclient certificate verification is not passed: FAILED\n\n\n\n=== TEST 22: mtls failed, at handshake phase\n--- exec\ncurl -k -v --resolve \"test.com:1994:127.0.0.1\" https://test.com:1994/hello\n--- error_log\npeer did not return a certificate\n"
  },
  {
    "path": "t/node/consumer-group.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: consumer group usage\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/consumer_groups/bar',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"hello\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"group_id\": \"bar\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhcg==\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            ngx.say(res.body)\n\n            local code, err = t('/apisix/admin/consumer_groups/bar',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"world\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            ngx.say(res.body)\n        }\n    }\n--- response_body\nhello\nworld\n\n\n\n=== TEST 2: validated plugins configuration via incremental sync (malformed data)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local core = require(\"apisix.core\")\n\n            assert(core.etcd.set(\"/consumer_groups/bar\",\n                {id = \"bar\", plugins = { [\"uri-blocker\"] = { block_rules =  1 }}}\n            ))\n            -- wait for sync\n            ngx.sleep(0.6)\n\n            assert(core.etcd.delete(\"/consumer_groups/bar\"))\n        }\n    }\n--- error_log\nproperty \"block_rules\" validation failed\n\n\n\n=== TEST 3: don't override the plugin in the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/consumer_groups/bar',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"hello\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"group_id\": \"bar\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        },\n                        \"response-rewrite\": {\n                            \"body\": \"world\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhcg==\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            ngx.say(res.body)\n        }\n    }\n--- response_body\nworld\n\n\n\n=== TEST 4: check consumer_group_id var\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/consumer_groups/bar',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function(_, ctx) ngx.say(ctx.var.consumer_group_id); ngx.exit(200); end\"]\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"group_id\": \"bar\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhcg==\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nbar\n"
  },
  {
    "path": "t/node/consumer-plugin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        },\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: enable key auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: invalid consumer\n--- request\nGET /hello\n--- more_headers\napikey: 123\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid API key in request\"}\n\n\n\n=== TEST 4: valid consumer\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\n--- response_body\nhello world\n\n\n\n=== TEST 5: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-one\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 6: use the new configuration after the consumer's configuration is updated\n--- config\n    location /t {\n        content_by_lua_block {\n            local function test()\n                local json_encode = require(\"toolkit.json\").encode\n                local http = require \"resty.http\"\n                local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n                local status_count = {}\n                for i = 1, 5 do\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri,\n                        {\n                            method = \"GET\",\n                            headers = {\n                                apikey = \"auth-one\",\n                            }\n                        }\n                    )\n                    if not res then\n                        ngx.say(err)\n                        return\n                    end\n\n                    local status = tostring(res.status)\n                    status_count[status] = (status_count[status] or 0) + 1\n                end\n                ngx.say(json_encode(status_count))\n            end\n\n            test()\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 4,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        },\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.sleep(0.1)\n\n            test()\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"200\":2,\"503\":3}\n{\"200\":4,\"503\":1}\n\n\n\n=== TEST 7: consumer with multiple auth plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"username\": \"John_Doe\",\n                    \"desc\": \"new consumer\",\n                    \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"consumer-plugin-John_Doe\"\n                            },\n                            \"hmac-auth\": {\n                                \"key_id\": \"my-access-key\",\n                                \"secret_key\": \"my-secret-key\",\n                                \"clock_skew\": 1\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: bind to routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.log(ngx.ERR, \"failed to bind route 1\")\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/status\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit consumer, key-auth\n--- request\nGET /hello\n--- more_headers\napikey: consumer-plugin-John_Doe\n--- response_body\nhello world\n--- error_log\nfind consumer John_Doe\n\n\n\n=== TEST 10: hit consumer, hmac-auth\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /status\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n        core.log.info(\"signing_string:\", signing_string)\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/status',\n            ngx.HTTP_GET,\n            nil,\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\nfind consumer John_Doe\n\n\n\n=== TEST 11: the plugins bound on the service should use the latest configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\":\"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"header\": \"Authorization\"\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello1\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\n                        \"GET\"\n                    ],\n                    \"uri\": \"/hello\",\n                    \"service_id\": \"1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local headers = {\n                [\"Authorization\"] = \"auth-jack\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.print(res.body)\n\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"header\": \"Authorization\"\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/server_port\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\n1980\n"
  },
  {
    "path": "t/node/consumer-plugin2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\n\nour $debug_config = t::APISIX::read_file(\"conf/debug.yaml\");\n$debug_config =~ s/basic:\\n  enable: false/basic:\\n  enable: true/;\n$debug_config =~ s/hook_conf:\\n  enable: false/hook_conf:\\n  enable: true/;\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: configure non-auth plugins in the consumer and run it's rewrite phase\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/uri/plugin_proxy_rewrite\",\n                            \"headers\": {\n                                \"X-Api-Engine\": \"APISIX\",\n                                \"X-CONSUMER-ID\": \"1\"\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {}\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- request\nGET /hello\n--- more_headers\napikey: auth-jack\n--- response_body\nuri: /uri/plugin_proxy_rewrite\napikey: auth-jack\nhost: localhost\nx-api-engine: APISIX\nx-consumer-id: 1\nx-consumer-username: jack\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 3: trace plugins info for debug\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local ngx_re = require(\"ngx.re\")\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local headers = {}\n            headers[\"apikey\"] = \"auth-jack\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = headers,\n                })\n            local debug_header = res.headers[\"Apisix-Plugins\"]\n            local arr = ngx_re.split(debug_header, \", \")\n            local hash = {}\n            for i, v in ipairs(arr) do\n                hash[v] = true\n            end\n            ngx.status = res.status\n            ngx.say(json.encode(hash))\n        }\n    }\n--- response_body\n{\"key-auth\":true,\"proxy-rewrite\":true}\n\n\n\n=== TEST 4: configure non-auth plugins in the route and run it's rewrite phase\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n                )\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {},\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api-Engine\": \"APISIX\",\n                                    \"X-CONSUMER-ID\": \"1\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- more_headers\napikey: auth-jack\n--- response_body\nuri: /uri/plugin_proxy_rewrite\napikey: auth-jack\nhost: localhost\nx-api-engine: APISIX\nx-consumer-id: 1\nx-consumer-username: jack\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 6: trace plugins info for debug\n--- debug_config eval: $::debug_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local ngx_re = require(\"ngx.re\")\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local headers = {}\n            headers[\"apikey\"] = \"auth-jack\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = headers,\n                })\n            local debug_header = res.headers[\"Apisix-Plugins\"]\n            local arr = ngx_re.split(debug_header, \", \")\n            local hash = {}\n            for i, v in ipairs(arr) do\n                hash[v] = true\n            end\n            ngx.status = res.status\n            ngx.say(json.encode(hash))\n        }\n    }\n--- response_body\n{\"key-auth\":true,\"proxy-rewrite\":true}\n\n\n\n=== TEST 7: configure non-auth plugins in the consumer and run it's rewrite phase\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"ip-restriction\": {\n                            \"blacklist\": [\n                                \"127.0.0.0/24\"\n                            ]\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {}\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes and ip-restriction work well\n--- request\nGET /hello\n--- more_headers\napikey: auth-jack\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n\n\n\n=== TEST 9: use the latest consumer modifiedIndex as lrucache key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\"]\n                        },\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugin_config_id\": \"1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhcg==\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            ngx.print(res.body)\n\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\", \"127.0.0.1\"]\n                        },\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bala\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhbGE=\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\nhello world\nhello world\n\n\n\n=== TEST 10: consumer should work if the etcd connection failed during starting\n--- extra_init_by_lua\nlocal etcd_apisix  = require(\"apisix.core.etcd\")\netcd_apisix.get_etcd_syncer = function ()\n    return nil, \"\", \"ouch\"\nend\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhbGE=\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello world\n--- error_log\nfailed to fetch data from etcd\n"
  },
  {
    "path": "t/node/consumer-plugin3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with csrf plugin (data encryption enabled)\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"key-a\"\n                        },\n                        \"csrf\": {\n                            \"key\": \"userkey\",\n                            \"expires\": 1000000000\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            -- verify csrf key is decrypted in admin API\n            local code, message, res = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_GET\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            local consumer = json.decode(res)\n            ngx.say(consumer.value.plugins[\"csrf\"].key)\n\n            -- verify csrf key is encrypted in etcd\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/jack'))\n            ngx.say(res.body.node.value.plugins[\"csrf\"].key)\n        }\n    }\n--- request\nGET /t\n--- response_body\nuserkey\nmt39FazQccyMqt4ctoRV7w==\n--- no_error_log\n[error]\n\n\n\n=== TEST 2: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: invalid request - no csrf token\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nPOST /hello\n--- more_headers\napikey: key-a\n--- error_code: 401\n--- response_body\n{\"error_msg\":\"no csrf token in headers\"}\n\n\n\n=== TEST 4: valid request - with csrf token\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nPOST /hello\n--- more_headers\napikey: key-a\napisix-csrf-token: eyJyYW5kb20iOjAuNDI5ODYzMTk3MTYxMzksInNpZ24iOiI0ODRlMDY4NTkxMWQ5NmJhMDc5YzQ1ZGI0OTE2NmZkYjQ0ODhjODVkNWQ0NmE1Y2FhM2UwMmFhZDliNjE5OTQ2IiwiZXhwaXJlcyI6MjY0MzExOTYyNH0=\nCookie: apisix-csrf-token=eyJyYW5kb20iOjAuNDI5ODYzMTk3MTYxMzksInNpZ24iOiI0ODRlMDY4NTkxMWQ5NmJhMDc5YzQ1ZGI0OTE2NmZkYjQ0ODhjODVkNWQ0NmE1Y2FhM2UwMmFhZDliNjE5OTQ2IiwiZXhwaXJlcyI6MjY0MzExOTYyNH0=\n--- response_body\nhello world\n--- no_error_log\n[error]\n"
  },
  {
    "path": "t/node/credential-plugin-basic-auth.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable basic-auth on the route /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create a consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create a credential with basic-auth plugin enabled for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"basic-auth\": {\"username\": \"foo\", \"password\": \"bar\"}\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"34010989-ce4e-4d61-9493-b54cca8edb31\",\n                        \"plugins\":{\n                            \"basic-auth\":{\"username\":\"foo\",\"password\":\"+kOEVUuRc5rC5ZwvvAMLwg==\"}\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: access with invalid basic-auth (invalid password)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmZvbwo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n\n\n\n=== TEST 5: access with valid basic-auth\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/credential-plugin-incremental-effective.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: test continuous watch etcd changes without APISIX reload\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- enable key-auth on /hello\n            t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            ngx.sleep(0.2) -- On some machines, changes may not be instantly watched, so sleep makes the test more robust.\n\n            -- request /hello without key-auth should response status 401\n            local code, body = t('/hello', ngx.HTTP_GET)\n            assert(code == 401)\n\n            -- add a consumer jack\n            t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\"\n                }]],\n                [[{\n                    \"key\": \"/apisix/consumers/jack\",\n                    \"value\":\n                    {\n                        \"username\":\"jack\"\n                    }\n                }]]\n            )\n\n            -- create first credential for consumer jack\n            t('/apisix/admin/consumers/jack/credentials/the-first-one',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\":{\"key-auth\":{\"key\":\"p7a3k6r4t9\"}}\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"the-first-one\",\n                        \"plugins\":{\"key-auth\":{\"key\":\"p7a3k6r4t9\"}}\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/the-first-one\"\n                }]]\n            )\n            ngx.sleep(0.2)\n\n            -- request /hello with credential a\n            local headers = {}\n            headers[\"apikey\"] = \"p7a3k6r4t9\"\n            code, body = t('/hello', ngx.HTTP_GET, \"\", nil, headers)\n            assert(code == 200)\n\n            -- create second credential for consumer jack\n            t('/apisix/admin/consumers/jack/credentials/the-second-one',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\":{\"key-auth\":{\"key\":\"v8p3q6r7t9\"}}\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"the-second-one\",\n                        \"plugins\":{\"key-auth\":{\"key\":\"v8p3q6r7t9\"}}\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/the-second-one\"\n                }]]\n            )\n            ngx.sleep(0.2)\n\n            -- request /hello with credential b\n            headers[\"apikey\"] = \"v8p3q6r7t9\"\n            code, body = t('/hello', ngx.HTTP_GET, \"\", nil, headers)\n            assert(code == 200)\n\n            -- delete the first credential\n            code, body = t('/apisix/admin/consumers/jack/credentials/the-first-one', ngx.HTTP_DELETE)\n            assert(code == 200)\n            ngx.sleep(0.2)\n\n            -- request /hello with credential a\n            headers[\"apikey\"] = \"p7a3k6r4t9\"\n            code, body = t('/hello', ngx.HTTP_GET, \"\", nil, headers)\n            assert(code == 401)\n        }\n    }\n--- request\nGET /t\n"
  },
  {
    "path": "t/node/credential-plugin-jwt-auth.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable jwt-auth on the route /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create a consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create a credential with jwt-auth plugin enabled for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"jwt-auth\": {\"key\": \"user-key\", \"secret\": \"my-secret-key\"}\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"34010989-ce4e-4d61-9493-b54cca8edb31\",\n                        \"plugins\":{\n                            \"jwt-auth\": {\"key\": \"user-key\", \"secret\": \"kK0lkbzXrE7aiTiyK/Z0Sw==\"}\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: access with invalid JWT token\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqd3QtdmF1bHQta2V5IiwiZXhwIjoxNjk1MTM4NjM1fQ.Au2liSZ8eQXUJR3SJESwNlIfqZdNyRyxIJK03L4dk_g\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid user key in JWT token\"}\n\n\n\n=== TEST 5: access with valid JWT token in header\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/credential-plugin-key-auth.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable key-auth on the route /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create a credential with key-auth plugin enabled for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\"key\": \"p7a3k6r4t9\"}\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"34010989-ce4e-4d61-9493-b54cca8edb31\",\n                        \"plugins\":{\n                            \"key-auth\": {\"key\": \"fsFPtg7BtXMXkvSnS9e1zw==\"}\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: request with an invalid key: should be not OK\n--- request\nGET /hello\n--- more_headers\napikey: 123\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid API key in request\"}\n\n\n\n=== TEST 5: request with the valid key: should be OK\n--- request\nGET /hello\n--- more_headers\napikey: p7a3k6r4t9\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/credential-plugin-multi-credentials.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable key-auth plugin on /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- basic-auth on route 1\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create a consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create the first credential with the key-auth plugin enabled for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/the-first-one',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\"key\": \"p7a3k6r4t9\"}\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"the-first-one\",\n                        \"plugins\":{\n                            \"key-auth\": {\"key\": \"fsFPtg7BtXMXkvSnS9e1zw==\"}\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/the-first-one\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: create the second credential with the key-auth plugin enabled for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/the-second-one',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\"key\": \"v8p3q6r7t9\"}\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"the-second-one\",\n                        \"plugins\":{\n                            \"key-auth\": {\"key\": \"QwGua2GjZjOiq+Mj3Mef2g==\"}\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/the-second-one\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: request /hello with the key of the first credential: should be OK\n--- request\nGET /hello\n--- more_headers\napikey: p7a3k6r4t9\n--- response_body\nhello world\n\n\n\n=== TEST 6: request /hello with the key of second credential: should be OK\n--- request\nGET /hello\n--- more_headers\napikey: v8p3q6r7t9\n--- response_body\nhello world\n\n\n\n=== TEST 7: delete the first credential\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/the-first-one', ngx.HTTP_DELETE)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: request /hello with the key of the first credential: should be not OK\n--- request\nGET /hello\n--- more_headers\napikey: p7a3k6r4t9\n--- error_code: 401\n\n\n\n=== TEST 9: request /hello with the key of the second credential: should be OK\n--- request\nGET /hello\n--- more_headers\napikey: v8p3q6r7t9\n--- response_body\nhello world\n\n\n\n=== TEST 10: delete the second credential\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/the-second-one', ngx.HTTP_DELETE)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: request /hello with the key of the second credential: should be not OK\n--- request\nGET /hello\n--- more_headers\napikey: v8p3q6r7t9\n--- error_code: 401\n"
  },
  {
    "path": "t/node/credential-plugin-set-request-header.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable key-auth on the route /echo\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create a credential with key-auth plugin enabled and 'custom_id' label for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\"key\": \"p7a3k6r4t9\"}\n                     },\n                     \"labels\": {\n                         \"custom_id\": \"271fc4a264bb\"\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"34010989-ce4e-4d61-9493-b54cca8edb31\",\n                        \"plugins\":{\n                            \"key-auth\": {\"key\": \"fsFPtg7BtXMXkvSnS9e1zw==\"}\n                        },\n                        \"labels\": {\n                            \"custom_id\": \"271fc4a264bb\"\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: request the route: 'x-consumer-username' and 'x-credential-identifier' is in response headers and 'x-consumer-custom-id' is not\n--- request\nGET /echo HTTP/1.1\n--- more_headers\napikey: p7a3k6r4t9\n--- response_headers\nx-consumer-username: jack\nx-credential-identifier: 34010989-ce4e-4d61-9493-b54cca8edb31\n!x-consumer-custom-id\n\n\n\n=== TEST 5: update the consumer add label \"custom_id\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"labels\": {\n                        \"custom_id\": \"495aec6a\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: request the route: the value of 'x-consumer-custom-id' come from the consumer but not the credential or downstream\n--- request\nGET /echo HTTP/1.1\n--- more_headers\napikey: p7a3k6r4t9\nx-consumer-custom-id: 271fc4a264bb\n--- response_headers\nx-consumer-username: jack\nx-credential-identifier: 34010989-ce4e-4d61-9493-b54cca8edb31\nx-consumer-custom-id: 495aec6a\n\n\n\n=== TEST 7: delete the credential\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31', ngx.HTTP_DELETE)\n\n            assert(code == 200)\n            ngx.status = code\n        }\n    }\n--- request\nGET /t\n--- response_body\n\n\n\n=== TEST 8: update the consumer to enable a key-auth plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"p7a3k6r4t9\"\n                            }\n                        }\n                }]],\n                [[{\n                    \"value\": {\n                        \"username\": \"jack\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"fsFPtg7BtXMXkvSnS9e1zw==\"\n                            }\n                        }\n                    },\n                    \"key\": \"/apisix/consumers/jack\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: request the route with headers x-credential-identifier and x-consumer-custom-id: these headers will be removed\n--- request\nGET /echo HTTP/1.1\n--- more_headers\napikey: p7a3k6r4t9\nx-credential-identifier: 34010989-ce4e-4d61-9493-b54cca8edb31\nx-consumer-custom-id: 271fc4a264bb\n--- response_headers\nx-consumer-username: jack\n!x-credential-identifier\n!x-consumer-custom-id\n"
  },
  {
    "path": "t/node/credential-plugin-work-with-other-plugin.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable key-auth on /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- basic-auth on route 1\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create a consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create a credential with the key-auth plugin enabled for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31',\n                ngx.HTTP_PUT,\n                [[{\n                     \"plugins\": {\n                         \"key-auth\": {\"key\": \"p7a3k6r4t9\"}\n                     }\n                }]],\n                [[{\n                    \"value\":{\n                        \"id\":\"34010989-ce4e-4d61-9493-b54cca8edb31\",\n                        \"plugins\":{\n                            \"key-auth\": {\"key\": \"fsFPtg7BtXMXkvSnS9e1zw==\"}\n                        }\n                    },\n                    \"key\":\"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31\"\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: request the route /hello multi times: should be OK\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: p7a3k6r4t9\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 5: enable plugin `limit-count` for the consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: request the route /hello multi times: should be not OK, exceed the limit-count\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: p7a3k6r4t9\n--- error_code eval\n[200, 200, 503, 503]\n"
  },
  {
    "path": "t/node/data_encrypt.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n# the sensitive data is encrypted in etcd, and it is normal to read it from the admin API\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/consumers/foo',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"basic-auth\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/foo'))\n            ngx.say(res.body.node.value.plugins[\"basic-auth\"].password)\n\n        }\n    }\n--- response_body\nbar\n77+NmbYqNfN+oLm0aX5akg==\n\n\n\n=== TEST 2: enable basic auth plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: verify\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n\n\n\n=== TEST 4: multiple auth plugins work well\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        },\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n            local code, message, res = t('/apisix/admin/consumers/foo',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 5: enable multiple auth plugins on route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {},\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: verify\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n\n\n\n=== TEST 7: disable data_encryption\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: false\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/foo'))\n            ngx.say(res.body.node.value.plugins[\"basic-auth\"].password)\n\n        }\n    }\n--- response_body\nbar\n\n\n\n=== TEST 8: etcd store unencrypted password, enable data_encryption, decryption fails, use original password\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res, err = core.etcd.set(\"/consumers/foo2\", core.json.decode([[{\n                \"username\":\"foo2\",\n                \"plugins\":{\n                    \"basic-auth\":{\n                        \"username\":\"foo2\",\n                        \"password\":\"bar\"\n                    }\n                }\n            }]]))\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/consumers/foo2',\n                ngx.HTTP_GET\n            )\n            res = core.json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"basic-auth\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/foo2'))\n            ngx.say(res.body.node.value.plugins[\"basic-auth\"].password)\n        }\n    }\n--- response_body\nbar\nbar\n--- error_log\nfailed to decrypt the conf of plugin [basic-auth] key [password], err: decrypt ssl key failed\n\n\n\n=== TEST 9: etcd stores both encrypted and unencrypted data\n# enable data_encryption, decryption of encrypted data succeeds\n# decryption of unencrypted data fails, make sure it works well\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res, err = core.etcd.set(\"/consumers/foo2\", core.json.decode([[{\n                \"username\":\"foo2\",\n                \"plugins\":{\n                    \"basic-auth\":{\n                        \"username\":\"foo2\",\n                        \"password\":\"bar\"\n                    },\n                    \"key-auth\": {\n                        \"key\": \"vU/ZHVJw7b0XscDJ1Fhtig==\"\n                    }\n                }\n            }]]))\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/consumers/foo2',\n                ngx.HTTP_GET\n            )\n            res = core.json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"basic-auth\"].password)\n            ngx.say(res.value.plugins[\"key-auth\"].key)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/foo2'))\n            ngx.say(res.body.node.value.plugins[\"basic-auth\"].password)\n            ngx.say(res.body.node.value.plugins[\"key-auth\"].key)\n        }\n    }\n--- response_body\nbar\nauth-two\nbar\nvU/ZHVJw7b0XscDJ1Fhtig==\n--- error_log\nfailed to decrypt the conf of plugin [basic-auth] key [password], err: decrypt ssl key failed\n\n\n\n=== TEST 10: verify, use the foo2 consumer\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nGET /hello\n--- more_headers\napikey: auth-two\nAuthorization: Basic Zm9vMjpiYXI=\n--- response_body\nhello world\n\n\n\n=== TEST 11: keyring rotate, encrypt with edd1c9f0985e76a2\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: keyring rotate, decrypt with qeddd145sfvddff3 would fail, but encrypt with edd1c9f0985e76a2 would success\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - qeddd145sfvddff3\n            - edd1c9f0985e76a2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n\n\n\n=== TEST 13: search consumer list\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            -- dletet exist consumers\n            t('/apisix/admin/consumers/foo', ngx.HTTP_DELETE)\n            t('/apisix/admin/consumers/foo2', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"test\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"test\",\n                            \"password\": \"test\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/consumers',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            local pwds = {}\n            table.insert(pwds, res.list[1].value.plugins[\"basic-auth\"].password)\n            table.insert(pwds, res.list[2].value.plugins[\"basic-auth\"].password)\n\n            ngx.say(json.encode(pwds))\n        }\n    }\n--- response_body\n[\"bar\",\"test\"]\n"
  },
  {
    "path": "t/node/data_encrypt2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: data encryption work well with plugins that not the auth plugins\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"abc123\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"batch_max_size\":1,\n                                \"inactive_timeout\":1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            ngx.sleep(0.5)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"clickhouse-logger\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"clickhouse-logger\"].password)\n        }\n    }\n--- response_body\nabc123\n7ipXoKyiZZUAgf3WWNPI5A==\n\n\n\n=== TEST 2: verify\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:abc123\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 5\n\n\n\n=== TEST 3: POST and get list\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes',\n                 ngx.HTTP_POST,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"abc123\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"batch_max_size\":1,\n                                \"inactive_timeout\":1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.list[1].value.plugins[\"clickhouse-logger\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local id = res.list[1].value.id\n            local key = \"/routes/\" .. id\n            local res = assert(etcd.get(key))\n\n            ngx.say(res.body.node.value.plugins[\"clickhouse-logger\"].password)\n        }\n    }\n--- response_body\nabc123\n7ipXoKyiZZUAgf3WWNPI5A==\n\n\n\n=== TEST 4: PATCH\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"abc123\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"batch_max_size\":1,\n                                \"inactive_timeout\":1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/routes/1/plugins',\n                ngx.HTTP_PATCH,\n                [[{\n                        \"clickhouse-logger\": {\n                            \"user\": \"default\",\n                            \"password\": \"def456\",\n                            \"database\": \"default\",\n                            \"logtable\": \"t\",\n                            \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                            \"batch_max_size\":1,\n                            \"inactive_timeout\":1\n                        }\n                 }]]\n            )\n\n            ngx.sleep(0.1)\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"clickhouse-logger\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"clickhouse-logger\"].password)\n        }\n    }\n--- response_body\ndef456\n3hlZu5mwUbqROm+cy0Vi9A==\n\n\n\n=== TEST 5: data encryption work well with services\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"clickhouse-logger\": {\n                            \"user\": \"default\",\n                            \"password\": \"abc123\",\n                            \"database\": \"default\",\n                            \"logtable\": \"t\",\n                            \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                            \"batch_max_size\":1,\n                            \"inactive_timeout\":1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_id\": \"1\",\n                    \"uri\": \"/opentracing\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/services/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            ngx.say(res.value.plugins[\"clickhouse-logger\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/services/1'))\n            ngx.say(res.body.node.value.plugins[\"clickhouse-logger\"].password)\n        }\n    }\n--- response_body\nabc123\n7ipXoKyiZZUAgf3WWNPI5A==\n\n\n\n=== TEST 6: verify\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:abc123\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 5\n\n\n\n=== TEST 7: data encryption work well with plugin_configs\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"clickhouse-logger\": {\n                            \"user\": \"default\",\n                            \"password\": \"abc123\",\n                            \"database\": \"default\",\n                            \"logtable\": \"t\",\n                            \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                            \"batch_max_size\":1,\n                            \"inactive_timeout\":1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugin_config_id\": 1,\n                    \"uri\": \"/opentracing\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            ngx.say(res.value.plugins[\"clickhouse-logger\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/plugin_configs/1'))\n            ngx.say(res.body.node.value.plugins[\"clickhouse-logger\"].password)\n        }\n    }\n--- response_body\nabc123\n7ipXoKyiZZUAgf3WWNPI5A==\n\n\n\n=== TEST 8: verify\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:abc123\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 5\n\n\n\n=== TEST 9: data encryption work well with global rule\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"test\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"test\",\n                            \"password\": \"test\"\n                        }\n                    },\n                    \"desc\": \"test description\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            -- sleep for data sync\n            ngx.sleep(0.5)\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/consumers/test',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            ngx.say(res.value.plugins[\"basic-auth\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/test'))\n            ngx.say(res.body.node.value.plugins[\"basic-auth\"].password)\n\n            -- hit the route with authorization\n            local code, body = t('/hello',\n                ngx.HTTP_PUT,\n                nil,\n                nil,\n                {Authorization = \"Basic dGVzdDp0ZXN0\"}\n            )\n            if code ~= 200 then\n                ngx.status = code\n                return\n            end\n\n            -- delete global rule\n            t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntest\n9QKrmTT3TkWGvjlIoe5XXw==\npassed\n\n\n\n=== TEST 10: data encryption work well with consumer groups\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/consumer_groups/company_a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/consumers/foobar',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foobar\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-two\"\n                        }\n                    },\n                    \"group_id\": \"company_a\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, key is decrypted\n            local code, message, res = t('/apisix/admin/consumers/foobar',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"key-auth\"].key)\n\n            -- get plugin conf from etcd, key is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/foobar'))\n            ngx.say(res.body.node.value.plugins[\"key-auth\"].key)\n        }\n    }\n--- response_body\nauth-two\nvU/ZHVJw7b0XscDJ1Fhtig==\n\n\n\n=== TEST 11: verify data encryption\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local t = require(\"lib.test_admin\").test\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"apikey\"] = \"auth-two\"\n                    }\n                })\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503]\n\n\n\n=== TEST 12: verify whether print warning log when disable data_encryption\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                         \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- reponse_body\npassed\n--- no_error_log\nfailed to get schema for plugin\n"
  },
  {
    "path": "t/node/ewma.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\n#no_long_string();\nno_root_location();\nlog_level('info');\nworker_connections(256);\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 100,\n                                \"127.0.0.1:1981\": 100\n                            },\n                            \"type\": \"ewma\"\n                        },\n                        \"uri\": \"/ewma\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: about latency\n--- timeout: 5\n--- config\n    location /t {\n        content_by_lua_block {\n            --node: \"127.0.0.1:1980\": latency is  0.001\n            --node: \"127.0.0.1:1981\": latency is  0.005\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/ewma\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                httpc:set_timeout(1000)\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":1,\"port\":\"1981\"},{\"count\":11,\"port\":\"1980\"}]\n--- error_code: 200\n\n\n\n=== TEST 3: about frequency\n--- timeout: 30\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/ewma\"\n\n            --node: \"127.0.0.1:1980\": latency is  0.001\n            --node: \"127.0.0.1:1981\": latency is  0.005\n            local ports_count = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n            end\n\n            --remove the 1981 node,\n            --add the 1982 node\n            --keep two nodes for triggering ewma logic in server_picker function of balancer phase\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 100,\n                                \"127.0.0.1:1982\": 100\n                            },\n                            \"type\": \"ewma\"\n                        },\n                        \"uri\": \"/ewma\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.say(\"update route failed\")\n                return\n            end\n\n            ngx.sleep(11)\n            --keep the node 1980 hot\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n            end\n\n            --recover the 1981 node\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 100,\n                                \"127.0.0.1:1981\": 100\n                            },\n                            \"type\": \"ewma\"\n                        },\n                        \"uri\": \"/ewma\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.say(\"update route failed\")\n                return\n            end\n\n            --should select the 1981 node,because it is idle\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(require(\"toolkit.json\").encode({port = res.body, count = 1}))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"count\":1,\"port\":\"1981\"}\n--- error_code: 200\n\n\n\n=== TEST 4: about filter tried servers\n--- timeout: 10\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            --remove the 1981 node,\n            --add the 9527 node (invalid node)\n            --keep two nodes for triggering ewma logic in server_picker function of balancer phase\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1,\n                                \"127.0.0.1:9527\": 1\n                            },\n                            \"type\": \"ewma\",\n                            \"timeout\": {\n                                \"connect\": 0.1,\n                                \"send\": 0.5,\n                                \"read\": 0.5\n                            }\n                        },\n                        \"uri\": \"/ewma\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.say(\"update route failed\")\n                return\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/ewma\"\n\n            --should always select the 1980 node, because 0 is invalid\n            local t = {}\n            local ports_count = {}\n            for i = 1, 12 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    httpc:set_timeout(2000)\n                    local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                    if not res then\n                        ngx.say(err)\n                        return\n                    end\n                    ports_count[res.body] = (ports_count[res.body] or 0) + 1\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- error_code: 200\n--- error_log\nConnection refused) while connecting to upstream\n\n\n\n=== TEST 5: about all endpoints have been retried\n--- timeout: 10\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            --add the 9527 node (invalid node)\n            --add the 9528 node (invalid node)\n            --keep two nodes for triggering ewma logic in server_picker function of balancer phase\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:9527\": 1,\n                                \"127.0.0.1:9528\": 1\n                            },\n                            \"type\": \"ewma\",\n                            \"timeout\": {\n                                \"connect\": 0.1,\n                                \"send\": 0.5,\n                                \"read\": 0.5\n                            }\n                        },\n                        \"uri\": \"/ewma\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.say(\"update route failed\")\n                return\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/ewma\"\n\n            --should always return 502, because both 9527 and 9528 are invalid\n            local t = {}\n            local ports_count = {}\n            for i = 1, 12 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    httpc:set_timeout(2000)\n                    local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                    if not res then\n                        ngx.say(err)\n                        return\n                    end\n                    ports_count[res.status] = (ports_count[res.status] or 0) + 1\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":502}]\n--- error_code: 200\n--- error_log\nConnection refused) while connecting to upstream\n"
  },
  {
    "path": "t/node/filter_func.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with filter_func\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"filter_func\": \"function(vars)\n                        return vars['arg_a1'] == 'a1' and vars['arg_a2'] == 'a2'\n                    end\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- request\nGET /hello?a1=a1&a2=a2\n--- response_body\nhello world\n\n\n\n=== TEST 3: miss route\n--- request\nGET /hello?a1=xxxx&a2=xxxx\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n"
  },
  {
    "path": "t/node/global-rule.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: /not_found\n--- request\nGET /hello\n--- error_code: 503\n\n\n\n=== TEST 6: global rule for internal api (should limit)\n--- yaml_config\nplugins:\n  - limit-count\n  - node-status\n--- request\nGET /apisix/status\n--- error_code: 503\n\n\n\n=== TEST 7: update global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"X-VERSION\":\"1.0\"\n                            }\n                        },\n                        \"uri-blocker\": {\n                            \"block_rules\": [\"select.+(from|limit)\", \"(?:(union(.*?)select))\"]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit global rules\n--- request\nGET /hello?name=;union%20select%20\n--- error_code: 403\n--- response_headers\nX-VERSION: 1.0\n\n\n\n=== TEST 9: hit global rules by internal api (only check uri-blocker)\n--- yaml_config\nplugins:\n  - response-rewrite\n  - uri-blocker\n  - node-status\n--- request\nGET /apisix/status?name=;union%20select%20\n--- error_code: 403\n--- response_headers\nX-VERSION: 1.0\n\n\n\n=== TEST 10: delete global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/not_found', ngx.HTTP_GET)\n            ngx.say(code)\n            local code, body = t('/not_found', ngx.HTTP_GET)\n            ngx.say(code)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n404\n404\n\n\n\n=== TEST 11: empty global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"changed\\n\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit global rules\n--- request\nGET /hello\n--- response_body\nchanged\n\n\n\n=== TEST 13: global rule works with the consumer, after deleting the global rule, ensure no stale plugins remaining\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"test\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"test\",\n                            \"password\": \"test\"\n                        }\n                    },\n                    \"desc\": \"test description\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            -- sleep for data sync\n            ngx.sleep(0.5)\n\n            -- hit the route without authorization, should be 401\n            local code, body = t('/hello',\n                ngx.HTTP_PUT\n            )\n\n            if code ~= 401 then\n                ngx.status = 400\n                return\n            end\n\n            -- hit the route with authorization\n            local code, body = t('/hello',\n                ngx.HTTP_PUT,\n                nil,\n                nil,\n                {Authorization = \"Basic dGVzdDp0ZXN0\"}\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(0.5)\n            -- hit the route with authorization, should be 200\n            local code, body = t('/hello',\n                ngx.HTTP_PUT\n            )\n\n            if code ~= 200 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/node/grpc-proxy-mtls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Unary API grpcs proxy test with mTLS\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpcs\n      tls:\n        client_cert: \"-----BEGIN CERTIFICATE-----\\nMIIDUzCCAjugAwIBAgIURw+Rc5FSNUQWdJD+quORtr9KaE8wDQYJKoZIhvcNAQEN\\nBQAwWDELMAkGA1UEBhMCY24xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\\nWmh1SGFpMRYwFAYDVQQDDA1jYS5hcGlzaXguZGV2MQwwCgYDVQQLDANvcHMwHhcN\\nMjIxMjAxMTAxOTU3WhcNNDIwODE4MTAxOTU3WjBOMQswCQYDVQQGEwJjbjESMBAG\\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxGjAYBgNVBAMMEWNsaWVu\\ndC5hcGlzaXguZGV2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzypq\\nkrsJ8MaqpS0kr2SboE9aRKOJzd6mY3AZLq3tFpio5cK5oIHkQLfeaaLcd4ycFcZw\\nFTpxc+Eth6I0X9on+j4tEibc5IpDnRSAQlzHZzlrOG6WxcOza4VmfcrKqj27oodr\\noqXv05r/5yIoRrEN9ZXfA8n2OnjhkP+C3Q68L6dBtPpv+e6HaAuw8MvcsEo+MQwu\\ncTZyWqWT2UzKVzToW29dHRW+yZGuYNWRh15X09VSvx+E0s+uYKzN0Cyef2C6VtBJ\\nKmJ3NtypAiPqw7Ebfov2Ym/zzU9pyWPi3P1mYPMKQqUT/FpZSXm4iSy0a5qTYhkF\\nrFdV1YuYYZL5YGl9aQIDAQABox8wHTAbBgNVHREEFDASghBhZG1pbi5hcGlzaXgu\\nZGV2MA0GCSqGSIb3DQEBDQUAA4IBAQBepRpwWdckZ6QdL5EuufYwU7p5SIqkVL/+\\nN4/l5YSjPoAZf/M6XkZu/PsLI9/kPZN/PX4oxjZSDH14dU9ON3JjxtSrebizcT8V\\naQ13TeW9KSv/i5oT6qBmj+V+RF2YCUhyzXdYokOfsSVtSlA1qMdm+cv0vkjYcImV\\nl3L9nVHRPq15dY9sbmWEtFBWvOzqNSuQYax+iYG+XEuL9SPaYlwKRC6eS/dbXa1T\\nPPWDQad2X/WmhxPzEHvjSl2bsZF1u0GEdKyhXWMOLCLiYIJo15G7bMz8cTUvkDN3\\n6WaWBd6bd2g13Ho/OOceARpkR/ND8PU78Y8cq+zHoOSqH+1aly5H\\n-----END CERTIFICATE-----\\n\"\n        client_key: \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEpAIBAAKCAQEAzypqkrsJ8MaqpS0kr2SboE9aRKOJzd6mY3AZLq3tFpio5cK5\\noIHkQLfeaaLcd4ycFcZwFTpxc+Eth6I0X9on+j4tEibc5IpDnRSAQlzHZzlrOG6W\\nxcOza4VmfcrKqj27oodroqXv05r/5yIoRrEN9ZXfA8n2OnjhkP+C3Q68L6dBtPpv\\n+e6HaAuw8MvcsEo+MQwucTZyWqWT2UzKVzToW29dHRW+yZGuYNWRh15X09VSvx+E\\n0s+uYKzN0Cyef2C6VtBJKmJ3NtypAiPqw7Ebfov2Ym/zzU9pyWPi3P1mYPMKQqUT\\n/FpZSXm4iSy0a5qTYhkFrFdV1YuYYZL5YGl9aQIDAQABAoIBAD7tUG//lnZnsj/4\\nJXONaORaFj5ROrOpFPuRemS+egzqFCuuaXpC2lV6RHnr+XHq6SKII1WfagTb+lt/\\nvs760jfmGQSxf1mAUidtqcP+sKc/Pr1mgi/SUTawz8AYEFWD6PHmlqBSLTYml+La\\nckd+0pGtk49wEnYSb9n+cv640hra9AYpm9LXUFaypiFEu+xJhtyKKWkmiVGrt/X9\\n3aG6MuYeZplW8Xq1L6jcHsieTOB3T+UBfG3O0bELBgTVexOQYI9O4Ejl9/n5/8WP\\nAbIw7PaAYc7fBkwOGh7/qYUdHnrm5o9MiRT6dPxrVSf0PZVACmA+JoNjCPv0Typf\\n3MMkHoECgYEA9+3LYzdP8j9iv1fP5hn5K6XZAobCD1mnzv3my0KmoSMC26XuS71f\\nvyBhjL7zMxGEComvVTF9SaNMfMYTU4CwOJQxLAuT69PEzW6oVEeBoscE5hwhjj6o\\n/lr5jMbt807J9HnldSpwllfj7JeiTuqRcCu/cwqKQQ1aB3YBZ7h5pZkCgYEA1ejo\\nKrR1hN2FMhp4pj0nZ5+Ry2lyIVbN4kIcoteaPhyQ0AQ0zNoi27EBRnleRwVDYECi\\nXAFrgJU+laKsg1iPjvinHibrB9G2p1uv3BEh6lPl9wPFlENTOjPkqjR6eVVZGP8e\\nVzxYxIo2x/QLDUeOpxySdG4pdhEHGfvmdGmr2FECgYBeknedzhCR4HnjcTSdmlTA\\nwI+p9gt6XYG0ZIewCymSl89UR9RBUeh++HQdgw0z8r+CYYjfH3SiLUdU5R2kIZeW\\nzXiAS55OO8Z7cnWFSI17sRz+RcbLAr3l4IAGoi9MO0awGftcGSc/QiFwM1s3bSSz\\nPAzYbjHUpKot5Gae0PCeKQKBgQCHfkfRBQ2LY2WDHxFc+0+Ca6jF17zbMUioEIhi\\n/X5N6XowyPlI6MM7tRrBsQ7unX7X8Rjmfl/ByschsTDk4avNO+NfTfeBtGymBYWX\\nN6Lr8sivdkwoZZzKOSSWSzdos48ELlThnO/9Ti706Lg3aSQK5iY+aakJiC+fXdfT\\n1TtsgQKBgQDRYvtK/Cpaq0W6wO3I4R75lHGa7zjEr4HA0Kk/FlwS0YveuTh5xqBj\\nwQz2YyuQQfJfJs7kbWOITBT3vuBJ8F+pktL2Xq5p7/ooIXOGS8Ib4/JAS1C/wb+t\\nuJHGva12bZ4uizxdL2Q0/n9ziYTiMc/MMh/56o4Je8RMdOMT5lTsRQ==\\n-----END RSA PRIVATE KEY-----\\n\"\n      nodes:\n        \"127.0.0.1:10053\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n\n\n\n=== TEST 2: Bidirectional API grpcs proxy test with mTLS\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHelloBidirectionalStream\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpcs\n      tls:\n        client_cert: \"-----BEGIN CERTIFICATE-----\\nMIIDUzCCAjugAwIBAgIURw+Rc5FSNUQWdJD+quORtr9KaE8wDQYJKoZIhvcNAQEN\\nBQAwWDELMAkGA1UEBhMCY24xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\\nWmh1SGFpMRYwFAYDVQQDDA1jYS5hcGlzaXguZGV2MQwwCgYDVQQLDANvcHMwHhcN\\nMjIxMjAxMTAxOTU3WhcNNDIwODE4MTAxOTU3WjBOMQswCQYDVQQGEwJjbjESMBAG\\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxGjAYBgNVBAMMEWNsaWVu\\ndC5hcGlzaXguZGV2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzypq\\nkrsJ8MaqpS0kr2SboE9aRKOJzd6mY3AZLq3tFpio5cK5oIHkQLfeaaLcd4ycFcZw\\nFTpxc+Eth6I0X9on+j4tEibc5IpDnRSAQlzHZzlrOG6WxcOza4VmfcrKqj27oodr\\noqXv05r/5yIoRrEN9ZXfA8n2OnjhkP+C3Q68L6dBtPpv+e6HaAuw8MvcsEo+MQwu\\ncTZyWqWT2UzKVzToW29dHRW+yZGuYNWRh15X09VSvx+E0s+uYKzN0Cyef2C6VtBJ\\nKmJ3NtypAiPqw7Ebfov2Ym/zzU9pyWPi3P1mYPMKQqUT/FpZSXm4iSy0a5qTYhkF\\nrFdV1YuYYZL5YGl9aQIDAQABox8wHTAbBgNVHREEFDASghBhZG1pbi5hcGlzaXgu\\nZGV2MA0GCSqGSIb3DQEBDQUAA4IBAQBepRpwWdckZ6QdL5EuufYwU7p5SIqkVL/+\\nN4/l5YSjPoAZf/M6XkZu/PsLI9/kPZN/PX4oxjZSDH14dU9ON3JjxtSrebizcT8V\\naQ13TeW9KSv/i5oT6qBmj+V+RF2YCUhyzXdYokOfsSVtSlA1qMdm+cv0vkjYcImV\\nl3L9nVHRPq15dY9sbmWEtFBWvOzqNSuQYax+iYG+XEuL9SPaYlwKRC6eS/dbXa1T\\nPPWDQad2X/WmhxPzEHvjSl2bsZF1u0GEdKyhXWMOLCLiYIJo15G7bMz8cTUvkDN3\\n6WaWBd6bd2g13Ho/OOceARpkR/ND8PU78Y8cq+zHoOSqH+1aly5H\\n-----END CERTIFICATE-----\\n\"\n        client_key: \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEpAIBAAKCAQEAzypqkrsJ8MaqpS0kr2SboE9aRKOJzd6mY3AZLq3tFpio5cK5\\noIHkQLfeaaLcd4ycFcZwFTpxc+Eth6I0X9on+j4tEibc5IpDnRSAQlzHZzlrOG6W\\nxcOza4VmfcrKqj27oodroqXv05r/5yIoRrEN9ZXfA8n2OnjhkP+C3Q68L6dBtPpv\\n+e6HaAuw8MvcsEo+MQwucTZyWqWT2UzKVzToW29dHRW+yZGuYNWRh15X09VSvx+E\\n0s+uYKzN0Cyef2C6VtBJKmJ3NtypAiPqw7Ebfov2Ym/zzU9pyWPi3P1mYPMKQqUT\\n/FpZSXm4iSy0a5qTYhkFrFdV1YuYYZL5YGl9aQIDAQABAoIBAD7tUG//lnZnsj/4\\nJXONaORaFj5ROrOpFPuRemS+egzqFCuuaXpC2lV6RHnr+XHq6SKII1WfagTb+lt/\\nvs760jfmGQSxf1mAUidtqcP+sKc/Pr1mgi/SUTawz8AYEFWD6PHmlqBSLTYml+La\\nckd+0pGtk49wEnYSb9n+cv640hra9AYpm9LXUFaypiFEu+xJhtyKKWkmiVGrt/X9\\n3aG6MuYeZplW8Xq1L6jcHsieTOB3T+UBfG3O0bELBgTVexOQYI9O4Ejl9/n5/8WP\\nAbIw7PaAYc7fBkwOGh7/qYUdHnrm5o9MiRT6dPxrVSf0PZVACmA+JoNjCPv0Typf\\n3MMkHoECgYEA9+3LYzdP8j9iv1fP5hn5K6XZAobCD1mnzv3my0KmoSMC26XuS71f\\nvyBhjL7zMxGEComvVTF9SaNMfMYTU4CwOJQxLAuT69PEzW6oVEeBoscE5hwhjj6o\\n/lr5jMbt807J9HnldSpwllfj7JeiTuqRcCu/cwqKQQ1aB3YBZ7h5pZkCgYEA1ejo\\nKrR1hN2FMhp4pj0nZ5+Ry2lyIVbN4kIcoteaPhyQ0AQ0zNoi27EBRnleRwVDYECi\\nXAFrgJU+laKsg1iPjvinHibrB9G2p1uv3BEh6lPl9wPFlENTOjPkqjR6eVVZGP8e\\nVzxYxIo2x/QLDUeOpxySdG4pdhEHGfvmdGmr2FECgYBeknedzhCR4HnjcTSdmlTA\\nwI+p9gt6XYG0ZIewCymSl89UR9RBUeh++HQdgw0z8r+CYYjfH3SiLUdU5R2kIZeW\\nzXiAS55OO8Z7cnWFSI17sRz+RcbLAr3l4IAGoi9MO0awGftcGSc/QiFwM1s3bSSz\\nPAzYbjHUpKot5Gae0PCeKQKBgQCHfkfRBQ2LY2WDHxFc+0+Ca6jF17zbMUioEIhi\\n/X5N6XowyPlI6MM7tRrBsQ7unX7X8Rjmfl/ByschsTDk4avNO+NfTfeBtGymBYWX\\nN6Lr8sivdkwoZZzKOSSWSzdos48ELlThnO/9Ti706Lg3aSQK5iY+aakJiC+fXdfT\\n1TtsgQKBgQDRYvtK/Cpaq0W6wO3I4R75lHGa7zjEr4HA0Kk/FlwS0YveuTh5xqBj\\nwQz2YyuQQfJfJs7kbWOITBT3vuBJ8F+pktL2Xq5p7/ooIXOGS8Ib4/JAS1C/wb+t\\nuJHGva12bZ4uizxdL2Q0/n9ziYTiMc/MMh/56o4Je8RMdOMT5lTsRQ==\\n-----END RSA PRIVATE KEY-----\\n\"\n      nodes:\n        \"127.0.0.1:10053\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"stream ended\"\n}\n"
  },
  {
    "path": "t/node/grpc-proxy-stream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Test server side streaming method through gRPC proxy\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHelloServerStream\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloServerStream\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n\n\n\n=== TEST 2: Test client side streaming method through gRPC proxy\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHelloClientStream\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloClientStream\n--- response_body\n{\n  \"message\": \"Hello apisix!Hello apisix!Hello apisix!Hello apisix!\"\n}\n\n\n\n=== TEST 3: Test bidirectional streaming method through gRPC proxy\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHelloBidirectionalStream\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"Hello apisix\"\n}\n{\n  \"message\": \"stream ended\"\n}\n"
  },
  {
    "path": "t/node/grpc-proxy-unary.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Unary API gRPC proxy\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n\n\n\n=== TEST 2: Unary API gRPC proxy test [the old way]\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n\n\n\n=== TEST 3: Unary API grpcs proxy test\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpcs\n      nodes:\n        \"127.0.0.1:10052\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n\n\n\n=== TEST 4: Unary API gRPC proxy with tls\n--- http2\n--- apisix_yaml\nssls:\n  -\n    id: 1\n    cert: \"-----BEGIN CERTIFICATE-----\\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\\n-----END CERTIFICATE-----\\n\"\n    key: \"-----BEGIN RSA PRIVATE KEY-----\\nMIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5\\njhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo\\neLj0efMiOepOSZflj9Ob4yKR2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5s\\nmPtW1Oc/BV5terhscJdOgmRrabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt\\n6iMWEGeQU6mwPENgvj1olji2WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiy\\nVt1TmtMWn1ztk6FfLRqwJWR/Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1o\\nnpRVeXhrBajbCRDRBMwaNw/1/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2\\nfzaqpIfyUbPST4GdqNG9NyIh/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI\\n1cGrGwyXbrieNp63AgMBAAECggGBAJM8g0duoHmIYoAJzbmKe4ew0C5fZtFUQNmu\\nO2xJITUiLT3ga4LCkRYsdBnY+nkK8PCnViAb10KtIT+bKipoLsNWI9Xcq4Cg4G3t\\n11XQMgPPgxYXA6m8t+73ldhxrcKqgvI6xVZmWlKDPn+CY/Wqj5PA476B5wEmYbNC\\nGIcd1FLl3E9Qm4g4b/sVXOHARF6iSvTR+6ol4nfWKlaXSlx2gNkHuG8RVpyDsp9c\\nz9zUqAdZ3QyFQhKcWWEcL6u9DLBpB/gUjyB3qWhDMe7jcCBZR1ALyRyEjmDwZzv2\\njlv8qlLFfn9R29UI0pbuL1eRAz97scFOFme1s9oSU9a12YHfEd2wJOM9bqiKju8y\\nDZzePhEYuTZ8qxwiPJGy7XvRYTGHAs8+iDlG4vVpA0qD++1FTpv06cg/fOdnwshE\\nOJlEC0ozMvnM2rZ2oYejdG3aAnUHmSNa5tkJwXnmj/EMw1TEXf+H6+xknAkw05nh\\nzsxXrbuFUe7VRfgB5ElMA/V4NsScgQKBwQDmMRtnS32UZjw4A8DsHOKFzugfWzJ8\\nGc+3sTgs+4dNIAvo0sjibQ3xl01h0BB2Pr1KtkgBYB8LJW/FuYdCRS/KlXH7PHgX\\n84gYWImhNhcNOL3coO8NXvd6+m+a/Z7xghbQtaraui6cDWPiCNd/sdLMZQ/7LopM\\nRbM32nrgBKMOJpMok1Z6zsPzT83SjkcSxjVzgULNYEp03uf1PWmHuvjO1yELwX9/\\ngoACViF+jst12RUEiEQIYwr4y637GQBy+9cCgcEA3pN9W5OjSPDVsTcVERig8++O\\nBFURiUa7nXRHzKp2wT6jlMVcu8Pb2fjclxRyaMGYKZBRuXDlc/RNO3uTytGYNdC2\\nIptU5N4M7iZHXj190xtDxRnYQWWo/PR6EcJj3f/tc3Itm1rX0JfuI3JzJQgDb9Z2\\ns/9/ub8RRvmQV9LM/utgyOwNdf5dyVoPcTY2739X4ZzXNH+CybfNa+LWpiJIVEs2\\ntxXbgZrhmlaWzwA525nZ0UlKdfktdcXeqke9eBghAoHARVTHFy6CjV7ZhlmDEtqE\\nU58FBOS36O7xRDdpXwsHLnCXhbFu9du41mom0W4UdzjgVI9gUqG71+SXrKr7lTc3\\ndMHcSbplxXkBJawND/Q1rzLG5JvIRHO1AGJLmRgIdl8jNgtxgV2QSkoyKlNVbM2H\\nWy6ZSKM03lIj74+rcKuU3N87dX4jDuwV0sPXjzJxL7NpR/fHwgndgyPcI14y2cGz\\nzMC44EyQdTw+B/YfMnoZx83xaaMNMqV6GYNnTHi0TO2TAoHBAKmdrh9WkE2qsr59\\nIoHHygh7Wzez+Ewr6hfgoEK4+QzlBlX+XV/9rxIaE0jS3Sk1txadk5oFDebimuSk\\nlQkv1pXUOqh+xSAwk5v88dBAfh2dnnSa8HFN3oz+ZfQYtnBcc4DR1y2X+fVNgr3i\\nnxruU2gsAIPFRnmvwKPc1YIH9A6kIzqaoNt1f9VM243D6fNzkO4uztWEApBkkJgR\\n4s/yOjp6ovS9JG1NMXWjXQPcwTq3sQVLnAHxZRJmOvx69UmK4QKBwFYXXjeXiU3d\\nbcrPfe6qNGjfzK+BkhWznuFUMbuxyZWDYQD5yb6ukUosrj7pmZv3BxKcKCvmONU+\\nCHgIXB+hG+R9S2mCcH1qBQoP/RSm+TUzS/Bl2UeuhnFZh2jSZQy3OwryUi6nhF0u\\nLDzMI/6aO1ggsI23Ri0Y9ZtqVKczTkxzdQKR9xvoNBUufjimRlS80sJCEB3Qm20S\\nwzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg==\\n-----END RSA PRIVATE KEY-----\\n\"\n    sni: test.com\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -insecure -d '{\"name\":\"apisix\"}' test.com:1994 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n"
  },
  {
    "path": "t/node/grpc-proxy.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\n# As the test framework doesn't support sending grpc request, this\n# test file is only for grpc irrelative configuration check.\n# To avoid confusion, we configure a closed port so if th configuration works,\n# the result will be `connect refused`.\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"POST /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: with upstream_id\n--- apisix_yaml\nupstreams:\n    - id: 1\n      type: roundrobin\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:9088\": 1\nroutes:\n    - id: 1\n      methods:\n          - POST\n      uri: \"/hello\"\n      upstream_id: 1\n#END\n--- error_code: 502\n--- error_log\nproxy request to 127.0.0.1:9088\n\n\n\n=== TEST 2: with consumer\n--- apisix_yaml\nconsumers:\n  - username: jack\n    plugins:\n        key-auth:\n            key: user-key\n#END\nroutes:\n    - id: 1\n      methods:\n          - POST\n      uri: \"/hello\"\n      plugins:\n          key-auth:\n          consumer-restriction:\n              whitelist:\n                  - jack\n      upstream:\n          scheme: grpc\n          type: roundrobin\n          nodes:\n              \"127.0.0.1:9088\": 1\n#END\n--- more_headers\napikey: user-key\n--- error_code: 502\n--- error_log\nConnection refused\n\n\n\n=== TEST 3: with upstream_id (old way)\n--- apisix_yaml\nupstreams:\n    - id: 1\n      type: roundrobin\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:9088\": 1\nroutes:\n    - id: 1\n      methods:\n          - POST\n      uri: \"/hello\"\n      upstream_id: 1\n#END\n--- error_code: 502\n--- error_log\nproxy request to 127.0.0.1:9088\n\n\n\n=== TEST 4: with consumer (old way)\n--- apisix_yaml\nconsumers:\n  - username: jack\n    plugins:\n        key-auth:\n            key: user-key\n#END\nroutes:\n    - id: 1\n      methods:\n          - POST\n      uri: \"/hello\"\n      plugins:\n          key-auth:\n          consumer-restriction:\n              whitelist:\n                  - jack\n      upstream:\n          type: roundrobin\n          scheme: grpc\n          nodes:\n              \"127.0.0.1:9088\": 1\n#END\n--- more_headers\napikey: user-key\n--- error_code: 502\n--- error_log\nConnection refused\n\n\n\n=== TEST 5: use 443 as the grpcs' default port\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        scheme: grpcs\n        nodes:\n            \"127.0.0.1\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nconnect() failed (111: Connection refused) while connecting to upstream\n\n\n\n=== TEST 6: use 80 as the grpc's default port\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        scheme: grpc\n        nodes:\n            \"127.0.0.1\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nconnect() failed (111: Connection refused) while connecting to upstream\n\n\n\n=== TEST 7: set authority header\n--- log_level: debug\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n--- grep_error_log eval\nqr/grpc header: \"(:authority|host): [^\"]+\"/\n--- grep_error_log_out eval\nqr/grpc header: \"(:authority|host): 127.0.0.1:1984\"/\n\n\n\n=== TEST 8: set authority header to node header\n--- log_level: debug\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      pass_host: node\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n--- grep_error_log eval\nqr/grpc header: \"(:authority|host): [^\"]+\"/\n--- grep_error_log_out eval\nqr/grpc header: \"(:authority|host): 127.0.0.1:10051\"/\n\n\n\n=== TEST 9: set authority header to specific value\n--- log_level: debug\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    upstream:\n      scheme: grpc\n      pass_host: rewrite\n      upstream_host: hello.world\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n--- grep_error_log eval\nqr/grpc header: \"(:authority|host): [^\"]+\"/\n--- grep_error_log_out eval\nqr/grpc header: \"(:authority|host): hello.world\"/\n"
  },
  {
    "path": "t/node/healthcheck-discovery.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nupstreams:\n    - service_name: mock\n      discovery_type: mock\n      type: roundrobin\n      id: 1\n      checks:\n        active:\n            http_path: \"/status\"\n            host: 127.0.0.1\n            port: 1988\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n\n            ngx.sleep(4)\n\n            ngx.say(res.status)\n        }\n    }\n--- grep_error_log eval\nqr/unhealthy TCP increment \\(1\\/2\\) for '127.0.0.1\\([^)]+\\)'/\n--- grep_error_log_out\nunhealthy TCP increment (1/2) for '127.0.0.1(127.0.0.1:1988)'\nunhealthy TCP increment (1/2) for '127.0.0.1(0.0.0.0:1988)'\n--- timeout: 5\n\n\n\n=== TEST 2: create new checker when nodes change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(3)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"127.0.0.2\", port = 1980, weight = 1},\n                        {host = \"127.0.0.3\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(20)\n            ngx.say(res.status)\n        }\n    }\n--- grep_error_log eval\nqr/(create new checker|releasing existing checker): table/\n--- grep_error_log_out\ncreate new checker: table\nreleasing existing checker: table\ncreate new checker: table\n--- timeout: 30\n\n\n\n=== TEST 3: don't create new checker when nodes don't change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(2)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(2)\n            ngx.say(res.status)\n        }\n    }\n--- grep_error_log eval\nqr/(create new checker|try to release checker): table/\n--- grep_error_log_out\ncreate new checker: table\n--- timeout: 5\n"
  },
  {
    "path": "t/node/healthcheck-dns.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nupstreams:\n  - id: 1\n    type: roundrobin\n    nodes:\n      \"test.com:1980\": 1\n    checks:\n      active:\n        http_path: \"/status\"\n        host: 127.0.0.1\n        port: 1988\n        healthy:\n          interval: 1\n          successes: 1\n        unhealthy:\n          interval: 1\n          http_failures: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: healthchecker recreation with changing DNS resolution\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream:\n        type: roundrobin\n        nodes:\n            \"test.com:1980\": 1\n        checks:\n            active:\n                http_path: \"/status\"\n                host: 127.0.0.1\n                port: 1988\n                healthy:\n                    interval: 1\n                    successes: 1\n                unhealthy:\n                    interval: 1\n                    http_failures: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Counter to track DNS resolution calls\n            local dns_call_count = 0\n\n            -- Override the core.resolver.parse_domain function\n            local core = require(\"apisix.core\")\n            local original_parse_domain = core.resolver.parse_domain\n            core.resolver.parse_domain = function(domain)\n                if domain == \"test.com\" then\n                    dns_call_count = dns_call_count + 1\n                    if dns_call_count == 1 then\n                        return \"127.0.0.1\", nil\n                    else\n                        return \"127.0.0.2\", nil\n                    end\n                end\n                return original_parse_domain(domain)\n            end\n\n            -- First request\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(\"First request status: \", res.status)\n\n            -- Wait for healthchecker to be created\n            ngx.sleep(2)\n\n            -- Second request - should trigger DNS resolution again\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(\"Second request status: \", res.status)\n\n            -- Wait for healthchecker recreation\n            ngx.sleep(4)\n\n            -- Restore original DNS function\n            core.resolver.parse_domain = original_parse_domain\n        }\n    }\n--- response_body\nFirst request status: 200\nSecond request status: 200\n--- error_log\ncreate new checker\nreleasing existing checker\ncreate new checker\n--- timeout: 10\n"
  },
  {
    "path": "t/node/healthcheck-global-switch.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two upstream node: one healthy + one unhealthy)\n--- yaml_config\napisix:\n    disable_upstream_healthcheck: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"retries\": 0,\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1970\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 2: switch on disable_upstream_healthcheck and hit routes\n--- yaml_config\napisix:\n    disable_upstream_healthcheck: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local get_port = function(body)\n                if body == \"1980\" then\n                    return \"1980\"\n                end\n                return \"1970\"\n            end\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[get_port(res.body)] = (ports_count[get_port(res.body)] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":6,\"port\":\"1980\"},{\"count\":6,\"port\":\"1970\"}]\n--- error_log\ndisabled upstream healthcheck\n--- no_error_log\n(upstream#/apisix/routes/1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\n(upstream#/apisix/routes/1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 6\n"
  },
  {
    "path": "t/node/healthcheck-https.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->http_config) {\n        my $http_config = <<'_EOC_';\nserver {\n    listen 8765 ssl;\n    ssl_certificate ../../certs/mtls_server.crt;\n    ssl_certificate_key ../../certs/mtls_server.key;\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n\n    location /ping {\n        return 200 '8765';\n    }\n\n    location /healthz {\n        return 200 'ok';\n    }\n}\n\nserver {\n    listen 8766 ssl;\n    ssl_certificate ../../certs/mtls_server.crt;\n    ssl_certificate_key ../../certs/mtls_server.key;\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n\n    location /ping {\n        return 200 '8766';\n    }\n\n    location /healthz {\n        return 500;\n    }\n}\n\n\nserver {\n    listen 8767 ssl;\n    ssl_certificate ../../certs/mtls_server.crt;\n    ssl_certificate_key ../../certs/mtls_server.key;\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n\n    location /ping {\n        return 200 '8766';\n    }\n\n    location /healthz {\n        return 200 'ok';\n    }\n}\n\nserver {\n    listen 8768 ssl;\n    ssl_certificate ../../certs/mtls_server.crt;\n    ssl_certificate_key ../../certs/mtls_server.key;\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n\n    location /ping {\n        return 200 '8766';\n    }\n\n    location /healthz {\n        return 500;\n    }\n}\n\n_EOC_\n        $block->set_value(\"http_config\", $http_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: https health check (two health nodes)\n--- config\n    location /t {\n        lua_ssl_trusted_certificate ../../certs/mtls_ca.crt;\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local key =  t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                uri = \"/ping\",\n                upstream = {\n                    scheme = \"https\",\n                    nodes = {\n                        [\"127.0.0.1:8765\"] = 1,\n                        [\"127.0.0.1:8767\"] = 1\n                    },\n                    tls = {\n                        client_cert = cert,\n                        client_key = key\n                    },\n                    retries = 2,\n                    checks = {\n                        active = {\n                            type = \"https\",\n                            http_path = \"/healthz\",\n                            https_verify_certificate = false,\n                            healthy = {\n                                interval = 1,\n                                successes = 1\n                            },\n                            unhealthy = {\n                                interval = 1,\n                                http_failures = 1\n                            },\n                        }\n                    }\n                }\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT, core.json.encode(data))\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ping\"\n            local _, _ = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(2)\n\n            local healthcheck_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/v1/healthcheck/routes/1\"\n            local httpc = http.new()\n            local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n            local json_data = core.json.decode(res.body)\n            assert(json_data.type == \"https\")\n            assert(#json_data.nodes == 2)\n\n            local function check_node_health(port, status)\n                for _, node in ipairs(json_data.nodes) do\n                    if node.port == port and node.status == status then\n                        return true\n                    end\n                end\n                return false\n            end\n\n            assert(check_node_health(8765, \"healthy\"), \"Port 8765 is not healthy\")\n            assert(check_node_health(8767, \"healthy\"), \"Port 8767 is not healthy\")\n        }\n    }\n--- request\nGET /t\n--- error_code: 200\n\n\n\n=== TEST 2: https health check (one healthy node, one unhealthy node)\n--- config\n    location /t {\n        lua_ssl_trusted_certificate ../../certs/mtls_ca.crt;\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local key =  t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                uri = \"/ping\",\n                upstream = {\n                    scheme = \"https\",\n                    nodes = {\n                        [\"127.0.0.1:8765\"] = 1,\n                        [\"127.0.0.1:8766\"] = 1\n                    },\n                    tls = {\n                        client_cert = cert,\n                        client_key = key\n                    },\n                    retries = 2,\n                    checks = {\n                        active = {\n                            type = \"https\",\n                            http_path = \"/healthz\",\n                            https_verify_certificate = false,\n                            healthy = {\n                                interval = 1,\n                                successes = 1\n                            },\n                            unhealthy = {\n                                interval = 1,\n                                http_failures = 1\n                            },\n                        }\n                    }\n                }\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT, core.json.encode(data))\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ping\"\n            local _, _ = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(4)\n\n            local healthcheck_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/v1/healthcheck/routes/1\"\n            local httpc = http.new()\n            local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n            local json_data = core.json.decode(res.body)\n            assert(json_data.type == \"https\")\n            assert(#json_data.nodes == 2)\n\n            local function check_node_health(port, status)\n                for _, node in ipairs(json_data.nodes) do\n                    if node.port == port and node.status == status then\n                        return true\n                    end\n                end\n                return false\n            end\n\n            assert(check_node_health(8765, \"healthy\"), \"Port 8765 is not healthy\")\n            assert(check_node_health(8766, \"unhealthy\"), \"Port 8766 is not unhealthy\")\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/\\([^)]+\\) unhealthy .* for '.*'/\n--- grep_error_log_out\n(upstream#/apisix/routes/1) unhealthy HTTP increment (1/1) for '127.0.0.1(127.0.0.1:8766)'\n--- timeout: 8\n\n\n\n=== TEST 3: https health check (two unhealthy nodes)\n--- config\n    location /t {\n        lua_ssl_trusted_certificate ../../certs/mtls_ca.crt;\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local key =  t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                uri = \"/ping\",\n                upstream = {\n                    scheme = \"https\",\n                    nodes = {\n                        [\"127.0.0.1:8766\"] = 1,\n                        [\"127.0.0.1:8768\"] = 1\n                    },\n                    tls = {\n                        client_cert = cert,\n                        client_key = key\n                    },\n                    retries = 2,\n                    checks = {\n                        active = {\n                            type = \"https\",\n                            http_path = \"/healthz\",\n                            https_verify_certificate = false,\n                            healthy = {\n                                interval = 1,\n                                successes = 1\n                            },\n                            unhealthy = {\n                                interval = 1,\n                                http_failures = 1\n                            },\n                        }\n                    }\n                }\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT, core.json.encode(data))\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ping\"\n            local _, _ = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(4)\n\n            local healthcheck_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/v1/healthcheck/routes/1\"\n            local httpc = http.new()\n            local res, _ = httpc:request_uri(healthcheck_uri, {method = \"GET\", keepalive = false})\n            local json_data = core.json.decode(res.body)\n            assert(json_data.type == \"https\")\n            assert(#json_data.nodes == 2)\n\n            local function check_node_health(port, status)\n                for _, node in ipairs(json_data.nodes) do\n                    if node.port == port and node.status == status then\n                        return true\n                    end\n                end\n                return false\n            end\n\n            assert(check_node_health(8766, \"unhealthy\"), \"Port 8766 is not unhealthy\")\n            assert(check_node_health(8768, \"unhealthy\"), \"Port 8768 is not unhealthy\")\n        }\n    }\n--- request\nGET /t\n--- error_code: 200\n--- timeout: 8\n"
  },
  {
    "path": "t/node/healthcheck-ipv6.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nadd_block_preprocessor(sub {\n    my $block = shift;\n    $block->set_value(\"listen_ipv6\", 1);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1970\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 1\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 2: hit routes (two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3) -- wait for new workers replacement to complete\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                ngx.log(ngx.ERR, \"It works\")\n            end\n\n            ngx.sleep(3)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                ngx.log(ngx.ERR, \"req \", i)\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ngx.log(ngx.ERR, \"req \", i, \" \", res.body)\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/unhealthy .* for '.*'/\n--- grep_error_log_out\nunhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\nunhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 10\n"
  },
  {
    "path": "t/node/healthcheck-leak-bugfix.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: ensure the old check is cleared after configuration updated\n--- extra_init_worker_by_lua\n    local healthcheck = require(\"resty.healthcheck\")\n    local new = healthcheck.new\n    healthcheck.new = function(...)\n        local obj = new(...)\n        local clear = obj.delayed_clear\n        obj.delayed_clear = function(...)\n            ngx.log(ngx.WARN, \"clear checker\")\n            return clear(...)\n        end\n        return obj\n    end\n\n--- extra_init_by_lua\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n        if domain == \"test2.com\" then\n            return {address = \"127.0.0.\" .. count+100}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local cfg = [[{\n            \"upstream\": {\n                \"nodes\": {\n                    \"test1.com:1980\": 1,\n                    \"test2.com:1980\": 1\n                },\n                \"type\": \"roundrobin\",\n                \"checks\":{\n                    \"active\":{\n                        \"healthy\":{\n                            \"http_statuses\":[\n                                200,\n                                302\n                            ],\n                            \"interval\":1,\n                            \"successes\":2\n                        },\n                        \"http_path\":\"/hello\",\n                        \"timeout\":1,\n                        \"type\":\"http\",\n                        \"unhealthy\":{\n                            \"http_failures\":5,\n                            \"http_statuses\":[\n                                429,\n                                404,\n                                500,\n                                501,\n                                502,\n                                503,\n                                504,\n                                505\n                            ],\n                            \"interval\":1,\n                            \"tcp_failures\":2,\n                            \"timeouts\":3\n                        }\n                    }\n                }\n            },\n            \"uri\": \"/hello\"\n        }]]\n        local t = require(\"lib.test_admin\").test\n        assert(t('/apisix/admin/routes/1', ngx.HTTP_PUT, cfg) < 300)\n        t('/hello', ngx.HTTP_GET)\n        ngx.sleep(2)\n        assert(t('/apisix/admin/routes/1', ngx.HTTP_PUT, cfg) < 300)\n        ngx.sleep(2)\n    }\n}\n\n--- request\nGET /t\n--- error_log\nclear checker\n--- timeout: 7\n"
  },
  {
    "path": "t/node/healthcheck-multiple-worker.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworkers(2);\nworker_connections(256);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1970\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 2: hit routes (two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3) -- wait for new workers replacement to complete\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/unhealthy TCP increment/\n--- grep_error_log_out\nunhealthy TCP increment\nunhealthy TCP increment\n--- timeout: 20\n"
  },
  {
    "path": "t/node/healthcheck-passive-resty-events.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    if ($ENV{TEST_EVENTS_MODULE} ne \"lua-resty-events\") {\n        $SkipReason = \"Only for lua-resty-events events module\";\n    }\n}\n\nuse Test::Nginx::Socket::Lua $SkipReason ? (skip_all => $SkipReason) : ();\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(passive)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 0,\n                            \"127.0.0.1:1\": 1\n                        },\n                        \"retries\": 0,\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 100,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 100,\n                                    \"http_failures\": 2\n                                }\n                            },]] .. [[\n                            \"passive\": {\n                                \"healthy\": {\n                                    \"http_statuses\": [200, 201],\n                                    \"successes\": 3\n                                },\n                                \"unhealthy\": {\n                                    \"http_statuses\": [502],\n                                    \"http_failures\": 1,\n                                    \"tcp_failures\": 1\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes (two healthy nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3) -- wait for sync\n\n            local json_sort = require(\"toolkit.json\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/server_port\"\n\n            local httpc = http.new()\n            -- Since a failed request attempt triggers a passive health check to report\n            -- a non-health condition, a request is first triggered manually here to\n            -- trigger a passive health check to refresh the monitoring state of the build\n            --\n            -- The reason for this is to avoid delays in event synchronization timing due\n            -- to non-deterministic asynchronous connections when using lua-resty-events\n            -- as an events module.\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.sleep(2) -- Wait for health check unhealthy events sync\n\n            local ports_count = {}\n            for i = 1, 6 do\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                local status = tostring(res.status)\n                ports_count[status] = (ports_count[status] or 0) + 1\n            end\n\n            ngx.say(json_sort.encode(ports_count))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"200\":5,\"502\":1}\n--- error_log\n(upstream#/apisix/routes/1) unhealthy HTTP increment (1/1)\n--- timeout: 10\n\n\n\n=== TEST 3: set route(only passive)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 0,\n                            \"127.0.0.1:1\": 1\n                        },\n                        \"retries\": 0,\n                        \"checks\": {\n                            \"passive\": {\n                                \"healthy\": {\n                                    \"http_statuses\": [200, 201],\n                                    \"successes\": 3\n                                },\n                                \"unhealthy\": {\n                                    \"http_statuses\": [502],\n                                    \"http_failures\": 1,\n                                    \"tcp_failures\": 1\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"checks\\\" validation failed: object matches none of the required: [\\\"active\\\"] or [\\\"active\\\",\\\"passive\\\"]\"}\n\n\n\n=== TEST 4: set route(only active + active & passive)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 0,\n                            \"127.0.0.1:1\": 1\n                        },\n                        \"retries\": 0,\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 100,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 100,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello_\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 0,\n                            \"127.0.0.1:1\": 1\n                        },\n                        \"retries\": 0,\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 100,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 100,\n                                    \"http_failures\": 2\n                                }\n                            },]] .. [[\n                            \"passive\": {\n                                \"healthy\": {\n                                    \"http_statuses\": [200, 201],\n                                    \"successes\": 3\n                                },\n                                \"unhealthy\": {\n                                    \"http_statuses\": [502],\n                                    \"http_failures\": 1,\n                                    \"tcp_failures\": 1\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: only one route should have passive healthcheck\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json_sort = require(\"toolkit.json\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            ngx.sleep(3.5)\n            local ports_count = {}\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri .. \"/hello_\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.status)\n            ngx.sleep(3.5)\n            --- The first request above triggers the passive healthcheck\n            --- The healthchecker is asynchronously created after a minimum of 1 second\n            --- So we need to wait for it to be created and sent another request to verify\n            -- only /hello_ has passive healthcheck\n            local res, err = httpc:request_uri(uri .. \"/hello_\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.sleep(2)\n            local res, err = httpc:request_uri(uri .. \"/hello\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n502\n502\n--- grep_error_log eval\nqr/enabled healthcheck passive/\n--- grep_error_log_out\nenabled healthcheck passive\n--- timeout: 15\n\n\n\n=== TEST 6: make sure passive healthcheck works (conf is not corrupted by the default value)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json_sort = require(\"toolkit.json\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri .. \"/hello\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.status)\n\n            -- The first time request to /hello_\n            -- Ensure that the event that triggers the healthchecker to perform\n            -- add_target has been sent and processed correctly\n            --\n            -- Due to the implementation of lua-resty-events, it relies on the kernel and\n            -- the Nginx event loop to process socket connections.\n            -- When lua-resty-healthcheck handles passive healthchecks and uses lua-resty-events\n            -- as the events module, the synchronization of the first event usually occurs\n            -- before the start of the passive healthcheck. So when the execution finishes and\n            -- healthchecker tries to record the healthcheck status, it will not be able to find\n            -- an existing target (because the synchronization event has not finished yet), which\n            -- will lead to some anomalies that deviate from the original test case, so compatibility\n            -- operations are performed here.\n            local res, err = httpc:request_uri(uri .. \"/hello_\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.status)\n\n            ngx.sleep(4) -- Wait for health check unhealthy events sync\n\n            -- The second time request to /hello_\n            local res, err = httpc:request_uri(uri .. \"/hello_\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n502\n502\n502\n--- grep_error_log eval\nqr/\\[healthcheck\\] \\([^)]+\\) unhealthy HTTP increment/\n--- grep_error_log_out\n[healthcheck] (upstream#/apisix/routes/2) unhealthy HTTP increment\n--- timeout: 6\n"
  },
  {
    "path": "t/node/healthcheck-service-discovery.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $services = <<_EOC_;\nservices:\n  - id: 1\n    upstream:\n      service_name: test_service\n      discovery_type: mock\n      type: roundrobin\n      checks:\n        active:\n            http_path: \"/status\"\n            host: 127.0.0.1\n            port: 1988\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $services);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: service with nested upstream - healthchecker created on first request\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /server_port\n    service_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"127.0.0.1\", port = 1981, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/server_port\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(5)\n        }\n    }\n--- grep_error_log eval\nqr/create new checker/\n--- grep_error_log_out\ncreate new checker\n--- timeout: 10\n\n\n\n=== TEST 2: service with nested upstream - healthchecker recreated when nodes change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /server_port\n    service_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n\n            -- First request with initial nodes\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"127.0.0.1\", port = 1981, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/server_port\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(2)\n\n            -- Second request with different nodes to trigger _nodes_ver change\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1982, weight = 1},\n                        {host = \"127.0.0.1\", port = 1983, weight = 1},\n                    }\n                end\n            }\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.sleep(2)\n        }\n    }\n--- error_log\ncreate new checker\nreleasing existing checker\ncreate new checker\n--- timeout: 10\n"
  },
  {
    "path": "t/node/healthcheck-stop-checker.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\n# the healthcheck stop test requires exiting worker to keep watching etcd for a while,\n# which is not the case when using gRPC.\nmy $yaml_config = <<_EOC_;\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    prefix: \"/apisix\"\n    host:\n      - \"http://127.0.0.1:2379\"\n    use_grpc: false\n  admin:\n    admin_key: null\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two healthy upstream nodes)\n--- request\nPUT /apisix/admin/routes/1\n{\"uri\":\"/server_port\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1:1980\":1,\"127.0.0.1:1981\":1},\"checks\":{\"active\":{\"http_path\":\"/status\",\"host\":\"foo.com\",\"healthy\":{\"interval\":1,\"successes\":1},\"unhealthy\":{\"interval\":1,\"http_failures\":2}}}}}\n--- error_code_like: ^20\\d$\n\n\n\n=== TEST 2: update + delete\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, status, body = t('/apisix/admin/routes/1',\n                \"PUT\",\n                [[{\"uri\":\"/server_port\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1:1980\":1,\"127.0.0.1:1981\":1},\"checks\":{\"active\":{\"http_path\":\"/status\",\"healthy\":{\"interval\":1,\"successes\":1},\"unhealthy\":{\"interval\":1,\"http_failures\":2}}}}}]]\n            )\n\n            if code < 300 then\n                code = 200\n            end\n            ngx.say(\"1 code: \", code)\n\n            ngx.sleep(3)\n            local code, body = t('/server_port', \"GET\")\n            ngx.say(\"2 code: \", code)\n\n            ngx.sleep(2)\n            code = t('/apisix/admin/routes/1', \"DELETE\")\n            ngx.say(\"3 code: \", code)\n\n            ngx.sleep(3)\n            local code, body = t('/server_port', \"GET\")\n            ngx.say(\"4 code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n1 code: 200\n2 code: 200\n3 code: 200\n4 code: 404\n--- grep_error_log eval\nqr/create new checker: table: 0x|try to release checker: table: 0x/\n--- grep_error_log_out\ncreate new checker: table: 0x\ntry to release checker: table: 0x\n--- timeout: 10\n\n\n\n=== TEST 3: set route(two healthy upstream nodes)\n--- request\nPUT /apisix/admin/routes/1\n{\"uri\":\"/server_port\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1:1980\":1,\"127.0.0.1:1981\":1},\"checks\":{\"active\":{\"http_path\":\"/status\",\"host\":\"foo.com\",\"healthy\":{\"interval\":1,\"successes\":1},\"unhealthy\":{\"interval\":1,\"http_failures\":2}}}}}\n--- error_code: 201\n\n\n\n=== TEST 4: update\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/server_port', \"GET\")\n            ngx.say(\"1 code: \", code)\n            ngx.sleep(2)\n            local code, status, body = t('/apisix/admin/routes/1',\n                \"PUT\",\n                [[{\"uri\":\"/server_port\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1:1980\":1,\"127.0.0.1:1981\":1},\"checks\":{\"active\":{\"http_path\":\"/status\",\"healthy\":{\"interval\":1,\"successes\":1},\"unhealthy\":{\"interval\":1,\"http_failures\":2}}}}}]]\n            )\n\n            if code < 300 then\n                code = 200\n            end\n            ngx.say(\"2 code: \", code)\n\n            ngx.sleep(2)\n            local code, body = t('/server_port', \"GET\")\n            ngx.say(\"3 code: \", code)\n            ngx.sleep(2)\n        }\n    }\n--- request\nGET /t\n--- response_body\n1 code: 200\n2 code: 200\n3 code: 200\n--- grep_error_log eval\nqr/create new checker: table: 0x|try to release checker: table: 0x/\n--- grep_error_log_out\ncreate new checker: table: 0x\ntry to release checker: table: 0x\ncreate new checker: table: 0x\n--- timeout: 7\n\n\n\n=== TEST 5: update + delete for /upstreams\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, body = t('/apisix/admin/upstreams/stopchecker',\n                \"PUT\",\n                [[{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1:1980\":1,\"127.0.0.1:1981\":1},\"checks\":{\"active\":{\"http_path\":\"/status\",\"healthy\":{\"interval\":1,\"successes\":1},\"unhealthy\":{\"interval\":1,\"http_failures\":2}}}}]]\n            )\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, _, body = t('/apisix/admin/routes/1',\n                \"PUT\",\n                [[{\"uri\":\"/server_port\",\"upstream_id\":\"stopchecker\"}]]\n            )\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(1)\n            code, _, body = t('/server_port', \"GET\")\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(3)\n\n            -- update\n            code, _, body = t('/apisix/admin/upstreams/stopchecker',\n                \"PUT\",\n                [[{\"type\":\"roundrobin\",\"nodes\":{\"127.0.0.1:1980\":1,\"127.0.0.1:1981\":1},\"checks\":{\"active\":{\"http_path\":\"/void\",\"healthy\":{\"interval\":1,\"successes\":1},\"unhealthy\":{\"interval\":1,\"http_failures\":1}}}}]]\n            )\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(3)\n            code, _, body = t('/server_port', \"GET\")\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(3)\n            -- delete\n            code, _, body = t('/apisix/admin/routes/1', \"DELETE\")\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(3) -- wait for routes delete event synced\n\n            code, _, body = t('/apisix/admin/upstreams/stopchecker', \"DELETE\")\n\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(10)\n            ngx.say(\"ok\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n--- grep_error_log eval\nqr/create new checker: table: 0x|try to release checker: table: 0x/\n--- grep_error_log_out\ncreate new checker: table: 0x\ntry to release checker: table: 0x\ncreate new checker: table: 0x\ntry to release checker: table: 0x\n--- timeout: 30\n"
  },
  {
    "path": "t/node/healthcheck.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two healthy upstream nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 2: hit routes (two healthy nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3) -- wait for sync\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            -- hit route before start test loop\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.sleep(3)\n            local ports_count = {}\n            for i = 1, 12 do\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":6,\"port\":\"1981\"},{\"count\":6,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n--- timeout: 10\n\n\n\n=== TEST 3: set route(two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1970\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 4: hit routes (two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/\\([^)]+\\) unhealthy .* for '.*'/\n--- grep_error_log_out\n(upstream#/apisix/routes/1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\n(upstream#/apisix/routes/1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 10\n\n\n\n=== TEST 5: chash route (two healthy nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1,\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"key\": \"remote_addr\",\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 6: hit routes (two healthy nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(2) -- wait for sync\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n--- timeout: 6\n\n\n\n=== TEST 7: chash route (upstream nodes: 1 healthy + 8 unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1970\": 1,\n                            \"127.0.0.1:1971\": 1,\n                            \"127.0.0.1:1972\": 1,\n                            \"127.0.0.1:1973\": 1,\n                            \"127.0.0.1:1974\": 1,\n                            \"127.0.0.1:1975\": 1,\n                            \"127.0.0.1:1976\": 1,\n                            \"127.0.0.1:1977\": 1\n                        },\n                        \"key\": \"remote_addr\",\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 8: hit routes (upstream nodes: 1 healthy + 8 unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out eval\nqr/Connection refused\\) while connecting to upstream/\n--- timeout: 10\n\n\n\n=== TEST 9: chash route (upstream nodes: 2 unhealthy)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\"uri\":\"/server_port\",\"upstream\":{\"type\":\"chash\",\"nodes\":{\"127.0.0.1:1960\":1,\"127.0.0.1:1961\":1},\"key\":\"remote_addr\",\"retries\":3,\"checks\":{\"active\":{\"http_path\":\"/status\",\"host\":\"foo.com\",\"healthy\":{\"interval\":999,\"successes\":3},\"unhealthy\":{\"interval\":999,\"http_failures\":3}},\"passive\":{\"healthy\":{\"http_statuses\":[200,201],\"successes\":3},\"unhealthy\":{\"http_statuses\":[500],\"http_failures\":3,\"tcp_failures\":3}}}}}]]\n        )\n\n        if code >= 300 then\n        ngx.status = code\n        end\n        ngx.say(body)\n  }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 10: hit routes (passive + retries)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri,\n                    {method = \"GET\", keepalive = false}\n                )\n                ngx.say(\"res: \", res.status, \" err: \", err)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nres: 502 err: nil\nres: 502 err: nil\n--- grep_error_log eval\nqr{\\[error\\].*while connecting to upstream.*}\n--- grep_error_log_out eval\nqr{.*http://127.0.0.1:1960/server_port.*\n.*http://127.0.0.1:1961/server_port.*\n.*http://127.0.0.1:1961/server_port.*\n.*http://127.0.0.1:1960/server_port.*\n.*http://127.0.0.1:1961/server_port.*\n.*http://127.0.0.1:1961/server_port.*}\n--- timeout: 10\n\n\n\n=== TEST 11: add new routh with healthcheck attribute\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 3 do\n                t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\": \"/server_port\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"checks\": {\n                                \"active\": {\n                                    \"http_path\": \"/status\",\n                                    \"host\": \"foo.com\",\n                                    \"healthy\": {\n                                        \"interval\": 1,\n                                        \"successes\": 1\n                                    },\n                                    \"unhealthy\": {\n                                        \"interval\": 1,\n                                        \"http_failures\": 2\n                                    }\n                                }\n                            }\n                        }\n                    }]]\n                )\n\n                ngx.sleep(0.1)\n\n                local code, body = t('/server_port', ngx.HTTP_GET)\n                ngx.say(\"code: \", code, \" body: \", body)\n\n                code, body = t('/apisix/admin/routes/' .. i, ngx.HTTP_DELETE)\n                ngx.say(\"delete code: \", code)\n\n                ngx.sleep(0.1)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ncode: 200 body: passed\ndelete code: 200\ncode: 200 body: passed\ndelete code: 200\ncode: 200 body: passed\ndelete code: 200\n\n\n\n=== TEST 12: add route (test health check config `host` valid)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1988\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: test health check config `host` valid\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n\n            ngx.sleep(4)\n\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200\n--- grep_error_log eval\nqr/^.*?\\[warn\\].*/\n--- grep_error_log_out eval\nqr/unhealthy TCP increment.*foo.com/\n--- timeout: 5\n\n\n\n=== TEST 14: add route (test health check customized `port`)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"port\": 1988,\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: test health check customized `port`\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n\n            ngx.sleep(4)\n\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- response_body\n200\n--- grep_error_log eval\nqr/^.*?\\[warn\\].*/\n--- grep_error_log_out eval\nqr/unhealthy TCP increment.*foo.com.*127.0.0.1:1988/\n--- timeout: 5\n\n\n\n=== TEST 16: add route (test health check customized `port` out of minimum range)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"port\": 0,\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/expected 0 to be at least 1/\n--- error_code chomp\n400\n\n\n\n=== TEST 17: add route (test health check customized `port` out of maximum range)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"port\": 65536,\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/expected 65536 to be at most 65535/\n--- error_code chomp\n400\n\n\n\n=== TEST 18: set route + upstream (two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1,\n                        \"127.0.0.1:1970\": 1\n                    },\n                    \"checks\": {\n                        \"active\": {\n                            \"http_path\": \"/status\",\n                            \"host\": \"foo.com\",\n                            \"healthy\": {\n                                \"interval\": 1,\n                                \"successes\": 1\n                            },\n                            \"unhealthy\": {\n                                \"interval\": 1,\n                                \"http_failures\": 2\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 19: hit routes, ensure the checker is bound to the upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/\\([^)]+\\) unhealthy .* for '.*'/\n--- grep_error_log_out\n(upstream#/apisix/upstreams/1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\n(upstream#/apisix/upstreams/1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 10\n"
  },
  {
    "path": "t/node/healthcheck2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: can't use service_name with nodes\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\nupstreams:\n    - service_name: abaaba\n      discovery_type: eureka\n      nodes:\n        \"127.0.0.1:80\": 1\n      type: roundrobin\n      id: 1\n#END\n--- error_log\nvalue should match only one schema, but matches both schemas 1 and 2\n--- request\nGET /hello\n--- error_code: 502\n\n\n\n=== TEST 2: route + service\n--- apisix_yaml\nservices:\n    - id: 1\n      upstream:\n          type: roundrobin\n          nodes:\n              \"127.0.0.1:1980\": 1\n              \"127.0.0.1:1970\": 1\n          checks:\n              active:\n                  http_path: /status\n                  host: foo.com\n                  healthy:\n                      interval: 1\n                      successes: 1\n                  unhealthy:\n                      interval: 1\n                      http_failures: 2\nroutes:\n  - service_id: 1\n    uri: /server_port\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.sleep(2.5)\n            ngx.exit(200)\n        }\n    }\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/\\([^)]+\\) unhealthy .* for '.*'/\n--- grep_error_log_out\n(upstream#/services/1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\n(upstream#/services/1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 10\n\n\n\n=== TEST 3: route override service\n--- apisix_yaml\nservices:\n    - id: 1\n      upstream:\n          type: roundrobin\n          nodes:\n              \"127.0.0.2:1980\": 1\n              \"127.0.0.2:1970\": 1\n          checks:\n              active:\n                  http_path: /status\n                  host: foo.com\n                  healthy:\n                      interval: 1\n                      successes: 1\n                  unhealthy:\n                      interval: 1\n                      http_failures: 2\nroutes:\n  - service_id: 1\n    uri: /server_port\n    upstream:\n        type: roundrobin\n        nodes:\n            \"127.0.0.1:1980\": 1\n            \"127.0.0.1:1970\": 1\n        checks:\n            active:\n                http_path: /status\n                host: foo.com\n                healthy:\n                    interval: 1\n                    successes: 1\n                unhealthy:\n                    interval: 1\n                    http_failures: 2\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            ngx.sleep(2.5)\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- response_body\n[{\"count\":12,\"port\":\"1980\"}]\n--- grep_error_log eval\nqr/\\([^)]+\\) unhealthy .* for '.*'/\n--- grep_error_log_out\n(upstream#/routes/arr_1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\n(upstream#/routes/arr_1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 10\n\n\n\n=== TEST 4: pass the configured host (pass_host == \"pass\")\n--- apisix_yaml\nroutes:\n    - id: 1\n      uri: /server_port\n      upstream:\n          type: roundrobin\n          nodes:\n              \"localhost:1980\": 1\n              \"127.0.0.1:1981\": 1\n          checks:\n              active:\n                  http_path: /status\n                  healthy:\n                      interval: 1\n                      successes: 1\n                  unhealthy:\n                      interval: 1\n                      http_failures: 2\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3)\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n            ngx.sleep(3)\n            --- active health check is created async after at least 1 second so it will take effect\n            --- from next request. And first request will just trigger it.\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n            ngx.sleep(3)\n        }\n    }\n--- error_log\nclient request host: 127.0.0.1\n--- timeout: 10\n\n\n\n=== TEST 5: pass the configured host (pass_host == \"node\")\n--- apisix_yaml\nroutes:\n    - id: 1\n      uri: /server_port\n      upstream:\n          type: roundrobin\n          pass_host: node\n          nodes:\n              \"localhost:1980\": 1\n              \"127.0.0.1:1981\": 1\n          checks:\n              active:\n                  http_path: /status\n                  healthy:\n                      interval: 1\n                      successes: 1\n                  unhealthy:\n                      interval: 1\n                      http_failures: 2\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n\n            --- active health check is created async after at least 1 second so it will take effect\n            --- from next request. And first request will just trigger it.\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n            ngx.sleep(3)\n        }\n    }\n--- error_log\nclient request host: localhost\nclient request host: 127.0.0.1\n--- timeout: 10\n\n\n\n=== TEST 6: pass the configured host (pass_host == \"rewrite\")\n--- apisix_yaml\nroutes:\n    - id: 1\n      uri: /server_port\n      upstream:\n          type: roundrobin\n          pass_host: rewrite\n          upstream_host: foo.com\n          nodes:\n              \"localhost:1980\": 1\n              \"127.0.0.1:1981\": 1\n          checks:\n              active:\n                  http_path: /status\n                  healthy:\n                      interval: 1\n                      successes: 1\n                  unhealthy:\n                      interval: 1\n                      http_failures: 2\n#END\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n            --- active health check is created async after at least 1 second so it will take effect\n            --- from next request. And first request will just trigger it.\n            do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            end\n            ngx.sleep(3)\n        }\n    }\n--- no_error_log\nclient request host: localhost\nclient request host: 127.0.0.1\n--- error_log\nclient request host: foo.com\n--- timeout: 10\n"
  },
  {
    "path": "t/node/healthcheck3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two healthy upstream nodes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"checks\": {\n                            \"active\": {\n                                \"http_path\": \"/status\",\n                                \"host\": \"foo.com\",\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"http_failures\": 2\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/^.*?\\[error\\](?!.*process exiting).*/\n--- grep_error_log_out\n\n\n\n=== TEST 2: In case of concurrency only one request can create a checker\n--- config\n    location /t {\n        content_by_lua_block {\n            local healthcheck = require(\"resty.healthcheck\")\n            local test = healthcheck.new\n            healthcheck.new = function(...)\n                ngx.sleep(1)\n                return test(...)\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local send_request = function()\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.log(ngx.ERR, err)\n                    return\n                end\n            end\n\n            local t = {}\n\n            for i = 1, 10 do\n                local th = assert(ngx.thread.spawn(send_request))\n                table.insert(t, th)\n            end\n\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n            ngx.sleep(4)\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/create new checker/\n--- grep_error_log_out\ncreate new checker\n"
  },
  {
    "path": "t/node/healthchecker-independent-upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: using route with an upstream id reference should also trigger healthcheck_manager\n--- extra_init_by_lua\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n\n        count = count + 1\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n        if domain == \"test2.com\" then\n            return {address = \"127.0.0.\" .. count+100}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"test1.com:1980\": 1,\n                        \"test2.com:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\",\n                    \"checks\": {\n                        \"active\": {\n                            \"http_path\": \"/status\",\n                            \"healthy\": {\n                                \"interval\": 1,\n                                \"successes\": 4\n                            },\n                            \"unhealthy\": {\n                                \"interval\": 1,\n                                \"http_failures\": 1\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            for _, _ in ipairs({1, 2, 3, 4, 5}) do\n                code, body = t('/hello', ngx.HTTP_GET)\n                if code >= 300 then\n                    ngx.status = code\n                    return\n                end\n                ngx.sleep(1)\n            end\n            ngx.say(body)\n        }\n    }\n--- timeout: 10\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/create new checker: table:/\n--- grep_error_log_out\ncreate new checker: table:\ncreate new checker: table:\ncreate new checker: table:\ncreate new checker: table:\ncreate new checker: table:\n"
  },
  {
    "path": "t/node/hosts.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"hosts\": [\"foo.com\", \"*.bar.com\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /hello\n--- more_headers\nHost: not_found.com\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: www.bar.com\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/http_host.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/uri\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- request\nGET /uri\n--- more_headers\nHost: foo.com:1984\n--- response_body\nuri: /uri\nhost: foo.com:1984\nx-real-ip: 127.0.0.1\n"
  },
  {
    "path": "t/node/https-proxy.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add route to HTTPS upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"scheme\": \"https\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1983\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit the upstream\n--- request\nGET /hello\n--- more_headers\nhost: www.sni.com\n--- error_log\nReceive SNI: www.sni.com\n\n\n\n=== TEST 3: use 443 as the default port\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        scheme: https\n        nodes:\n            \"127.0.0.1\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nupstream: \"https://127.0.0.1:443/hello\"\n\n\n\n=== TEST 4: use 80 as the http's default port\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1\": 1\n        type: roundrobin\n#END\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nupstream: \"http://127.0.0.1:80/hello\"\n\n\n\n=== TEST 5: rewrite SNI\n--- log_level: debug\n--- apisix_yaml\nroutes:\n  -\n    uri: /uri\n    upstream:\n        scheme: https\n        nodes:\n            \"127.0.0.1:1983\": 1\n        type: roundrobin\n        pass_host: \"rewrite\"\n        upstream_host: \"www.test.com\"\n#END\n--- request\nGET /uri\n--- more_headers\nhost: www.sni.com\n--- error_log\nReceive SNI: www.test.com\n--- response_body\nuri: /uri\nhost: www.test.com\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 6: node's SNI\n--- log_level: debug\n--- apisix_yaml\nroutes:\n  -\n    uri: /uri\n    upstream:\n        scheme: https\n        nodes:\n            \"localhost:1983\": 1\n        type: roundrobin\n        pass_host: \"node\"\n#END\n--- request\nGET /uri\n--- more_headers\nhost: www.sni.com\n--- error_log\nReceive SNI: localhost\n--- response_body\nuri: /uri\nhost: localhost:1983\nx-real-ip: 127.0.0.1\n"
  },
  {
    "path": "t/node/invalid-port.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream with a invalid node port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"nodes\": [{\n                        \"port\": 65536,\n                        \"host\": \"127.0.0.1\",\n                        \"weight\": 1\n                    }],\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            ngx.status = code\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"invalid configuration: property \\\\\\\"nodes\\\\\\\" validation failed: object matches none of the required\"}\n\n\n\n=== TEST 2: set upstream with a node port greater than 65535\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"nodes\": {\n                        \"127.0.0.1:65536\": 1\n                     }\n                }]]\n                )\n\n            ngx.status = code\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"invalid port 65536\"}\n\n\n\n=== TEST 3: set upstream with a node port less than 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                     \"nodes\": {\n                     \"127.0.0.1:0\": 1\n                     }\n                }]]\n                )\n\n            ngx.status = code\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"invalid port 0\"}\n"
  },
  {
    "path": "t/node/invalid-route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set invalid route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/routes/1\", [[mexxxxxxxxxxxxxxx]])\n\n            if res.status >= 300 then\n                res.status = code\n            end\n\n            ngx.print(require(\"toolkit.json\").encode(res.body))\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/routes/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n--- response_body_like eval\nqr/\"value\":\"mexxxxxxxxxxxxxxx\"/\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/routes/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n\n\n\n=== TEST 3: set valid route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/routes/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n\n\n\n=== TEST 4: no error log\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 5: set route(with invalid host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"key\": \"remote_addr\",\n                        \"type\": \"chash\",\n                        \"nodes\": {\n                            \"xxxx.invalid:1980\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route\n--- request\nGET /server_port\n--- error_code: 503\n--- error_log\nfailed to parse domain: xxxx.invalid\n--- timeout: 10\n"
  },
  {
    "path": "t/node/invalid-service.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set invalid service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/services/1\", [[mexxxxxxxxxxxxxxx]])\n\n            if res.status >= 300 then\n                ngx.status = code\n                return ngx.say(res.body)\n            end\n\n            ngx.print(require(\"toolkit.json\").encode(res.body))\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/services/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n--- response_body_like eval\nqr/\"value\":\"mexxxxxxxxxxxxxxx\"/\n\n\n\n=== TEST 2: try /not_found, got error log\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/services/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n\n\n\n=== TEST 3: set valid service(id: 1), cover the old one\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/services/1\", core.json.decode([[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }\n            }]]))\n\n            if res.status >= 300 then\n                ngx.status = code\n            end\n\n            ngx.print(require(\"toolkit.json\").encode(res.body))\n        }\n    }\n--- request\nGET /t\n--- ret_code: 200\n--- response_body_like eval\nqr/\"nodes\":\\{\"127.0.0.1:1980\":1\\}/\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/services/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n\n\n\n=== TEST 4: no error log\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n"
  },
  {
    "path": "t/node/invalid-upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set invalid upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/upstreams/1\", [[mexxxxxxxxxxxxxxx]])\n\n            if res.status >= 300 then\n                res.status = code\n            end\n\n            ngx.print(require(\"toolkit.json\").encode(res.body))\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/upstreams/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n--- response_body_like eval\nqr/\"value\":\"mexxxxxxxxxxxxxxx\"/\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- wait: 1\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/upstreams/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n\n\n\n=== TEST 3: delete invalid upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.delete(\"/upstreams/1\")\n\n            if res.status >= 300 then\n                res.status = code\n            end\n\n            ngx.say(\"passed\")\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr{invalid item data of \\[/apisix/upstreams/1\\], val: mexxxxxxxxxxxxxxx, it should be an object}\n\n\n\n=== TEST 4: set valid upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/upstreams/1\", core.json.decode([[{\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]))\n            if res.status >= 300 then\n                res.status = code\n            end\n            ngx.print(require(\"toolkit.json\").encode(res.body))\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/\"nodes\":\\{\"127.0.0.1:1980\":1\\}/\n\n\n\n=== TEST 5: no error log\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n"
  },
  {
    "path": "t/node/least_conn.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    my $route = <<_EOC_;\nroutes:\n  - upstream_id: 1\n    uris:\n      - /mysleep\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $route);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /mysleep?seconds=0.1\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: select highest weight\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        \"127.0.0.1:1980\": 2\n        \"127.0.0.1:1981\": 1\n--- grep_error_log eval\nqr/proxy request to \\S+ while connecting to upstream/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1980 while connecting to upstream\n\n\n\n=== TEST 2: select least conn\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        \"127.0.0.1:1980\": 3\n        \"0.0.0.0:1980\": 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/mysleep?seconds=0.1\"\n\n            local t = {}\n            for i = 1, 3 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri..i, {method = \"GET\"})\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/proxy request to \\S+ while connecting to upstream/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1980 while connecting to upstream\nproxy request to 0.0.0.0:1980 while connecting to upstream\nproxy request to 127.0.0.1:1980 while connecting to upstream\n\n\n\n=== TEST 3: retry\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        \"127.0.0.1:1999\": 2\n        \"127.0.0.1:1980\": 1\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+ while connecting to upstream/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1999 while connecting to upstream\nproxy request to 127.0.0.1:1980 while connecting to upstream\n\n\n\n=== TEST 4: retry all nodes, failed\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        \"127.0.0.1:1999\": 2\n        \"0.0.0.0:1999\": 1\n--- error_log\nconnect() failed\n--- error_code: 502\n--- grep_error_log eval\nqr/proxy request to \\S+ while connecting to upstream/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1999 while connecting to upstream\nproxy request to 0.0.0.0:1999 while connecting to upstream\n"
  },
  {
    "path": "t/node/least_conn2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: upstream across multiple routes should not share the same version\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"type\": \"least_conn\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 3,\n                        \"0.0.0.0:1980\": 2\n                    }\n                 }]]\n            )\n            assert(code < 300, body)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"host\": \"1.com\",\n                    \"uri\": \"/mysleep\",\n                    \"upstream_id\": \"1\"\n                 }]]\n            )\n            assert(code < 300, body)\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"host\": \"2.com\",\n                    \"uri\": \"/mysleep\",\n                    \"upstream_id\": \"1\"\n                 }]]\n            )\n            assert(code < 300, body)\n        }\n    }\n\n\n\n=== TEST 2: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/mysleep?seconds=0.1\"\n\n            local t = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri, {headers = {Host = i..\".com\"}})\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n        }\n    }\n--- grep_error_log eval\nqr/proxy request to \\S+ while connecting to upstream/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1980 while connecting to upstream\nproxy request to 0.0.0.0:1980 while connecting to upstream\n"
  },
  {
    "path": "t/node/merge-route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nworker_connections(256);\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route (different upstream)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.2)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_port\",\n                    \"service_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /server_port\n--- response_headers\nX-RateLimit-Limit: 2\nX-RateLimit-Remaining: 1\n--- response_body eval\nqr/1981/\n\n\n\n=== TEST 5: set route with empty plugins, should do nothing\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.2)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {},\n                    \"uri\": \"/server_port\",\n                    \"service_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes\n--- request\nGET /server_port\n--- response_headers\nX-RateLimit-Limit: 2\nX-RateLimit-Remaining: 1\n--- response_body eval\nqr/1980/\n\n\n\n=== TEST 7: disable plugin `limit-count`\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.2)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"_meta\": {\n                                \"disable\": true\n                            }\n                        }\n                    },\n                    \"uri\": \"/server_port\",\n                    \"service_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes\n--- request\nGET /server_port\n--- raw_response_headers_unlike eval\nqr/X-RateLimit-Limit/\n--- response_body eval\nqr/1980/\n\n\n\n=== TEST 9: hit routes two times, checker service configuration\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(0.5)\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/server_port',\n            ngx.HTTP_GET\n        )\n        ngx.say(body)\n\n        code, body = t('/server_port',\n            ngx.HTTP_GET\n        )\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_log eval\n[qr/merge_service_route.*\"time_window\":60/,\nqr/merge_service_route.*\"time_window\":60/]\n\n\n\n=== TEST 10: set service(only upstream with host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"scheme\": \"http\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"test.com:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: set route(bind service 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/fake\",\n                    \"host\": \"test.com\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/echo\"\n                        }\n                    },\n                    \"service_id\": \"1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route\n--- request\nGET /fake\n--- more_headers\nhost: test.com\n--- response_headers\nhost: test.com\n\n\n\n=== TEST 13: not hit route\n--- request\nGET /fake\n--- more_headers\nhost: test.comxxx\n--- error_code: 404\n\n\n\n=== TEST 14: enabled websocket in service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"enable_websocket\": true,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: set route(bind service 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/33',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/uri\",\n                    \"service_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route\n--- request\nGET /uri\n--- response_body\nuri: /uri\nconnection: close\nhost: localhost\nx-real-ip: 127.0.0.1\n--- error_log\nenabled websocket for route: 33\n\n\n\n=== TEST 17: delete rout\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/33',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: labels exist if only route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        ngx.say(core.json.encode(ctx.matched_route.value.labels));\n                                        end\"]\n                        }\n                    },\n                    \"labels\": {\n                        \"version\": \"v2\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: hit routes\n--- request\nGET /hello\n--- response_body\n{\"version\":\"v2\"}\n\n\n\n=== TEST 20: labels exist if merge route and service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(0.6) -- wait for sync\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        ngx.say(core.json.encode(ctx.matched_route.value.labels));\n                                        end\"]\n                        }\n                    },\n                    \"labels\": {\n                        \"version\": \"v2\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"service_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: hit routes\n--- request\nGET /hello\n--- response_body\n{\"version\":\"v2\"}\n"
  },
  {
    "path": "t/node/not-exist-service.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: invalid service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/routes/1\", {\n                    service_id = \"999999999\",\n                    uri = \"/hello\"\n                })\n\n\n            if res.status >= 300 then\n                ngx.status = res.status\n                return ngx.exit(res.status)\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- request\nGET /hello\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n--- wait_etcd_sync: 0.3\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr/failed to fetch service configuration by id/\n\n\n\n=== TEST 3: set valid route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/not-exist-upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {},\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: hit routes\n--- request\nGET /hello\n--- error_code_like: ^(?:50\\d)$\n--- response_body eval\nqr/502 Bad Gateway|503 Service Temporarily Unavailable/\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr/missing upstream configuration in Route or Service/\n"
  },
  {
    "path": "t/node/plugin-configs.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: change plugin config will cause the conf_version change\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"hello\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugin_config_id\": 1,\n                    \"plugins\": {\n                        \"example-plugin\": {\n                            \"i\": 1\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, err, org_body = t('/hello')\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(org_body)\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"world\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, err, org_body = t('/hello')\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(org_body)\n        }\n    }\n--- response_body\nhello\nworld\n--- grep_error_log eval\nqr/conf_version: \\d+#\\d+/\n--- grep_error_log_out eval\nqr/conf_version: \\d+#\\d+\nconf_version: \\d+#\\d+\n/\n\n\n\n=== TEST 2: validated plugins configuration via incremental sync\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local core = require(\"apisix.core\")\n\n            assert(core.etcd.set(\"/plugin_configs/1\",\n                {id = 1, plugins = { [\"uri-blocker\"] = { block_rules =  {\"root.exe\",\"root.m+\"} }}}\n            ))\n            -- wait for sync\n            ngx.sleep(0.6)\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello?x=root.exe\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.status = res.status\n            ngx.say(uri)\n            ngx.say(res.body)\n\n        }\n    }\n--- error_code: 403\n\n\n\n=== TEST 3: validated plugins configuration via incremental sync (malformed data)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local core = require(\"apisix.core\")\n\n            assert(core.etcd.set(\"/plugin_configs/1\",\n                {id = 1, plugins = { [\"uri-blocker\"] = { block_rules =  1 }}}\n            ))\n            -- wait for sync\n            ngx.sleep(0.6)\n\n            assert(core.etcd.delete(\"/plugin_configs/1\"))\n        }\n    }\n--- error_log\nproperty \"block_rules\" validation failed\n\n\n\n=== TEST 4: recover plugin when plugin_config changed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"hello\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugin_config_id\": 1\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, err, org_body = t('/hello')\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(org_body)\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, err, org_body = t('/hello')\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.print(org_body)\n        }\n    }\n--- response_body\nhello\nhello world\n\n\n\n=== TEST 5: don't override the plugin in the route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                        },\n                        \"response-rewrite\": {\n                            \"body\": \"hello\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/helloaa\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugin_config_id\": 1,\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"world\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, err, org_body = t('/helloaa')\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(org_body)\n        }\n    }\n--- response_body\nworld\n\n\n\n=== TEST 6: use the latest plugin_consigs after merge the plugins from consumer and route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\"]\n                        },\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugin_config_id\": \"1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local headers = {\n                [\"Authorization\"] = \"Basic Zm9vOmJhcg==\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            ngx.print(res.body)\n\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\", \"127.0.0.1\"]\n                        },\n                        \"basic-auth\": {}\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\nhello world\n\n\n\n=== TEST 7: configure plugin configs with limit-count to check parent association\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugin_config_id\": 1\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n"
  },
  {
    "path": "t/node/plugin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set custom log format\n--- extra_init_by_lua\n    local exp = require(\"apisix.plugins.example-plugin\")\n    exp.destroy = function()\n        ngx.log(ngx.WARN, \"destroy method called\")\n    end\n--- config\n    location /t {\n        return 200 \"dummy\";\n    }\n--- shutdown_error_log\ndestroy method called\n"
  },
  {
    "path": "t/node/plugin1.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse strict;\nuse warnings FATAL => 'all';\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /apisix/admin/routes\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\nrun_tests;\n__DATA__\n\n=== TEST 1: set up configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                          \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {},\n                            \"proxy-rewrite\": {\n                               \"headers\": {\n                                   \"add\": {\n                                      \"xtest\": \"123\"\n                                    }\n                               }\n                            },\n                            \"serverless-post-function\": {\n                              \"functions\": [\n                                \"return function(conf, ctx) \\n ngx.say(ngx.req.get_headers().xtest); \\n end\"\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- timeout: 15\n--- response_body\npassed\n\n\n\n=== TEST 2: the proxy-rewrite runs at 'rewrite' phase and should get executed only once, hence the response body is expected '123' not '123123'\n--- request\nGET /hello\n--- more_headers\napikey: auth-jack\n--- timeout: 15\n--- response_body\n123\n"
  },
  {
    "path": "t/node/priority-balancer/health-checker.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ($block->apisix_yaml) {\n        if (!$block->yaml_config) {\n            my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n            $block->set_value(\"yaml_config\", $yaml_config);\n        }\n\n        my $route = <<_EOC_;\nroutes:\n  - upstream_id: 1\n    uris:\n      - /hello\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $route);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: all are down detected by health checker\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 123\n        - host: 127.0.0.2\n          port: 1979\n          weight: 3\n          priority: -1\n    checks:\n        active:\n            http_path: \"/status\"\n            healthy:\n                interval: 1\n                successes: 1\n            unhealthy:\n                interval: 1\n                http_failures: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local httpc = http.new()\n            httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(2.5)\n            -- still use all nodes\n            httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(2.5)\n        }\n    }\n--- request\nGET /t\n--- error_log\nconnect() failed\nunhealthy TCP increment (2/2) for '127.0.0.1(127.0.0.1:1979)\nunhealthy TCP increment (2/2) for '127.0.0.2(127.0.0.2:1979)\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\n--- timeout: 8\n\n\n\n=== TEST 2: use priority as backup (setup rule)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": [\n                                {\"host\": \"127.0.0.1\", \"port\": 1979, \"weight\": 2000},\n                                {\"host\": \"127.0.0.1\", \"port\": 1980,\n                                 \"weight\": 1, \"priority\": -1}\n                            ],\n                            \"checks\": {\n                                \"active\": {\n                                    \"http_path\": \"/status\",\n                                    \"healthy\": {\n                                        \"interval\": 1,\n                                        \"successes\": 1\n                                    },\n                                    \"unhealthy\": {\n                                        \"interval\": 1,\n                                        \"http_failures\": 1\n                                    }\n                                }\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: use priority as backup\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local httpc = http.new()\n            httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(2.5)\n            httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(2.5)\n        }\n    }\n--- request\nGET /t\n--- error_log\nconnect() failed\nunhealthy TCP increment (2/2) for '127.0.0.1(127.0.0.1:1979)\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.1:1980\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.1:1980\n--- timeout: 8\n"
  },
  {
    "path": "t/node/priority-balancer/sanity.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2); # repeat each test to ensure after_balance is called correctly\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ($block->apisix_yaml) {\n        if (!$block->yaml_config) {\n            my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n            $block->set_value(\"yaml_config\", $yaml_config);\n        }\n\n        my $route = <<_EOC_;\nroutes:\n  - upstream_id: 1\n    uris:\n        - /hello\n        - /mysleep\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $route);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 1\n        - host: 127.0.0.3\n          port: 1979\n          weight: 2\n          priority: 0\n        - host: 127.0.0.4\n          port: 1979\n          weight: 1\n          priority: 0\n        - host: 127.0.0.1\n          port: 1980\n          weight: 2\n          priority: -1\n--- response_body\nhello world\n--- error_log\nconnect() failed\nfailed to get server from current priority 1, try next one\nfailed to get server from current priority 0, try next one\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.3:1979\nproxy request to 127.0.0.4:1979\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 2: all failed\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 0\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: -1\n--- error_code: 502\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.1:1979\n\n\n\n=== TEST 3: default priority is zero\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n        - host: 127.0.0.1\n          port: 1980\n          weight: 2\n          priority: -1\n--- response_body\nhello world\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 4: least_conn\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: least_conn\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.1\n          port: 1980\n          weight: 3\n          priority: -1\n        - host: 0.0.0.0\n          port: 1980\n          weight: 2\n          priority: -1\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/mysleep?seconds=0.1\"\n\n            local t = {}\n            for i = 1, 3 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    -- the retry can be happened before starting the new request\n                    -- so we exclude all the first tries from the expected log\n                    local res, err = httpc:request_uri(uri..i, {method = \"GET\"})\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n        }\n    }\n--- request\nGET /t\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+:1980 while connecting to upstream/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1980 while connecting to upstream\nproxy request to 0.0.0.0:1980 while connecting to upstream\nproxy request to 127.0.0.1:1980 while connecting to upstream\n\n\n\n=== TEST 5: roundrobin\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: roundrobin\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 1000\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 1\n        - host: 127.0.0.3\n          port: 1979\n          weight: 1000\n          priority: -1\n        - host: 127.0.0.4\n          port: 1979\n          weight: 1\n          priority: -1\n--- error_code: 502\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.3:1979\nproxy request to 127.0.0.4:1979\n\n\n\n=== TEST 6: ewma\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: ewma\n    key: remote_addr\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 0\n        - host: 127.0.0.3\n          port: 1979\n          weight: 2\n          priority: -1\n--- error_code: 502\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.3:1979\n\n\n\n=== TEST 7: chash\n--- apisix_yaml\nupstreams:\n  - id: 1\n    type: chash\n    key: remote_addr\n    nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 1\n        - host: 127.0.0.3\n          port: 1979\n          weight: 2\n          priority: -1\n        - host: 127.0.0.4\n          port: 1979\n          weight: 1\n          priority: -1\n--- error_code: 502\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.4:1979\nproxy request to 127.0.0.3:1979\n"
  },
  {
    "path": "t/node/remote-addr-ipv6.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route: remote addr = ::1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"remote_addr\": \"::1\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: IPv6 /not_found\n--- listen_ipv6\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(0.2)\n        local t = require(\"lib.test_admin\").test_ipv6\n        t('/not_found')\n    }\n}\n--- request\nGET /t\n--- response_body eval\nqr/\"error_msg\":\"404 Route Not Found\"/\n\n\n\n=== TEST 3: IPv4 /not_found\n--- listen_ipv6\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body eval\nqr/\"error_msg\":\"404 Route Not Found\"/\n\n\n\n=== TEST 4: IPv6 /hello\n--- listen_ipv6\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(0.2)\n        local t = require(\"lib.test_admin\").test_ipv6\n        t('/hello')\n    }\n}\n--- request\nGET /t\n--- response_body eval\nqr{connected: 1\nrequest sent: 59\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived:\nreceived: hello world\nfailed to receive a line: closed \\[\\]\nclose: 1 nil}\n\n\n\n=== TEST 5: IPv4 /hello\n--- listen_ipv6\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n"
  },
  {
    "path": "t/node/remote-addr.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route: remote addr = 127.0.0.1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"remote_addr\": \"127.0.0.1\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: set route: remote addr = 127.0.0.2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"remote_addr\": \"127.0.0.2\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: not hit route: 127.0.0.2 =~ 127.0.0.1\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 6: remote addr: 127.0.0.3/24 =~ 127.0.0.1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"remote_addr\": \"127.0.0.3/24\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/remote_addrs.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"remote_addrs\": [\"192.0.0.0/8\", \"127.0.0.3\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- http_config\nreal_ip_header X-Real-IP;\nset_real_ip_from 127.0.0.1;\nset_real_ip_from unix:;\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- http_config\nreal_ip_header X-Real-IP;\nset_real_ip_from 127.0.0.1;\nset_real_ip_from unix:;\n--- request\nGET /hello\n--- more_headers\nHost: not_found.com\n--- error_code: 404\n\n\n\n=== TEST 4: hit routes\n--- http_config\nreal_ip_header X-Real-IP;\nset_real_ip_from 127.0.0.1;\nset_real_ip_from unix:;\n--- request\nGET /hello\n--- more_headers\nX-Real-IP: 192.168.1.100\n--- response_body\nhello world\n\n\n\n=== TEST 5: hit routes\n--- http_config\nreal_ip_header X-Real-IP;\nset_real_ip_from 127.0.0.1;\nset_real_ip_from unix:;\n--- request\nGET /hello\n--- more_headers\nX-Real-IP: 127.0.0.3\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/route-delete.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: clear all routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 200 do\n                t('/apisix/admin/routes/' .. i, ngx.HTTP_DELETE)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- timeout: 5\n\n\n\n=== TEST 2: create 106 routes + delete them\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 106 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            for i = 1, 106 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            for i = 1, 106 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_DELETE\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            for i = 1, 106 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. i .. [[\"\n                    }]]\n                )\n            end\n\n            ngx.sleep(0.5)\n\n            for i = 1, 106 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_DELETE\n                )\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- wait: 1\n--- grep_error_log eval\nqr/\\w+ (data by key: 103)/\n--- grep_error_log_out\ninsert data by key: 103\ninsert data by key: 103\nupdate data by key: 103\nupdate data by key: 103\ndelete data by key: 103\ndelete data by key: 103\ninsert data by key: 103\ninsert data by key: 103\ndelete data by key: 103\ndelete data by key: 103\n--- timeout: 30\n"
  },
  {
    "path": "t/node/route-domain-with-local-dns.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    # for test\n    $ENV{ENABLE_LOCAL_DNS} = \"true\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1,\n                                \"www.apiseven.com:80\": 0\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_log eval\nqr/.*init_resolver\\(\\): dns resolver \\[.+\\]/\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n--- error_log eval\nqr/.*init_resolver\\(\\): dns resolver \\[.+\\]/\n\n\n\n=== TEST 3: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/dns resolver domain: www.apiseven.com to \\d+.\\d+.\\d+.\\d+/\n"
  },
  {
    "path": "t/node/route-domain.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1,\n                                \"www.apiseven.com:80\": 0\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/dns resolver domain: www.apiseven.com to \\d+.\\d+.\\d+.\\d+/\n--- timeout: 10\n\n\n\n=== TEST 4: set route(id: 1, using `rewrite` mode to pass upstream host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\",\n                            \"pass_host\": \"rewrite\",\n                            \"upstream_host\": \"test.com\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route\n--- request\nGET /echo\n--- response_headers\nhost: test.com\n\n\n\n=== TEST 6: set route(id: 1, using `node` mode to pass upstream host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"test.com:1980\": 1\n                            },\n                            \"type\": \"roundrobin\",\n                            \"desc\": \"new upstream\",\n                            \"pass_host\": \"node\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route\n--- request\nGET /echo\n--- response_headers\nhost: test.com:1980\n\n\n\n=== TEST 8: test domain with roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"localhost:1981\": 2,\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_port\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 3 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1981, 1981\n"
  },
  {
    "path": "t/node/route-filter-func.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"filter_func\": \"function(vars) return vars.arg_name == 'json' end\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: not hit: name=unknown\n--- request\nGET /hello?name=unknown\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: hit routes\n--- request\nGET /hello?name=json\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/route-host.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"host\": \"foo.com\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /hello\n--- more_headers\nHost: not_found.com\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 6: hit routes, uppercase\n--- request\nGET /hello\n--- more_headers\nHost: FOO.com\n--- response_body\nhello world\n\n\n\n=== TEST 7: set route(host is uppercase)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"host\": \"FOO.com\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 9: hit routes, uppercase\n--- request\nGET /hello\n--- more_headers\nHost: FOO.com\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/route-status.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: default enable route(id: 1) with uri match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 3: disable route\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local data = {status = 0}\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                core.json.encode(data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: route not found, failed by disable\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: default enable route(id: 1) with host_uri match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 7: disable route\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n\n            local data = {status = 0}\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                core.json.encode(data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: route not found, failed by disable\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 9: specify an invalid status value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"status\": 100,\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid configuration: property \\\\\"status\\\\\" validation failed: matches none of the enum values\"\\}/\n\n\n\n=== TEST 10: compatible with old route data in etcd which not has status\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res, err = core.etcd.set(\"/routes/1\", core.json.decode([[{\n                    \"uri\": \"/hello\",\n                    \"priority\": 0,\n                    \"id\": \"1\",\n                    \"upstream\": {\n                        \"hash_on\": \"vars\",\n                        \"pass_host\": \"pass\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]))  ---mock old route data in etcd\n            if res.status >= 300 then\n                res.status = code\n            end\n            ngx.print(require(\"toolkit.json\").encode(res.body))\n            ngx.sleep(1)\n        }\n    }\n--- request\nGET /t\n--- response_body_unlike eval\nqr/status/\n\n\n\n=== TEST 11: hit route(old route data in etcd)\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/route-uris.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uris\": [\"/hello\",\"/hello1\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n\n\n\n=== TEST 3: hit routes1\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4:  hit routes2\n--- request\nGET /hello1\n--- response_body\nhello1 world\n"
  },
  {
    "path": "t/node/rr-balance.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(two upstream node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":6,\"port\":\"1981\"},{\"count\":6,\"port\":\"1980\"}]\n\n\n\n=== TEST 3: set route(three upstream node)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1,\n                            \"127.0.0.1:1982\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n            --- to trigger the healthchecker\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(3)\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":4,\"port\":\"1982\"},{\"count\":4,\"port\":\"1981\"},{\"count\":4,\"port\":\"1980\"}]\n\n\n\n=== TEST 5: set route(three upstream node and different weight)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 3,\n                            \"127.0.0.1:1981\": 2,\n                            \"127.0.0.1:1982\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n            --- to trigger the healthchecker\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(3)\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":2,\"port\":\"1982\"},{\"count\":4,\"port\":\"1981\"},{\"count\":6,\"port\":\"1980\"}]\n\n\n\n=== TEST 7: set route(weight is 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 3,\n                            \"127.0.0.1:1981\": 0,\n                            \"127.0.0.1:1982\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n            --- to trigger the healthchecker\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.sleep(3)\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[{\"count\":3,\"port\":\"1982\"},{\"count\":9,\"port\":\"1980\"}]\n"
  },
  {
    "path": "t/node/sanity-radixtree.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit routes：/hello\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: hit routes: /hello1\n--- request\nGET /hello1\n--- response_body\nhello1 world\n\n\n\n=== TEST 7: hit routes: /hello2\n--- request\nGET /hello2\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n\n\n\n=== TEST 8: hit routes: /hel\n--- request\nGET /hel\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n"
  },
  {
    "path": "t/node/service-empty.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set empty service. (id: 1)（allow empty `service` object）\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                '{}'\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: route binding empty service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"service_id\": \"1\",\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: /hello\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/ssl-protocols.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nmy $openssl_bin = $ENV{OPENSSL_BIN};\nif (! -x $openssl_bin) {\n    $ENV{OPENSSL_BIN} = '/usr/local/openresty/openssl3/bin/openssl';\n    if (! -x $ENV{OPENSSL_BIN}) {\n        plan(skip_all => \"openssl3 not installed\");\n    }\n}\n\nplan('no_plan');\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - 9100\n  enable_resolv_search_opt: false\n  ssl:\n    ssl_protocols: TLSv1.1 TLSv1.2 TLSv1.3\n    ssl_ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uris\": [\"/hello\", \"/world\"]\n            }]]\n        )\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(message)\n            return\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2:  create ssl for test.com (unset ssl_protocols)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"ssl_protocols\": null,\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: Successfully, access test.com with TLSv1.3\n--- exec\necho -n \"Q\"  | $OPENSSL_BIN s_client -connect 127.0.0.1:1994 -servername test.com -tls1_3 2>&1 | cat\n--- response_body eval\nqr/Server certificate/\n\n\n\n=== TEST 4: Successfully, access test.com with TLSv1.2\n--- exec\ncurl -k -v --tls-max 1.2 --tlsv1.2 --resolve \"test.com:1994:127.0.0.1\" https://test.com:1994/hello 2>&1 | cat\n--- response_body eval\nqr/TLSv1\\.2 \\(IN\\), TLS handshake, Server hello(?s).*hello world/\n\n\n\n=== TEST 5: Successfully, access test.com with TLSv1.1\n--- exec\necho -n \"Q\"  | $OPENSSL_BIN s_client -connect 127.0.0.1:1994 -servername test.com -tls1_1 2>&1 | cat\n--- response_body eval\nqr/Server certificate/\n\n\n\n=== TEST 6: set TLSv1.2 and TLSv1.3 for test.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\", ssl_protocols = {\"TLSv1.2\", \"TLSv1.3\"}}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"ssl_protocols\": [\"TLSv1.2\", \"TLSv1.3\"],\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: Set TLSv1.3 for the test2.com\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"test2.com\", ssl_protocols = {\"TLSv1.3\"}}\n\n        local code, body = t.test('/apisix/admin/ssls/2',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"test2.com\"\n                },\n                \"key\": \"/apisix/ssls/2\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n--- request\nGET /t\n\n\n\n=== TEST 8: Successfully, access test.com with TLSv1.3\n--- exec\necho -n \"Q\"  | $OPENSSL_BIN s_client -connect 127.0.0.1:1994 -servername test.com -tls1_3 2>&1 | cat\n--- response_body eval\nqr/Server certificate/\n\n\n\n=== TEST 9: Successfully, access test.com with TLSv1.2\n--- exec\ncurl -k -v --tls-max 1.2 --tlsv1.2 --resolve \"test.com:1994:127.0.0.1\" https://test.com:1994/hello 2>&1 | cat\n--- response_body eval\nqr/TLSv1\\.2 \\(IN\\), TLS handshake, Server hello(?s).*hello world/\n\n\n\n=== TEST 10: Successfully, access test2.com with TLSv1.3\n--- exec\necho -n \"Q\"  | $OPENSSL_BIN s_client -connect 127.0.0.1:1994 -servername test2.com -tls1_3 2>&1 | cat\n--- response_body eval\nqr/Server certificate/\n\n\n\n=== TEST 11: Failed, access test2.com with TLSv1.2\n--- exec\ncurl -k -v --tls-max 1.2 --tlsv1.2 --resolve \"test2.com:1994:127.0.0.1\" https://test2.com:1994/hello 2>&1 | cat\n--- response_body eval\nqr/TLSv1\\.2 \\(IN\\), TLS alert/\n\n\n\n=== TEST 12: set TLSv1.1 for test.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\", ssl_protocols = {\"TLSv1.1\"}}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"sni\": \"test.com\",\n                        \"ssl_protocols\": [\"TLSv1.1\"],\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: Successfully, access test.com with TLSv1.1\n--- exec\necho -n \"Q\"  | $OPENSSL_BIN s_client -connect 127.0.0.1:1994 -servername test.com -tls1_1 2>&1 | cat\n--- response_body eval\nqr/Server certificate/\n\n\n\n=== TEST 14: Failed, access test.com with TLSv1.3\n--- exec\necho -n \"Q\"  | $OPENSSL_BIN s_client -connect 127.0.0.1:1994 -servername test.com -tls1_3 2>&1 | cat\n--- response_body eval\nqr/tlsv1 alert/\n"
  },
  {
    "path": "t/node/ssl.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    sub set_env_from_file {\n        my ($env_name, $file_path) = @_;\n\n        open my $fh, '<', $file_path or die $!;\n        my $content = do { local $/; <$fh> };\n        close $fh;\n\n        $ENV{$env_name} = $content;\n    }\n    # set env\n    set_env_from_file('TEST_CERT', 't/certs/apisix.crt');\n    set_env_from_file('TEST_KEY', 't/certs/apisix.key');\n    set_env_from_file('TEST2_CERT', 't/certs/test2.crt');\n    set_env_from_file('TEST2_KEY', 't/certs/test2.key');\n}\n\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\n\nsub set_env_from_file {\n    my ($env_name, $file_path) = @_;\n\n    open my $fh, '<', $file_path or die $!;\n    my $content = do { local $/; <$fh> };\n    close $fh;\n\n    $ENV{$env_name} = $content;\n}\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: store two certs and keys in vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/ssl \\\n    test.com.crt=@t/certs/apisix.crt \\\n    test.com.key=@t/certs/apisix.key \\\n    test.com.2.crt=@t/certs/test2.crt \\\n    test.com.2.key=@t/certs/test2.key\n--- response_body\nSuccess! Data written to: kv/apisix/ssl\n\n\n\n=== TEST 2: set secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/secrets/vault/test',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://0.0.0.0:8200\",\n                    \"prefix\": \"kv/apisix\",\n                    \"token\": \"root\"\n                }]],\n                [[{\n                    \"key\": \"/apisix/secrets/vault/test\",\n                    \"value\": {\n                        \"uri\": \"http://0.0.0.0:8200\",\n                        \"prefix\": \"kv/apisix\",\n                        \"token\": \"root\"\n                    }\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: set ssl with two certs and keys in vault\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                snis = {\"test.com\"},\n                key =  \"$secret://vault/test/ssl/test.com.key\",\n                cert = \"$secret://vault/test/ssl/test.com.crt\",\n                keys = {\"$secret://vault/test/ssl/test.com.2.key\"},\n                certs = {\"$secret://vault/test/ssl/test.com.2.crt\"}\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"snis\": [\"test.com\"],\n                        \"key\": \"$secret://vault/test/ssl/test.com.key\",\n                        \"cert\": \"$secret://vault/test/ssl/test.com.crt\",\n                        \"keys\": [\"$secret://vault/test/ssl/test.com.2.key\"],\n                        \"certs\": [\"$secret://vault/test/ssl/test.com.2.crt\"]\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n              )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: access to https with test.com\n--- exec\ncurl -s -k https://test.com:1994/hello\n--- response_body\nhello world\n--- error_log\nfetching data from secret uri\nfetching data from secret uri\nfetching data from secret uri\nfetching data from secret uri\n\n\n\n=== TEST 6: set ssl with two certs and keys in env\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                snis = {\"test.com\"},\n                key =  \"$env://TEST_KEY\",\n                cert = \"$env://TEST_CERT\",\n                keys = {\"$env://TEST2_KEY\"},\n                certs = {\"$env://TEST2_CERT\"}\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"snis\": [\"test.com\"],\n                        \"key\": \"$env://TEST_KEY\",\n                        \"cert\": \"$env://TEST_CERT\",\n                        \"keys\": [\"$env://TEST2_KEY\"],\n                        \"certs\": [\"$env://TEST2_CERT\"]\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n              )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: access to https with test.com\n--- exec\ncurl -s -k https://test.com:1994/hello\n--- response_body\nhello world\n--- error_log\nfetching data from env uri\nfetching data from env uri\nfetching data from env uri\nfetching data from env uri\n"
  },
  {
    "path": "t/node/timeout-upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\",\n                            \"timeout\": {\n                                \"connect\": 0.5,\n                                \"send\": 0.5,\n                                \"read\": 0.5\n                            }\n                        },\n                        \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes (timeout)\n--- request\nGET /mysleep?seconds=1\n--- error_code: 504\n--- response_body eval\nqr/504 Gateway Time-out/\n--- error_log\ntimed out) while reading response header from upstream\n\n\n\n=== TEST 3: set custom timeout for route(overwrite upstream timeout)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"timeout\": {\n                            \"connect\": 0.5,\n                            \"send\": 0.5,\n                            \"read\": 0.5\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\",\n                            \"timeout\": {\n                                \"connect\": 2,\n                                \"send\": 2,\n                                \"read\": 2\n                            }\n                        },\n                        \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes (timeout)\n--- request\nGET /mysleep?seconds=1\n--- error_code: 504\n--- response_body eval\nqr/504 Gateway Time-out/\n--- error_log\ntimed out) while reading response header from upstream\n\n\n\n=== TEST 5: set route inherit hosts from service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local scode, sbody = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                       \"desc\":\"test-service\",\n                       \"hosts\": [\"foo.com\"]\n                }]]\n                )\n\n            if scode >= 300 then\n                ngx.status = scode\n            end\n            ngx.say(sbody)\n\n            local rcode, rbody = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"service_id\": \"1\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\",\n                            \"timeout\": {\n                                \"connect\": 0.5,\n                                \"send\": 0.5,\n                                \"read\": 0.5\n                            }\n                        },\n                        \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if rcode >= 300 then\n                ngx.status = rcode\n            end\n            ngx.say(rbody)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\npassed\n\n\n\n=== TEST 6: hit service route (timeout)\n--- request\nGET /mysleep?seconds=1\n--- more_headers\nHost: foo.com\n--- error_code: 504\n--- response_body eval\nqr/504 Gateway Time-out/\n--- error_log\ntimed out) while reading response header from upstream\n"
  },
  {
    "path": "t/node/upstream-array-nodes.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [{\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 1980,\n                        \"weight\": 1\n                    }],\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1980,\n                                \"weight\": 1\n                            }],\n                            \"type\": \"roundrobin\",\n                            \"desc\": \"new upstream\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: set services(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1980,\n                                \"weight\": 1\n                            }],\n                            \"type\": \"roundrobin\",\n                            \"desc\": \"new upstream\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"service_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/upstream-discovery-dynamic.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: dynamic host based discovery\n--- extra_yaml_config\nnginx_config:\n    worker_processes: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local discovery = require(\"apisix.discovery.init\").discovery\n            local core = require(\"apisix.core\")\n            discovery.demo_discover = {\n                nodes = function()\n                    local demo_nodes_tab = {\n                        a = { host = \"127.0.0.1\", port = 1111 },\n                        b = { host = \"127.0.0.1\", port = 2222 }\n                    }\n                    local host = ngx.var.host\n                    local service_id = host:match(\"([^.]+).myhost.com\")\n                    local demo_node = demo_nodes_tab[service_id]\n\n                    local node_list = core.table.new(1, 0)\n                    core.table.insert(node_list, {\n                        host = demo_node.host,\n                        port = tonumber(demo_node.port),\n                        weight = 100,\n                    })\n\n                    return node_list\n                end\n            }\n\n            local code, body = t('/apisix/admin/services/',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": \"demo_service\",\n                    \"name\": \"demo_service\",\n                    \"upstream\": {\n                        \"discovery_type\": \"demo_discover\",\n                        \"service_name\": \"demo_service\",\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(0.5)\n\n            local code, body = t('/apisix/admin/routes/',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"id\": \"demo_route\",\n                    \"name\": \"demo_route\",\n                    \"uri\": \"/*\",\n                    \"hosts\":[\n                    \"*.myhost.com\"\n                    ],\n                    \"service_id\": \"demo_service\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.sleep(0.5)\n\n            local hosts = {\n                \"a.myhost.com\",\n                \"a.myhost.com\",\n                \"b.myhost.com\",\n                \"b.myhost.com\",\n                \"a.myhost.com\",\n                \"b.myhost.com\",\n                \"b.myhost.com\",\n                \"a.myhost.com\",\n                \"b.myhost.com\",\n                \"a.myhost.com\",\n            }\n\n            for i, url_host in ipairs(hosts) do\n                local http = require \"resty.http\"\n                local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/\"\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false, headers = {\n                    [\"Host\"] = url_host\n                }})\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/upstream: \\S+, host: \\S+/\n--- grep_error_log_out\nupstream: \"http://127.0.0.1:1111/\", host: \"a.myhost.com\",\nupstream: \"http://127.0.0.1:1111/\", host: \"a.myhost.com\",\nupstream: \"http://127.0.0.1:2222/\", host: \"b.myhost.com\",\nupstream: \"http://127.0.0.1:2222/\", host: \"b.myhost.com\",\nupstream: \"http://127.0.0.1:1111/\", host: \"a.myhost.com\",\nupstream: \"http://127.0.0.1:2222/\", host: \"b.myhost.com\",\nupstream: \"http://127.0.0.1:2222/\", host: \"b.myhost.com\",\nupstream: \"http://127.0.0.1:1111/\", host: \"a.myhost.com\",\nupstream: \"http://127.0.0.1:2222/\", host: \"b.myhost.com\",\nupstream: \"http://127.0.0.1:1111/\", host: \"a.myhost.com\",\n"
  },
  {
    "path": "t/node/upstream-discovery.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nupstreams:\n    - service_name: mock\n      discovery_type: mock\n      type: roundrobin\n      id: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create new server picker when nodes change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.2\", port = 1980, weight = 1},\n                        {host = \"127.0.0.3\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\ncreate_obj_fun(): upstream nodes:\n\n\n\n=== TEST 2: don't create new server picker if nodes don't change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\n\n\n\n=== TEST 3: create new server picker when nodes change, up_conf doesn't come from upstream\n--- apisix_yaml\nroutes:\n  - uris:\n        - /hello\n    service_id: 1\nservices:\n  - id: 1\n    upstream:\n        service_name: mock\n        discovery_type: mock\n        type: roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.2\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\ncreate_obj_fun(): upstream nodes:\n\n\n\n=== TEST 4: don't create new server picker if nodes don't change (port missing)\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", weight = 1},\n                        {host = \"0.0.0.0\", weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"0.0.0.0\", weight = 1},\n                        {host = \"127.0.0.1\", weight = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\n--- error_log\nconnect() failed\n\n\n\n=== TEST 5: create new server picker when priority change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1, priority = 1},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\ncreate_obj_fun(): upstream nodes:\n\n\n\n=== TEST 6: default priority of discovered node is 0\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1979, weight = 1, priority = 1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1},\n                        {host = \"127.0.0.2\", port = 1979, weight = 1, priority = -1},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n        }\n    }\n--- error_log\nconnect() failed\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 0.0.0.0:1980\n\n\n\n=== TEST 7: create new server picker when metadata change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1, metadata = {a = 1}},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1, metadata = {}},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1, metadata = {a = 1}},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1, metadata = {b = 1}},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\ncreate_obj_fun(): upstream nodes:\n\n\n\n=== TEST 8: don't create new server picker when metadata doesn't change\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            local meta1 = {a = 1}\n            local meta2 = {b = 2}\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1, metadata = meta1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1, metadata = meta2},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = 1, metadata = meta1},\n                        {host = \"0.0.0.0\", port = 1980, weight = 1, metadata = meta2},\n                    }\n                end\n            }\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        }\n    }\n--- grep_error_log eval\nqr/create_obj_fun\\(\\): upstream nodes:/\n--- grep_error_log_out\ncreate_obj_fun(): upstream nodes:\n\n\n\n=== TEST 9: bad nodes return by the discovery\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return {\n                        {host = \"127.0.0.1\", port = 1980, weight = \"0\"},\n                    }\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            ngx.say(res.status)\n        }\n    }\n--- response_body\n503\n--- error_log\ninvalid nodes format: failed to validate item 1: property \"weight\" validation failed: wrong type: expected integer, got string\n\n\n\n=== TEST 10: compare nodes by value only once when nodes's address be changed but values are same\n--- log_level: debug\n--- apisix_yaml\nroutes:\n  -\n    uris:\n        - /hello\n    upstream_id: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local old_nodes = {{host = \"127.0.0.1\", port = 1980, weight = 1}, {host = \"127.0.0.2\", port = 1980, weight = 1}}\n            local discovery = require(\"apisix.discovery.init\").discovery\n            discovery.mock = {\n                nodes = function()\n                    return old_nodes\n                end\n            }\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            for i = 1, 10 do\n                local res = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if res.status ~= 200 then\n                    ngx.say(\"request failed: \", res.status)\n                    return\n                end\n            end\n\n            local new_nodes = {{host = \"127.0.0.2\", port = 1980, weight = 1}, {host = \"127.0.0.1\", port = 1980, weight = 1}}\n            discovery.mock = {\n                nodes = function()\n                    return new_nodes\n                end\n            }\n            for i = 1, 10 do\n                local res = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if res.status ~= 200 then\n                    ngx.say(\"request failed: \", res.status)\n                    return\n                end\n            end\n            ngx.say(\"pass\")\n        }\n    }\n--- response_body\npass\n--- grep_error_log eval\nqr/compare upstream nodes by value|fill node info for upstream/\n--- grep_error_log_out\nfill node info for upstream\ncompare upstream nodes by value\nfill node info for upstream\n"
  },
  {
    "path": "t/node/upstream-domain-with-special-dns.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{CUSTOM_DNS_SERVER} = \"127.0.0.1:1053\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    my $routes = <<_EOC_;\nroutes:\n  -\n    uri: /hello\n    upstream_id: 1\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $routes);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: AAAA\n--- listen_ipv6\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n        ipv6.test.local:1980: 1\n    type: roundrobin\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 2: default ttl\n--- log_level: debug\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n        ttl.test.local:1980: 1\n    type: roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res or res.body ~= \"hello world\\n\" then\n                    ngx.say(err)\n                    return\n                end\n            end\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"ttl\":300\n--- grep_error_log eval\nqr/connect to 127.0.0.1:1053/\n--- grep_error_log_out\nconnect to 127.0.0.1:1053\n\n\n\n=== TEST 3: override ttl\n--- log_level: debug\n--- yaml_config\napisix:\n    node_listen: 1984\n    dns_resolver_valid: 900\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n        ttl.test.local:1980: 1\n    type: roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if not res or res.body ~= \"hello world\\n\" then\n                    ngx.say(err)\n                    return\n                end\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/connect to 127.0.0.1:1053/\n--- grep_error_log_out\nconnect to 127.0.0.1:1053\n--- error_log\n\"ttl\":900\n\n\n\n=== TEST 4: cache expire\n--- log_level: debug\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n        ttl.1s.test.local:1980: 1\n    type: roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            for i = 1, 2 do\n                for j = 1, 3 do\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                    if not res or res.body ~= \"hello world\\n\" then\n                        ngx.say(err)\n                        return\n                    end\n                end\n\n                if i < 2 then\n                    ngx.sleep(1.1)\n                end\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/connect to 127.0.0.1:1053/\n--- grep_error_log_out\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\n\n\n\n=== TEST 5: cache expire (override ttl)\n--- log_level: debug\n--- yaml_config\napisix:\n    node_listen: 1984\n    dns_resolver_valid: 1\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n        ttl.test.local:1980: 1\n    type: roundrobin\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            for i = 1, 2 do\n                for j = 1, 3 do\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                    if not res or res.body ~= \"hello world\\n\" then\n                        ngx.say(err)\n                        return\n                    end\n                end\n\n                if i < 2 then\n                    ngx.sleep(1.1)\n                end\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/connect to 127.0.0.1:1053/\n--- grep_error_log_out\nconnect to 127.0.0.1:1053\nconnect to 127.0.0.1:1053\n"
  },
  {
    "path": "t/node/upstream-domain-with-special-ipv6-dns.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{CUSTOM_DNS_SERVER} = \"[::1]:1053\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('debug');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    my $routes = <<_EOC_;\nroutes:\n  -\n    uri: /hello\n    upstream_id: 1\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $routes);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: AAAA\n--- listen_ipv6\n--- apisix_yaml\nupstreams:\n  - id: 1\n    nodes:\n        ipv6.test.local:1980: 1\n    type: roundrobin\n--- request\nGET /hello\n--- error_log\nconnect to [::1]:1053\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/upstream-domain.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"foo.com:80\": 0,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/dns resolver domain: foo.com to \\d+.\\d+.\\d+.\\d+/\n\n\n\n=== TEST 5: set upstream(invalid node host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"test.comx:80\": 0\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6:\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local function test()\n                local code, body = t('/hello', ngx.HTTP_GET)\n\n                ngx.say(\"status: \", code)\n            end\n            test()\n            test()\n        }\n    }\n--- request\nGET /t\n--- response_body\nstatus: 503\nstatus: 503\n--- error_log\nfailed to parse domain: test.comx\nfailed to parse domain: test.comx\n--- timeout: 10\n\n\n\n=== TEST 7: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: delete upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: set upstream(with domain)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"foo.com:80\": 0,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: set empty service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"desc\": \"new service\",\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: set route(with upstream)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"foo.com\": 0,\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"service_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit routes, parse the domain of upstream node\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/dns resolver domain: foo.com to \\d+.\\d+.\\d+.\\d+/\n\n\n\n=== TEST 13: set route(with upstream)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"localhost:1981\": 2,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port\",\n                    \"service_id\": \"1\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: roundrobin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 3 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1981, 1981\n\n\n\n=== TEST 15: set route(with upstream)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"foo.com.\": 0,\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"desc\": \"new upstream\"\n                    },\n                    \"service_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit routes, parse the domain of upstream node\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/dns resolver domain: foo.com. to \\d+.\\d+.\\d+.\\d+/\n"
  },
  {
    "path": "t/node/upstream-ipv6.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my $block = shift;\n    $block->set_value(\"listen_ipv6\", 1);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"[::1]:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"weight\": 100,\n                            \"priority\": 0,\n                            \"host\": \"[::1]\",\n                            \"port\": 1980\n                        }\n                    ],\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 7: set upstream, one array item to specify node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"weight\": 100,\n                            \"priority\": 0,\n                            \"host\": \"[::1]\",\n                            \"port\": 1980\n                        }\n                    ],\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 9: set upstream, one hash key to specify node, in wrong format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"::1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nfailed to set server peer [::1:1980:80] err: invalid port while connecting to upstream\n\n\n\n=== TEST 11: set upstream, two array items to specify nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"weight\": 100,\n                            \"priority\": 0,\n                            \"host\": \"[::1]\",\n                            \"port\": 1980\n                        },\n                        {\n                            \"weight\": 100,\n                            \"priority\": 0,\n                            \"host\": \"[::1]\",\n                            \"port\": 1980\n                        }\n                    ],\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/upstream-keepalive-pool.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('debug');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: bad pool size\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1983\": 1\n                    },\n                    \"keepalive_pool\": {\n                        \"size\": 0\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"keepalive_pool\\\" validation failed: property \\\"size\\\" validation failed: expected 0 to be at least 1\"}\n\n\n\n=== TEST 2: set route/upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"keepalive_pool\": {\n                        \"size\": 4,\n                        \"idle_timeout\": 8,\n                        \"requests\": 16\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\":\"/hello\",\n                    \"upstream_id\": 1\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n\n\n\n=== TEST 3: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n        }\n    }\n--- response_body\nhello world\nhello world\nhello world\n--- grep_error_log eval\nqr/lua balancer: keepalive .*/\n--- grep_error_log_out eval\nqr/^lua balancer: keepalive create pool, crc32: \\S+, size: 4\nlua balancer: keepalive no free connection, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\nlua balancer: keepalive reusing connection \\S+, requests: 1, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\nlua balancer: keepalive reusing connection \\S+, requests: 2, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\n$/\n\n\n\n=== TEST 4: only reuse one time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"keepalive_pool\": {\n                        \"size\": 1,\n                        \"idle_timeout\": 8,\n                        \"requests\": 2\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n\n\n\n=== TEST 5: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n        }\n    }\n--- response_body\nhello world\nhello world\nhello world\n--- grep_error_log eval\nqr/lua balancer: keepalive .*/\n--- grep_error_log_out eval\nqr/^lua balancer: keepalive create pool, crc32: \\S+, size: 1\nlua balancer: keepalive no free connection, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\nlua balancer: keepalive reusing connection \\S+, requests: 1, cpool: \\S+\nlua balancer: keepalive not saving connection \\S+, cpool: \\S+, connections: 0\nlua balancer: keepalive free pool \\S+, crc32: \\S+\nlua balancer: keepalive create pool, crc32: \\S+, size: 1\nlua balancer: keepalive no free connection, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\n$/\n\n\n\n=== TEST 6: set upstream without keepalive_pool\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n        }\n    }\n\n\n\n=== TEST 7: should not override default value\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n        }\n    }\n--- response_body\nhello world\nhello world\nhello world\n--- grep_error_log eval\nqr/lua balancer: keepalive .*/\n--- grep_error_log_out eval\nqr/^lua balancer: keepalive create pool, crc32: \\S+, size: 320\nlua balancer: keepalive no free connection, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\nlua balancer: keepalive reusing connection \\S+, requests: 1, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\nlua balancer: keepalive reusing connection \\S+, requests: 2, cpool: \\S+\nlua balancer: keepalive saving connection \\S+, cpool: \\S+, connections: 1\n$/\n\n\n\n=== TEST 8: upstreams with different client cert\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local test = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local ssl_cert2 = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key2 = t.read_file(\"t/certs/apisix.key\")\n\n            local code, body = test('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1983\": 1\n                    },\n                    \"keepalive_pool\": {\n                        \"size\": 4\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                tls = {\n                    client_cert = ssl_cert,\n                    client_key = ssl_key,\n                },\n                keepalive_pool = {\n                    size = 8\n                }\n            }\n            local code, body = test('/apisix/admin/upstreams/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                tls = {\n                    client_cert = ssl_cert2,\n                    client_key = ssl_key2,\n                },\n                keepalive_pool = {\n                    size = 16\n                }\n            }\n            local code, body = test('/apisix/admin/upstreams/3',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            for i = 1, 3 do\n                local code, body = test('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\":\"/hello/]] .. i .. [[\",\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/hello\"\n                            }\n                        },\n                        \"upstream_id\": ]] .. i .. [[\n                    }]])\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.print(body)\n                    return\n                end\n            end\n        }\n    }\n--- response_body\n\n\n\n=== TEST 9: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            for i = 1, 12 do\n                local idx = (i % 3) + 1\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri .. \"/hello/\" .. idx)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                if idx == 2 then\n                    assert(res.status == 200)\n                else\n                    assert(res.status == 400)\n                end\n            end\n        }\n    }\n\n\n\n=== TEST 10: upstreams with different client cert (without pool)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local test = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local ssl_cert2 = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key2 = t.read_file(\"t/certs/apisix.key\")\n\n            local code, body = test('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1983\": 1\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                tls = {\n                    client_cert = ssl_cert,\n                    client_key = ssl_key,\n                }\n            }\n            local code, body = test('/apisix/admin/upstreams/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                tls = {\n                    client_cert = ssl_cert2,\n                    client_key = ssl_key2,\n                }\n            }\n            local code, body = test('/apisix/admin/upstreams/3',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            for i = 1, 3 do\n                local code, body = test('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\":\"/hello/]] .. i .. [[\",\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/hello\"\n                            }\n                        },\n                        \"upstream_id\": ]] .. i .. [[\n                    }]])\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.print(body)\n                    return\n                end\n            end\n        }\n    }\n--- response_body\n\n\n\n=== TEST 11: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n\n            for i = 1, 12 do\n                local idx = (i % 3) + 1\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri .. \"/hello/\" .. idx)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                if idx == 2 then\n                    assert(res.status == 200)\n                else\n                    assert(res.status == 400)\n                end\n            end\n        }\n    }\n\n\n\n=== TEST 12: upstreams with different SNI\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local test = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local code, body = test('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1983\": 1\n                    },\n                    \"pass_host\": \"rewrite\",\n                    \"upstream_host\": \"a.com\",\n                    \"keepalive_pool\": {\n                        \"size\": 4\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                pass_host = \"rewrite\",\n                upstream_host = \"b.com\",\n                keepalive_pool = {\n                    size = 8\n                }\n            }\n            local code, body = test('/apisix/admin/upstreams/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            for i = 1, 2 do\n                local code, body = test('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\":\"/hello/]] .. i .. [[\",\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/hello\"\n                            }\n                        },\n                        \"upstream_id\": ]] .. i .. [[\n                    }]])\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.print(body)\n                    return\n                end\n            end\n        }\n    }\n--- response_body\n\n\n\n=== TEST 13: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            for i = 1, 4 do\n                local idx = i % 2 + 1\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri .. \"/hello/\" .. idx)\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n        }\n    }\n--- grep_error_log eval\nqr/lua balancer: keepalive create pool, .*/\n--- grep_error_log_out eval\nqr/^lua balancer: keepalive create pool, crc32: \\S+, size: 8\nlua balancer: keepalive create pool, crc32: \\S+, size: 4\n$/\n\n\n\n=== TEST 14: upstreams with SNI, then without SNI\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local test = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local code, body = test('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"scheme\": \"https\",\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1983\": 1\n                    },\n                    \"pass_host\": \"rewrite\",\n                    \"upstream_host\": \"a.com\",\n                    \"keepalive_pool\": {\n                        \"size\": 4\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            local data = {\n                scheme = \"http\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1980\"] = 1,\n                },\n                pass_host = \"rewrite\",\n                upstream_host = \"b.com\",\n                keepalive_pool = {\n                    size = 8\n                }\n            }\n            local code, body = test('/apisix/admin/upstreams/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            for i = 1, 2 do\n                local code, body = test('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\":\"/hello/]] .. i .. [[\",\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/hello\"\n                            }\n                        },\n                        \"upstream_id\": ]] .. i .. [[\n                    }]])\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.print(body)\n                    return\n                end\n            end\n        }\n    }\n--- response_body\n\n\n\n=== TEST 15: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            for i = 0, 1 do\n                local idx = i % 2 + 1\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri .. \"/hello/\" .. idx)\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n        }\n    }\n--- grep_error_log eval\nqr/lua balancer: keepalive create pool, .*/\n--- grep_error_log_out eval\nqr/^lua balancer: keepalive create pool, crc32: \\S+, size: 4\nlua balancer: keepalive create pool, crc32: \\S+, size: 8\n$/\n\n\n\n=== TEST 16: backend serve http and grpc with the same port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local test = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local data = {\n                uri = \"\",\n                upstream = {\n                    scheme = \"\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:10054\"] = 1,\n                    },\n                    keepalive_pool = {\n                        size = 4\n                    }\n                }\n            }\n\n            data.uri = \"/helloworld.Greeter/SayHello\"\n            data.upstream.scheme = \"grpc\"\n            local code, body = test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n            data.uri = \"/hello\"\n            data.upstream.scheme = \"http\"\n            local code, body = test('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n\n        }\n    }\n--- response_body\n\n\n\n=== TEST 17: hit http\n--- request\nGET /hello\n--- response_body chomp\nhello http\n\n\n\n=== TEST 18: hit grpc\n--- http2\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n"
  },
  {
    "path": "t/node/upstream-mtls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: tls without key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"upstream\\\" validation failed: property \\\"tls\\\" validation failed: failed to validate dependent schema for \\\"client_cert\\\": property \\\"client_key\\\" is required\"}\n\n\n\n=== TEST 2: tls with bad key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = (\"AAA\"):rep(128),\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to decrypt previous encrypted key\"}\n--- error_log\ndecrypt ssl key failed\n\n\n\n=== TEST 3: encrypt key by default\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.upstream.tls.client_key == ssl_key)\n\n            -- upstream\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                tls = {\n                    client_cert = ssl_cert,\n                    client_key = ssl_key,\n                }\n            }\n            local code, body = t.test('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, res = t.test('/apisix/admin/upstreams/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.tls.client_key == ssl_key)\n\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n            }\n            local code, body = t.test('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, res = t.test('/apisix/admin/services/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.upstream.tls.client_key == ssl_key)\n        }\n    }\n--- request\nGET /t\n--- response_body\nfalse\nfalse\nfalse\n\n\n\n=== TEST 4: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: wrong cert\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key = t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello\n--- error_code: 400\n--- error_log\nclient SSL certificate verify error\n\n\n\n=== TEST 7: clean old data\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            assert(t.test('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE\n            ))\n            assert(t.test('/apisix/admin/services/1',\n                ngx.HTTP_DELETE\n            ))\n            assert(t.test('/apisix/admin/upstreams/1',\n                ngx.HTTP_DELETE\n            ))\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 8: don't encrypt key\n--- yaml_config\napisix:\n    node_listen: 1984\n    data_encryption:\n        keyring: null\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.upstream.tls.client_key == ssl_key)\n\n            -- upstream\n            local data = {\n                scheme = \"https\",\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1983\"] = 1,\n                },\n                tls = {\n                    client_cert = ssl_cert,\n                    client_key = ssl_key,\n                }\n            }\n            local code, body = t.test('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, res = t.test('/apisix/admin/upstreams/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.tls.client_key == ssl_key)\n\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n            }\n            local code, body = t.test('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, res = t.test('/apisix/admin/services/1',\n                ngx.HTTP_GET\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            res = json.decode(res)\n            ngx.say(res.value.upstream.tls.client_key == ssl_key)\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\ntrue\ntrue\n\n\n\n=== TEST 9: bind upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local data = {\n                upstream_id = 1,\n                uri = \"/server_port\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 10: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /server_port\n--- response_body chomp\n1983\n\n\n\n=== TEST 11: bind service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local data = {\n                service_id = 1,\n                uri = \"/hello_chunked\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 12: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello_chunked\n--- response_body\nhello world\n\n\n\n=== TEST 13: get cert by tls.client_cert_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                type = \"client\",\n                cert = ssl_cert,\n                key = ssl_key\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert_id = 1\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 14: hit\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 15: change ssl object type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                type = \"server\",\n                sni = \"test.com\",\n                cert = ssl_cert,\n                key = ssl_key\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 16: hit, ssl object type mismatch\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nfailed to get ssl cert: ssl type should be 'client'\n\n\n\n=== TEST 17: delete ssl object\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n\n            local code, body = t.test('/apisix/admin/ssls/1', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 18: hit, ssl object not exits\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nfailed to get ssl cert: ssl id [1] not exits\n\n\n\n=== TEST 19: `tls.verify` only\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        verify = true\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: hit\nWhen only `tls.verify` is present, the matching logic related to\n`client_cert`, `client_key` or `client_cert_id` should not be entered\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 21: set `verify` with `client_cert`, `client_key`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1983\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                        verify = true\n                    }\n                },\n                uri = \"/hello\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: hit\n`tls.verify` does not affect the parsing of `client_cert`, `client_key`\n--- upstream_server_config\n    ssl_client_certificate ../../certs/mtls_ca.crt;\n    ssl_verify_client on;\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/upstream-node-dns.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: route with one upstream node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test1.com:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route, resolve upstream node to \"127.0.0.2\" always\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.2\"}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 3: hit route, resolve upstream node to different values\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        core.log.info(\"call /hello\")\n        local code, body = t('/hello', ngx.HTTP_GET)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/dns resolver domain: test1.com to 127.0.0.\\d|call \\/hello|proxy request to 127.0.0.\\d:1980/\n--- grep_error_log_out\ncall /hello\ndns resolver domain: test1.com to 127.0.0.1\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 4: set route with two upstream nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test1.com:1980\": 1,\n                            \"test2.com:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route, resolve the upstream node to \"127.0.0.2\"\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        if domain == \"test1.com\" or domain == \"test2.com\" then\n            return {address = \"127.0.0.2\"}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: hit route, resolve upstream node to different values\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n\n        if domain == \"test1.com\" or domain == \"test2.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        core.log.info(\"call /hello\")\n        local code, body = t('/hello', ngx.HTTP_GET)\n        core.log.warn(\"code: \", code)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/dns resolver domain: \\w+.com to 127.0.0.\\d|call \\/hello|proxy request to 127.0.0.\\d:1980/\n--- grep_error_log_out eval\nqr/call \\/hello(\ndns resolver domain: test1.com to 127.0.0.1\ndns resolver domain: test2.com to 127.0.0.2|\ndns resolver domain: test2.com to 127.0.0.1\ndns resolver domain: test1.com to 127.0.0.2)\nproxy request to 127.0.0.[12]:1980\n/\n\n\n\n=== TEST 7: upstream with one upstream node\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"test1.com:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: set route with upstream_id 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route, resolve upstream node to different values\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        core.log.info(\"call /hello\")\n        local code, body = t('/hello', ngx.HTTP_GET)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/dns resolver domain: test1.com to 127.0.0.\\d|call \\/hello|proxy request to 127.0.0.\\d:1980/\n--- grep_error_log_out\ncall /hello\ndns resolver domain: test1.com to 127.0.0.1\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 10: two upstream nodes in upstream object\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"test1.com:1980\": 1,\n                        \"test2.com:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route, resolve upstream node to different values\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n\n        if domain == \"test1.com\" or domain == \"test2.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        core.log.info(\"call /hello\")\n        local code, body = t('/hello', ngx.HTTP_GET)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/dns resolver domain: \\w+.com to 127.0.0.\\d|call \\/hello|proxy request to 127.0.0.\\d:1980/\n--- grep_error_log_out eval\nqr/call \\/hello(\ndns resolver domain: test1.com to 127.0.0.1\ndns resolver domain: test2.com to 127.0.0.2|\ndns resolver domain: test2.com to 127.0.0.1\ndns resolver domain: test1.com to 127.0.0.2)\nproxy request to 127.0.0.[12]:1980\n/\n\n\n\n=== TEST 12: dns cached expired, resolve the domain always with same value\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 1\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        if domain == \"test1.com\" or domain == \"test2.com\" then\n            return {address = \"127.0.0.1\"}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        core.log.info(\"call /hello\")\n        local code, body = t('/hello', ngx.HTTP_GET)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/dns resolver domain: \\w+.com to 127.0.0.\\d|call \\/hello|proxy request to 127.0.0.\\d:1980/\n--- grep_error_log_out eval\nqr/call \\/hello(\ndns resolver domain: test1.com to 127.0.0.1\ndns resolver domain: test2.com to 127.0.0.1|\ndns resolver domain: test2.com to 127.0.0.1\ndns resolver domain: test1.com to 127.0.0.1)\nproxy request to 127.0.0.1:1980\n/\n\n\n\n=== TEST 13: two upstream nodes in upstream object (one host + one IP)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"test1.com:1980\": 1,\n                        \"127.0.0.5:1981\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: dns cached expired, resolve the domain with different values\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n        if domain == \"test1.com\" or domain == \"test2.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        core.log.info(\"call /hello\")\n        local code, body = t('/hello', ngx.HTTP_GET)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/dns resolver domain: \\w+.com to 127.0.0.\\d|call \\/hello|proxy request to 127.0.0.\\d:198\\d/\n--- grep_error_log_out eval\nqr/call \\/hello\ndns resolver domain: test1.com to 127.0.0.1\nproxy request to 127.0.0.(1:1980|5:1981)\n/\n\n\n\n=== TEST 15: route with upstream node, the domain's IP is changed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test1.com:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    local count = 0\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        count = count + 1\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.\" .. count}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        t('/hello', ngx.HTTP_GET)\n        -- avoid adding more \"dns_value\" into the route\n        t('/hello', ngx.HTTP_GET)\n    }\n}\n\n--- request\nGET /t\n--- grep_error_log eval\nqr/parse route which contain domain: .+(\"dns_value\":.+){3}/\n--- grep_error_log_out\n"
  },
  {
    "path": "t/node/upstream-retries.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1), by default retries count = number of nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1\": 1,\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.3:1\": 1,\n                        \"127.0.0.4:1\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- grep_error_log eval\nqr/\\[error\\]/\n--- grep_error_log_out\n[error]\n[error]\n[error]\n[error]\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nconnect() failed\n\n\n\n=== TEST 6: set upstream(id: 1), retries = 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1\": 1,\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.3:1\": 1,\n                        \"127.0.0.4:1\": 1\n                    },\n                    \"retries\": 1,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- grep_error_log eval\nqr/\\[error\\]/\n--- grep_error_log_out\n[error]\n[error]\n\n\n\n=== TEST 8: set upstream(id: 1), retries = 0\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1\": 1,\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.3:1\": 1,\n                        \"127.0.0.4:1\": 1\n                    },\n                    \"retries\": 0,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- grep_error_log eval\nqr/\\[error\\]/\n--- grep_error_log_out\n[error]\n\n\n\n=== TEST 10: set upstream, retries > number of nodes, only try number of nodes time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1\": 1,\n                        \"127.0.0.2:1\": 1\n                    },\n                    \"retries\": 3,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nall upstream servers tried\n--- grep_error_log eval\nqr/connect\\(\\) failed/\n--- grep_error_log_out\nconnect() failed\nconnect() failed\n\n\n\n=== TEST 12: don't retry the same node twice\n--- request\nGET /hello\n--- error_code: 502\n--- error_log\nConnection refused\nfailed to find valid upstream server\nproxy request to 127.0.0.1:1\nproxy request to 127.0.0.2:1\n\n\n\n=== TEST 13: stop proxy to next upstream by retry_timeout\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 100,\n                                \"127.0.0.1:1981\": 100,\n                                \"127.0.0.1:1982\": 100\n                            },\n                            \"retries\": 10,\n                            \"retry_timeout\": 0.15,\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code ~= 200 then\n                ngx.say(body)\n                return\n            end\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/mysleep?abort=true&seconds=0.1\"\n            local res, err = httpc:request_uri(uri)\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.status = res.status\n            ngx.say(res.status)\n        }\n    }\n--- request\nGET /t\n--- error_code: 502\n--- error_log\nproxy retry timeout, retry count: 1\n"
  },
  {
    "path": "t/node/upstream-status-5xx.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1) and available upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit the route and $upstream_status is 200\n--- request\nGET /hello\n--- response_body\nhello world\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 200/\n--- grep_error_log_out\n\n\n\n=== TEST 3: set route(id: 1) and set the timeout field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"timeout\": {\n                            \"connect\": 0.5,\n                            \"send\": 0.5,\n                            \"read\": 0.5\n                        }\n                    },\n                    \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes (timeout) and $upstream_status is 504\n--- request\nGET /mysleep?seconds=1\n--- error_code: 504\n--- response_body eval\nqr/504 Gateway Time-out/\n--- response_headers\nX-APISIX-Upstream-Status: 504\n--- error_log\nConnection timed out\n\n\n\n=== TEST 5: set route(id: 1), upstream service is not available\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes and $upstream_status is 502\n--- request\nGET /hello\n--- error_code: 502\n--- response_body eval\nqr/502 Bad Gateway/\n--- response_headers\nX-APISIX-Upstream-Status: 502\n--- error_log\nConnection refused\n\n\n\n=== TEST 7: set route(id: 1) and uri is `/server_error`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_error\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes and $upstream_status is 500\n--- request\nGET /server_error\n--- error_code: 500\n--- response_body eval\nqr/500 Internal Server Error/\n--- response_headers\nX-APISIX-Upstream-Status: 500\n--- error_log\n500 Internal Server Error\n\n\n\n=== TEST 9: set upstream(id: 1, retries = 2), has available upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"retries\": 2,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: set route(id: 1) and bind the upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit routes and $upstream_status is `502, 200`\n--- request\nGET /hello\n--- response_body\nhello world\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 502, 200/\n--- grep_error_log_out\n\n\n\n=== TEST 12: set upstream(id: 1, retries = 2), all upstream nodes are unavailable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.3:1\": 1,\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.1:1\": 1\n\n                    },\n                    \"retries\": 2,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: hit routes, retry between upstream failed, $upstream_status is `502, 502, 502`\n--- request\nGET /hello\n--- error_code: 502\n--- response_headers_raw_like eval\nqr/X-APISIX-Upstream-Status: 502, 502, 502/\n--- error_log\nConnection refused\n\n\n\n=== TEST 14: return 500 status code from APISIX\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 500,\n                                \"body\": \"Fault Injection!\\n\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit routes, status code is 500\n--- request\nGET /hello\n--- error_code: 500\n--- response_body\nFault Injection!\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 500/\n--- grep_error_log_out\n\n\n\n=== TEST 16: return 200 status code from APISIX\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 200,\n                                \"body\": \"Fault Injection!\\n\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: hit routes, status code is 200\n--- request\nGET /hello\n--- response_body\nFault Injection!\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 200/\n--- grep_error_log_out\n"
  },
  {
    "path": "t/node/upstream-status-all.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1) and available upstream and show_upstream_status_in_response_header: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit the route and $upstream_status is 200\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /hello\n--- response_body\nhello world\n--- response_headers\nX-APISIX-Upstream-Status: 200\n\n\n\n=== TEST 3: set route(id: 1) and set the timeout field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"timeout\": {\n                            \"connect\": 0.5,\n                            \"send\": 0.5,\n                            \"read\": 0.5\n                        }\n                    },\n                    \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes (timeout) and $upstream_status is 504\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /mysleep?seconds=1\n--- error_code: 504\n--- response_body eval\nqr/504 Gateway Time-out/\n--- response_headers\nX-APISIX-Upstream-Status: 504\n--- error_log\nConnection timed out\n\n\n\n=== TEST 5: set route(id: 1), upstream service is not available\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes and $upstream_status is 502\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /hello\n--- error_code: 502\n--- response_body eval\nqr/502 Bad Gateway/\n--- response_headers\nX-APISIX-Upstream-Status: 502\n--- error_log\nConnection refused\n\n\n\n=== TEST 7: set route(id: 1) and uri is `/server_error`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_error\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit routes and $upstream_status is 500\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /server_error\n--- error_code: 500\n--- response_body eval\nqr/500 Internal Server Error/\n--- response_headers\nX-APISIX-Upstream-Status: 500\n--- error_log\n500 Internal Server Error\n\n\n\n=== TEST 9: set upstream(id: 1, retries = 2), has available upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.1.0.2:1\": 1,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"retries\": 2,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: set route(id: 1) and bind the upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit routes and $upstream_status is `502, 200`\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /hello\n--- response_body\nhello world\n--- response_headers_raw_like eval\nqr/X-APISIX-Upstream-Status: 502, 200|X-APISIX-Upstream-Status: 200/\n--- error_log\n\n\n\n=== TEST 12: set upstream(id: 1, retries = 2), all upstream nodes are unavailable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.3:1\": 1,\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.1:1\": 1\n\n                    },\n                    \"retries\": 2,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit routes, retry between upstream failed, $upstream_status is `502, 502, 502`\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /hello\n--- error_code: 502\n--- response_headers_raw_like eval\nqr/X-APISIX-Upstream-Status: 502, 502, 502/\n--- error_log\nConnection refused\n\n\n\n=== TEST 14: return 500 status code from APISIX\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 500,\n                                \"body\": \"Fault Injection!\\n\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: hit routes, status code is 500\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /hello\n--- error_code: 500\n--- response_body\nFault Injection!\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 500/\n--- grep_error_log_out\n\n\n\n=== TEST 16: return 200 status code from APISIX\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 200,\n                                \"body\": \"Fault Injection!\\n\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: hit routes, status code is 200\n--- yaml_config\napisix:\n  show_upstream_status_in_response_header: true\n--- request\nGET /hello\n--- response_body\nFault Injection!\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 200/\n--- grep_error_log_out\n\n\n\n=== TEST 18: return 200 status code from APISIX (with show_upstream_status_in_response_header:false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 200,\n                                \"body\": \"Fault Injection!\\n\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: hit routes, status code is 200\n--- yaml_config\napisix:\n    show_upstream_status_in_response_header: false\n--- request\nGET /hello\n--- response_body\nFault Injection!\n--- grep_error_log eval\nqr/X-APISIX-Upstream-Status: 200/\n--- grep_error_log_out\n"
  },
  {
    "path": "t/node/upstream-websocket.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream with websocket (id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"enable_websocket\": true,\n                    \"uri\": \"/websocket_handshake\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: send websocket\n--- raw_request eval\n\"GET /websocket_handshake HTTP/1.1\\r\nHost: server.example.com\\r\nUpgrade: websocket\\r\nConnection: Upgrade\\r\nSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\\r\nSec-WebSocket-Protocol: chat\\r\nSec-WebSocket-Version: 13\\r\nOrigin: http://example.com\\r\n\\r\n\"\n--- response_headers\nUpgrade: websocket\nConnection: upgrade\nSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\nSec-WebSocket-Protocol: chat\n!Content-Type\n--- raw_response_headers_like: ^HTTP/1.1 101 Switching Protocols\\r\\n\n--- response_body_like eval\nqr/hello/\n--- error_code: 101\n\n\n\n=== TEST 3: set upstream(id: 6)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/6',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1981\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: set route with upstream websocket enabled(id: 6)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/6',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/websocket_handshake/route\",\n                    \"enable_websocket\": true,\n                    \"upstream_id\": \"6\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: send success websocket to upstream\n--- raw_request eval\n\"GET /websocket_handshake/route HTTP/1.1\\r\nHost: server.example.com\\r\nUpgrade: websocket\\r\nConnection: Upgrade\\r\nSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\\r\nSec-WebSocket-Protocol: chat\\r\nSec-WebSocket-Version: 13\\r\nOrigin: http://example.com\\r\n\\r\n\"\n--- response_headers\nUpgrade: websocket\nConnection: upgrade\nSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\nSec-WebSocket-Protocol: chat\n!Content-Type\n--- raw_response_headers_like: ^HTTP/1.1 101 Switching Protocols\\r\\n\n--- response_body_like eval\nqr/hello/\n--- error_code: 101\n\n\n\n=== TEST 6: disable websocket(id: 6)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/6',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/websocket_handshake/route\",\n                    \"enable_websocket\": false,\n                    \"upstream_id\": \"6\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: send websocket to upstream without header support\n--- raw_request eval\n\"GET /websocket_handshake/route HTTP/1.1\\r\nHost: server.example.com\\r\nUpgrade: websocket\\r\nConnection: Upgrade\\r\nSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\\r\nSec-WebSocket-Protocol: chat\\r\nSec-WebSocket-Version: 13\\r\nOrigin: http://example.com\\r\n\\r\n\"\n[error]\n--- error_code: 400\n--- grep_error_log eval\nqr/failed to new websocket: bad \"upgrade\" request header: nil/\n--- grep_error_log_out\n\n\n\n=== TEST 8: set wss\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local code, body = t.test('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"scheme\": \"https\",\n                        \"nodes\": {\n                            \"127.0.0.1:1983\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"enable_websocket\": true,\n                    \"uri\": \"/websocket_handshake\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {cert = ssl_cert, key = ssl_key, sni = \"127.0.0.1\"}\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: send websocket\n--- config\n    location /t {\n        content_by_lua_block {\n            local client = require \"resty.websocket.client\"\n            local wb = client:new()\n            local uri = \"wss://127.0.0.1:1994/websocket_handshake\"\n            local opts = {\n                server_name = \"127.0.0.1\"\n            }\n            local ok, err = wb:connect(uri, opts)\n            if not ok then\n                ngx.say(\"failed to connect: \" .. err)\n                return\n            end\n\n            local typ\n            data, typ, err = wb:recv_frame()\n            if not data then\n                ngx.say(\"failed to receive 2nd frame: \", err)\n                return\n            end\n\n            ngx.say(\"received: \", data, \" (\", typ, \")\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nreceived: hello (text)\n"
  },
  {
    "path": "t/node/upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1) invalid parameters\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 2: set upstream(id: 1) nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 6: delete upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.5)\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.print(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 400 message: {\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n\n\n\n=== TEST 7: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/routes/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 8: delete upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 9: delete upstream again(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code)\n        }\n    }\n--- request\nGET /t\n--- response_body\n[delete] code: 404\n\n\n\n=== TEST 10: set upstream(id: 1, using `node` mode to pass upstream host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"test.com:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\",\n                    \"pass_host\": \"node\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: set route(id: 1, using `node` mode to pass upstream host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route\n--- request\nGET /echo\n--- response_headers\nhost: test.com:1980\n\n\n\n=== TEST 13: set upstream(using `rewrite` mode to pass upstream host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\",\n                    \"pass_host\": \"rewrite\",\n                    \"upstream_host\": \"test.com\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: set route(using `rewrite` mode to pass upstream host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route\n--- request\nGET /echo\n--- response_headers\nhost: test.com\n\n\n\n=== TEST 16: delete upstream in used\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            ngx.sleep(0.5) -- wait for data synced\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"can not delete this upstream, route [1] is still using it now\"}\n\n\n\n=== TEST 17: multi nodes with `node` mode to pass host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"localhost:1979\": 1000,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"pass_host\": \"node\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/uri\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route\n--- request\nGET /uri\n--- response_body eval\nqr/host: 127.0.0.1/\n--- error_log\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 19: multi nodes with `node` mode to pass host, the second node has domain\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1979\": 1000,\n                        \"localhost:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"pass_host\": \"node\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/uri\",\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: hit route\n--- request\nGET /uri\n--- response_body eval\nqr/host: localhost/\n--- error_log\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 21: check that including port in host header is supported when pass_host = node and port is not standard\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"localhost:1980\": 1000\n                    },\n                    \"type\": \"roundrobin\",\n                    \"pass_host\": \"node\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream_id\": \"1\",\n                        \"uri\": \"/uri\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: hit route\n--- request\nGET /uri\n--- response_body eval\nqr/host: localhost:1980/\n\n\n\n=== TEST 23: check that including port in host header is supported when retrying and pass_host = node and port is not standard\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1979\": 1000,\n                        \"localhost:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"pass_host\": \"node\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream_id\": \"1\",\n                        \"uri\": \"/uri\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: hit route\n--- log_level: debug\n--- request\nGET /uri\n--- error_log\nHost: 127.0.0.1:1979\n\n\n\n=== TEST 25: distinguish different upstreams even they have the same addr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 {\n                    nodes = {[\"localhost:1980\"] = 1},\n                    type = \"roundrobin\"\n                 }\n                )\n            assert(code < 300)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream_id\": \"1\",\n                        \"uri\": \"/server_port\"\n                }]]\n                )\n            assert(code < 300)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/server_port\"\n\n            local ports_count = {}\n            for i = 1, 24 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ports_count[res.body] = (ports_count[res.body] or 0) + 1\n\n                local code, body = t('/apisix/admin/upstreams/1',\n                    ngx.HTTP_PUT,\n                    {\n                        nodes = {[\"localhost:\" .. (1980 + i % 3)] = 1},\n                        type = \"roundrobin\"\n                    }\n                    )\n                assert(code < 300)\n            end\n\n            local ports_arr = {}\n            for port, count in pairs(ports_count) do\n                table.insert(ports_arr, {port = port, count = count})\n            end\n\n            local function cmd(a, b)\n                return a.port > b.port\n            end\n            table.sort(ports_arr, cmd)\n\n            ngx.say(require(\"toolkit.json\").encode(ports_arr))\n        }\n    }\n--- request\nGET /t\n--- timeout: 5\n--- response_body\n[{\"count\":8,\"port\":\"1982\"},{\"count\":8,\"port\":\"1981\"},{\"count\":8,\"port\":\"1980\"}]\n"
  },
  {
    "path": "t/node/vars.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(only arg_k)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [ [\"arg_k\", \"==\", \"v\"] ],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /hello?k=not-hit\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /hello?k=v\n--- response_body\nhello world\n\n\n\n=== TEST 5: set route(cookie)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [[\"cookie_k\", \"==\", \"v\"]],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 7: /not_found\n--- more_headers\nCookie: k=not-hit; kkk=vvv;\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 8: hit routes\n--- more_headers\nCookie: k=v; kkk=vvv;\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 9: set route(header)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [[\"http_k\", \"==\", \"v\"]],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 11: /not_found\n--- more_headers\nk: not-hit\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 12: hit routes\n--- more_headers\nk: v\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 13: set route(uri arg + header + cookie)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [[\"http_k\", \"==\", \"header\"], [\"cookie_k\", \"==\", \"cookie\"], [\"arg_k\", \"==\", \"uri_arg\"]],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 15: /not_found\n--- more_headers\nk: header\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 16: hit routes\n--- more_headers\nCookie: k=cookie\nk: header\n--- request\nGET /hello?k=uri_arg\n--- response_body\nhello world\n\n\n\n=== TEST 17: set route(only post arg)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/hello\",\n                    \"vars\": [[\"post_arg_k\", \"==\", \"post_form\"]],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: not_found (GET request)\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 19: not_found (wrong request body)\n--- request\nPOST /hello\n123\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 20: not_found (wrong content type)\n--- request\nPOST /hello\nk=post_form\n--- more_headers\nContent-Type: multipart/form-data\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 21: hit routes\n--- request\nPOST /hello\nk=post_form\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- response_body\nhello world\n"
  },
  {
    "path": "t/node/wildcard-host.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: host: *.foo.com\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\": \"*.foo.com\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: not found, missing host\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: host: a.foo.com\n--- request\nGET /hello\n--- more_headers\nHost: a.foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 5: host: a.b.foo.com\n--- request\nGET /hello\n--- more_headers\nHost: a.b.foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 6: host: .foo.com\n--- request\nGET /hello\n--- more_headers\nHost: .foo.com\n--- response_body\nhello world\n"
  },
  {
    "path": "t/package.json",
    "content": "{\n  \"name\": \"apisix-test-suite\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"NODE_OPTIONS=--experimental-vm-modules jest --verbose\"\n  },\n  \"devDependencies\": {\n    \"@jest/globals\": \"^29.7.0\",\n    \"@modelcontextprotocol/sdk\": \"1.26.0\",\n    \"@trivago/prettier-plugin-sort-imports\": \"^5.2.2\",\n    \"@types/google-protobuf\": \"^3.15.12\",\n    \"@types/jest\": \"29.5.14\",\n    \"@types/node\": \"22.14.1\",\n    \"axios\": \"^1.13.5\",\n    \"docker-compose\": \"^1.2.0\",\n    \"google-protobuf\": \"^3.21.4\",\n    \"graphql\": \"^16.11.0\",\n    \"graphql-request\": \"^7.1.2\",\n    \"grpc-web\": \"^1.5.0\",\n    \"jest\": \"29.7.0\",\n    \"lago-javascript-client\": \"^1.26.0\",\n    \"simple-git\": \"^3.27.0\",\n    \"ts-jest\": \"29.3.2\",\n    \"ts-node\": \"10.9.2\",\n    \"xhr2\": \"^0.2.1\",\n    \"yaml\": \"^2.7.1\"\n  },\n  \"packageManager\": \"pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748\"\n}\n"
  },
  {
    "path": "t/plugin/ai-aliyun-content-moderation.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"debug\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    ngx.status = 200\n                    ngx.say([[\n{\n\"choices\": [\n{\n  \"finish_reason\": \"stop\",\n  \"index\": 0,\n  \"message\": { \"content\": \"I will kill you.\", \"role\": \"assistant\" }\n}\n],\n\"created\": 1723780938,\n\"id\": \"chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P\",\n\"model\": \"gpt-3.5-turbo\",\n\"object\": \"chat.completion\",\n\"usage\": { \"completion_tokens\": 5, \"prompt_tokens\": 8, \"total_tokens\": 10 }\n}\n                    ]])\n                }\n            }\n\n            location / {\n                content_by_lua_block {\n                    local core = require(\"apisix.core\")\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    if not body then\n                        ngx.status(400)\n                        return\n                    end\n\n                    ngx.status = 200\n                    if core.string.find(body, \"kill\") then\n                        ngx.say([[\n{\n  \"Message\": \"OK\",\n  \"Data\": {\n    \"Advice\": [\n      {\n        \"HitLabel\": \"violent_incidents\",\n        \"Answer\": \"As an AI language model, I cannot write unethical or controversial content for you.\"\n      }\n    ],\n    \"RiskLevel\": \"high\",\n    \"Result\": [\n      {\n        \"RiskWords\": \"kill\",\n        \"Description\": \"suspected extremist content\",\n        \"Confidence\": 100.0,\n        \"Label\": \"violent_incidents\"\n      }\n    ]\n  },\n  \"Code\": 200\n}\n                    ]])\n                    else\n                        ngx.say([[\n{\n  \"RequestId\": \"3262D562-1FBA-5ADF-86CB-3087603A4DF3\",\n  \"Message\": \"OK\",\n  \"Data\": {\n    \"RiskLevel\": \"none\",\n    \"Result\": [\n      {\n        \"Description\": \"no risk detected\",\n        \"Label\": \"nonLabel\"\n      }\n    ]\n  },\n  \"Code\": 200\n}\n                    ]])\n                    end\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create a route with ai-aliyun-content-moderation plugin only\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/chat\",\n                    \"plugins\": {\n                      \"ai-aliyun-content-moderation\": {\n                        \"endpoint\": \"http://localhost:6724\",\n                        \"region_id\": \"cn-shanghai\",\n                        \"access_key_id\": \"fake-key-id\",\n                        \"access_key_secret\": \"fake-key-secret\",\n                        \"risk_level_bar\": \"high\",\n                        \"check_request\": true\n                      }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: use ai-aliyun-content-moderation plugin without ai-proxy or ai-proxy-multi plugin should failed\n--- request\nPOST /chat\n{\"prompt\": \"What is 1+1?\"}\n--- error_code: 500\n--- response_body_chomp\nno ai instance picked, ai-aliyun-content-moderation plugin must be used with ai-proxy or ai-proxy-multi plugin\n\n\n\n=== TEST 3: check prompt in request\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/chat\",\n                    \"plugins\": {\n                      \"ai-proxy\": {\n                          \"provider\": \"openai\",\n                          \"auth\": {\n                              \"header\": {\n                                  \"Authorization\": \"Bearer wrongtoken\"\n                              }\n                          },\n                          \"override\": {\n                              \"endpoint\": \"http://localhost:6724\"\n                          }\n                      },\n                      \"ai-aliyun-content-moderation\": {\n                        \"endpoint\": \"http://localhost:6724\",\n                        \"region_id\": \"cn-shanghai\",\n                        \"access_key_id\": \"fake-key-id\",\n                        \"access_key_secret\": \"fake-key-secret\",\n                        \"risk_level_bar\": \"high\",\n                        \"check_request\": true\n                      }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: invalid chat completions request should fail\n--- request\nPOST /chat\n{\"prompt\": \"What is 1+1?\"}\n--- error_code: 400\n--- response_body_chomp\nrequest format doesn't match schema: property \"messages\" is required\n\n\n\n=== TEST 5: non-violent prompt should succeed\n--- request\nPOST /chat\n{ \"messages\": [ { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 200\n--- response_body_like eval\nqr/kill you/\n\n\n\n=== TEST 6: violent prompt should failed\n--- request\nPOST /chat\n{ \"messages\": [ { \"role\": \"user\", \"content\": \"I want to kill you\"} ] }\n--- error_code: 200\n--- response_body_like eval\nqr/As an AI language model, I cannot write unethical or controversial content for you./\n\n\n\n=== TEST 7: check ai response (stream=false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/chat\",\n                    \"plugins\": {\n                      \"ai-proxy\": {\n                          \"provider\": \"openai\",\n                          \"auth\": {\n                              \"header\": {\n                                  \"Authorization\": \"Bearer wrongtoken\"\n                              }\n                          },\n                          \"override\": {\n                              \"endpoint\": \"http://localhost:6724\"\n                          }\n                      },\n                      \"ai-aliyun-content-moderation\": {\n                        \"endpoint\": \"http://localhost:6724\",\n                        \"region_id\": \"cn-shanghai\",\n                        \"access_key_id\": \"fake-key-id\",\n                        \"access_key_secret\": \"fake-key-secret\",\n                        \"risk_level_bar\": \"high\",\n                        \"check_request\": true,\n                        \"check_response\": true,\n                        \"deny_code\": 400,\n                        \"deny_message\": \"your request is rejected\"\n                      }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: violent response should failed\n--- request\nPOST /chat\n{ \"messages\": [ { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 400\n--- response_body_like eval\nqr/your request is rejected/\n\n\n\n=== TEST 9: check ai request\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, provider in ipairs({\"openai\", \"deepseek\", \"openai-compatible\"}) do\n                local code, body = t('/apisix/admin/routes/' .. provider,\n                    ngx.HTTP_PUT,\n                    string.format([[{\n                        \"uri\": \"/chat-%s\",\n                        \"plugins\": {\n                          \"ai-proxy\": {\n                              \"provider\": \"%s\",\n                              \"auth\": {\n                                  \"header\": {\n                                      \"Authorization\": \"Bearer wrongtoken\"\n                                  }\n                              },\n                              \"override\": {\n                                  \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                              }\n                          },\n                          \"ai-aliyun-content-moderation\": {\n                            \"endpoint\": \"http://localhost:6724\",\n                            \"region_id\": \"cn-shanghai\",\n                            \"access_key_id\": \"fake-key-id\",\n                            \"access_key_secret\": \"fake-key-secret\",\n                            \"risk_level_bar\": \"high\",\n                            \"check_request\": true,\n                            \"check_response\": false,\n                            \"deny_code\": 400,\n                            \"deny_message\": \"your request is rejected\"\n                          }\n                        }\n                    }]], provider, provider)\n                )\n                if code >= 300 then\n                    ngx.status = code\n                    return\n                end\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: violent response should failed for openai provider\n--- request\nPOST /chat-openai\n{ \"messages\": [ { \"role\": \"user\", \"content\": \"I want to kill you\"} ] }\n--- error_code: 400\n--- response_body_like eval\nqr/your request is rejected/\n\n\n\n=== TEST 11: violent response should failed for deepseek provider\n--- request\nPOST /chat-deepseek\n{ \"messages\": [ { \"role\": \"user\", \"content\": \"I want to kill you\"} ] }\n--- error_code: 400\n--- response_body_like eval\nqr/your request is rejected/\n\n\n\n=== TEST 12: violent response should failed for openai-compatible provider\n--- request\nPOST /chat-openai-compatible\n{ \"messages\": [ { \"role\": \"user\", \"content\": \"I want to kill you\"} ] }\n--- error_code: 400\n--- response_body_like eval\nqr/your request is rejected/\n\n\n\n=== TEST 13: content moderation should keep usage data in response\n--- request\nPOST /chat-openai\n{\"messages\":[{\"role\":\"user\",\"content\":\"I want to kill you\"}]}\n--- error_code: 400\n--- response_body_like eval\nqr/completion_tokens/\n\n\n\n=== TEST 14: content moderation should keep real llm model in response\n--- request\nPOST /chat-openai\n{\"model\": \"gpt-3.5-turbo\",\"messages\":[{\"role\":\"user\",\"content\":\"I want to kill you\"}]}\n--- error_code: 400\n--- response_body_like eval\nqr/gpt-3.5-turbo/\n\n\n\n=== TEST 15: content moderation should keep usage data in response\n--- request\nPOST /chat-openai\n{\"messages\":[{\"role\":\"user\",\"content\":\"I want to kill you\"}]}\n--- error_code: 400\n--- response_body_like eval\nqr/completion_tokens/\n\n\n\n=== TEST 16: content moderation should keep real llm model in response\n--- request\nPOST /chat-openai\n{\"model\": \"gpt-3.5-turbo\",\"messages\":[{\"role\":\"user\",\"content\":\"I want to kill you\"}]}\n--- error_code: 400\n--- response_body_like eval\nqr/gpt-3.5-turbo/\n\n\n\n=== TEST 17: set route with stream = true (SSE) and stream_mode = final_packet\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions?offensive=true\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-aliyun-content-moderation\": {\n                            \"endpoint\": \"http://localhost:6724\",\n                            \"region_id\": \"cn-shanghai\",\n                            \"access_key_id\": \"fake-key-id\",\n                            \"access_key_secret\": \"fake-key-secret\",\n                            \"risk_level_bar\": \"high\",\n                            \"check_request\": false,\n                            \"check_response\": true,\n                            \"deny_code\": 400,\n                            \"deny_message\": \"your request is rejected\"\n                          }\n                    }\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: test is SSE works as expected when response is offensive\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local final_res = {}\n            local inspect = require(\"inspect\")\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                core.log.warn(\"CHUNK IS \", inspect(chunk))\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n            ngx.print(final_res[5])\n        }\n    }\n--- response_body_like eval\nqr/\"risk_level\":\"high\"/\n\n\n\n=== TEST 19: set route with stream = true (SSE) and stream_mode = realtime\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions?offensive=true\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-aliyun-content-moderation\": {\n                            \"endpoint\": \"http://localhost:6724\",\n                            \"region_id\": \"cn-shanghai\",\n                            \"access_key_id\": \"fake-key-id\",\n                            \"access_key_secret\": \"fake-key-secret\",\n                            \"risk_level_bar\": \"high\",\n                            \"check_request\": false,\n                            \"check_response\": true,\n                            \"deny_code\": 400,\n                            \"deny_message\": \"your request is rejected\",\n                            \"stream_check_mode\": \"realtime\",\n                            \"stream_check_cache_size\": 5\n                          }\n                    }\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: test is SSE works as expected when third response chunk is offensive and stream_mode = realtime\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local final_res = {}\n            local inspect = require(\"inspect\")\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                core.log.warn(\"CHUNK IS \", inspect(chunk))\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n            ngx.print(final_res[3])\n        }\n    }\n--- response_body_like eval\nqr/your request is rejected/\n--- grep_error_log eval\nqr/execute content moderation/\n--- grep_error_log_out\nexecute content moderation\nexecute content moderation\n\n\n\n=== TEST 21: set route with stream = true (SSE) and stream_mode = realtime with larger buffer and large timeout\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-aliyun-content-moderation\": {\n                            \"endpoint\": \"http://localhost:6724\",\n                            \"region_id\": \"cn-shanghai\",\n                            \"access_key_id\": \"fake-key-id\",\n                            \"access_key_secret\": \"fake-key-secret\",\n                            \"risk_level_bar\": \"high\",\n                            \"check_request\": false,\n                            \"check_response\": true,\n                            \"deny_code\": 400,\n                            \"deny_message\": \"your request is rejected\",\n                            \"stream_check_mode\": \"realtime\",\n                            \"stream_check_cache_size\": 30000,\n                            \"stream_check_interval\": 30\n                          }\n                    }\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: test is SSE works, stream_mode = realtime, large buffer + large timeout but content moderation should be called once\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local final_res = {}\n            local inspect = require(\"inspect\")\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                core.log.warn(\"CHUNK IS \", inspect(chunk))\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data:/\n--- grep_error_log eval\nqr/execute content moderation/\n--- grep_error_log_out\nexecute content moderation\n\n\n\n=== TEST 23: set route with stream = true (SSE) and stream_mode = realtime with small buffer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-aliyun-content-moderation\": {\n                            \"endpoint\": \"http://localhost:6724\",\n                            \"region_id\": \"cn-shanghai\",\n                            \"access_key_id\": \"fake-key-id\",\n                            \"access_key_secret\": \"fake-key-secret\",\n                            \"risk_level_bar\": \"high\",\n                            \"check_request\": false,\n                            \"check_response\": true,\n                            \"deny_code\": 400,\n                            \"deny_message\": \"your request is rejected\",\n                            \"stream_check_mode\": \"realtime\",\n                            \"stream_check_cache_size\": 1,\n                            \"stream_check_interval\": 3\n                          }\n                    }\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 24: test is SSE works, stream_mode = realtime, small buffer. content moderation will be called on each chunk\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local final_res = {}\n            local inspect = require(\"inspect\")\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                core.log.warn(\"CHUNK IS \", inspect(chunk))\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data:/\n--- grep_error_log eval\nqr/execute content moderation/\n--- grep_error_log_out\nexecute content moderation\nexecute content moderation\nexecute content moderation\n\n\n\n=== TEST 25: set route with stream = true (SSE) and stream_mode = realtime with large buffer but small timeout\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions?delay=true\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-aliyun-content-moderation\": {\n                            \"endpoint\": \"http://localhost:6724\",\n                            \"region_id\": \"cn-shanghai\",\n                            \"access_key_id\": \"fake-key-id\",\n                            \"access_key_secret\": \"fake-key-secret\",\n                            \"risk_level_bar\": \"high\",\n                            \"check_request\": false,\n                            \"check_response\": true,\n                            \"deny_code\": 400,\n                            \"deny_message\": \"your request is rejected\",\n                            \"stream_check_mode\": \"realtime\",\n                            \"stream_check_cache_size\": 10000,\n                            \"stream_check_interval\": 0.1\n                          }\n                    }\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 26: test is SSE works, stream_mode = realtime, large buffer + small timeout: content moderation will be called on each chunke\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local final_res = {}\n            local inspect = require(\"inspect\")\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                core.log.warn(\"CHUNK IS \", inspect(chunk))\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data:/\n--- grep_error_log eval\nqr/execute content moderation/\n--- grep_error_log_out\nexecute content moderation\nexecute content moderation\nexecute content moderation\n"
  },
  {
    "path": "t/plugin/ai-aws-content-moderation-secrets.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n    $ENV{SECRET_ACCESS_KEY} = \"super-secret\";\n    $ENV{ACCESS_KEY_ID} = \"access-key-id\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $main_config = $block->main_config // <<_EOC_;\n        env AWS_REGION=us-east-1;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            listen 2668;\n\n            default_type 'application/json';\n\n            location / {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n                    local core = require(\"apisix.core\")\n                    local open = io.open\n\n                    local f = open('t/assets/content-moderation-responses.json', \"r\")\n                    local resp = f:read(\"*a\")\n                    f:close()\n\n                    if not resp then\n                        ngx.status(503)\n                        ngx.say(\"[INTERNAL FAILURE]: failed to open response.json file\")\n                    end\n\n                    local responses = json.decode(resp)\n                    if not responses then\n                        ngx.status(503)\n                        ngx.say(\"[INTERNAL FAILURE]: failed to decode response.json contents\")\n                    end\n\n                    local headers = ngx.req.get_headers()\n                    local auth_header = headers[\"Authorization\"]\n                    if core.string.find(auth_header, \"access-key-id\") then\n                        ngx.say(json.encode(responses[\"good_request\"]))\n                        return\n                    end\n                    ngx.status = 403\n                    ngx.say(\"unauthorized\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo secret_access_key=super-secret\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo access_key_id=access-key-id\n--- response_body\nSuccess! Data written to: kv/apisix/foo\nSuccess! Data written to: kv/apisix/foo\n\n\n\n=== TEST 2: set secret_access_key and access_key_id as a reference to secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-aws-content-moderation\": {\n                            \"comprehend\": {\n                                \"access_key_id\": \"$secret://vault/test1/foo/access_key_id\",\n                                \"secret_access_key\": \"$secret://vault/test1/foo/secret_access_key\",\n                                \"region\": \"us-east-1\",\n                                \"endpoint\": \"http://localhost:2668\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            ngx.say(\"success\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 3: good request should pass\n--- request\nPOST /echo\n{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"good_request\"}]}\n--- error_code: 200\n--- response_body chomp\n{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"good_request\"}]}\n\n\n\n=== TEST 4: set secret_access_key as a reference to env variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-aws-content-moderation\": {\n                            \"comprehend\": {\n                                \"access_key_id\": \"$env://ACCESS_KEY_ID\",\n                                \"secret_access_key\": \"$env://SECRET_ACCESS_KEY\",\n                                \"region\": \"us-east-1\",\n                                \"endpoint\": \"http://localhost:2668\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(\"success\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 5: good request should pass\n--- request\nPOST /echo\n{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"good_request\"}]}\n--- error_code: 200\n--- response_body chomp\n{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"good_request\"}]}\n"
  },
  {
    "path": "t/plugin/ai-aws-content-moderation.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $main_config = $block->main_config // <<_EOC_;\n        env AWS_REGION=us-east-1;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            listen 2668;\n\n            default_type 'application/json';\n\n            location / {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n                    local open = io.open\n                    local f = open('t/assets/content-moderation-responses.json', \"r\")\n                    local resp = f:read(\"*a\")\n                    f:close()\n\n                    if not resp then\n                        ngx.status(503)\n                        ngx.say(\"[INTERNAL FAILURE]: failed to open response.json file\")\n                    end\n\n                    local responses = json.decode(resp)\n                    if not responses then\n                        ngx.status(503)\n                        ngx.say(\"[INTERNAL FAILURE]: failed to decode response.json contents\")\n                    end\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    if not body then\n                        ngx.status(503)\n                        ngx.say(\"[INTERNAL FAILURE]: failed to get request body: \", err)\n                    end\n\n                    body, err = json.decode(body)\n                    if not body then\n                        ngx.status(503)\n                        ngx.say(\"[INTERNAL FAILURE]: failed to decoded request body: \", err)\n                    end\n                    local result = body.TextSegments[1].Text\n                    local final_response = responses[result] or \"invalid\"\n\n                    if final_response == \"invalid\" then\n                        ngx.status = 500\n                    end\n                    ngx.say(json.encode(final_response))\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-aws-content-moderation\": {\n                            \"comprehend\": {\n                                \"access_key_id\": \"access\",\n                                \"secret_access_key\": \"ea+secret\",\n                                \"region\": \"us-east-1\",\n                                \"endpoint\": \"http://localhost:2668\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: toxic request should fail\n--- request\nPOST /echo\ntoxic\n--- error_code: 400\n--- response_body chomp\nrequest body exceeds toxicity threshold\n\n\n\n=== TEST 3: good request should pass\n--- request\nPOST /echo\ngood_request\n--- error_code: 200\n\n\n\n=== TEST 4: profanity filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-aws-content-moderation\": {\n                            \"comprehend\": {\n                                \"access_key_id\": \"access\",\n                                \"secret_access_key\": \"ea+secret\",\n                                \"region\": \"us-east-1\",\n                                \"endpoint\": \"http://localhost:2668\"\n                            },\n                            \"moderation_categories\": {\n                                \"PROFANITY\": 0.5\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: profane request should fail\n--- request\nPOST /echo\nprofane\n--- error_code: 400\n--- response_body chomp\nrequest body exceeds PROFANITY threshold\n\n\n\n=== TEST 6: very profane request should also fail\n--- request\nPOST /echo\nvery_profane\n--- error_code: 400\n--- response_body chomp\nrequest body exceeds PROFANITY threshold\n\n\n\n=== TEST 7: good_request should pass\n--- request\nPOST /echo\ngood_request\n--- error_code: 200\n\n\n\n=== TEST 8: set profanity = 0.7 (allow profane request but disallow very_profane)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-aws-content-moderation\": {\n                            \"comprehend\": {\n                                \"access_key_id\": \"access\",\n                                \"secret_access_key\": \"ea+secret\",\n                                \"region\": \"us-east-1\",\n                                \"endpoint\": \"http://localhost:2668\"\n                            },\n                            \"moderation_categories\": {\n                                \"PROFANITY\": 0.7\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: profane request should pass profanity check but fail toxicity check\n--- request\nPOST /echo\nprofane\n--- error_code: 400\n--- response_body chomp\nrequest body exceeds toxicity threshold\n\n\n\n=== TEST 10: profane_but_not_toxic request should pass\n--- request\nPOST /echo\nprofane_but_not_toxic\n--- error_code: 200\n\n\n\n=== TEST 11: but very profane request will fail\n--- request\nPOST /echo\nvery_profane\n--- error_code: 400\n--- response_body chomp\nrequest body exceeds PROFANITY threshold\n\n\n\n=== TEST 12: good_request should pass\n--- request\nPOST /echo\ngood_request\n--- error_code: 200\n"
  },
  {
    "path": "t/plugin/ai-aws-content-moderation2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $main_config = $block->main_config // <<_EOC_;\n        env AWS_REGION=us-east-1;\n_EOC_\n\n    $block->set_value(\"main_config\", $main_config);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-aws-content-moderation\": {\n                            \"comprehend\": {\n                                \"access_key_id\": \"access\",\n                                \"secret_access_key\": \"ea+secret\",\n                                \"region\": \"us-east-1\",\n                                \"endpoint\": \"http://localhost:2668\"\n                            },\n                            \"llm_provider\": \"openai\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: request should fail\n--- request\nPOST /echo\ntoxic\n--- error_code: 500\n--- response_body chomp\nComprehend:detectToxicContent() failed to connect to 'http://localhost:2668': connection refused\n--- error_log\nfailed to send request to http://localhost: Comprehend:detectToxicContent() failed to connect to 'http://localhost:2668': connection refused\n"
  },
  {
    "path": "t/plugin/ai-prompt-decorator.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity: configure prepend only\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-decorator\": {\n                            \"prepend\":[\n                                {\n                                    \"role\": \"system\",\n                                    \"content\": \"some content\"\n                                }\n                            ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: test prepend\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, actual_resp = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"some content\" },\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed\")\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: sanity: configure append only\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-decorator\": {\n                            \"append\":[\n                                {\n                                    \"role\": \"system\",\n                                    \"content\": \"some content\"\n                                }\n                            ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 4: test append\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, actual_resp = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" },\n                            { \"role\": \"system\", \"content\": \"some content\" }\n                        ]\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed\")\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: sanity: configure append and prepend both\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-decorator\": {\n                            \"append\":[\n                                {\n                                    \"role\": \"system\",\n                                    \"content\": \"some append\"\n                                }\n                            ],\n                            \"prepend\":[\n                                {\n                                    \"role\": \"system\",\n                                    \"content\": \"some prepend\"\n                                }\n                            ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 6: test append\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, actual_resp = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"some prepend\" },\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" },\n                            { \"role\": \"system\", \"content\": \"some append\" }\n                        ]\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed\")\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: verify no message accumulation across multiple requests\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- Configure route with prepend\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-decorator\": {\n                            \"prepend\":[\n                                {\n                                    \"role\": \"system\",\n                                    \"content\": \"system prompt\"\n                                }\n                            ]\n                        }\n                    }\n            }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to configure route\")\n                return\n            end\n\n            -- First request\n            local code1, body1, actual_resp1 = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"user\", \"content\": \"first message\" }\n                        ]\n                    }]],\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"system prompt\" },\n                            { \"role\": \"user\", \"content\": \"first message\" }\n                        ]\n                    }]]\n            )\n\n            if code1 >= 300 then\n                ngx.status = code1\n                ngx.say(\"first request failed\")\n                return\n            end\n\n            -- Second request should have the same structure, not accumulating history\n            local code2, body2, actual_resp2 = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"user\", \"content\": \"second message\" }\n                        ]\n                    }]],\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"system prompt\" },\n                            { \"role\": \"user\", \"content\": \"second message\" }\n                        ]\n                    }]]\n            )\n\n            if code2 >= 300 then\n                ngx.status = code2\n                ngx.say(\"second request failed\")\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: sanity: configure neither append nor prepend should fail\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-decorator\": {\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body_eval\nqr/.*failed to check the configuration of plugin ai-prompt-decorator err.*/\n--- error_code: 400\n"
  },
  {
    "path": "t/plugin/ai-prompt-guard.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: wrong regex should fail validation\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                        \"match_all_roles\": true,\n                        \"deny_patterns\": [\n                            \"(abc\"\n                        ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body eval\nqr/.*failed to check the configuration of plugin ai-prompt-guard.*/\n--- error_code: 400\n\n\n\n=== TEST 2: setup route with both allow and deny with match_all_roles\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                        \"match_all_roles\": true,\n                            \"allow_patterns\": [\n                                \"goodword\"\n                            ],\n                        \"deny_patterns\": [\n                            \"badword\"\n                        ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 3: send request with good word\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"goodword\" }\n    ]\n}\n\n\n\n=== TEST 4: send request with bad word\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"badword\" }\n    ]\n}\n--- response_body\n{\"message\":\"Request doesn't match allow patterns\"}\n--- error_code: 400\n\n\n\n=== TEST 5: setup route with only deny with match_all_roles\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                        \"match_all_roles\": true,\n                        \"deny_patterns\": [\n                            \"badword\"\n                        ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 6: send request with good word\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"goodword\" }\n    ]\n}\n\n\n\n=== TEST 7: send request with bad word\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"badword\" }\n    ]\n}\n--- response_body\n{\"message\":\"Request contains prohibited content\"}\n--- error_code: 400\n\n\n\n=== TEST 8: setup route with only allow with match_all_roles=false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                            \"allow_patterns\": [\n                                \"goodword\"\n                            ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 9: send request with bad word and it will pass for non user\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"badword\" }\n    ]\n}\n\n\n\n=== TEST 10: send request with bad word\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"user\", \"content\": \"badword\" }\n    ]\n}\n--- response_body\n{\"message\":\"Request doesn't match allow patterns\"}\n--- error_code: 400\n\n\n\n=== TEST 11: setup route with only deny with match_all_conversation_history\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                        \"match_all_conversation_history\": true,\n                        \"match_all_roles\": true,\n                        \"deny_patterns\": [\n                            \"badword\"\n                        ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 12: send request with good word but had bad word in history\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"goodword\" },\n        { \"role\": \"system\", \"content\": \"badword\" },\n        { \"role\": \"system\", \"content\": \"goodword\" }\n    ]\n}\n--- response_body\n{\"message\":\"Request contains prohibited content\"}\n--- error_code: 400\n\n\n\n=== TEST 13: setup route with only deny with match_all_conversation_history=false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                        \"match_all_conversation_history\": false,\n                        \"match_all_roles\": true,\n                        \"deny_patterns\": [\n                            \"badword\"\n                        ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 14: send request with good word but had bad word in history\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"goodword\" },\n        { \"role\": \"system\", \"content\": \"badword\" },\n        { \"role\": \"system\", \"content\": \"goodword\" }\n    ]\n}\n\n\n\n=== TEST 15: setup route + deny + match_all_roles + pattern match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-guard\": {\n                        \"match_all_roles\": true,\n                        \"deny_patterns\": [\n                            \"^[A-Za-z0-9_]+badword$\"\n                        ]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 16: send request with good word\n--- request\nPOST /hello\n{\n    \"messages\": [\n        { \"role\": \"system\", \"content\": \"anaapsanaapbadword\" }\n    ]\n}\n--- response_body\n{\"message\":\"Request contains prohibited content\"}\n--- error_code: 400\n"
  },
  {
    "path": "t/plugin/ai-prompt-template.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-template\": {\n                            \"templates\":[\n                                {\n                                    \"name\": \"programming question\",\n                                    \"template\": {\n                                        \"model\": \"some model\",\n                                        \"messages\": [\n                                            { \"role\": \"system\", \"content\": \"You are a {{ language }} programmer.\" },\n                                            { \"role\": \"user\", \"content\": \"Write a {{ program_name }} program.\" }\n                                        ]\n                                    }\n                                },\n                                {\n                                    \"name\": \"level of detail\",\n                                    \"template\": {\n                                        \"model\": \"some model\",\n                                        \"messages\": [\n                                            { \"role\": \"user\", \"content\": \"Explain about {{ topic }} in {{ level }}.\" }\n                                        ]\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: no templates\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-template\": {\n                            \"templates\":[]\n                        }\n                    }\n                }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- error_code: 400\n--- response_body eval\nqr/.*property \\\\\"templates\\\\\" validation failed: expect array to have at least 1 items.*/\n\n\n\n=== TEST 3: test template insertion\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"apisix.core.json\")\n            local code, body, actual_resp = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"template_name\": \"programming question\",\n                        \"language\": \"python\",\n                        \"program_name\": \"quick sort\"\n                    }]],\n                    [[{\n                        \"model\": \"some model\",\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a python programmer.\" },\n                            { \"role\": \"user\", \"content\": \"Write a quick sort program.\" }\n                        ]\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: multiple templates\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ai-prompt-template\": {\n                            \"templates\":[\n                                {\n                                    \"name\": \"programming question\",\n                                    \"template\": {\n                                        \"model\": \"some model\",\n                                        \"messages\": [\n                                            { \"role\": \"system\", \"content\": \"You are a {{ language }} programmer.\" },\n                                            { \"role\": \"user\", \"content\": \"Write a {{ program_name }} program.\" }\n                                        ]\n                                    }\n                                },\n                                {\n                                    \"name\": \"level of detail\",\n                                    \"template\": {\n                                        \"model\": \"some model\",\n                                        \"messages\": [\n                                            { \"role\": \"user\", \"content\": \"Explain about {{ topic }} in {{ level }}.\" }\n                                        ]\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 5: test second template\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"apisix.core.json\")\n            local code, body, actual_resp = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"template_name\": \"level of detail\",\n                        \"topic\": \"psychology\",\n                        \"level\": \"brief\"\n                    }]],\n                    [[{\n                        \"model\": \"some model\",\n                        \"messages\": [\n                            { \"role\": \"user\", \"content\": \"Explain about psychology in brief.\" }\n                        ]\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: missing template items\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"apisix.core.json\")\n            local code, body, actual_resp = t('/echo',\n                    ngx.HTTP_POST,\n                    [[{\n                        \"template_name\": \"level of detail\",\n                        \"topic-missing\": \"psychology\",\n                        \"level-missing\": \"brief\"\n                    }]],\n                    [[{\n                        \"model\": \"some model\",\n                        \"messages\": [\n                            { \"role\": \"user\", \"content\": \"Explain about  in .\" }\n                        ]\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: request body contains non-existent template\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"apisix.core.json\")\n            local code, body, actual_resp = t('/echo',\n                ngx.HTTP_POST,\n                [[{\n                    \"template_name\": \"random\",\n                    \"some-key\": \"some-value\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/.*template: random not configured.*/\n\n\n\n=== TEST 8: request body contains non-existent template\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"apisix.core.json\")\n            local code, body, actual_resp = t('/echo',\n                ngx.HTTP_POST,\n                [[{\n                    \"missing-template-name\": \"haha\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/.*template name is missing in request.*/\n\n\n\n=== TEST 9: (cache test) same template name in different routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 5, 1 do\n                local code = t('/apisix/admin/routes/' .. i,\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\": \"/]] .. i .. [[\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ai-prompt-template\": {\n                                \"templates\":[\n                                    {\n                                        \"name\": \"same name\",\n                                        \"template\": {\n                                            \"model\": \"some model\",\n                                            \"messages\": [\n                                                { \"role\": \"system\", \"content\": \"Field: {{ field }} in route]] .. i .. [[.\" }\n                                            ]\n                                        }\n                                    }\n                                ]\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        }\n                    }]]\n                )\n\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.say(\"failed\")\n                    return\n                end\n            end\n\n            for i = 1, 5, 1 do\n                local code, body = t('/' .. i,\n                    ngx.HTTP_POST,\n                    [[{\n                        \"template_name\": \"same name\",\n                        \"field\": \"foo\"\n                    }]],\n                    [[{\n                        \"model\": \"some model\",\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"Field: foo in route]] .. i .. [[.\" }\n                        ]\n                    }]]\n                )\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.say(body)\n                    return\n                end\n            end\n            ngx.status = 200\n            ngx.say(\"passed\")\n        }\n    }\n\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/ai-proxy-anthropic.t",
    "content": "\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/openai-compatible-api-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name anthropic;\n            listen 6725;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"anthropic\",\n                                    \"provider\": \"anthropic\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"claude-sonnet-4-5\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6725/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 3: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"anthropic\",\n                                    \"provider\": \"anthropic\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"claude-sonnet-4-5\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data: \\[DONE\\]\\n\\n/\n"
  },
  {
    "path": "t/plugin/ai-proxy-azure-openai.t",
    "content": "\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n                        if body.model then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request. model passed\"}]])\n                            return\n                        end\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"azure-openai\",\n                                    \"provider\": \"azure-openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4o\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 3: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"azure-openai\",\n                                    \"provider\": \"azure-openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4o\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data: \\[DONE\\]\\n\\n/\n"
  },
  {
    "path": "t/plugin/ai-proxy-gemini.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"debug\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"gemini\",\n                                    \"provider\": \"gemini\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 3: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"gemini\",\n                                    \"provider\": \"gemini\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data: \\[DONE\\]\\n\\n/\n"
  },
  {
    "path": "t/plugin/ai-proxy-kafka-log.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /v1/embeddings {\n                content_by_lua_block {\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"unsupported request method: \", ngx.req.get_method())\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    if header_auth ~= \"Bearer token\" then\n                        ngx.status = 401\n                        ngx.say(\"unauthorized\")\n                        return\n                    end\n\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    local json = require(\"cjson.safe\")\n                    body, err = json.decode(body)\n                    if err then\n                        ngx.status = 400\n                        ngx.say(\"failed to get request body: \", err)\n                    end\n\n                    if body.model ~= \"text-embedding-ada-002\" then\n                        ngx.status = 400\n                        ngx.say(\"unsupported model: \", body.model)\n                        return\n                    end\n\n                    if body.encoding_format ~= \"float\" then\n                        ngx.status = 400\n                        ngx.say(\"unsupported encoding format: \", body.encoding_format)\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.say([[\n                        {\n                          \"object\": \"list\",\n                          \"data\": [\n                            {\n                              \"object\": \"embedding\",\n                              \"embedding\": [\n                                0.0023064255,\n                                -0.009327292,\n                                -0.0028842222\n                              ],\n                              \"index\": 0\n                            }\n                          ],\n                          \"model\": \"text-embedding-ada-002\",\n                          \"usage\": {\n                            \"prompt_tokens\": 8,\n                            \"total_tokens\": 8\n                          }\n                        }\n                    ]])\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with logging summaries and payloads\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false,\n                            \"logging\": {\n                                \"summaries\": true,\n                                \"payloads\": true\n                            }\n                        },\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_log\nsend data to kafka:\nllm_request\nllm_summary\nYou are a mathematician\ngpt-35-turbo-instruct\nllm_response_text\n\n\n\n=== TEST 3: set route with logging summary but no payload\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false,\n                            \"logging\": {\n                                \"summaries\": true,\n                                \"payloads\": false\n                            }\n                        },\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_log\nsend data to kafka:\nllm_summary\ngpt-35-turbo-instruct\n--- no_error_log\nllm_request\nllm_response_text\n\n\n\n=== TEST 5: set route with no logging summary and payload - default behaviour\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false,\n                            \"logging\": {\n                                \"summaries\": false,\n                                \"payloads\": false\n                            }\n                        },\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- no_error_log\nllm_request\nllm_response_text\nllm_summary\n\n\n\n=== TEST 7: set route with stream = true (SSE) with ai-proxy-multi plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false,\n                            \"logging\": {\n                                \"summaries\": true,\n                                \"payloads\": true\n                            }\n                        },\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"stream\": true,\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ]\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_eval\nqr/6data: \\[DONE\\]\\n\\n/\n--- error_log\nsend data to kafka:\nllm_request\nllm_summary\nsome content\n"
  },
  {
    "path": "t/plugin/ai-proxy-multi.balancer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai_rate_limit;\n            default_type 'application/json';\n            listen 6726;\n            location / {\n              content_by_lua_block {\n                ngx.status = 429\n                ngx.say([[{ \"error\": {\"message\":\"rate limit exceeded\"}}]])\n                return\n              }\n            }\n        }\n        server {\n            server_name openai_internal_error;\n            default_type 'application/json';\n            listen 6727;\n            location / {\n              content_by_lua_block {\n                ngx.status = 500\n                ngx.say([[{ \"error\": {\"message\":\"internal server error\"}}]])\n                return\n              }\n            }\n        }\n        server {\n            server_name openai_internal_error;\n            default_type 'application/json';\n            listen 6728;\n            location / {\n              content_by_lua_block {\n                ngx.status = 503\n                ngx.say([[{ \"error\": {\"message\":\"service unavailable\"}}]])\n                return\n              }\n            }\n        }\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.print(\"openai\")\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.print(\"deepseek\")\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with roundrobin balancer, weight 4 and 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 4,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"deepseek\",\n                                    \"provider\": \"deepseek\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"deepseek-chat\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: test\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n\n            local restab = {}\n\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n\n            table.sort(restab)\n            ngx.log(ngx.WARN, \"test picked instances: \", table.concat(restab, \".\"))\n\n        }\n    }\n--- request\nGET /t\n--- error_log\ndeepseek.deepseek.openai.openai.openai.openai.openai.openai.openai.openai\n\n\n\n=== TEST 3: set route with chash balancer, weight 4 and 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"balancer\": {\n                                \"algorithm\": \"chash\",\n                                \"hash_on\": \"vars\",\n                                \"key\": \"query_string\"\n                            },\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 4,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"deepseek\",\n                                    \"provider\": \"deepseek\",\n                                    \"weight\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"deepseek-chat\",\"max_tokens\": 512,\"temperature\": 1.0},\n                                    \"override\": {\"endpoint\": \"http://localhost:6724/chat/completions\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n\n            local restab = {}\n\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: deepseek: 2\ndistribution: openai: 8\n\n\n\n=== TEST 5: set route with fallback_strategy with 500 response code openai.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": [\"http_5xx\"],\n                            \"balancer\": {\n                                \"algorithm\": \"chash\",\n                                \"hash_on\": \"vars\",\n                                \"key\": \"query_string\"\n                            },\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 4,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6727\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"deepseek\",\n                                    \"provider\": \"deepseek\",\n                                    \"weight\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"deepseek-chat\",\"max_tokens\": 512,\"temperature\": 1.0},\"override\": {\"endpoint\": \"http://localhost:6724/chat/completions\"}}\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: test all requests success with fallback deepseek\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n            local restab = {}\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: deepseek: 10\n\n\n\n=== TEST 7: set route with fallback_strategy with too many requests openai.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": [\"http_429\"],\n                            \"balancer\": {\n                                \"algorithm\": \"chash\",\n                                \"hash_on\": \"vars\",\n                                \"key\": \"query_string\"\n                            },\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 4,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6726\"\n                                    }\n                                },\n                                {\"name\":\"deepseek\",\"provider\":\"deepseek\",\"weight\":1,\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"options\":{\"model\":\"deepseek-chat\",\"max_tokens\":512,\"temperature\":1.0},\"override\":{\"endpoint\":\"http://localhost:6724/chat/completions\"}}\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: test all requests success with fallback deepseek\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n            local restab = {}\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: deepseek: 10\n\n\n\n=== TEST 9: set route with fallback_strategy with unreachable openai.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": [\"http_5xx\"],\n                            \"balancer\": {\n                                \"algorithm\": \"chash\",\n                                \"hash_on\": \"vars\",\n                                \"key\": \"query_string\"\n                            },\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 4,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6725\"\n                                    }\n                                },\n                                {\"name\":\"deepseek\",\"provider\":\"deepseek\",\"weight\":1,\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"options\":{\"model\":\"deepseek-chat\",\"max_tokens\":512,\"temperature\":1.0},\"override\":{\"endpoint\":\"http://localhost:6724/chat/completions\"}}\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: test all requests success with fallback deepseek\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n            local restab = {}\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: deepseek: 10\n\n\n\n=== TEST 11: set route with fallback_strategy with service unavailable openai.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": [\"http_5xx\"],\n                            \"balancer\": {\n                                \"algorithm\": \"chash\",\n                                \"hash_on\": \"vars\",\n                                \"key\": \"query_string\"\n                            },\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 4,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6728\"\n                                    }\n                                },\n                                {\"name\":\"deepseek\",\"provider\":\"deepseek\",\"weight\":1,\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"options\":{\"model\":\"deepseek-chat\",\"max_tokens\":512,\"temperature\":1.0},\"override\":{\"endpoint\":\"http://localhost:6724/chat/completions\"}}\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: test all requests success with fallback deepseek\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n            local restab = {}\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: deepseek: 10\n\n\n\n=== TEST 13: set route with fallback_strategy with service unavailable openai having high priority.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"balancer\": {\n                                \"algorithm\": \"roundrobin\",\n                                \"hash_on\": \"vars\"\n                            },\n                            \"fallback_strategy\": [\n                                \"http_429\",\n                                \"http_5xx\"\n                            ],\n                            \"instances\": [\n                               {\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"name\":\"mock-429\",\"override\":{\"endpoint\":\"http://localhost:6726\"},\"priority\":10,\"provider\":\"openai-compatible\",\"weight\":10},{\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"name\":\"mock-500\",\"override\":{\"endpoint\":\"http://localhost:6727\"},\"priority\":0,\"provider\":\"openai-compatible\",\"weight\":10},{\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"name\":\"mock-200\",\"override\":{\"endpoint\":\"http://localhost:6724/chat/completions\"},\"priority\":0,\"provider\":\"openai-compatible\",\"weight\":1}\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: test all requests success with fallback deepseek\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n            local restab = {}\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.body)\n            end\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: deepseek: 10\n\n\n\n=== TEST 15: set route with fallback_strategy with only service unavailable and 429.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"balancer\": {\n                                \"algorithm\": \"roundrobin\",\n                                \"hash_on\": \"vars\"\n                            },\n                            \"fallback_strategy\": [\n                                \"http_429\",\n                                \"http_5xx\"\n                            ],\n                            \"instances\": [\n                                {\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"name\": \"mock-429\",\n                                    \"override\": {\n                                        \"endpoint\":  \"http://localhost:6726\"\n                                    },\n                                    \"priority\": 10,\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 10\n                                    },\n                                {\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"name\": \"mock-500\",\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6727\"\n                                    },\n                                    \"priority\": 0,\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 10\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: test all requests success with fallback deepseek should return 502\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/anything\"\n            local restab = {}\n            local body = [[{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }]]\n            for i = 1, 10 do\n                local httpc = http.new()\n                local query = {\n                    index = i\n                }\n                local res, err = httpc:request_uri(uri, {method = \"POST\", body = body, query = query})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(restab, res.status)\n            end\n            local count = {}\n            for _, value in ipairs(restab) do\n                count[value] = (count[value] or 0) + 1\n            end\n            for p, num in pairs(count) do\n                ngx.log(ngx.WARN, \"distribution: \", p, \": \", num)\n            end\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- error_log\ndistribution: 502: 10\n\n\n\n=== TEST 17: set route with only one instance and configure it with http_5xx fallback_strategy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local request_body = {\n                uri = \"/anything\",\n                plugins = {\n                    [\"ai-proxy-multi\"] = {\n                        fallback_strategy = {\"http_5xx\"},\n                        instances = {\n                          {\n                            name = \"deepseek\",\n                            provider = \"deepseek\",\n                            auth = {\n                                header = {Authorization = \"Bearer token\" }\n                            },\n                            weight = 1,\n                            override = { endpoint = \"http://localhost:6727\" }\n                          }\n                        },\n                        ssl_verify = false\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(request_body)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nHost: openai_internal_error\n--- error_code: 500\n--- no_error_log\n[error]\n"
  },
  {
    "path": "t/plugin/ai-proxy-multi.openai-compatible.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 3: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"custom-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data: \\[DONE\\]\\n\\n/\n"
  },
  {
    "path": "t/plugin/ai-proxy-multi.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: minimal viable configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-proxy-multi\")\n            local ok, err = plugin.check_schema({\n                instances = {\n                    {\n                        name = \"openai-official\",\n                        provider = \"openai\",\n                        options = {\n                            model = \"gpt-4\",\n                        },\n                        weight = 1,\n                        auth = {\n                            header = {\n                                some_header = \"some_value\"\n                            }\n                        }\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: unsupported provider\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-proxy-multi\")\n            local ok, err = plugin.check_schema({\n                instances = {\n                    {\n                        name = \"self-hosted\",\n                        provider = \"some-unique\",\n                        options = {\n                            model = \"gpt-4\",\n                        },\n                        weight = 1,\n                        auth = {\n                            header = {\n                                some_header = \"some_value\"\n                            }\n                        }\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body eval\nqr/.*property \"provider\" validation failed: matches none of the enum values*/\n\n\n\n=== TEST 3: set route with wrong auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer wrongtoken\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 401\n--- response_body\nUnauthorized\n\n\n\n=== TEST 5: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 7: send request with empty body\n--- request\nPOST /anything\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 400\n--- response_body_chomp\nfailed to get request body: request body is empty\n\n\n\n=== TEST 8: send request with wrong method (GET) should work\n--- request\nGET /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 9: wrong JSON in request body should give error\n--- request\nGET /anything\n{}\"messages\": [ { \"role\": \"system\", \"cont\n--- error_code: 400\n--- response_body\n{\"message\":\"could not get parse JSON request body: Expected the end but found T_STRING at character 3\"}\n\n\n\n=== TEST 10: content-type should be JSON\n--- request\nPOST /anything\nprompt%3Dwhat%2520is%25201%2520%252B%25201\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 400\n--- response_body chomp\nunsupported content-type: application/x-www-form-urlencoded, only application/json is supported\n\n\n\n=== TEST 11: model options being merged to request body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"some-model\",\n                                        \"foo\": \"bar\",\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"test-type\"] = \"options\",\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            ngx.status = code\n            ngx.say(actual_body)\n\n        }\n    }\n--- error_code: 200\n--- response_body_chomp\noptions_works\n\n\n\n=== TEST 12: override path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"some-model\",\n                                        \"foo\": \"bar\",\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/random\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"test-type\"] = \"path\",\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            ngx.status = code\n            ngx.say(actual_body)\n\n        }\n    }\n--- response_body_chomp\npath override works\n\n\n\n=== TEST 13: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-35-turbo-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ]\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_eval\nqr/6data: \\[DONE\\]\\n\\n/\n\n\n\n=== TEST 15: set route with wrong override endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http//localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/.invalid endpoint.*/\n\n\n\n=== TEST 16: schema accepts 'logging'\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-proxy-multi\")\n\n            local ok, err = plugin.check_schema({\n                instances = {\n                    {\n                        name = \"openai-1\",\n                        provider = \"openai\",\n                        weight = 1,\n                        auth = { header = { apikey = \"token\" } },\n                        options = { model = \"gpt-4\" },\n                    },\n                },\n                logging = { summaries = true },\n            })\n\n            if ok then\n                ngx.say(\"ok\")\n            else\n                ngx.say(\"bad:\" .. (err or \"\"))\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n"
  },
  {
    "path": "t/plugin/ai-proxy-multi2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n\n                    if query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n\n                    ngx.status = 200\n                    ngx.say(\"passed\")\n                }\n            }\n\n\n            location /test/params/in/overridden/endpoint {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n                    local core = require(\"apisix.core\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n                    ngx.log(ngx.INFO, \"found query params: \", core.json.stably_encode(ngx.req.get_uri_args()))\n\n                    if query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.say(\"passed\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with wrong query param\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"query\": {\n                                            \"api_key\": \"wrong_key\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-35-turbo-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 401\n--- response_body\nUnauthorized\n\n\n\n=== TEST 3: set route with right query param\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"query\": {\n                                            \"api_key\": \"apikey\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-35-turbo-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 5: set route without overriding the endpoint_url\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"some-key\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-35-turbo-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: send request\n--- custom_trusted_cert: /etc/ssl/certs/ca-certificates.crt\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 401\n\n\n\n=== TEST 7: query params in override.endpoint should be sent to LLM\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-official\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"query\": {\n                                            \"api_key\": \"apikey\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-35-turbo-instruct\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                       \"endpoint\": \"http://localhost:6724/test/params/in/overridden/endpoint?some_query=yes\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 200\n--- error_log\nfound query params: {\"api_key\":\"apikey\",\"some_query\":\"yes\"}\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/ai-proxy-multi3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse Test::Nginx::Socket::Lua $SkipReason ? (skip_all => $SkipReason) : ();\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 16724;\n\n            default_type 'application/json';\n\n            location /anything {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body = ngx.req.get_body_data()\n\n                    if body ~= \"SELECT * FROM STUDENTS\" then\n                        ngx.status = 503\n                        ngx.say(\"passthrough doesn't work\")\n                        return\n                    end\n                    ngx.say('{\"foo\", \"bar\"}')\n                }\n            }\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say(string.format([[\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": { \"content\": \"1 + 1 = 2.\", \"role\": \"assistant\" }\n    }\n  ],\n  \"created\": 1723780938,\n  \"id\": \"chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P\",\n  \"model\": \"%s\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": { \"completion_tokens\": 5, \"prompt_tokens\": 8, \"total_tokens\": 10 }\n}\n                        ]], body.model))\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n\n            location ~ ^/status.* {\n                content_by_lua_block {\n                    local test_dict = ngx.shared[\"test\"]\n                    local uri = ngx.var.uri\n                    local total_key = uri .. \"#total\"\n                    local count_key = uri .. \"#count\"\n                    local total = test_dict:get(total_key)\n                    if not total then\n                        return\n                    end\n\n                    local count = test_dict:incr(count_key, 1, 0)\n                    ngx.log(ngx.INFO, \"uri: \", uri, \" total: \", total, \" count: \", count)\n                    if count < total then\n                        return\n                    end\n                    ngx.status = 500\n                    ngx.say(\"error\")\n                }\n            }\n\n            location /error {\n                content_by_lua_block {\n                    ngx.status = 500\n                    ngx.say(\"error\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route, only one instance has checker\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-4\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"},\n                                    \"checks\": {\n                                        \"active\": {\n                                            \"timeout\": 5,\n                                            \"http_path\": \"/status/gpt4\",\n                                            \"host\": \"foo.com\",\n                                            \"healthy\": {\"interval\": 1,\"successes\": 1},\n                                            \"unhealthy\": {\"interval\": 1,\"http_failures\": 1},\n                                            \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                                        }\n                                    }\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-3\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: once instance changes from unhealthy to healthy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local test_dict = ngx.shared[\"test\"]\n\n            local send_request = function()\n                local code, _, body = t(\"/ai\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"request should be successful\")\n                return body\n            end\n\n            -- set the instance to unhealthy\n            test_dict:set(\"/status/gpt4#total\", 0)\n            -- trigger the health check\n            send_request()\n            ngx.sleep(1)\n\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            assert(instances_count[\"gpt-4\"] <= 2, \"gpt-4 should be unhealthy\")\n            assert(instances_count[\"gpt-3\"] >= 8, \"gpt-3 should be healthy\")\n\n            -- set the instance to healthy\n            test_dict:set(\"/status/gpt4#total\", 30)\n            ngx.sleep(1)\n\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n\n            local v = instances_count[\"gpt-4\"] - instances_count[\"gpt-3\"]\n            assert(v <= 2, \"difference between gpt-4 and gpt-3 should be less than 2\")\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 20\n--- response_body\npassed\n\n\n\n=== TEST 3: set service, only one instance has checker\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-4\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"},\n                                    \"checks\": {\n                                        \"active\": {\n                                            \"timeout\": 5,\n                                            \"http_path\": \"/status/gpt4\",\n                                            \"host\": \"foo.com\",\n                                            \"healthy\": {\"interval\": 1,\"successes\": 1},\n                                            \"unhealthy\": {\"interval\": 1,\"http_failures\": 1},\n                                            \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                                        }\n                                    }\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-3\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: set route 1 related to service 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"service_id\": 1\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: instance changes from unhealthy to healthy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local test_dict = ngx.shared[\"test\"]\n            local send_request = function()\n                local code, _, body = t(\"/ai\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"request should be successful\")\n                return body\n            end\n            -- set the instance to unhealthy\n            test_dict:set(\"/status/gpt4#total\", 0)\n            -- trigger the health check\n            send_request()\n            ngx.sleep(2)\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            assert(instances_count[\"gpt-4\"] <= 2, \"gpt-4 should be unhealthy\")\n            assert(instances_count[\"gpt-3\"] >= 8, \"gpt-3 should be healthy\")\n            -- set the instance to healthy\n            test_dict:set(\"/status/gpt4#total\", 30)\n            ngx.sleep(2)\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            local diff = instances_count[\"gpt-4\"] - instances_count[\"gpt-3\"]\n            assert(diff <= 2, \"difference between gpt-4 and gpt-3 should be less than 2\")\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 20\n--- response_body\npassed\n\n\n\n=== TEST 6: set route, two instances have checker\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local checks_tmp = [[\n                \"checks\": {\n                    \"active\": {\n                        \"timeout\": 5,\n                        \"http_path\": \"/status/%s\",\n                        \"host\": \"foo.com\",\n                        \"healthy\": {\n                            \"interval\": 1,\n                            \"successes\": 1\n                        },\n                        \"unhealthy\": {\n                            \"interval\": 1,\n                            \"http_failures\": 1\n                        },\n                        \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                    }\n                }\n            ]]\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    },\n                                    ]] .. string.format(checks_tmp, \"gpt4\").. [[\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-3\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    },\n                                    ]] .. string.format(checks_tmp, \"gpt3\") .. [[\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: healthy conversion of two instances\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local test_dict = ngx.shared[\"test\"]\n            local send_request = function()\n                local code, _, body = t(\"/ai\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"request should be successful\")\n                return body\n            end\n            -- set the gpt4 instance to unhealthy\n            -- set the gpt3 instance to healthy\n            test_dict:set(\"/status/gpt4#total\", 0)\n            test_dict:set(\"/status/gpt3#total\", 50)\n            -- trigger the health check\n            send_request()\n            ngx.sleep(2)\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            assert(instances_count[\"gpt-4\"] <= 2, \"gpt-4 should be unhealthy\")\n            assert(instances_count[\"gpt-3\"] >= 8, \"gpt-3 should be healthy\")\n            -- set the gpt4 instance to healthy\n            -- set the gpt3 instance to unhealthy\n            test_dict:set(\"/status/gpt4#total\", 50)\n            test_dict:set(\"/status/gpt3#total\", 0)\n            ngx.sleep(2)\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            assert(instances_count[\"gpt-4\"] >= 8, \"gpt-4 should be healthy\")\n            assert(instances_count[\"gpt-3\"] <= 2, \"gpt-3 should be unhealthy\")\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 20\n--- response_body\npassed\n\n\n\n=== TEST 8: set route, two instances have checker\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local checks_tmp = [[\n                \"checks\": {\n                    \"active\": {\n                        \"timeout\": 5,\n                        \"http_path\": \"/status/%s\",\n                        \"host\": \"foo.com\",\n                        \"healthy\": {\n                            \"interval\": 1,\n                            \"successes\": 1\n                        },\n                        \"unhealthy\": {\n                            \"interval\": 1,\n                            \"http_failures\": 1\n                        },\n                        \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                    }\n                }\n            ]]\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    },\n                                    ]] .. string.format(checks_tmp, \"gpt4\").. [[\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-3\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    },\n                                    ]] .. string.format(checks_tmp, \"gpt3\") .. [[\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: set route 1 related to service 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"service_id\": 1\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: healthy conversion of two instances\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local test_dict = ngx.shared[\"test\"]\n            local send_request = function()\n                local code, _, body = t(\"/ai\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"request should be successful\")\n                return body\n            end\n            -- set the gpt4 instance to unhealthy\n            -- set the gpt3 instance to healthy\n            test_dict:set(\"/status/gpt4#total\", 0)\n            test_dict:set(\"/status/gpt3#total\", 50)\n            -- trigger the health check\n            send_request()\n            ngx.sleep(1.2)\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            assert(instances_count[\"gpt-4\"] <= 2, \"gpt-4 should be unhealthy\")\n            assert(instances_count[\"gpt-3\"] >= 8, \"gpt-3 should be healthy\")\n            -- set the gpt4 instance to healthy\n            -- set the gpt3 instance to unhealthy\n            test_dict:set(\"/status/gpt4#total\", 50)\n            test_dict:set(\"/status/gpt3#total\", 0)\n            ngx.sleep(1.2)\n            local instances_count = {\n                [\"gpt-4\"] = 0,\n                [\"gpt-3\"] = 0,\n            }\n            for i = 1, 10 do\n                local resp = send_request()\n                if core.string.find(resp, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = instances_count[\"gpt-4\"] + 1\n                else\n                    instances_count[\"gpt-3\"] = instances_count[\"gpt-3\"] + 1\n                end\n                if i == 1 then\n                    ngx.sleep(4) -- trigger healthcheck\n                end\n            end\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n            assert(instances_count[\"gpt-4\"] >= 8, \"gpt-4 should be healthy\")\n            assert(instances_count[\"gpt-3\"] <= 2, \"gpt-3 should be unhealthy\")\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 20\n--- response_body\npassed\n\n\n\n=== TEST 11: configure health check for well-known ai service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\"\n                                    },\n                                    \"checks\": {\n                                        \"active\": {\n                                            \"timeout\": 5,\n                                            \"http_path\": \"/\",\n                                            \"healthy\": {\n                                                \"interval\": 1,\n                                                \"successes\": 1\n                                            },\n                                            \"unhealthy\": {\n                                                \"interval\": 1,\n                                                \"http_failures\": 1\n                                            },\n                                            \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                                        }\n                                    }\n                                },\n                                {\"name\":\"openai-gpt3\",\"provider\":\"openai\",\"weight\":1,\"priority\":1,\"auth\":{\"header\":{\"Authorization\":\"Bearer token\"}},\"options\":{\"model\":\"gpt-3\"}}\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: send request to /ai should failed with 401\n--- request\nPOST /ai\n{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"write a haiku about ai\"\n    }\n  ]\n}\n--- error_code: 401\n\n\n\n=== TEST 13: DNS change doesn't cause health check errors\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local resolver = require(\"apisix.core.resolver\")\n            -- Mock resolver.parse_domain to return different IPs on different calls\n            local original_parse_domain = resolver.parse_domain\n            local call_count = 0\n            resolver.parse_domain = function(host)\n                if host == \"test.example.com\" then\n                    call_count = call_count + 1\n                    if call_count == 1 then\n                        return \"127.0.0.1\"\n                    else\n                        return \"127.0.0.2\"\n                    end\n                end\n                return original_parse_domain(host)\n            end\n            -- Create a route with health check that uses the domain\n            local core = require(\"apisix.core\")\n            local route_config = {\n                uri = \"/ai\",\n                plugins = {\n                    [\"ai-proxy-multi\"] = {\n                        instances = {\n                            {\n                                name = \"openai-test\",\n                                provider = \"openai\",\n                                weight = 1,\n                                priority = 1,\n                                auth = {\n                                    header = {\n                                        Authorization = \"Bearer token\"\n                                    }\n                                },\n                                options = {\n                                    model = \"gpt-4\"\n                                },\n                                override = {\n                                    endpoint = \"http://test.example.com:16724\"\n                                },\n                                checks = {\n                                    active = {\n                                        timeout = 5,\n                                        http_path = \"/status/test\",\n                                        host = \"test.example.com\",\n                                        healthy = {\n                                            interval = 1,\n                                            successes = 1\n                                        },\n                                        unhealthy = {\n                                            interval = 1,\n                                            http_failures = 1\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                name = \"openai-test-2\",\n                                provider = \"openai\",\n                                weight = 1,\n                                priority = 1,\n                                auth = {\n                                    header = {\n                                        Authorization = \"Bearer token\"\n                                    }\n                                },\n                                options = {\n                                    model = \"gpt-4\"\n                                },\n                                override = {\n                                    endpoint = \"http://test.example.com:16724\"\n                                },\n                                checks = {\n                                    active = {\n                                        timeout = 5,\n                                        http_path = \"/status/test\",\n                                        host = \"test.example.com\",\n                                        healthy = {\n                                            interval = 1,\n                                            successes = 1\n                                        },\n                                        unhealthy = {\n                                            interval = 1,\n                                            http_failures = 1\n                                        }\n                                    }\n                                }\n                            }\n                        },\n                        ssl_verify = false\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 core.json.encode(route_config)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, _, body = t(\"/ai\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            -- Wait a bit for health check to run\n            ngx.sleep(1.5)\n\n            local code, _, body = t(\"/ai\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            -- Restore original function\n            resolver.parse_domain = original_parse_domain\n            ngx.sleep(3)\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\npassed\n--- no_error_log\nfailed to get health check target status\n--- error_log\nreleasing existing checker\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/ai-proxy-openrouter.t",
    "content": "\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/openai-compatible-api-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openrouter\",\n                                    \"provider\": \"openrouter\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"openai/gpt-4o\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 3: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openrouter\",\n                                    \"provider\": \"openrouter\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"openai/gpt-4o\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0,\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"stream\": true\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data: \\[DONE\\]\\n\\n/\n"
  },
  {
    "path": "t/plugin/ai-proxy-vertex-ai.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"debug\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugins:\n  - ai-proxy-multi\n  - prometheus\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n\n    my $extra_init_worker_by_lua = <<_EOC_;\n    local gcp_accesstoken = require \"apisix.utils.google-cloud-oauth\"\n    local ttl = 0\n    gcp_accesstoken.refresh_access_token = function(self)\n        ngx.log(ngx.NOTICE, \"[test] mocked gcp_accesstoken called\")\n        ttl = ttl + 5\n        self.access_token_ttl = ttl\n        self.access_token = \"ya29.c.Kp8B...\"\n    end\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"toolkit.json\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    if body and body.instances then\n                        local vertex_response = {\n                            [\"predictions\"] = {\n                                {\n                                    [\"embeddings\"] = {\n                                        [\"statistics\"] = {\n                                            [\"token_count\"] = 7\n                                        },\n                                        [\"values\"] = {\n                                            0.0123,\n                                            -0.0456,\n                                            0.0789,\n                                            0.0012\n                                        }\n                                    }\n                                },\n                            }\n                        }\n                        local body = json.encode(vertex_response)\n                        ngx.status = 200\n                        ngx.say(body)\n                        return\n                    end\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" and header_auth ~= \"Bearer ya29.c.Kp8B...\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" or header_auth == \"Bearer ya29.c.Kp8B...\" then\n                        if header_auth == \"Bearer ya29.c.Kp8B...\" then\n                            ngx.log(ngx.NOTICE, \"[test] GCP service account auth works\")\n                        end\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n\n            location ~ ^/status.* {\n                content_by_lua_block {\n                    local test_dict = ngx.shared[\"test\"]\n                    local uri = ngx.var.uri\n                    local total_key = uri .. \"#total\"\n                    local count_key = uri .. \"#count\"\n                    local total = test_dict:get(total_key)\n                    if not total then\n                        return\n                    end\n\n                    local count = test_dict:incr(count_key, 1, 0)\n                    ngx.log(ngx.INFO, \"uri: \", uri, \" total: \", total, \" count: \", count)\n                    if count < total then\n                        return\n                    end\n                    ngx.status = 500\n                    ngx.say(\"error\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"vertex-ai\",\n                                    \"provider\": \"vertex-ai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\"content\":\"1 \\+ 1 = 2\\.\"/\n\n\n\n=== TEST 3: request embeddings, check values field in response\n--- request\nPOST /anything\n{\"input\": \"Your text string goes here\"}\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\"embedding\":\\[0.0123,-0.0456,0.0789,0.0012\\]/\n\n\n\n=== TEST 4: request embeddings, check token_count field in response\n--- request\nPOST /anything\n{\"input\": \"Your text string goes here\"}\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\"total_tokens\":7/\n\n\n\n=== TEST 5: set route with right auth gcp service account\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"vertex-ai\",\n                                    \"provider\": \"vertex-ai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"gcp\": { \"max_ttl\": 8 }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 200\n--- error_log\n[test] GCP service account auth works\n--- response_body eval\nqr/\"content\":\"1 \\+ 1 = 2\\.\"/\n\n\n\n=== TEST 7: check gcp access token caching works\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local send_request = function()\n                local code, _, body = t(\"/anything\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"request should be successful\")\n                return body\n            end\n            for i = 1, 6 do\n                send_request()\n            end\n\n            ngx.sleep(5.5)\n            send_request()\n\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 7\n--- response_body\npassed\n--- error_log\n[test] mocked gcp_accesstoken called\n[test] mocked gcp_accesstoken called\nset gcp access token in cache with ttl: 5\nset gcp access token in cache with ttl: 8\n\n\n\n=== TEST 8: set route with multiple instances and gcp service account\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"vertex-ai-one\",\n                                    \"provider\": \"vertex-ai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"gcp\": {}\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"vertex-ai-multi\",\n                                    \"provider\": \"vertex-ai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"gcp\": {}\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: check gcp access token caching works\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local send_request = function()\n                local code, _, body = t(\"/anything\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"request should be successful\")\n                return body\n            end\n            for i = 1, 12 do\n                send_request()\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 7\n--- response_body\npassed\n--- error_log\n#vertex-ai-one\n#vertex-ai-multi\n\n\n\n=== TEST 10: set ai-proxy-multi with health checks\n--- config\n    location /t {\n        content_by_lua_block {\n            local checks = [[\n            \"checks\": {\n                \"active\": {\n                    \"timeout\": 5,\n                    \"http_path\": \"/status/gpt4\",\n                    \"host\": \"foo.com\",\n                    \"healthy\": {\n                        \"interval\": 1,\n                        \"successes\": 1\n                    },\n                    \"unhealthy\": {\n                        \"interval\": 1,\n                        \"http_failures\": 1\n                    },\n                    \"req_headers\": [\"User-Agent: curl/7.29.0\"]\n                }\n            }]]\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 string.format([[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"vertex-ai\",\n                                    \"provider\": \"vertex-ai\",\n                                    \"weight\": 1,\n                                    \"priority\": 2,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gemini-2.0-flash\",\n                                        \"max_tokens\": 512,\n                                        \"temperature\": 1.0\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                                    },\n                                    %s\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-3\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:6724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]], checks)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: check health check works\n--- wait: 5\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\"content\":\"1 \\+ 1 = 2\\.\"/\n--- error_log\ncreating healthchecker for upstream\nrequest head: GET /status/gpt4\n"
  },
  {
    "path": "t/plugin/ai-proxy.openai-compatible.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai-compatible\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"custom\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 3: override path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai-compatible\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"some-model\",\n                                \"foo\": \"bar\",\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/random\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"test-type\"] = \"path\",\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            ngx.status = code\n            ngx.say(actual_body)\n\n        }\n    }\n--- response_body_chomp\npath override works\n\n\n\n=== TEST 4: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai-compatible\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"custom\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0,\n                                \"stream\": true\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:7737/v1/chat/completions\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ]\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_eval\nqr/6data: \\[DONE\\]\\n\\n/\n"
  },
  {
    "path": "t/plugin/ai-proxy.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.print(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    if test_type == \"header_forwarding\" then\n                        ngx.say(json.encode(ngx.req.get_headers()))\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say([[$resp]])\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /v1/embeddings {\n                content_by_lua_block {\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"unsupported request method: \", ngx.req.get_method())\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    if header_auth ~= \"Bearer token\" then\n                        ngx.status = 401\n                        ngx.say(\"unauthorized\")\n                        return\n                    end\n\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    local json = require(\"cjson.safe\")\n                    body, err = json.decode(body)\n                    if err then\n                        ngx.status = 400\n                        ngx.say(\"failed to get request body: \", err)\n                    end\n\n                    if body.model ~= \"text-embedding-ada-002\" then\n                        ngx.status = 400\n                        ngx.say(\"unsupported model: \", body.model)\n                        return\n                    end\n\n                    if body.encoding_format ~= \"float\" then\n                        ngx.status = 400\n                        ngx.say(\"unsupported encoding format: \", body.encoding_format)\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.say([[\n                        {\n                          \"object\": \"list\",\n                          \"data\": [\n                            {\n                              \"object\": \"embedding\",\n                              \"embedding\": [\n                                0.0023064255,\n                                -0.009327292,\n                                -0.0028842222\n                              ],\n                              \"index\": 0\n                            }\n                          ],\n                          \"model\": \"text-embedding-ada-002\",\n                          \"usage\": {\n                            \"prompt_tokens\": 8,\n                            \"total_tokens\": 8\n                          }\n                        }\n                    ]])\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.print(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: minimal viable configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-proxy\")\n            local ok, err = plugin.check_schema({\n                provider = \"openai\",\n                options = {\n                    model = \"gpt-4\",\n                },\n                auth = {\n                    header = {\n                        some_header = \"some_value\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: unsupported provider\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-proxy\")\n            local ok, err = plugin.check_schema({\n                provider = \"some-unique\",\n                options = {\n                    model = \"gpt-4\",\n                },\n                auth = {\n                    header = {\n                        some_header = \"some_value\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body eval\nqr/.*property \"provider\" validation failed: matches none of the enum values.*/\n\n\n\n=== TEST 3: set route with wrong auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer wrongtoken\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 401\n--- response_body\nUnauthorized\n\n\n\n=== TEST 5: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 7: send request with empty body\n--- request\nPOST /anything\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 400\n--- response_body_chomp\nfailed to get request body: request body is empty\n\n\n\n=== TEST 8: send request with wrong method (GET) should work\n--- request\nGET /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- error_code: 200\n--- response_body eval\nqr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/\n\n\n\n=== TEST 9: wrong JSON in request body should give error\n--- request\nGET /anything\n{}\"messages\": [ { \"role\": \"system\", \"cont\n--- error_code: 400\n--- response_body\n{\"message\":\"could not get parse JSON request body: Expected the end but found T_STRING at character 3\"}\n\n\n\n=== TEST 10: content-type should be JSON\n--- request\nPOST /anything\nprompt%3Dwhat%2520is%25201%2520%252B%25201\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 400\n--- response_body chomp\nunsupported content-type: application/x-www-form-urlencoded, only application/json is supported\n\n\n\n=== TEST 11: model options being merged to request body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"some-model\",\n                                \"foo\": \"bar\",\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"test-type\"] = \"options\",\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            ngx.status = code\n            ngx.say(actual_body)\n\n        }\n    }\n--- error_code: 200\n--- response_body\noptions works\n\n\n\n=== TEST 12: override path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"model\": \"some-model\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"foo\": \"bar\",\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/random\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"test-type\"] = \"path\",\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            ngx.status = code\n            ngx.say(actual_body)\n\n        }\n    }\n--- response_body\npath override works\n\n\n\n=== TEST 13: set route with stream = true (SSE)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0,\n                                \"stream\": true\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:7737\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: test is SSE works as expected\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ]\n                }]],\n            }\n\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local final_res = {}\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_eval\nqr/6data: \\[DONE\\]\\n\\n/\n\n\n\n=== TEST 15: proxy embedding endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/embeddings\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"text-embedding-ada-002\",\n                                \"encoding_format\": \"float\"\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/v1/embeddings\"\n                            }\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: send request to embedding api\n--- request\nPOST /embeddings\n{\n    \"input\": \"The food was delicious and the waiter...\"\n}\n--- error_code: 200\n--- response_body_like eval\nqr/.*text-embedding-ada-002*/\n\n\n\n=== TEST 17: schema accepts 'logging'\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-proxy\")\n\n            local ok, err = plugin.check_schema({\n                provider = \"openai\",\n                auth = { header = { apikey = \"token\" } },\n                options = { model = \"gpt-4\" },\n                logging = { summaries = true, payloads = false },\n            })\n\n            if ok then\n                ngx.say(\"ok\")\n            else\n                ngx.say(\"bad:\" .. (err or \"\"))\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n\n\n\n=== TEST 18: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\ntest-type: header_forwarding\n--- error_code: 200\n--- response_body eval\nqr/\"test-type\":\"header_forwarding\"/\n\n\n\n=== TEST 20: set route with right auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        },\n                        \"request-id\": {\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\ntest-type: header_forwarding\n--- error_code: 200\n--- response_body eval\nqr/\"x-request-id\":\"[\\d\\w-]+\"/\n\n\n\n=== TEST 22: send request with Authorization header\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer wrong token\n--- error_code: 200\n"
  },
  {
    "path": "t/plugin/ai-proxy2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n\n                    if query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n\n                    ngx.status = 200\n                    ngx.say(\"passed\")\n                }\n            }\n\n\n            location /test/params/in/overridden/endpoint {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n                    local core = require(\"apisix.core\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n                    ngx.log(ngx.INFO, \"found query params: \", core.json.stably_encode(ngx.req.get_uri_args()))\n\n                    if query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.say(\"passed\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with wrong query param\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"wrong_key\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 401\n--- response_body\nUnauthorized\n\n\n\n=== TEST 3: set route with right query param\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 5: set route without overriding the endpoint_url\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"some-key\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-4\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: send request\n--- custom_trusted_cert: /etc/ssl/certs/ca-certificates.crt\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 401\n\n\n\n=== TEST 7: query params in override.endpoint should be sent to LLM\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"options\": {\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/test/params/in/overridden/endpoint?some_query=yes\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: send request\n--- request\nPOST /anything\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- error_code: 200\n--- error_log\nfound query params: {\"api_key\":\"apikey\",\"some_query\":\"yes\"}\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/ai-proxy3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n\n                    if query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n            local res = [[\n{\n  \"id\": \"chatcmpl-12345\",\n  \"object\": \"chat.completion\",\n  \"created\": 1691234567,\n  \"model\": \"gpt-3.5-turbo\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": \"这是一个示例回复。\"\n      },\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": {\n    \"prompt_tokens\": 10,\n    \"completion_tokens\": 20,\n    \"total_tokens\": 30\n  }\n}]]\n                    ngx.status = 200\n                    ngx.say(res)\n                }\n            }\n\n            location /null-content {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n            local res = [[\n{\n  \"model\": \"gpt-3.5-turbo\",\n  \"choices\": [\n    {\n      \"index\": 0,\n      \"message\": {\n        \"role\": \"assistant\",\n        \"content\": null\n      },\n      \"finish_reason\": \"stop\"\n    }\n  ],\n  \"usage\": null\n}]]\n                    ngx.status = 200\n                    ngx.say(res)\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set access log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-3.5-turbo\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/v1/chat/completions\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: send request\n--- request\nPOST /anything\n{\"messages\":[{\"role\":\"system\",\"content\":\"You are a mathematician\"},{\"role\":\"user\",\"content\":\"What is 1+1?\"}], \"model\": \"gpt-4\"}\n--- error_code: 200\n--- response_body eval\nqr/.*completion_tokens.*/\n--- access_log eval\nqr/.*[\\d.]+ \\\"http:\\/\\/localhost\\\" gpt-4 gpt-3.5-turbo \\d+ 10 20.*/\n\n\n\n=== TEST 3: proxy to /null-content ai endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/null-content\"\n                            }\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request\n--- request\nPOST /anything\n{\"messages\":[{\"role\":\"user\",\"content\":\"What is 1+1?\"}], \"model\": \"gpt-4\"}\n--- error_code: 200\n--- response_body eval\nqr/.*assistant.*/\n--- no_error_log\n\n\n\n=== TEST 5: create a ai-proxy-multi route with delay streaming ai endpoint(every event delay 200ms)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"self-hosted\",\n                                    \"provider\": \"openai-compatible\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-3.5-turbo\",\n                                        \"stream\": true\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:7737/v1/chat/completions?delay=true\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        }\n                    }\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: assert access log contains right llm variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local core = require(\"apisix.core\")\n            local ok, err = httpc:connect({\n                scheme = \"http\",\n                host = \"localhost\",\n                port = ngx.var.server_port,\n            })\n            if not ok then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local params = {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/json\",\n                },\n                path = \"/anything\",\n                body = [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"some content\" }\n                    ],\n                    \"model\": \"gpt-4\"\n                }]],\n            }\n            local res, err = httpc:request(params)\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n            local final_res = {}\n            local inspect = require(\"inspect\")\n            while true do\n                local chunk, err = res.body_reader() -- will read chunk by chunk\n                if err then\n                    core.log.error(\"failed to read response chunk: \", err)\n                    break\n                end\n                if not chunk then\n                    break\n                end\n                core.table.insert_tail(final_res, chunk)\n            end\n            ngx.print(#final_res .. final_res[6])\n        }\n    }\n--- response_body_like eval\nqr/6data: \\[DONE\\]\\n\\n/\n--- access_log eval\nqr/.*[\\d.]+ \\\"http:\\/\\/localhost:1984\\\" gpt-4 gpt-3.5-turbo 2\\d\\d 15 20.*/\n"
  },
  {
    "path": "t/plugin/ai-rag.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/embeddings.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $embeddings = do { local $/; <$fh> };\nclose($fh);\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            listen 3623;\n\n            default_type 'application/json';\n\n            location /embeddings {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                        return\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local header_auth = ngx.req.get_headers()[\"api-key\"]\n\n                    if header_auth ~= \"key\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.say([[$embeddings]])\n                }\n            }\n\n            location /search {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"api-key\"]\n                    if header_auth ~= \"key\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n                    if body.vectorQueries[1].vector[1] ~= 123456789 then\n                        ngx.status = 500\n                        ngx.say({ error = \"occurred\" })\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.print(\"passed\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: minimal viable configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-rag\")\n            local ok, err = plugin.check_schema({\n                embeddings_provider = {\n                    azure_openai = {\n                        api_key = \"sdfjasdfh\",\n                        endpoint = \"http://a.b.com\"\n                    }\n                },\n                vector_search_provider = {\n                    azure_ai_search = {\n                        api_key = \"iuhsdf\",\n                        endpoint = \"http://a.b.com\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: vector search provider missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-rag\")\n            local ok, err = plugin.check_schema({\n                embeddings_provider = {\n                    azure_openai = {\n                        api_key = \"sdfjasdfh\",\n                        endpoint = \"http://a.b.com\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nproperty \"vector_search_provider\" is required\n\n\n\n=== TEST 3: embeddings provider missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-rag\")\n            local ok, err = plugin.check_schema({\n                vector_search_provider = {\n                    azure_ai_search = {\n                        api_key = \"iuhsdf\",\n                        endpoint = \"http://a.b.com\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nproperty \"embeddings_provider\" is required\n\n\n\n=== TEST 4: wrong auth header for embeddings provider\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-rag\": {\n                            \"embeddings_provider\": {\n                                \"azure_openai\": {\n                                    \"endpoint\": \"http://localhost:3623/embeddings\",\n                                    \"api_key\": \"wrongkey\"\n                                }\n                            },\n                            \"vector_search_provider\": {\n                                \"azure_ai_search\": {\n                                    \"endpoint\": \"http://localhost:3623/search\",\n                                    \"api_key\": \"key\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"scheme\": \"http\",\n                        \"pass_host\": \"node\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: send request\n--- request\nPOST /echo\n{\"ai_rag\":{\"vector_search\":{\"fields\":\"contentVector\"},\"embeddings\":{\"input\":\"which service is good for devops\",\"dimensions\":1024}}}\n--- error_code: 401\n--- response_body\nUnauthorized\n--- error_log\ncould not get embeddings: Unauthorized\n\n\n\n=== TEST 6: wrong auth header for search provider\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-rag\": {\n                            \"embeddings_provider\": {\n                                \"azure_openai\": {\n                                    \"endpoint\": \"http://localhost:3623/embeddings\",\n                                    \"api_key\": \"key\"\n                                }\n                            },\n                            \"vector_search_provider\": {\n                                \"azure_ai_search\": {\n                                    \"endpoint\": \"http://localhost:3623/search\",\n                                    \"api_key\": \"wrongkey\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"scheme\": \"http\",\n                        \"pass_host\": \"node\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: send request\n--- request\nPOST /echo\n{\"ai_rag\":{\"vector_search\":{\"fields\":\"contentVector\"},\"embeddings\":{\"input\":\"which service is good for devops\",\"dimensions\":1024}}}\n--- error_code: 401\n--- error_log\ncould not get vector_search result: Unauthorized\n\n\n\n=== TEST 8: send request with empty body\n--- request\nPOST /echo\n--- error_code: 400\n--- response_body_chomp\nfailed to get request body: request body is empty\n\n\n\n=== TEST 9: send request with vector search fields missing\n--- request\nPOST /echo\n{\"ai_rag\":{\"vector_search\":{\"missing-fields\":\"something\"},\"embeddings\":{\"input\":\"which service is good for devops\",\"dimensions\":1024}}}\n--- error_code: 400\n--- error_log\nrequest body fails schema check: property \"ai_rag\" validation failed: property \"vector_search\" validation failed: property \"fields\" is required\n\n\n\n=== TEST 10: send request with embedding input missing\n--- request\nPOST /echo\n{\"ai_rag\":{\"vector_search\":{\"fields\":\"something\"},\"embeddings\":{\"missinginput\":\"which service is good for devops\"}}}\n--- error_code: 400\n--- error_log\nrequest body fails schema check: property \"ai_rag\" validation failed: property \"embeddings\" validation failed: property \"input\" is required\n\n\n\n=== TEST 11: configure plugin with right auth headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ai-rag\": {\n                            \"embeddings_provider\": {\n                                \"azure_openai\": {\n                                    \"endpoint\": \"http://localhost:3623/embeddings\",\n                                    \"api_key\": \"key\"\n                                }\n                            },\n                            \"vector_search_provider\": {\n                                \"azure_ai_search\": {\n                                    \"endpoint\": \"http://localhost:3623/search\",\n                                    \"api_key\": \"key\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"scheme\": \"http\",\n                        \"pass_host\": \"node\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: send request with embedding input missing\n--- request\nPOST /echo\n{\"ai_rag\":{\"vector_search\":{\"fields\":\"something\"},\"embeddings\":{\"input\":\"which service is good for devops\"}}}\n--- error_code: 200\n--- response_body eval\nqr/\\{\"messages\":\\[\\{\"content\":\"passed\",\"role\":\"user\"\\}\\]\\}|\\{\"messages\":\\[\\{\"role\":\"user\",\"content\":\"passed\"\\}\\]\\}/\n"
  },
  {
    "path": "t/plugin/ai-rate-limiting-consumer-isolation.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 16724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local test_type = ngx.req.get_headers()[\"test-type\"]\n                    if test_type == \"options\" then\n                        if body.foo == \"bar\" then\n                            ngx.status = 200\n                            ngx.say(\"options works\")\n                        else\n                            ngx.status = 500\n                            ngx.say(\"model options feature doesn't work\")\n                        end\n                        return\n                    end\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say(string.format([[\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": { \"content\": \"1 + 1 = 2.\", \"role\": \"assistant\" }\n    }\n  ],\n  \"created\": 1723780938,\n  \"id\": \"chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P\",\n  \"model\": \"%s\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": { \"completion_tokens\": 5, \"prompt_tokens\": 8, \"total_tokens\": 10 }\n}\n                        ]], body.model))\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with consumers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: add consumer jack1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"jack1\"\n                        },\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:16724\"\n                            },\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"limit\": 30,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: add consumer jack2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack2\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"jack2\"\n                        },\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:16724\"\n                            },\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"limit\": 30,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: reject the 3rd request\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\napikey: jack1\n--- error_code eval\n[200, 200, 200, 503]\n\n\n\n=== TEST 5: reject the 3rd request\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\napikey: jack2\n--- error_code eval\n[200, 200, 200, 503]\n"
  },
  {
    "path": "t/plugin/ai-rate-limiting.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\n\nmy $resp_file = 't/assets/ai-proxy-response.json';\nopen(my $fh, '<', $resp_file) or die \"Could not open file '$resp_file' $!\";\nmy $resp = do { local $/; <$fh> };\nclose($fh);\n\nprint \"Hello, World!\\n\";\nprint $resp;\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 16724;\n\n            default_type 'application/json';\n\n            location /anything {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body = ngx.req.get_body_data()\n\n                    if body ~= \"SELECT * FROM STUDENTS\" then\n                        ngx.status = 503\n                        ngx.say(\"passthrough doesn't work\")\n                        return\n                    end\n                    ngx.say('{\"foo\", \"bar\"}')\n                }\n            }\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    if ngx.req.get_method() ~= \"POST\" then\n                        ngx.status = 400\n                        ngx.say(\"Unsupported request method: \", ngx.req.get_method())\n                    end\n                    ngx.req.read_body()\n                    local body, err = ngx.req.get_body_data()\n                    body, err = json.decode(body)\n\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"apikey\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    if header_auth == \"Bearer token\" or query_auth == \"apikey\" then\n                        ngx.req.read_body()\n                        local body, err = ngx.req.get_body_data()\n                        body, err = json.decode(body)\n\n                        if not body.messages or #body.messages < 1 then\n                            ngx.status = 400\n                            ngx.say([[{ \"error\": \"bad request\"}]])\n                            return\n                        end\n\n                        if body.messages[1].content == \"write an SQL query to get all rows from student table\" then\n                            ngx.print(\"SELECT * FROM STUDENTS\")\n                            return\n                        end\n\n                        ngx.status = 200\n                        ngx.say(string.format([[\n{\n  \"choices\": [\n    {\n      \"finish_reason\": \"stop\",\n      \"index\": 0,\n      \"message\": { \"content\": \"1 + 1 = 2.\", \"role\": \"assistant\" }\n    }\n  ],\n  \"created\": 1723780938,\n  \"id\": \"chatcmpl-9wiSIg5LYrrpxwsr2PubSQnbtod1P\",\n  \"model\": \"%s\",\n  \"object\": \"chat.completion\",\n  \"system_fingerprint\": \"fp_abc28019ad\",\n  \"usage\": { \"completion_tokens\": 5, \"prompt_tokens\": 8, \"total_tokens\": 10 }\n}\n                        ]], body.model))\n                        return\n                    end\n\n\n                    ngx.status = 503\n                    ngx.say(\"reached the end of the test suite\")\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n                    ngx.say(\"path override works\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local configs = {\n                {\n                    time_window = 60,\n                },\n                {\n                    limit = 30,\n                },\n                {\n                    limit = 30,\n                    time_window = 60,\n                    rejected_code = 199,\n                },\n                {\n                    limit = 30,\n                    time_window = 60,\n                    limit_strategy = \"invalid\",\n                },\n                {\n                    limit = 30,\n                    time_window = 60,\n                    instances = {\n                        {\n                            name = \"instance1\",\n                            limit = 30,\n                            time_window = 60,\n                        },\n                        {\n                            limit = 30,\n                            time_window = 60,\n                        }\n                    },\n                },\n                {\n                    time_window = 60,\n                    instances = {\n                        {\n                            name = \"instance1\",\n                            limit = 30,\n                            time_window = 60,\n                        }\n                    },\n                },\n                {\n                    limit = 30,\n                    instances = {\n                        {\n                            name = \"instance1\",\n                            limit = 30,\n                            time_window = 60,\n                        }\n                    },\n                },\n                {\n                    instances = {\n                        {\n                            name = \"instance1\",\n                            limit = 30,\n                            time_window = 60,\n                        }\n                    },\n                },\n                {\n                    limit = 30,\n                    time_window = 60,\n                    rejected_code = 403,\n                    rejected_msg = \"rate limit exceeded\",\n                    limit_strategy = \"completion_tokens\",\n                },\n                {\n                    limit = 30,\n                    time_window = 60,\n                    instances = {\n                        {\n                            name = \"instance1\",\n                            limit = 30,\n                            time_window = 60,\n                        }\n                    },\n                }\n            }\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.ai-rate-limiting\")\n            for _, config in ipairs(configs) do\n                local ok, err = plugin.check_schema(config)\n                if not ok then\n                    ngx.say(err)\n                else\n                    ngx.say(\"passed\")\n                end\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"limit\" is required when \"time_window\" is set\nproperty \"time_window\" is required when \"limit\" is set\nproperty \"rejected_code\" validation failed: expected 199 to be at least 200\nproperty \"limit_strategy\" validation failed: matches none of the enum values\nproperty \"instances\" validation failed: failed to validate item 2: property \"name\" is required\nproperty \"limit\" is required when \"time_window\" is set\nproperty \"time_window\" is required when \"limit\" is set\npassed\npassed\npassed\ndone\n\n\n\n=== TEST 2: set route 1, default limit_strategy: total_tokens\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:16724\"\n                            },\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"limit\": 30,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: reject the 3th request\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 503]\n\n\n\n=== TEST 4: set rejected_code to 403, rejected_msg to \"rate limit exceeded\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:16724\"\n                            },\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"limit\": 30,\n                            \"time_window\": 60,\n                            \"rejected_code\": 403,\n                            \"rejected_msg\": \"rate limit exceeded\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: check code and message\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 403]\n--- response_body eval\n[\n    qr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/,\n    qr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/,\n    qr/\\{ \"content\": \"1 \\+ 1 = 2\\.\", \"role\": \"assistant\" \\}/,\n    qr/\\{\"error_msg\":\"rate limit exceeded\"\\}/,\n]\n\n\n\n=== TEST 6: check rate limit headers\n--- request\nPOST /ai\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- response_headers\nX-AI-RateLimit-Limit-ai-proxy-openai: 30\nX-AI-RateLimit-Remaining-ai-proxy-openai: 29\nX-AI-RateLimit-Reset-ai-proxy-openai: 60\n\n\n\n=== TEST 7: check rate limit headers after 4 requests\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_header\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 403]\n--- response_headers eval\n[\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 29\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 19\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 9\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 0\",\n]\n\n\n\n=== TEST 8: set route2 with limit_strategy: completion_tokens\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai2\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"options\": {\n                                \"model\": \"gpt-35-turbo-instruct\",\n                                \"max_tokens\": 512,\n                                \"temperature\": 1.0\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:16724\"\n                            },\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"limit\": 20,\n                            \"time_window\": 45,\n                            \"limit_strategy\": \"completion_tokens\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: reject the 5th request\n--- pipelined_requests eval\n[\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 200, 503]\n\n\n\n=== TEST 10: check rate limit headers\n--- request\nPOST /ai2\n{ \"messages\": [ { \"role\": \"system\", \"content\": \"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 1+1?\"} ] }\n--- more_headers\nAuthorization: Bearer token\n--- response_headers\nX-AI-RateLimit-Limit-ai-proxy-openai: 20\nX-AI-RateLimit-Remaining-ai-proxy-openai: 19\nX-AI-RateLimit-Reset-ai-proxy-openai: 45\n\n\n\n=== TEST 11: multi-request\n--- pipelined_requests eval\n[\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_header\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 200, 503]\n--- response_headers eval\n[\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 19\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 14\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 9\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 4\",\n    \"X-AI-RateLimit-Remaining-ai-proxy-openai: 0\",\n]\n\n\n\n=== TEST 12: request route 1 and route 2\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai2\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 403, 503]\n\n\n\n=== TEST 13: ai-rate-limiting & ai-proxy-multi, with instance_health_and_rate_limiting strategy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 0,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-3\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"limit\": 10,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: fallback strategy should works\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local code, _, body = t(\"/ai\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            assert(code == 200, \"first request should be successful\")\n            assert(core.string.find(body, \"gpt-4\"),\n                        \"first request should be handled by higher priority instance\")\n\n            local code, _, body = t(\"/ai\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            assert(code == 200, \"second request should be successful\")\n            assert(core.string.find(body, \"gpt-3\"),\n                        \"second request should be handled by lower priority instance\")\n\n            local code, body  = t(\"/ai\",\n                ngx.HTTP_POST,\n                [[{\n                    \"messages\": [\n                        { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                        { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                    ]\n                }]],\n                nil,\n                {\n                    [\"Content-Type\"] = \"application/json\",\n                }\n            )\n\n            assert(code == 503, \"third request should be failed\")\n            assert(core.string.find(body, \"all servers tried\"), \"all servers tried\")\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: limiting to only one instance\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-4\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 0,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-3\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"limit\": 20,\n                                    \"time_window\": 60\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: 10 requests, 8 should be handled by gpt-3, 2 should be handled by gpt-4\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n\n            local instances_count = {}\n            for i = 1, 10 do\n                local code, _, body = t(\"/ai\",\n                    ngx.HTTP_POST,\n                    [[{\n                        \"messages\": [\n                            { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                            { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                        ]\n                    }]],\n                    nil,\n                    {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                )\n                assert(code == 200, \"first request should be successful\")\n                if core.string.find(body, \"gpt-4\") then\n                    instances_count[\"gpt-4\"] = (instances_count[\"gpt-4\"] or 0) + 1\n                else\n                    instances_count[\"gpt-3\"] = (instances_count[\"gpt-3\"] or 0) + 1\n                end\n            end\n\n            ngx.log(ngx.INFO, \"instances_count test:\", core.json.delay_encode(instances_count))\n\n            assert(instances_count[\"gpt-4\"] <= 2, \"gpt-4 should be handled by higher priority instance\")\n            assert(instances_count[\"gpt-3\"] >= 8, \"gpt-3 should be handled by lower priority instance\")\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: each instance uses different current limiting\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 0,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-3\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\"instances\": [{\"name\": \"openai-gpt3\",\"limit\": 50,\"time_window\": 60},{\"name\": \"openai-gpt4\",\"limit\": 20,\"time_window\": 60}]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: gpt3 allows 5 requests, gpt4 allows 2 requests\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 503, 503]\n\n\n\n=== TEST 19: set limit & instances\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"fallback_strategy\": \"instance_health_and_rate_limiting\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai-gpt4\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"options\": {\n                                        \"model\": \"gpt-4\"\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    }\n                                },\n                                {\n                                    \"name\": \"openai-gpt3\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"priority\": 0,\n                                    \"auth\": {\"header\": {\"Authorization\": \"Bearer token\"}},\n                                    \"options\": {\"model\": \"gpt-3\"},\n                                    \"override\": {\"endpoint\": \"http://localhost:16724\"}\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\"limit\": 20, \"time_window\": 60, \"instances\": [{\"name\": \"openai-gpt3\",\"limit\": 50,\"time_window\": 60}]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: gpt3 allows 5 requests, gpt4 allows 2 requests\n--- pipelined_requests eval\n[\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n    \"POST /ai\\n\" . \"{ \\\"messages\\\": [ { \\\"role\\\": \\\"system\\\", \\\"content\\\": \\\"You are a mathematician\\\" }, { \\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"What is 1+1?\\\"} ] }\",\n]\n--- more_headers\nAuthorization: Bearer token\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 503, 503]\n\n\n\n=== TEST 21: use variable in count and time_window with default value\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local data = {\n                uri = \"/ai\",\n                plugins = {\n                    [\"ai-proxy-multi\"] = {\n                        fallback_strategy = \"instance_health_and_rate_limiting\",\n                        instances = {\n                            {\n                                name = \"deepseek\",\n                                provider = \"openai\",\n                                weight = 1,\n                                priority = 1,\n                                auth = {\n                                    header = {\n                                        Authorization = \"Bearer token\"\n                                    }\n                                },\n                                override = {\n                                    endpoint = \"http://localhost:16724\"\n                                }\n                            },\n                            {\n                                name = \"openai\",\n                                provider = \"openai\",\n                                weight = 1,\n                                priority = 0,\n                                auth = {\n                                    header = {\n                                        Authorization = \"Bearer token\"\n                                    }\n                                },\n                                override = {\n                                    endpoint = \"http://localhost:16724\"\n                                }\n                            }\n                        },\n                        ssl_verify = false\n                    },\n                    [\"ai-rate-limiting\"] = {\n                        limit = \"${http_count ?? 10}\",\n                        time_window = \"${http_time_window ?? 60}\",\n                        instances = {\n                            {\n                                name = \"openai\",\n                                limit = \"${http_openai_count ?? 20}\",\n                                time_window = \"${http_time_window ?? 60}\"\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"canbeanything.com\"] = 1\n                    }\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: request with default variable values\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n\n            local test_cases = {\n                { code = 200 },\n                { code = 200 },\n                { code = 200 },\n                { code = 503 },\n            }\n\n            local httpc = http.new()\n            for i, case in ipairs(test_cases) do\n                local res = httpc:request_uri(\n                    \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ai\",\n                    {\n                        method = \"POST\",\n                        body = [[{\n                            \"messages\": [\n                                { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                                { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                            ]\n                        }]],\n                        headers = {\n                            [\"Content-Type\"] = \"application/json\",\n                        }\n                    }\n                )\n                if res.status ~= case.code then\n                    ngx.say( i  .. \"th request should return \" .. case.code .. \", but got \" .. res.status)\n                    return\n                end\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- response_body\npassed\n--- grep_error_log eval\nqr/picked instance: [^,]+/\n--- grep_error_log_out\npicked instance: deepseek\npicked instance: openai\npicked instance: openai\npicked instance: nil\n\n\n\n=== TEST 23: request with custom count/time_window headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n\n            local test_cases = {\n                { count = 20, openai_count = 30, time_window = 2, code = 200 },\n                { count = 20, openai_count = 30, time_window = 2, code = 200 },\n                { count = 20, openai_count = 30, time_window = 2, code = 200 },\n                { count = 20, openai_count = 30, time_window = 2, code = 200 },\n                { count = 20, openai_count = 30, time_window = 2, code = 200 },\n                { count = 20, openai_count = 30, time_window = 2, code = 503 },\n            }\n\n            local run_tests = function()\n                local httpc = http.new()\n                for i, case in ipairs(test_cases) do\n                    local res = httpc:request_uri(\n                        \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ai\",\n                        {\n                            method = \"POST\",\n                            body = [[{\n                                \"messages\": [\n                                    { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                                    { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                                ]\n                            }]],\n                            headers = {\n                                [\"Content-Type\"] = \"application/json\",\n                                [\"count\"] = tostring(case.count),\n                                [\"time-window\"] = tostring(case.time_window),\n                                [\"openai-count\"] = tostring(case.openai_count),\n                            }\n                        }\n                    )\n                    if res.status ~= case.code then\n                        ngx.say( i  .. \"th request should return \" .. case.code .. \", but got \" .. res.status)\n                        ngx.exit(500)\n                    end\n                end\n            end\n\n            run_tests()\n            ngx.sleep(2)\n            run_tests()\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- response_body\npassed\n--- grep_error_log eval\nqr/picked instance: [^,]+/\n--- grep_error_log_out\npicked instance: deepseek\npicked instance: deepseek\npicked instance: openai\npicked instance: openai\npicked instance: openai\npicked instance: nil\npicked instance: deepseek\npicked instance: deepseek\npicked instance: openai\npicked instance: openai\npicked instance: openai\npicked instance: nil\n\n\n\n=== TEST 24: configure instances and rules at the same time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-rate-limiting\": {\n                            \"limit\": \"${http_count ?? 10}\",\n                            \"time_window\": \"${http_time_window ?? 60}\",\n                            \"instances\": [\n                                {\n                                    \"name\": \"openai\",\n                                    \"limit\": \"${http_openai_count ?? 20}\",\n                                    \"time_window\": \"${http_time_window ?? 60}\"\n                                }\n                            ],\n                            \"rules\": [\n                                {\n                                    \"count\": 1,\n                                    \"time_window\": 10,\n                                    \"key\": \"${http_company}\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin ai-rate-limiting err: value should match only one schema, but matches both schemas 1 and 2\"}\n\n\n\n=== TEST 25: setup route with rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"deepseek\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://127.0.0.1:16724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"rejected_code\": 429,\n                            \"rules\": [\n                                {\n                                    \"count\": 20,\n                                    \"time_window\": 10,\n                                    \"key\": \"${http_user}\"\n                                },\n                                {\n                                    \"count\": \"${http_count ?? 30}\",\n                                    \"time_window\": \"${http_window ?? 10}\",\n                                    \"key\": \"${http_project}\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: request to confirm rules work\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n\n            local run_tests = function(name, test_cases)\n                local httpc = http.new()\n                for i, case in ipairs(test_cases) do\n                    case.headers[\"Content-Type\"] = \"application/json\"\n                    local res = httpc:request_uri(\n                        \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ai\",\n                        {\n                            method = \"POST\",\n                            body = [[{\n                                \"messages\": [\n                                    { \"role\": \"system\", \"content\": \"You are a mathematician\" },\n                                    { \"role\": \"user\", \"content\": \"What is 1+1?\" }\n                                ]\n                            }]],\n                            headers = case.headers\n                        }\n                    )\n                    if res.status ~= case.code then\n                        ngx.say(name .. \": \" .. i  .. \"th request should return \" .. case.code .. \", but got \" .. res.status)\n                        ngx.exit(500)\n                    end\n                    -- Add delay to ensure rate limit counters are updated properly\n                    ngx.sleep(0.01)\n                end\n            end\n\n            -- for user rule\n            run_tests(\"user_rule\", {\n                { headers = { [\"user\"] = \"jack\" }, code = 200 },\n                { headers = { [\"user\"] = \"jack\" }, code = 200 },\n                { headers = { [\"user\"] = \"jack\" }, code = 429 },\n                { headers = { [\"user\"] = \"rose\" }, code = 200 },\n                { headers = { [\"user\"] = \"rose\" }, code = 200 },\n                { headers = { [\"user\"] = \"rose\" }, code = 429 },\n            })\n\n            -- for project rule with default variable value\n            run_tests(\"project_rule_default_value\", {\n                { headers = { [\"project\"] = \"apisix\" }, code = 200 },\n                { headers = { [\"project\"] = \"apisix\" }, code = 200 },\n                { headers = { [\"project\"] = \"apisix\" }, code = 200 },\n                { headers = { [\"project\"] = \"apisix\" }, code = 429 },\n            })\n\n            -- for project rule with custom variable value\n            run_tests(\"project_rule_custom_variables\", {\n                { headers = { [\"project\"] = \"linux\", [\"count\"] = \"20\", [\"window\"] = \"2\" }, code = 200 },\n                { headers = { [\"project\"] = \"linux\", [\"count\"] = \"20\", [\"window\"] = \"2\" }, code = 200 },\n                { headers = { [\"project\"] = \"linux\", [\"count\"] = \"20\", [\"window\"] = \"2\" }, code = 429 },\n            })\n            ngx.sleep(2.1)\n            run_tests(\"project_rule_custom_variables2\", {\n                { headers = { [\"project\"] = \"linux\", [\"count\"] = \"20\", [\"window\"] = \"2\" }, code = 200 },\n                { headers = { [\"project\"] = \"linux\", [\"count\"] = \"20\", [\"window\"] = \"2\" }, code = 200 },\n                { headers = { [\"project\"] = \"linux\", [\"count\"] = \"20\", [\"window\"] = \"2\" }, code = 429 },\n            })\n\n            -- no rule hit\n            run_tests(\"no_rules\", {\n                { headers = {}, code = 500 },\n            })\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- response_body\npassed\n--- error_log\nfailed to get rate limit rules\n\n\n\n=== TEST 27: setup route with rules without header prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"deepseek\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"rejected_code\": 429,\n                            \"rules\": [\n                                {\n                                    \"count\": 20,\n                                    \"time_window\": 10,\n                                    \"key\": \"${http_user}\"\n                                },\n                                {\n                                    \"count\": \"${http_count ?? 30}\",\n                                    \"time_window\": \"${http_window ?? 10}\",\n                                    \"key\": \"${http_project}\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: request to confirm headers with rule index are sent - target user\n--- request\nPOST /ai\n{\"messages\":[{\"role\":\"system\",\"content\":\"Youareamathematician\"},{\"role\":\"user\",\"content\":\"Whatis1+1?\"}]}\n--- more_headers\nuser: jack\n--- response_headers\nX-AI-1-RateLimit-Limit: 20\nX-AI-1-RateLimit-Remaining: 19\nX-AI-1-RateLimit-Reset: 10\n\n\n\n=== TEST 29: request to confirm headers with rule index are sent - target project\n--- request\nPOST /ai\n{\"messages\":[{\"role\":\"system\",\"content\":\"Youareamathematician\"},{\"role\":\"user\",\"content\":\"Whatis1+1?\"}]}\n--- more_headers\nproject: apisix\n--- response_headers\nX-AI-2-RateLimit-Limit: 30\nX-AI-2-RateLimit-Remaining: 29\nX-AI-2-RateLimit-Reset: 10\n\n\n\n=== TEST 30: setup route with rules with header prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/ai\",\n                    \"plugins\": {\n                        \"ai-proxy-multi\": {\n                            \"instances\": [\n                                {\n                                    \"name\": \"deepseek\",\n                                    \"provider\": \"openai\",\n                                    \"weight\": 1,\n                                    \"auth\": {\n                                        \"header\": {\n                                            \"Authorization\": \"Bearer token\"\n                                        }\n                                    },\n                                    \"override\": {\n                                        \"endpoint\": \"http://localhost:16724\"\n                                    }\n                                }\n                            ],\n                            \"ssl_verify\": false\n                        },\n                        \"ai-rate-limiting\": {\n                            \"rejected_code\": 429,\n                            \"rules\": [\n                                {\n                                    \"count\": 20,\n                                    \"time_window\": 10,\n                                    \"key\": \"${http_user}\",\n                                    \"header_prefix\": \"user\"\n                                },\n                                {\n                                    \"count\": \"${http_count ?? 30}\",\n                                    \"time_window\": \"${http_window ?? 10}\",\n                                    \"key\": \"${http_project}\",\n                                    \"header_prefix\": \"project\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"canbeanything.com\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 31: request to confirm headers with rule header prefix are sent - target user\n--- request\nPOST /ai\n{\"messages\":[{\"role\":\"system\",\"content\":\"Youareamathematician\"},{\"role\":\"user\",\"content\":\"Whatis1+1?\"}]}\n--- more_headers\nuser: jack\n--- response_headers\nX-AI-User-RateLimit-Limit: 20\nX-AI-User-RateLimit-Remaining: 19\nX-AI-User-RateLimit-Reset: 10\n\n\n\n=== TEST 32: request to confirm headers with rule header prefix are sent - target project\n--- request\nPOST /ai\n{\"messages\":[{\"role\":\"system\",\"content\":\"Youareamathematician\"},{\"role\":\"user\",\"content\":\"Whatis1+1?\"}]}\n--- more_headers\nproject: apisix\n--- response_headers\nX-AI-Project-RateLimit-Limit: 30\nX-AI-Project-RateLimit-Remaining: 29\nX-AI-Project-RateLimit-Reset: 10\n"
  },
  {
    "path": "t/plugin/ai-request-rewrite.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n\n                    ngx.req.read_body()\n                    local body = ngx.req.get_body_data()\n\n                    local json = require(\"cjson.safe\")\n                    local request_data = json.decode(body)\n                    local header_auth = ngx.req.get_headers()[\"authorization\"]\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n\n                    if header_auth ~= \"Bearer token\" and query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    local response = {\n                        choices = {\n                            {\n                                message = {\n                                    content = request_data.messages[1].content .. ' ' .. request_data.messages[2].content\n                                }\n                            }\n                        }\n                        }\n                    local json = require(\"cjson.safe\")\n                    local json_response = json.encode(response)\n                    ngx.say(json_response)\n                }\n            }\n\n            location /random {\n                content_by_lua_block {\n\n                    local response = {\n                        choices = {\n                            {\n                                message = {\n                                    content = 'return by random endpoint'\n                                }\n                            }\n                        }\n                        }\n                    local json = require(\"cjson.safe\")\n                    local json_response = json.encode(response)\n                    ngx.say(json_response)\n                }\n            }\n\n            location /internalservererror {\n                content_by_lua_block {\n                    ngx.status = 500\n                    ngx.say(\"Internal Server Error\")\n                    return\n                }\n            }\n\n            location /bad_request {\n                content_by_lua_block {\n                    ngx.status = 400\n                    ngx.say(\"Bad Request\")\n                    return\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: minimal viable configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"openai\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.print(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: missing prompt field should not pass\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                provider = \"openai\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nproperty \"prompt\" is required\n\n\n\n=== TEST 3: missing auth field should not pass\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"openai\",\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nproperty \"auth\" is required\n\n\n\n=== TEST 4: missing provider field should not pass\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nproperty \"provider\" is required\n\n\n\n=== TEST 5: provider must be one of: deepseek, openai, aimlapi, openai-compatible\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"invalid-provider\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nproperty \"provider\" validation failed: matches none of the enum values\n\n\n\n=== TEST 6: provider deepseek\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"deepseek\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: provider openai-compatible\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"openai-compatible\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                },\n                override = {\n                    endpoint = \"http://localhost:6724\"\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: override endpoint works\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt\",\n                            \"provider\": \"openai\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer token\"\n                                }\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/random\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n            local json = require(\"cjson.safe\")\n            local response_data = json.decode(actual_body)\n\n            if response_data.data == 'return by random endpoint' then\n                ngx.say(\"passed\")\n            else\n                ngx.say(actual_body)\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: set route with wrong auth header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt\",\n                            \"auth\": {\n                                \"header\": {\n                                    \"Authorization\": \"Bearer wrong-token\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            if code == 500 then\n                ngx.say('passed')\n                return\n            end\n        }\n    }\n\n--- error_log\nLLM service returned error status: 401\n--- response_body\npassed\n\n\n\n=== TEST 10: set route with correct query param\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.org:80\": 1\n                        }\n                    }\n                }]]\n            )\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            local json = require(\"cjson.safe\")\n            local response_data = json.decode(actual_body)\n\n            if response_data.data == \"some prompt some random content\" then\n                ngx.say(\"passed\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: set route with wrong query param\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"wrong_key\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.org:80\": 1\n                        }\n                    }\n                }]]\n            )\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            if code == 500 then\n                ngx.say('passed')\n                return\n            end\n        }\n    }\n\n--- error_log\nLLM service returned error status: 401\n--- response_body\npassed\n\n\n\n=== TEST 12: prompt passed correctly to LLM service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt to test\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            local json = require(\"cjson.safe\")\n            local response_data = json.decode(actual_body)\n\n            if response_data.data == \"some prompt to test some random content\" then\n                ngx.say(\"passed\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: check LLM bad request\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt to test\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/bad_request\"\n                            },\n                            \"ssl_verify\": false,\n                            \"options\": {\n                                \"model\": \"check_options_model\",\n                                \"temperature\": 0.5,\n                                \"max_tokens\": 100,\n                                \"top_p\": 1,\n                                \"frequency_penalty\": 0,\n                                \"presence_penalty\": 0\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            if code == 500 then\n                ngx.say('passed')\n                return\n            end\n        }\n    }\n--- error_log\nLLM service returned error status: 400\n--- response_body\npassed\n\n\n\n=== TEST 14: check LLM internal server error\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt to test\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/internalservererror\"\n                            },\n                            \"ssl_verify\": false,\n                            \"options\": {\n                                \"model\": \"check_options_model\",\n                                \"temperature\": 0.5,\n                                \"max_tokens\": 100,\n                                \"top_p\": 1,\n                                \"frequency_penalty\": 0,\n                                \"presence_penalty\": 0\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            if code == 500 then\n                ngx.say('passed')\n                return\n            end\n        }\n    }\n--- error_log\nLLM service returned error status: 500\n--- response_body\npassed\n\n\n\n=== TEST 15: provider aimlapi\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"aimlapi\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/ai-request-rewrite2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            server_name openai;\n            listen 6724;\n\n            default_type 'application/json';\n\n\n            location /check_extra_options {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n\n                    ngx.req.read_body()\n                    local body = ngx.req.get_body_data()\n                    local request_data = json.decode(body)\n\n                    if request_data.extra_option ~= \"extra option\" then\n                        ngx.status = 400\n                        ngx.say(\"extra option not match\")\n                        return\n                    end\n\n                    local response = {\n                        choices = {\n                            {\n                                message = {\n                                    content = request_data.messages[1].content .. ' ' .. request_data.messages[2].content\n                                }\n                            }\n                        }\n                        }\n                    local json = require(\"cjson.safe\")\n                    local json_response = json.encode(response)\n                    ngx.say(json_response)\n                }\n            }\n\n            location /test/params/in/overridden/endpoint {\n                content_by_lua_block {\n                    local json = require(\"cjson.safe\")\n                    local core = require(\"apisix.core\")\n\n                    local query_auth = ngx.req.get_uri_args()[\"api_key\"]\n                    ngx.log(ngx.INFO, \"found query params: \", core.json.stably_encode(ngx.req.get_uri_args()))\n\n                    if query_auth ~= \"apikey\" then\n                        ngx.status = 401\n                        ngx.say(\"Unauthorized\")\n                        return\n                    end\n\n                    ngx.status = 200\n                    ngx.say(\"passed\")\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: check plugin options send to llm service correctly\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt to test\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/check_extra_options\"\n                            },\n                            \"ssl_verify\": false,\n                            \"options\": {\n                                \"model\": \"check_options_model\",\n                                \"extra_option\": \"extra option\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                \"some random content\",\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            if code == 200 then\n                ngx.say('passed')\n                return\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: openai-compatible provider should use with override.endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ai-request-rewrite\")\n            local ok, err = plugin.check_schema({\n                prompt = \"some prompt\",\n                provider = \"openai-compatible\",\n                auth = {\n                    header = {\n                        Authorization =  \"Bearer token\"\n                    }\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\noverride.endpoint is required for openai-compatible provider\n\n\n\n=== TEST 3: query params in override.endpoint should be sent to LLM\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-proxy\": {\n                           \"provider\": \"openai\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"model\": {\n                                \"provider\": \"openai\",\n                                \"name\": \"gpt-35-turbo-instruct\",\n                                \"options\": {\n                                    \"max_tokens\": 512,\n                                    \"temperature\": 1.0\n                                }\n                            },\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/test/params/in/overridden/endpoint?some_query=yes\"\n                            },\n                            \"ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: send request without body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/anything\",\n                    \"plugins\": {\n                        \"ai-request-rewrite\": {\n                            \"prompt\": \"some prompt to test\",\n                            \"auth\": {\n                                \"query\": {\n                                    \"api_key\": \"apikey\"\n                                }\n                            },\n                            \"provider\": \"openai\",\n                            \"override\": {\n                                \"endpoint\": \"http://localhost:6724/check_extra_options\"\n                            },\n                            \"ssl_verify\": false,\n                            \"options\": {\n                                \"model\": \"check_options_model\",\n                                \"extra_option\": \"extra option\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        }\n                    }\n                }]]\n            )\n\n\n            local code, body, actual_body = t(\"/anything\",\n                ngx.HTTP_POST,\n                nil,\n                nil,\n                {\n                    [\"Content-Type\"] = \"text/plain\",\n                }\n            )\n\n            if code == 200 then\n                ngx.say('passed')\n                return\n            end\n        }\n    }\n--- error_log eval\nqr/missing request body/\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/ai.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: enable route cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local t = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri)\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nuse ai plane to match route\n\n\n\n=== TEST 2: route has vars, disable route cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"vars\": [ [\"arg_k\", \"~=\", \"v\"] ],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code = t('/hello??k=a', ngx.HTTP_GET)\n            ngx.say(code)\n\n            local code = t('/hello??k=v', ngx.HTTP_GET)\n            ngx.say(code)\n        }\n    }\n--- response_body\n200\n404\n--- no_error_log\nuse ai plane to match route\n\n\n\n=== TEST 3: method changed, create different route cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local t = {}\n            for i = 1, 4 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err\n                    if i % 2 == 0 then\n                        res, err = httpc:request_uri(uri, { method = \"POST\" })\n                    else\n                        res, err = httpc:request_uri(uri)\n                    end\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nuse ai plane to match route\n\n\n\n=== TEST 4: route with plugins, enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 9999,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local t = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri)\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nuse ai plane to match route\n\n\n\n=== TEST 5: enable -> disable -> enable -> disable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1?k=a\"\n            local uri3 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1?k=v\"\n\n            -- round 1: all routes without vars or filter_fun, enable route cache\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local threads1 = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri)\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(threads1, th)\n            end\n\n            for i, th in ipairs(threads1) do\n                ngx.thread.wait(th)\n            end\n\n            -- round 2: routes with vars or filter_fun, disable route cache\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"vars\": [ [\"arg_k\", \"~=\", \"v\"] ],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local threads2 = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err\n                    if i == 1 then\n                        -- arg_k = a, match route 2\n                        res, err = httpc:request_uri(uri2)\n                        assert(res.status == 200)\n                    else\n                        -- arg_k = v, not match route 2\n                        res, err = httpc:request_uri(uri3)\n                        assert(res.status == 404)\n                    end\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(threads2, th)\n            end\n\n            for i, th in ipairs(threads2) do\n                ngx.thread.wait(th)\n            end\n\n           -- round 3: delete route with vars, the remaining route\n           -- has no vars or filter_fun, enable route cache\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local threads3 = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri)\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(threads3, th)\n            end\n\n            for i, th in ipairs(threads3) do\n                ngx.thread.wait(th)\n            end\n\n            -- round 4: routes with vars or filter_fun, disable route cache\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"vars\": [ [\"arg_k\", \"~=\", \"v\"] ],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local threads4 = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err\n                    if i == 1 then\n                        -- arg_k = a, match route 2\n                        res, err = httpc:request_uri(uri2)\n                        assert(res.status == 200)\n                    else\n                        -- arg_k = v, not match route 2\n                        res, err = httpc:request_uri(uri3)\n                        assert(res.status == 404)\n                    end\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(threads4, th)\n            end\n\n            for i, th in ipairs(threads4) do\n                ngx.thread.wait(th)\n            end\n\n            -- clean route 2\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/use ai plane to match route/\n--- grep_error_log_out\nuse ai plane to match route\nuse ai plane to match route\n\n\n\n=== TEST 6: route key: uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(1)\n\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                assert(res.status == 200)\n                if not res then\n                    ngx.log(ngx.ERR, err)\n                    return\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nroute cache key: /hello\n\n\n\n=== TEST 7: route key: uri + method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(1)\n\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                assert(res.status == 200)\n                if not res then\n                    ngx.log(ngx.ERR, err)\n                    return\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nroute cache key: /hello#GET\n\n\n\n=== TEST 8: route key: uri + method + host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"host\": \"127.0.0.1\",\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(1)\n\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                assert(res.status == 200)\n                if not res then\n                    ngx.log(ngx.ERR, err)\n                    return\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nroute cache key: /hello#GET#127.0.0.1\n\n\n\n=== TEST 9: enable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nenable sample upstream\n\n\n\n=== TEST 10: route has plugins and run before_proxy, disable samply upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"before_proxy\",\n                            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, \\\"run before_proxy phase balancer_ip : \\\", ctx.balancer_ip) end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nrun before_proxy phase balancer_ip : 127.0.0.1\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 11: upstream has more than one nodes, disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 12: node has domain, disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"admin.apisix.dev:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 13: enable --> disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"enable_websocket\": true,\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream/\n--- grep_error_log_out\nenable sample upstream\n\n\n\n=== TEST 14: renew route cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            for k = 1, 2 do\n                local code, body = t('/apisix/admin/routes/' .. k,\n                     ngx.HTTP_PUT,\n                     [[{\n                        \"host\": \"127.0.0.1\",\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/hello\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello]] .. k .. [[\"\n                    }]]\n                )\n                if code >= 300 then\n                    ngx.status = code\n                    ngx.say(body)\n                    return\n                end\n                ngx.sleep(1)\n                for i = 1, 2 do\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri .. k)\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nrenew route cache: count=3001\nrenew route cache: count=3002\n"
  },
  {
    "path": "t/plugin/ai2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->extra_init_by_lua) {\n        my $extra_init_by_lua = <<_EOC_;\n            local apisix = require(\"apisix\")\n            apisix.http_header_filter_phase = function ()\n                ngx.header.content_length = 14\n            end\n\n            apisix.http_body_filter_phase = function ()\n                ngx.arg[1] = \"do body filter\"\n            end\n_EOC_\n\n        $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: enable skip body filter\n--- extra_init_by_lua\n    local apisix = require(\"apisix\")\n    apisix.http_header_filter_phase = function ()\n        ngx.header.content_length = nil\n    end\n\n    apisix.http_body_filter_phase = function ()\n        ngx.arg[1] = \"do body filter\"\n    end\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 2: route with plugin_config_id, disable skip body filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"before_proxy\",\n                            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, \\\"run before_proxy phase balancer_ip : \\\", ctx.balancer_ip) end\"]\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugin_config_id\": \"1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- response_body\ndo body filter\n--- error_log\nrun before_proxy phase balancer_ip : 127.0.0.1\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 3: route with plugins, disable skip body filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"serverless-pre-function\": {\n                            \"phase\": \"before_proxy\",\n                            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, \\\"run before_proxy phase balancer_ip : \\\", ctx.balancer_ip) end\"]\n                        }\n                    }\n                }]]\n            )\n            ngx.sleep(0.5)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local headers = {\n                [\"apikey\"] = \"auth-jack\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- response_body\ndo body filter\n--- error_log\nrun before_proxy phase balancer_ip : 127.0.0.1\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 4: one of route has plugins, disable skip body filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"before_proxy\",\n                            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, \\\"run before_proxy phase balancer_ip : \\\", ctx.balancer_ip) end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local httpc = http.new()\n            local headers = {\n                [\"apikey\"] = \"auth-jack\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers})\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- response_body\ndo body filter\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 5: exist global_rules, disable skip body filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"before_proxy\",\n                            \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, \\\"run before_proxy phase balancer_ip : \\\", ctx.balancer_ip) end\"]\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- response_body\ndo body filter\n--- error_log\nrun before_proxy phase balancer_ip : 127.0.0.1\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 6: upstream with keepalive_pool, disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"keepalive_pool\": {\n                            \"size\": 1,\n                            \"idle_timeout\": 8,\n                            \"requests\": 2\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- response_body\ndo body filter\n--- error_log\nproxy request to 127.0.0.1:1980\n--- no_error_log\nenable sample upstream\n"
  },
  {
    "path": "t/plugin/ai3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: keep priority behavior consistent\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"priority\": 1,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_port\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"priority\": 10,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_port\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/server_port\"\n            local t = {}\n            for i = 1, 2 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri)\n                    assert(res.status == 200)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    ngx.say(res.body)\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n        }\n    }\n--- response_body\n1981\n1981\n--- error_log\nuse ai plane to match route\n\n\n\n=== TEST 2: keep route cache as latest data\n# update the attributes that do not participate in the route cache key to ensure\n# that the route cache use the latest data\n--- yaml_config\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/pm',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"public-api\": {}\n                    },\n                    \"uri\": \"/apisix/prometheus/metrics\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"foo\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"prometheus\": {\n                            \"prefer_name\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(1)\n            local metrics_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/apisix/prometheus/metrics\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(metrics_uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            local m, err = ngx.re.match(res.body, \"apisix_bandwidth{type=\\\"ingress\\\",route=\\\"foo\\\"\", \"jo\")\n            ngx.say(m[0])\n\n            -- update name by patch\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"name\": \"bar\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(1)\n            local metrics_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/apisix/prometheus/metrics\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(metrics_uri)\n            assert(res.status == 200)\n            if not res then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            local m, err = ngx.re.match(res.body, \"apisix_bandwidth{type=\\\"ingress\\\",route=\\\"bar\\\"\", \"jo\")\n            ngx.say(m[0])\n        }\n    }\n--- response_body\napisix_bandwidth{type=\"ingress\",route=\"foo\"\napisix_bandwidth{type=\"ingress\",route=\"bar\"\n\n\n\n==== TEST 3: route has filter_func, disable route cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"filter_func\": \"function(vars) return vars.arg_k ~= 'v' end\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code = t('/hello??k=a', ngx.HTTP_GET)\n            ngx.say(code)\n\n            local code = t('/hello??k=v', ngx.HTTP_GET)\n            ngx.say(code)\n        }\n    }\n--- response_body\n200\n404\n--- no_error_log\nuse ai plane to match route\n"
  },
  {
    "path": "t/plugin/ai4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->extra_init_by_lua) {\n        my $extra_init_by_lua = <<_EOC_;\n        add_eligible_route = function(id, uri)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/' .. id,\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"]] .. uri .. [[\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        end\n\n        add_ineligible_route = function(id, uri)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/' .. id,\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1,\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"]] .. uri .. [[\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        end\n\n        update_route_to_ineligible = function(id)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/' .. id .. '/upstream/nodes',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"127.0.0.1:1980\": 1,\n                    \"127.0.0.1:1981\": 1\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        end\n\n        update_route_to_eligible = function(id)\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/' .. id .. '/upstream/nodes',\n                ngx.HTTP_PATCH,\n                [[{\n                    \"127.0.0.1:1980\": 1\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        end\n\n        clear_route = function(id)\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/routes/' .. id, ngx.HTTP_DELETE)\n            return code\n        end\n_EOC_\n\n        $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: enable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_eligible_route(1, \"/hello\")\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nenable sample upstream\n\n\n\n=== TEST 2: enable sample upstream, add ineligible route lead to disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_eligible_route(1, \"/hello\")\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            add_ineligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream|proxy request to/\n--- grep_error_log_out\nenable sample upstream\nproxy request to\n\n\n\n=== TEST 3: enable sample upstream, update route as ineligible lead to disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_eligible_route(1, \"/hello\")\n            add_eligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            update_route_to_ineligible(2)\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream|proxy request to/\n--- grep_error_log_out\nenable sample upstream\nproxy request to\n\n\n\n=== TEST 4: enable sample upstream, add eligible route and keep sample upstream as enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_eligible_route(1, \"/hello\")\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            add_eligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream/\n--- grep_error_log_out\nenable sample upstream\nenable sample upstream\n--- no_error_log eval\nqr/proxy request to \\S+/\n\n\n\n=== TEST 5: enable sample upstream, delete route and keep sample upstream as enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_eligible_route(1, \"/hello\")\n            add_eligible_route(2, \"/hello1\")\n\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n            assert(clear_route(2) == 200)\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream/\n--- grep_error_log_out\nenable sample upstream\nenable sample upstream\n--- no_error_log eval\nqr/proxy request to \\S+/\n\n\n\n=== TEST 6: enable sample upstream, delete all routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_eligible_route(1, \"/hello\")\n            add_eligible_route(2, \"/hello1\")\n\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 404)\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream/\n--- grep_error_log_out\nenable sample upstream\n--- no_error_log eval\nqr/proxy request to \\S+/\n\n\n\n=== TEST 7: disable sample upstream, add eligible route and keep sample upstream as disable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_ineligible_route(1, \"/hello\")\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            add_ineligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/proxy request to/\n--- grep_error_log_out\nproxy request to\nproxy request to\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 8: disable sample upstream, add eligible route and keep disable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_ineligible_route(1, \"/hello\")\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            add_eligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/proxy request to/\n--- grep_error_log_out\nproxy request to\nproxy request to\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 9: disable sample upstream, delete some ineligible route and keep sample upstream as disable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_ineligible_route(1, \"/hello\")\n            add_ineligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(2) == 200)\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/proxy request to/\n--- grep_error_log_out\nproxy request to\nproxy request to\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 10: disable sample upstream, update some of ineligible route to eligible, keep sample upstream as disable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_ineligible_route(1, \"/hello\")\n            add_ineligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            update_route_to_eligible(1)\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/proxy request to/\n--- grep_error_log_out\nproxy request to\nproxy request to\n--- no_error_log\nenable sample upstream\n\n\n\n=== TEST 11: disable sample upstream, delete all ineligible route, enable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_ineligible_route(1, \"/hello\")\n            add_ineligible_route(2, \"/hello1\")\n            add_eligible_route(3, \"/server_port\")\n\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n            local code, body = t(\"/server_port\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(3) == 200)\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream|proxy request to/\n--- grep_error_log_out\nproxy request to\nenable sample upstream\n\n\n\n=== TEST 12: disable sample upstream, update all of ineligible route to eligible, enable sample upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            add_ineligible_route(1, \"/hello\")\n            add_ineligible_route(2, \"/hello1\")\n            local code, body = t(\"/hello1\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            update_route_to_eligible(1)\n            update_route_to_eligible(2)\n            local code, body = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            assert(clear_route(1) == 200)\n            assert(clear_route(2) == 200)\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/enable sample upstream|proxy request to/\n--- grep_error_log_out\nproxy request to\nenable sample upstream\n"
  },
  {
    "path": "t/plugin/ai5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->extra_init_by_lua) {\n        my $extra_init_by_lua = <<_EOC_;\n        unload_ai_module = function ()\n            local t = require(\"lib.test_admin\").test\n            local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n            ]]\n            require(\"lib.test_admin\").set_config_yaml(data)\n\n            local code, body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        end\n\n        load_ai_module = function ()\n            local t = require(\"lib.test_admin\").test\n            local data = [[\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n  - ai\n            ]]\n            require(\"lib.test_admin\").set_config_yaml(data)\n\n            local code, body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n        end\n_EOC_\n\n        $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: enable(default) -> disable -> enable\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- register route\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"host\": \"127.0.0.1\",\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            -- enable route cache\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            -- disable ai plugin\n            unload_ai_module()\n\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            -- enable ai plugin\n            load_ai_module()\n\n            -- TODO: The route cache should be enabled, but since no new routes are registered,\n            -- the route tree is not rebuilt,\n            -- so it is not possible to switch to route cache mode, we should fix it\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200, \"enable: access /hello\")\n\n            -- register a new route and trigger a route tree rebuild\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"host\": \"127.0.0.1\",\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/route match mode: \\S[^,]+/\n--- grep_error_log_out\nroute match mode: ai_match\nroute match mode: radixtree_host_uri\nroute match mode: radixtree_host_uri\nroute match mode: radixtree_host_uri\nroute match mode: ai_match\nroute match mode: radixtree_host_uri\n\n\n\n=== TEST 2: disable(default) -> enable -> disable\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n--- http_config eval: $::HttpConfig\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- register route\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"host\": \"127.0.0.1\",\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            -- enable ai plugin\n            load_ai_module()\n\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            -- register a new route and trigger a route tree rebuild\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"host\": \"127.0.0.1\",\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            -- disable ai plugin\n            unload_ai_module()\n\n            local code = t('/hello', ngx.HTTP_GET)\n            assert(code == 200)\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/route match mode: \\S[^,]+/\n--- grep_error_log_out\nroute match mode: radixtree_host_uri\nroute match mode: radixtree_host_uri\nroute match mode: ai_match\nroute match mode: radixtree_host_uri\nroute match mode: radixtree_host_uri\n"
  },
  {
    "path": "t/plugin/api-breaker.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.api-breaker\")\n            local ok, err = plugin.check_schema({\n                break_response_code = 502,\n                unhealthy = {\n                    http_statuses = {500},\n                    failures = 1,\n                },\n                healthy = {\n                    http_statuses = {200},\n                    successes = 1,\n                },\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: default configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.api-breaker\")\n            local conf = {\n                break_response_code = 502\n            }\n\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"break_response_code\":502,\"healthy\":{\"http_statuses\":[200],\"successes\":3},\"max_breaker_sec\":300,\"unhealthy\":{\"failures\":3,\"http_statuses\":[500]}}\n\n\n\n=== TEST 3: default `healthy`\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.api-breaker\")\n            local conf = {\n                break_response_code = 502,\n                healthy = {}\n            }\n\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"break_response_code\":502,\"healthy\":{\"http_statuses\":[200],\"successes\":3},\"max_breaker_sec\":300,\"unhealthy\":{\"failures\":3,\"http_statuses\":[500]}}\n\n\n\n=== TEST 4: default `unhealthy`\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.api-breaker\")\n            local conf = {\n                break_response_code = 502,\n                unhealthy = {}\n            }\n\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"break_response_code\":502,\"healthy\":{\"http_statuses\":[200],\"successes\":3},\"max_breaker_sec\":300,\"unhealthy\":{\"failures\":3,\"http_statuses\":[500]}}\n\n\n\n=== TEST 5: bad break_response_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 199,\n                            \"unhealthy\": {\n                                \"http_statuses\": [500, 503],\n                                \"failures\": 3\n                            },\n                            \"healthy\": {\n                                \"http_statuses\": [200, 206],\n                                \"successes\": 3\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin api-breaker err: property \\\"break_response_code\\\" validation failed: expected 199 to be at least 200\"}\n\n\n\n=== TEST 6: bad max_breaker_sec\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 200,\n                            \"max_breaker_sec\": -1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 7: bad unhealthy.http_statuses\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 200,\n                            \"max_breaker_sec\": 40,\n                            \"unhealthy\": {\n                                \"http_statuses\": [500, 603],\n                                \"failures\": 3\n                            },\n                            \"healthy\": {\n                                \"http_statuses\": [200, 206],\n                                \"successes\": 3\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 8: same http_statuses in healthy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 500,\n                            \"unhealthy\": {\n                                \"http_statuses\": [500, 503],\n                                \"failures\": 3\n                            },\n                            \"healthy\": {\n                                \"http_statuses\": [206, 206],\n                                \"successes\": 3\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin api-breaker err: property \\\"healthy\\\" validation failed: property \\\"http_statuses\\\" validation failed: expected unique items but items 1 and 2 are equal\"}\n\n\n\n=== TEST 9: set route, http_statuses: [500, 503]\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 599,\n                            \"unhealthy\": {\n                                \"http_statuses\": [500, 503],\n                                \"failures\": 3\n                            },\n                            \"healthy\": {\n                                \"http_statuses\": [200, 206],\n                                \"successes\": 3\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: trigger breaker\n--- request eval\n[\n    \"GET /api_breaker?code=200\", \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=503\", \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=500\", \"GET /api_breaker?code=500\"\n]\n--- error_code eval\n[200, 500, 503, 500, 599, 599]\n\n\n\n=== TEST 11: trigger reset status\n--- request eval\n[\n    \"GET /api_breaker?code=500\", \"GET /api_breaker?code=500\",\n\n    \"GET /api_breaker?code=200\", \"GET /api_breaker?code=200\",\n    \"GET /api_breaker?code=200\",\n\n    \"GET /api_breaker?code=500\", \"GET /api_breaker?code=500\"\n]\n--- error_code eval\n[\n    500, 500,\n    200, 200, 200,\n    500, 500\n]\n\n\n\n=== TEST 12: trigger del healthy numeration\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local json = require(\"toolkit.json\")\n\n        -- trigger to unhealth\n        for i = 1, 4 do\n            local code = t('/api_breaker?code=500', ngx.HTTP_GET)\n            ngx.say(\"code: \", code)\n        end\n\n        -- break for 3 seconds\n        ngx.sleep(3)\n\n        -- make a try\n        for i = 1, 4 do\n            local code = t('/api_breaker?code=200', ngx.HTTP_GET)\n            ngx.say(\"code: \", code)\n        end\n\n        for i = 1, 4 do\n            local code = t('/api_breaker?code=500', ngx.HTTP_GET)\n            ngx.say(\"code: \", code)\n        end\n    }\n}\n--- request\nGET /t\n--- response_body\ncode: 500\ncode: 500\ncode: 500\ncode: 599\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 500\ncode: 500\ncode: 500\ncode: 599\n--- no_error_log\n[error]\nbreaker_time: 4\n--- error_log\nbreaker_time: 2\n\n\n\n=== TEST 13: add plugin with default config value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 502,\n                            \"break_response_body\": \"{\\\"message\\\":\\\"breaker opened.\\\"}\",\n                            \"break_response_headers\": [{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Content-Type\",\"value\":\"application/json+v1\"}],\n                            \"unhealthy\": {\n                                \"failures\": 3\n                            },\n                            \"healthy\": {\n                                \"successes\": 3\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: default value\n--- request\nGET /api_breaker?code=500\n--- error_code: 500\n\n\n\n=== TEST 15: trigger default value of unhealthy.http_statuses breaker\n--- request eval\n[\n    \"GET /api_breaker?code=200\", \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=503\", \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=500\", \"GET /api_breaker?code=500\"\n]\n--- error_code eval\n[200, 500, 503, 500, 500, 502]\n--- response_headers eval\n[\"Content-Type: text/plain\", \"Content-Type: text/html\", \"Content-Type: text/html\", \"Content-Type: text/html\", \"Content-Type: text/html\", \"Content-Type: application/json+v1\"]\n--- response_body_like eval\n[\".*\", \".*\", \".*\", \".*\", \".*\", \"{\\\"message\\\":\\\"breaker opened.\\\"}\"]\n\n\n\n=== TEST 16: unhealthy -> timeout -> normal\n--- config\n    location /mysleep {\n        proxy_pass \"http://127.0.0.1:1980/mysleep?seconds=1\";\n    }\n--- request eval\n[\n    \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=200\",\n\n    \"GET /mysleep\",\n    \"GET /mysleep\",\n    \"GET /mysleep\",\n\n    \"GET /api_breaker?code=200\",\n    \"GET /api_breaker?code=200\",\n    \"GET /api_breaker?code=200\",\n    \"GET /api_breaker?code=200\",\n    \"GET /api_breaker?code=200\"]\n--- error_code eval\n[\n    500, 500, 500, 502,\n    200, 200, 200,\n    200, 200, 200, 200,200\n]\n\n\n\n=== TEST 17: unhealthy -> timeout -> unhealthy\n--- config\nlocation /mysleep {\n    proxy_pass \"http://127.0.0.1:1980/mysleep?seconds=1\";\n}\n--- request eval\n[\n    \"GET /api_breaker?code=500\", \"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=500\", \"GET /api_breaker?code=200\",\n\n    \"GET /mysleep\", \"GET /mysleep\", \"GET /mysleep\",\n\n    \"GET /api_breaker?code=500\",\"GET /api_breaker?code=500\",\n    \"GET /api_breaker?code=500\",\"GET /api_breaker?code=500\"\n    ]\n--- error_code eval\n[\n    500, 500, 500, 502,\n    200, 200, 200,\n    500,500,500,502\n]\n\n\n\n=== TEST 18: enable plugin, unhealthy.failures=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"api-breaker\": {\n                            \"break_response_code\": 502,\n                            \"max_breaker_sec\": 10,\n                            \"unhealthy\": {\n                                \"http_statuses\": [500, 503],\n                                \"failures\": 1\n                            },\n                            \"healthy\": {\n                                \"successes\": 3\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api_breaker\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: hit route 20 times, confirm the breaker time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local status_count = {}\n            for i = 1, 20 do\n                local code = t('/api_breaker?code=500', ngx.HTTP_GET)\n                code = tostring(code)\n                status_count[code] = (status_count[code] or 0) + 1\n                ngx.sleep(1)\n            end\n\n            ngx.say(json.encode(status_count))\n        }\n    }\n--- request\nGET /t\n--- no_error_log\n[error]\nphase_func(): breaker_time: 16\n--- error_log\nphase_func(): breaker_time: 2\nphase_func(): breaker_time: 4\nphase_func(): breaker_time: 8\nphase_func(): breaker_time: 10\n--- response_body\n{\"500\":4,\"502\":16}\n--- timeout: 25\n\n\n\n=== TEST 20: reject invalid schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {input = {\n                    break_response_code = 200,\n                    break_response_headers = {{[\"content-type\"] = \"application/json\"}}\n                }},\n            }) do\n                local code, body = t('/apisix/admin/global_rules/1',\n                    ngx.HTTP_PUT,\n                    {\n                        id = \"1\",\n                        plugins = {\n                            [\"api-breaker\"] = case.input\n                        }\n                    }\n                )\n                ngx.print(require(\"toolkit.json\").decode(body).error_msg)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/failed to check the configuration of plugin api-breaker err: property \\\"break_response_headers\\\" validation failed: failed to validate item 1: property \\\"(key|value)\\\" is required/\n"
  },
  {
    "path": "t/plugin/attach-consumer-label.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: invalid schema (missing headers)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.attach-consumer-label\")\n            local ok, err = plugin.check_schema({})\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"headers\" is required\n--- no_error_log\n[error]\n\n\n\n=== TEST 2: invalid schema (headers is an empty object)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.attach-consumer-label\")\n            local ok, err = plugin.check_schema({\n                headers = {}\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"headers\" validation failed: expect object to have at least 1 properties\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: invalid schema (missing $ prefix)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.attach-consumer-label\")\n            local ok, err = plugin.check_schema({\n                headers = {\n                    [\"X-Consumer-Department\"] = \"department\",\n                    [\"X-Consumer-Company\"] = \"$company\"\n                }\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"headers\" validation failed: failed to validate additional property X-Consumer-Department: failed to match pattern \"^\\\\$.*\" with \"department\"\n--- no_error_log\n[error]\n\n\n\n=== TEST 4: valid schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.attach-consumer-label\")\n            local ok, err = plugin.check_schema({\n                headers = {\n                    [\"X-Consumer-Department\"] = \"$department\",\n                    [\"X-Consumer-Company\"] = \"$company\"\n                }\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\n[error]\n\n\n\n=== TEST 5: add consumer with labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"labels\": {\n                        \"department\": \"devops\",\n                        \"company\": \"api7\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/consumers/jack/credentials/a',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"key-a\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 6: add route with only attach-consumer-label plugin (no key-auth)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"attach-consumer-label\": {\n                            \"_meta\": {\n                                \"disable\": false\n                            },\n                            \"headers\": {\n                                \"X-Consumer-Department\": \"$department\",\n                                \"X-Consumer-Company\": \"$company\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 7: access without auth (should not contain consumer labels)\n--- request\nGET /echo\n--- response_headers\n!X-Consumer-Department\n!X-Consumer-Company\n--- no_error_log\n[error]\n\n\n\n=== TEST 8: add route with attach-consumer-label plugin (with key-auth)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"attach-consumer-label\": {\n                            \"headers\": {\n                                \"X-Consumer-Department\": \"$department\",\n                                \"X-Consumer-Company\": \"$company\",\n                                \"X-Consumer-Role\": \"$role\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 9: access with auth (should contain consumer labels headers, but no x-consumer-role)\n--- request\nGET /echo\n--- more_headers\napikey: key-a\nX-Consumer-Role: admin\n--- response_headers\nX-Consumer-Company: api7\nX-Consumer-Department: devops\n!X-Consumer-Role\n--- no_error_log\n[error]\n\n\n\n=== TEST 10: modify consumer without labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 11: access with auth (should not contain headers because consumer has no labels)\n--- request\nGET /echo\n--- more_headers\napikey: key-a\n--- response_headers\n!X-Consumer-Company\n!X-Consumer-Department\n--- noerror_log\n[error]\n\n\n\n=== TEST 12: modify consumer with labels\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"labels\": {\n                        \"department\": \"devops\",\n                        \"company\": \"api7\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 13: modify route without attach-consumer-label plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 14: add global rule with attach-consumer-label plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"attach-consumer-label\": {\n                            \"headers\": {\n                                \"X-Global-Consumer-Department\": \"$department\",\n                                \"X-Global-Consumer-Company\": \"$company\"\n                            }\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 15: access with auth (should contain expected consumer labels headers)\n--- request\nGET /echo\n--- more_headers\napikey: key-a\n--- response_headers\nX-Global-Consumer-Company: api7\nX-Global-Consumer-Department: devops\n--- no_error_log\n[error]\n"
  },
  {
    "path": "t/plugin/authz-casbin/model.conf",
    "content": "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\n"
  },
  {
    "path": "t/plugin/authz-casbin/policy.csv",
    "content": "p, *, /, GET\np, admin, *, *\ng, alice, admin\n"
  },
  {
    "path": "t/plugin/authz-casbin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casbin\")\n            local conf = {\n                model_path = \"/path/to/model.conf\",\n                policy_path = \"/path/to/policy.csv\",\n                username = \"user\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: username missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casbin\")\n            local conf = {\n                model_path = \"/path/to/model.conf\",\n                policy_path = \"/path/to/policy.csv\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches none\n\n\n\n=== TEST 3: put model and policy text in metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casbin\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/authz-casbin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"model\": \"[request_definition]\n                    r = sub, obj, act\n\n                    [policy_definition]\n                    p = sub, obj, act\n\n                    [role_definition]\n                    g = _, _\n\n                    [policy_effect]\n                    e = some(where (p.eft == allow))\n\n                    [matchers]\n                    m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n\n                    \"policy\": \"p, *, /, GET\n                    p, admin, *, *\n                    g, alice, admin\"\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: Enforcer from text without files\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casbin\")\n            local t = require(\"lib.test_admin\").test\n\n            local conf = {\n                username = \"user\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 5: enable authz-casbin by Admin API\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"authz-casbin\": {\n                            \"username\" : \"user\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: no username header passed\n--- request\nGET /hello\n--- error_code: 403\n--- response_body_like eval\nqr/\"Access Denied\"/\n\n\n\n=== TEST 7: username passed but user not authorized\n--- request\nGET /hello\n--- more_headers\nuser: bob\n--- error_code: 403\n--- response_body\n{\"message\":\"Access Denied\"}\n\n\n\n=== TEST 8: authorized user\n--- request\nGET /hello\n--- more_headers\nuser: admin\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 9: authorized user (rbac)\n--- request\nGET /hello\n--- more_headers\nuser: alice\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 10: unauthorized user before policy update\n--- request\nGET /hello\n--- more_headers\nuser: jack\n--- error_code: 403\n--- response_body\n{\"message\":\"Access Denied\"}\n\n\n\n=== TEST 11: update model and policy text in metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casbin\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/authz-casbin',\n                ngx.HTTP_PUT,\n                [[{\n                    \"model\": \"[request_definition]\n                    r = sub, obj, act\n\n                    [policy_definition]\n                    p = sub, obj, act\n\n                    [role_definition]\n                    g = _, _\n\n                    [policy_effect]\n                    e = some(where (p.eft == allow))\n\n                    [matchers]\n                    m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n\n                    \"policy\": \"p, *, /, GET\n                    p, admin, *, *\n                    p, jack, /hello, GET\n                    g, alice, admin\"\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: authorized user after policy update\n--- request\nGET /hello\n--- more_headers\nuser: jack\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 13: enable authz-casbin using model/policy files\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"authz-casbin\": {\n                            \"model_path\": \"t/plugin/authz-casbin/model.conf\",\n                            \"policy_path\": \"t/plugin/authz-casbin/policy.csv\",\n                            \"username\" : \"user\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: authorized user as per policy\n--- request\nGET /hello\n--- more_headers\nuser: alice\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 15: unauthorized user as per policy\n--- request\nGET /hello\n--- more_headers\nuser: bob\n--- error_code: 403\n--- response_body\n{\"message\":\"Access Denied\"}\n\n\n\n=== TEST 16: enable authz-casbin using model/policy text\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"authz-casbin\": {\n                            \"model\": \"\n                            [request_definition]\n                            r = sub, obj, act\n\n                            [policy_definition]\n                            p = sub, obj, act\n\n                            [role_definition]\n                            g = _, _\n\n                            [policy_effect]\n                            e = some(where (p.eft == allow))\n\n                            [matchers]\n                            m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n                            \"policy\": \"\n                            p, *, /, GET\n                            p, admin, *, *\n                            g, jack, admin\",\n                            \"username\" : \"user\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: authorized user as per policy\n--- request\nGET /hello\n--- more_headers\nuser: jack\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 18: unauthorized user as per policy\n--- request\nGET /hello\n--- more_headers\nuser: bob\n--- error_code: 403\n--- response_body\n{\"message\":\"Access Denied\"}\n\n\n\n=== TEST 19: disable authz-casbin by Admin API\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {},\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: support different policy shapes at different routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"authz-casbin\": {\n                            \"model\": \"\n                            [request_definition]\n                            r = sub, obj, act\n\n                            [policy_definition]\n                            p = obj, act\n\n                            [policy_effect]\n                            e = some(where (p.eft == allow))\n\n                            [matchers]\n                            m = regexMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n                            \"policy\": \"\n                            p, ^/server_port$, GET\",\n                            \"username\" : \"user\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_port\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"authz-casbin\": {\n                            \"model\": \"\n                            [request_definition]\n                            r = sub, obj, act\n\n                            [policy_definition]\n                            p = sub, obj, act\n\n                            [policy_effect]\n                            e = some(where (p.eft == allow))\n\n                            [matchers]\n                            m = keyMatch(r.sub, p.sub) && regexMatch(r.obj, p.obj) && keyMatch(r.act, p.act)\",\n                            \"policy\": \"\n                            p, *, ^/server_port_route2$, GET\",\n                            \"username\" : \"user\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/server_port_route2\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: verify sequence policy1 -> policy2 -> policy1 (issues #12974, #12889)\n--- request eval\n[\n    \"GET /server_port\",\n    \"GET /server_port_route2\",\n    \"GET /server_port\"\n]\n--- more_headers\nuser: bob\n--- error_code eval\n[200, 200, 200]\n--- no_error_log\ncasbin enforce error\ninvalid request size\n"
  },
  {
    "path": "t/plugin/authz-casdoor.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    server {\n        listen 10420;\n        location /api/login/oauth/access_token {\n            content_by_lua_block {\n                local json_encode = require(\"toolkit.json\").encode\n                ngx.req.read_body()\n                local arg = ngx.req.get_post_args()[\"code\"]\n\n                local core = require(\"apisix.core\")\n                local log = core.log\n\n                if arg == \"wrong\" then\n                    ngx.status = 200\n                    ngx.say(json_encode({ access_token = \"bbbbbbbbbb\", expires_in = 0 }))\n                    return\n                end\n\n                ngx.status = 200\n                ngx.say(json_encode({ access_token = \"aaaaaaaaaaaaaaaa\", expires_in = 1000000 }))\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local fake_uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback\"\n            local conf = {\n                callback_url = callback_url,\n                endpoint_addr = fake_uri,\n                client_id = \"7ceb9b7fda4a9061ec1c\",\n                client_secret = \"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            local conf2 = {\n                callback_url = callback_url .. \"/?code=aaa\",\n                endpoint_addr = fake_uri,\n                client_id = \"7ceb9b7fda4a9061ec1c\",\n                client_secret = \"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n            }\n            ok, err = plugin.check_schema(conf2)\n            if ok then\n                ngx.say(\"err: shouldn't have passed sanity check\")\n            end\n\n            local conf3 = {\n                callback_url = callback_url,\n                endpoint_addr = fake_uri .. \"/\",\n                client_id = \"7ceb9b7fda4a9061ec1c\",\n                client_secret = \"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n            }\n            ok, err = plugin.check_schema(conf3)\n            if ok then\n                ngx.say(\"err: shouldn't have passed sanity check\")\n            end\n\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: enable plugin test redirect\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local t = require(\"lib.test_admin\").test\n\n            local fake_uri = \"http://127.0.0.1:10420\"\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback\"\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/anything/*\",\n                    \"plugins\": {\n                        \"authz-casdoor\": {\n                            \"callback_url\":\"]] .. callback_url .. [[\",\n                            \"endpoint_addr\":\"]] .. fake_uri .. [[\",\n                            \"client_id\":\"7ceb9b7fda4a9061ec1c\",\n                            \"client_secret\":\"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/echo\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"test.com:1980\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.say(\"failed to set up routing rule\")\n            end\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 3: test redirect\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/anything/d?param1=foo&param2=bar', ngx.HTTP_GET, [[]])\n            if code ~= 302 then\n                ngx.say(\"should have redirected\")\n            end\n\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 4: enable fake casdoor\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                        \"uri\": \"/api/login/oauth/access_token\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: test fake casdoor\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local t = require(\"lib.test_admin\").test\n            local httpc = require(\"resty.http\").new()\n            local cjson = require(\"cjson\")\n            local fake_uri = \"http://127.0.0.1:10420/api/login/oauth/access_token\"\n\n            local res, err = httpc:request_uri(fake_uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n            end\n            local data = cjson.decode(res.body)\n            if not data then\n                ngx.say(\"invalid res.body\")\n            end\n            if not data.access_token == \"aaaaaaaaaaaaaaaa\" then\n                ngx.say(\"invalid token\")\n            end\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 6: test code handling\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local core = require(\"apisix.core\")\n            local log = core.log\n            local t = require(\"lib.test_admin\").test\n            local cjson = require(\"cjson\")\n            local fake_uri = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                \"/anything/d?param1=foo&param2=bar\"\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback?code=aaa&state=\"\n\n            local httpc = require(\"resty.http\").new()\n            local res1, err1 = httpc:request_uri(fake_uri, {method = \"GET\"})\n            if not res1 then\n                ngx.say(err1)\n            end\n\n            local cookie = res1.headers[\"Set-Cookie\"]\n            local re_url = res1.headers[\"Location\"]\n            local m, err = ngx.re.match(re_url, \"state=([0-9]*)\")\n            if err or not m then\n                log.error(err)\n                ngx.exit()\n            end\n            local state = m[1]\n\n            local res2, err2 = httpc:request_uri(callback_url..state, {\n                method = \"GET\",\n                headers = {Cookie = cookie}\n            })\n            if not res2 then\n                ngx.say(err2)\n            end\n            if res2.status ~= 302 then\n                log.error(res2.status)\n            end\n\n            local cookie2 = res2.headers[\"Set-Cookie\"]\n            local res3, err3 = httpc:request_uri(fake_uri, {\n                method = \"GET\",\n                headers = {Cookie = cookie2}\n\n            })\n            if not res3 then\n                ngx.say(err3)\n            end\n            if res3.status >= 300 then\n                log.error(res3.status,res3.headers[\"Location\"])\n            end\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 7: incorrect test code handling\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local t = require(\"lib.test_admin\").test\n            local cjson = require(\"cjson\")\n\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback?code=aaa&state=bbb\"\n\n            local httpc = require(\"resty.http\").new()\n            local res1, err1 = httpc:request_uri(callback_url, {method = \"GET\"})\n            if res1.status ~= 503 then\n                ngx.say(res1.status)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nno session found\n\n\n\n=== TEST 8: incorrect state handling\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local core = require(\"apisix.core\")\n            local log = core.log\n            local t = require(\"lib.test_admin\").test\n            local cjson = require(\"cjson\")\n            local fake_uri = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                \"/anything/d?param1=foo&param2=bar\"\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback?code=aaa&state=\"\n\n            local httpc = require(\"resty.http\").new()\n            local res1, err1 = httpc:request_uri(fake_uri, {method = \"GET\"})\n            if not res1 then\n                ngx.say(err1)\n            end\n\n            local cookie = res1.headers[\"Set-Cookie\"]\n            local re_url = res1.headers[\"Location\"]\n            local m, err = ngx.re.match(re_url, \"state=([0-9]*)\")\n            if err or not m then\n                log.error(err)\n            end\n            local state = m[1]+10\n\n            local res2, err2 = httpc:request_uri(callback_url..state, {\n                method = \"GET\",\n                headers = {Cookie = cookie}\n            })\n            if not res2 then\n                ngx.say(err2)\n            end\n            if res2.status ~= 302 then\n                log.error(res2.status)\n            end\n\n            local cookie2 = res2.headers[\"Set-Cookie\"]\n            local res3, err3 = httpc:request_uri(fake_uri, {\n                method = \"GET\",\n                headers = {Cookie = cookie2}\n            })\n            if not res3 then\n                ngx.say(err3)\n            end\n            if res3.status ~= 503 then\n                log.error(res3.status)\n            end\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n--- error_log\ninvalid state\n\n\n\n=== TEST 9: test incorrect access_token\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local core = require(\"apisix.core\")\n            local log = core.log\n            local t = require(\"lib.test_admin\").test\n            local cjson = require(\"cjson\")\n            local fake_uri = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                \"/anything/d?param1=foo&param2=bar\"\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback?code=wrong&state=\"\n\n            local httpc = require(\"resty.http\").new()\n            local res1, err1 = httpc:request_uri(fake_uri, {method = \"GET\"})\n            if not res1 then\n                ngx.say(err1)\n            end\n\n            local cookie = res1.headers[\"Set-Cookie\"]\n            local re_url = res1.headers[\"Location\"]\n            local m, err = ngx.re.match(re_url, \"state=([0-9]*)\")\n            if err or not m then\n                log.error(err)\n                ngx.exit()\n            end\n            local state = m[1]\n\n            local res2, err2 = httpc:request_uri(callback_url..state, {\n                method = \"GET\",\n                headers = {Cookie = cookie}\n            })\n            if not res2 then\n                ngx.say(err2)\n            end\n            if res2.status ~= 302 then\n                log.error(res2.status)\n            end\n\n            local cookie2 = res2.headers[\"Set-Cookie\"]\n            local res3, err3 = httpc:request_uri(fake_uri, {\n                method = \"GET\",\n                headers = {Cookie = cookie2}\n\n            })\n            if not res3 then\n                ngx.say(err3)\n            end\n            if res3.status ~= 503 then\n                log.error(res3.status)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nfailed when accessing token: invalid access_token\n\n\n\n=== TEST 10: data encryption for client_secret\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                 \"/anything/callback\"\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/anything/*\",\n                    \"plugins\": {\n                        \"authz-casdoor\": {\n                            \"callback_url\":\"]] .. callback_url .. [[\",\n                            \"endpoint_addr\": \"http://127.0.0.1:10420\",\n                            \"client_id\":\"7ceb9b7fda4a9061ec1c\",\n                            \"client_secret\":\"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/echo\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"test.com:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"authz-casdoor\"].client_secret)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"authz-casdoor\"].client_secret)\n        }\n    }\n--- response_body\n3416238e1edf915eac08b8fe345b2b95cdba7e04\nYUfqAO0kPXjZIoAbPSuryCkUDksEmwSq08UDTIUWolN6KQwEUrh72TazePueo4/S\n"
  },
  {
    "path": "t/plugin/authz-keycloak.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: minimal valid configuration w/o discovery\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                token_endpoint = \"https://host.domain/realms/foo/protocol/openid-connect/token\"\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: minimal valid configuration with discovery\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                discovery = \"https://host.domain/realms/foo/.well-known/uma2-configuration\"\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 3: minimal valid configuration w/o discovery when lazy_load_paths=true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                lazy_load_paths = true,\n                                token_endpoint = \"https://host.domain/realms/foo/protocol/openid-connect/token\",\n                                resource_registration_endpoint = \"https://host.domain/realms/foo/authz/protection/resource_set\"\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 4: minimal valid configuration with discovery when lazy_load_paths=true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                lazy_load_paths = true,\n                                discovery = \"https://host.domain/realms/foo/.well-known/uma2-configuration\"\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 5: full schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                discovery = \"https://host.domain/realms/foo/.well-known/uma2-configuration\",\n                                token_endpoint = \"https://host.domain/realms/foo/protocol/openid-connect/token\",\n                                resource_registration_endpoint = \"https://host.domain/realms/foo/authz/protection/resource_set\",\n                                client_id = \"University\",\n                                client_secret = \"secret\",\n                                grant_type = \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                policy_enforcement_mode = \"ENFORCING\",\n                                permissions = {\"res:customer#scopes:view\"},\n                                lazy_load_paths = false,\n                                http_method_as_scope = false,\n                                timeout = 1000,\n                                ssl_verify = false,\n                                cache_ttl_seconds = 1000,\n                                keepalive = true,\n                                keepalive_timeout = 10000,\n                                keepalive_pool = 5,\n                                access_token_expires_in = 300,\n                                access_token_expires_leeway = 0,\n                                refresh_token_expires_in = 3600,\n                                refresh_token_expires_leeway = 0,\n                                password_grant_token_generation_incoming_uri = \"/api/token\",\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 6: token_endpoint and discovery both missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({client_id = \"foo\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nallOf 1 failed: object matches none of the required: [\"discovery\"] or [\"token_endpoint\"]\ndone\n\n\n\n=== TEST 7: client_id missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({discovery = \"https://host.domain/realms/foo/.well-known/uma2-configuration\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"client_id\" is required\ndone\n\n\n\n=== TEST 8: resource_registration_endpoint and discovery both missing and lazy_load_paths is true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                token_endpoint = \"https://host.domain/realms/foo/protocol/openid-connect/token\",\n                                lazy_load_paths = true\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nallOf 2 failed: object matches none of the required\ndone\n\n\n\n=== TEST 9: Add https endpoint with ssl_verify true (default)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#delete\"],\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: TEST with fake token and https endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                }\n             })\n\n            ngx.status = res.status\n\n            if res.status == 200 then\n                ngx.say(true)\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nfalse\n--- error_log\nError while sending authz request to https://127.0.0.1:8443/realms/University/protocol/openid-connect/token: 18\n--- error_code: 503\n\n\n\n=== TEST 11: Add https endpoint with ssl_verify false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#delete\"],\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: TEST for https based token verification with ssl_verify false\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                }\n             })\n\n            if res.status == 200 then\n                ngx.say(true)\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nfalse\n--- error_log\nRequest denied: HTTP 401 Unauthorized. Body: {\"error\":\"HTTP 401 Unauthorized\"}\n\n\n\n=== TEST 13: set enforcement mode is \"ENFORCING\", lazy_load_paths and permissions use default values\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"http://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"policy_enforcement_mode\": \"ENFORCING\",\n                                \"timeout\": 3000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: test for permission is empty and enforcement mode is \"ENFORCING\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                }\n             })\n\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"error\":\"access_denied\",\"error_description\":\"not_authorized\"}\n--- no_error_log\n\n\n\n=== TEST 15: set enforcement mode is \"ENFORCING\", lazy_load_paths and permissions use default values , access_denied_redirect_uri is \"http://127.0.0.1/test\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"http://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"policy_enforcement_mode\": \"ENFORCING\",\n                                \"timeout\": 3000,\n                                \"access_denied_redirect_uri\": \"http://127.0.0.1/test\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: test for permission is empty and enforcement mode is \"ENFORCING\" , access_denied_redirect_uri is \"http://127.0.0.1/test\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                }\n             })\n            if res.status >= 300 then\n                ngx.status = res.status\n                ngx.header[\"Location\"] = res.headers[\"Location\"]\n            end\n        }\n    }\n--- request\nGET /t\n--- response_headers\nLocation: http://127.0.0.1/test\n--- error_code: 307\n\n\n\n=== TEST 17: Add https endpoint with password_grant_token_generation_incoming_uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#view\"],\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false,\n                                \"password_grant_token_generation_incoming_uri\": \"/api/token\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/api/token\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/token\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = {\n                    [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n                },\n\n                body =  ngx.encode_args({\n                    username = \"teacher@gmail.com\",\n                    password = \"123456\",\n                }),\n            })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n                local refreshToken = body[\"refresh_token\"]\n\n                if accessToken and refreshToken then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 18: no username or password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#view\"],\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false,\n                                \"password_grant_token_generation_incoming_uri\": \"/api/token\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/api/token\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/token\"\n            local headers = {\n                [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n            }\n\n            -- no username\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = headers,\n                body =  ngx.encode_args({\n                    password = \"123456\",\n                }),\n            })\n            ngx.print(res.body)\n\n            -- no password\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = headers,\n                body =  ngx.encode_args({\n                    username = \"teacher@gmail.com\",\n                }),\n            })\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"message\":\"username is missing.\"}\n{\"message\":\"password is missing.\"}\n"
  },
  {
    "path": "t/plugin/authz-keycloak2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin with view course permissions (using token endpoint)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#view\"],\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: Get access token for teacher and access view course route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(res.status)\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 3: invalid access token\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer wrong_token\",\n                    }\n                })\n            if res.status == 401 then\n                ngx.say(true)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n--- error_log\nInvalid bearer token\n\n\n\n=== TEST 4: add plugin with view course permissions (using discovery)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/uma2-configuration\",\n                                \"permissions\": [\"course_resource#view\"],\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: Get access token for teacher and access view course route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 6: invalid access token\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer wrong_token\",\n                    }\n                })\n            if res.status == 401 then\n                ngx.say(true)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n--- error_log\nInvalid bearer token\n\n\n\n=== TEST 7: add plugin for delete course route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#delete\"],\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: Get access token for student and delete course\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 403 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n--- error_log\n{\"error\":\"access_denied\",\"error_description\":\"not_authorized\"}\n\n\n\n=== TEST 9: add plugin with lazy_load_paths and http_method_as_scope\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/uma2-configuration\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"lazy_load_paths\": true,\n                                \"http_method_as_scope\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/course/foo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: Get access token for teacher and access view course route.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 11: Get access token for student and access view course route.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 12: Get access token for teacher and delete course.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"DELETE\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 13: Get access token for student and try to delete course. Should fail.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"DELETE\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 403 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n--- error_log\n{\"error\":\"access_denied\",\"error_description\":\"not_authorized\"}\n\n\n\n=== TEST 14: Get access token for teacher and access view course route.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 15: Get access token for student and access view course route.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 200 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n\n\n\n=== TEST 16: add plugin with lazy_load_paths when resource_registration_endpoint is neither in config nor in the discovery doc\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"lazy_load_paths\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/course/foo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: Get access token for student and access view course route.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                if res.status == 503 then\n                    ngx.say(true)\n                else\n                    ngx.say(false)\n                end\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n--- error_log\nUnable to determine registration endpoint.\n\n\n\n=== TEST 18: add plugin with lazy_load_paths for query string test\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/uma2-configuration\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"lazy_load_paths\": true,\n                                \"http_method_as_scope\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/course/foo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: access route with query string should succeed (query string stripped for resource matching)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status ~= 200 then\n                ngx.say(false)\n            else\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/course/foo?param=value&foo=bar\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                })\n\n                ngx.say(res.status == 200)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ntrue\n"
  },
  {
    "path": "t/plugin/authz-keycloak3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: access_denied_redirect_uri works with request denied in token_endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\",\n                                \"access_denied_redirect_uri\": \"http://127.0.0.1/test\",\n                                \"permissions\": [\"course_resource#delete\"],\n                                \"client_id\": \"course_management\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=student@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            if res.status == 200 then\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. accessToken,\n                    }\n                 })\n\n                 ngx.status = res.status\n                 ngx.header[\"Location\"] = res.headers[\"Location\"]\n            end\n        }\n    }\n--- error_code: 307\n--- response_headers\nLocation: http://127.0.0.1/test\n\n\n\n=== TEST 3: data encryption for client_secret\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource#view\"],\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false,\n                                \"password_grant_token_generation_incoming_uri\": \"/api/token\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/api/token\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"authz-keycloak\"].client_secret)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"authz-keycloak\"].client_secret)\n        }\n    }\n--- response_body\nd1ec69e9-55d2-4109-a3ea-befa071579d5\nFz1juZEEvh9PPXOmWFdMMJkREt3ZSzEVWcUZPxNP6achk3fosEvn37oN0qH4YgKB\n"
  },
  {
    "path": "t/plugin/authz-keycloak4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n    $ENV{CLIENT_SECRET} = \"d1ec69e9-55d2-4109-a3ea-befa071579d5\";\n}\n\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5\n--- response_body\nSuccess! Data written to: kv/apisix/foo\n\n\n\n=== TEST 2: set client_secret as a reference to secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource\"],\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"$secret://vault/test1/foo/client_secret\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false,\n                                \"password_grant_token_generation_incoming_uri\": \"/api/token\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/api/token\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/token\"\n            local headers = {\n                [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n            }\n\n            -- no username\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = headers,\n                body =  ngx.encode_args({\n                    username = \"teacher@gmail.com\",\n                    password = \"123456\",\n                }),\n            })\n            if res.status == 200 then\n                ngx.print(\"success\\n\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 3: set client_secret as a reference to env variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource\"],\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"$env://CLIENT_SECRET\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false,\n                                \"password_grant_token_generation_incoming_uri\": \"/api/token\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/api/token\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/token\"\n            local headers = {\n                [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n            }\n\n            -- no username\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = headers,\n                body =  ngx.encode_args({\n                    username = \"teacher@gmail.com\",\n                    password = \"123456\",\n                }),\n            })\n            if res.status == 200 then\n                ngx.print(\"success\\n\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n\n\n\n=== TEST 4: set invalid client_secret as a reference to env variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"authz-keycloak\": {\n                                \"token_endpoint\": \"https://127.0.0.1:8443/realms/University/protocol/openid-connect/token\",\n                                \"permissions\": [\"course_resource\"],\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"$env://INVALID_CLIENT_SECRET\",\n                                \"grant_type\": \"urn:ietf:params:oauth:grant-type:uma-ticket\",\n                                \"timeout\": 3000,\n                                \"ssl_verify\": false,\n                                \"password_grant_token_generation_incoming_uri\": \"/api/token\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/api/token\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/token\"\n            local headers = {\n                [\"Content-Type\"] = \"application/x-www-form-urlencoded\",\n            }\n\n            -- no username\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = headers,\n                body =  ngx.encode_args({\n                    username = \"teacher@gmail.com\",\n                    password = \"123456\",\n                }),\n            })\n            if res.status == 200 then\n                ngx.print(\"success\\n\")\n            end\n        }\n    }\n--- request\nGET /t\n--- grep_error_log eval\nqr/Invalid client secret/\n--- grep_error_log_out\nInvalid client secret\nInvalid client secret\n"
  },
  {
    "path": "t/plugin/aws-lambda.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $inside_lua_block = $block->inside_lua_block // \"\";\n    chomp($inside_lua_block);\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 8765;\n\n        location /httptrigger {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local msg = \"aws lambda invoked\"\n                ngx.header['Content-Length'] = #msg + 1\n                ngx.header['Connection'] = \"Keep-Alive\"\n                ngx.say(msg)\n            }\n        }\n\n        location /generic {\n            content_by_lua_block {\n                $inside_lua_block\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: checking iam schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.aws-lambda\")\n            local ok, err = plugin.check_schema({\n                function_uri = \"https://api.amazonaws.com\",\n                authorization = {\n                    iam = {\n                        accesskey = \"key1\",\n                        secretkey = \"key2\"\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: missing fields in iam schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.aws-lambda\")\n            local ok, err = plugin.check_schema({\n                function_uri = \"https://api.amazonaws.com\",\n                authorization = {\n                    iam = {\n                        secretkey = \"key2\"\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\nproperty \"authorization\" validation failed: property \"iam\" validation failed: property \"accesskey\" is required\n\n\n\n=== TEST 3: create route with aws plugin enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"aws-lambda\": {\n                                \"function_uri\": \"http://localhost:8765/httptrigger\",\n                                \"authorization\": {\n                                    \"apikey\" : \"testkey\"\n                                }\n                            }\n                        },\n                        \"uri\": \"/aws\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test plugin endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n\n            local code, _, body, headers = t(\"/aws\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            -- headers proxied 2 times -- one by plugin, another by this test case\n            core.response.set_header(headers)\n            ngx.print(body)\n        }\n    }\n--- response_body\naws lambda invoked\n--- response_headers\nContent-Length: 19\n\n\n\n=== TEST 5: check authz header - apikey\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- passing an apikey\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"aws-lambda\": {\n                                \"function_uri\": \"http://localhost:8765/generic\",\n                                \"authorization\": {\n                                    \"apikey\": \"test_key\"\n                                }\n                            }\n                        },\n                        \"uri\": \"/aws\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            local code, _, body = t(\"/aws\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- inside_lua_block\nlocal headers = ngx.req.get_headers() or {}\nngx.say(\"Authz-Header - \" .. headers[\"x-api-key\"] or \"\")\n\n--- response_body\npassed\nAuthz-Header - test_key\n\n\n\n=== TEST 6: check authz header - IAM v4 signing\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- passing the iam access and secret keys\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"aws-lambda\": {\n                                \"function_uri\": \"http://localhost:8765/generic\",\n                                \"authorization\": {\n                                    \"iam\": {\n                                        \"accesskey\": \"KEY1\",\n                                        \"secretkey\": \"KeySecret\"\n                                    }\n                                }\n                            }\n                        },\n                        \"uri\": \"/aws\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            local code, _, body, headers = t(\"/aws\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- inside_lua_block\nlocal headers = ngx.req.get_headers() or {}\nngx.say(\"Authz-Header - \" .. headers[\"Authorization\"] or \"\")\nngx.say(\"AMZ-Date - \" .. headers[\"X-Amz-Date\"] or \"\")\nngx.print(\"invoked\")\n\n--- response_body eval\nqr/passed\nAuthz-Header - AWS4-HMAC-SHA256 [ -~]*\nAMZ-Date - [\\d]+T[\\d]+Z\ninvoked/\n"
  },
  {
    "path": "t/plugin/azure-functions.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $inside_lua_block = $block->inside_lua_block // \"\";\n    chomp($inside_lua_block);\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 8765;\n\n        location /httptrigger {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local msg = \"faas invoked\"\n                ngx.header['Content-Length'] = #msg + 1\n                ngx.header['X-Extra-Header'] = \"MUST\"\n                ngx.header['Connection'] = \"Keep-Alive\"\n                ngx.say(msg)\n            }\n        }\n\n        location  /api {\n           content_by_lua_block {\n                ngx.say(\"invocation /api successful\")\n            }\n        }\n\n        location /api/httptrigger {\n           content_by_lua_block {\n                ngx.say(\"invocation /api/httptrigger successful\")\n            }\n        }\n\n        location /api/http/trigger {\n           content_by_lua_block {\n                ngx.say(\"invocation /api/http/trigger successful\")\n            }\n        }\n\n        location /azure-demo {\n            content_by_lua_block {\n                $inside_lua_block\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.azure-functions\")\n            local conf = {\n                function_uri = \"http://some-url.com\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: function_uri missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.azure-functions\")\n            local ok, err = plugin.check_schema({})\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\nproperty \"function_uri\" is required\n\n\n\n=== TEST 3: create route with azure-function plugin enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"azure-functions\": {\n                                \"function_uri\": \"http://localhost:8765/httptrigger\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/azure\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: Test plugin endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n\n            local code, _, body, headers = t(\"/azure\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            -- headers proxied 2 times -- one by plugin, another by this test case\n            core.response.set_header(headers)\n            ngx.print(body)\n        }\n    }\n--- response_body\nfaas invoked\n--- response_headers\nContent-Length: 13\nX-Extra-Header: MUST\n\n\n\n=== TEST 5: http2 check response body and headers\n--- http2\n--- request\nGET /azure\n--- more_headers\nContent-Length: 0\n--- response_body\nfaas invoked\n\n\n\n=== TEST 6: check HTTP/2 response headers (must not contain any connection specific info)\nFirst fetch the header from curl with -I then check the count of Connection\nThe full header looks like the format shown below\n\nHTTP/2 200\ncontent-type: text/plain\nx-extra-header: MUST\ncontent-length: 13\ndate: Wed, 17 Nov 2021 13:53:08 GMT\nserver: APISIX/2.10.2\n\n--- http2\n--- request\nHEAD /azure\n--- more_headers\nContent-Length: 0\n--- response_headers\nConnection:\nUpgrade:\nKeep-Alive:\ncontent-type: text/plain\nx-extra-header: MUST\ncontent-length: 13\n\n\n\n=== TEST 7: check authz header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- passing an apikey\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"azure-functions\": {\n                                \"function_uri\": \"http://localhost:8765/azure-demo\",\n                                \"authorization\": {\n                                    \"apikey\": \"test_key\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/azure\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            local code, _, body = t(\"/azure\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- inside_lua_block\nlocal headers = ngx.req.get_headers() or {}\nngx.say(\"Authz-Header - \" .. headers[\"x-functions-key\"] or \"\")\n\n--- response_body\npassed\nAuthz-Header - test_key\n\n\n\n=== TEST 8: check if apikey doesn't get overridden passed by client to the gateway\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local header = {}\n            header[\"x-functions-key\"] = \"must_not_be_overrided\"\n\n            -- plugin schema already contains apikey with value \"test_key\" which won't be respected\n            local code, _, body = t(\"/azure\", \"GET\", nil, nil, header)\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- inside_lua_block\nlocal headers = ngx.req.get_headers() or {}\nngx.say(\"Authz-Header - \" .. headers[\"x-functions-key\"] or \"\")\n\n--- response_body\nAuthz-Header - must_not_be_overrided\n\n\n\n=== TEST 9: fall back to metadata master key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, meta_body = t('/apisix/admin/plugin_metadata/azure-functions',\n                ngx.HTTP_PUT,\n                [[{\n                    \"master_apikey\":\"metadata_key\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(meta_body)\n\n            -- update plugin attribute\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"azure-functions\": {\n                                \"function_uri\": \"http://localhost:8765/azure-demo\"\n                            }\n                        },\n                        \"uri\": \"/azure\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            -- plugin schema already contains apikey with value \"test_key\" which won't be respected\n            local code, _, body = t(\"/azure\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- inside_lua_block\nlocal headers = ngx.req.get_headers() or {}\nngx.say(\"Authz-Header - \" .. headers[\"x-functions-key\"] or \"\")\n\n--- response_body\npassed\npassed\nAuthz-Header - metadata_key\n\n\n\n=== TEST 10: check if url path being forwarded correctly by creating a semi correct path uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- creating a semi path route\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"azure-functions\": {\n                                \"function_uri\": \"http://localhost:8765/api\"\n                            }\n                        },\n                        \"uri\": \"/azure/*\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            local code, _, body = t(\"/azure/httptrigger\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\npassed\ninvocation /api/httptrigger successful\n\n\n\n=== TEST 11: check multilevel url path forwarding\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/azure/http/trigger\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\ninvocation /api/http/trigger successful\n\n\n\n=== TEST 12: check url path forwarding containing multiple slashes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/azure///http////trigger\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\ninvocation /api/http/trigger successful\n\n\n\n=== TEST 13: check url path forwarding with no excess path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/azure/\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\ninvocation /api successful\n\n\n\n=== TEST 14: create route with azure-function plugin enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"azure-functions\": {\n                                \"function_uri\": \"http://localhost:8765/httptrigger\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/azure\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: http2 failed to check response body and headers\n--- http2\n--- request\nGET /azure\n--- error_code: 400\n--- error_log\nHTTP2/HTTP3 request without a Content-Length header,\n"
  },
  {
    "path": "t/plugin/basic-auth-anonymous-consumer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer jack and anonymous\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 4,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"anonymous\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\npassed\n\n\n\n=== TEST 2: add basic auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"anonymous_consumer\": \"anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: normal consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 5, 1 do\n                local code, body = t('/hello',\n                    ngx.HTTP_GET,\n                    nil,\n                    nil,\n                    {\n                        Authorization = \"Basic Zm9vOmJhcg==\"\n                    }\n                )\n\n                if code >= 300 then\n                    ngx.say(\"failed\" .. code)\n                    return\n                end\n                ngx.say(body .. i)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed1\npassed2\npassed3\npassed4\nfailed503\n\n\n\n=== TEST 4: request without basic-auth header will be from anonymous consumer and it will pass\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: request without basic-auth header will be from anonymous consumer and different rate limit will apply\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 6: add basic auth plugin with non-existent anonymous_consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"anonymous_consumer\": \"not-found-anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: anonymous-consumer configured in the route should not be found\n--- request\nGET /hello\n--- error_code: 401\n--- error_log\nfailed to get anonymous consumer not-found-anonymous\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n"
  },
  {
    "path": "t/plugin/basic-auth-realm.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity, default realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: verify default realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"basic\"\n\n\n\n=== TEST 3: set custom realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"realm\": \"secure-zone\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: verify custom realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"secure-zone\"\n\n\n\n=== TEST 5: set anonymous consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"anonymous_consumer\": \"missing-consumer\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: verify anonymous consumer missing returns realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"basic\"\n--- error_log\nfailed to get anonymous consumer\n"
  },
  {
    "path": "t/plugin/basic-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.basic-auth\")\n            local ok, err = plugin.check_schema({username = 'foo', password = 'bar'}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 2: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.basic-auth\")\n            local ok, err = plugin.check_schema({username = 123, password = \"bar\"}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"username\" validation failed: wrong type: expected string, got number\ndone\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 3: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 4: enable basic auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: verify, missing authorization\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing authorization in request\"}\n\n\n\n=== TEST 6: verify, invalid basic authorization header\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bad_header YmFyOmJhcgo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid authorization in request\"}\n--- grep_error_log eval\nqr/Invalid authorization header format/\n--- grep_error_log_out\nInvalid authorization header format\n\n\n\n=== TEST 7: verify, invalid authorization value (bad base64 str)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic aca_a\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid authorization in request\"}\n--- grep_error_log eval\nqr/Failed to decode authentication header: aca_a/\n--- grep_error_log_out\nFailed to decode authentication header: aca_a\n\n\n\n=== TEST 8: verify, invalid authorization value (no password)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic YmFy\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid authorization in request\"}\n--- grep_error_log eval\nqr/Split authorization err: invalid decoded data: bar/\n--- grep_error_log_out\nSplit authorization err: invalid decoded data: bar\n\n\n\n=== TEST 9: verify, invalid username\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic YmFyOmJhcgo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n\n\n\n=== TEST 10: verify, invalid password\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmZvbwo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n\n\n\n=== TEST 11: verify capitalization scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n--- error_log\nfind consumer foo\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 12: invalid schema, only one field `username`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin basic-auth err: property \\\"password\\\" is required\"}\n\n\n\n=== TEST 13: invalid schema, not field given\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/\\{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin basic-auth err: property \\\\\"(username|password)\\\\\" is required\"\\}/\n\n\n\n=== TEST 14: invalid schema, not a table\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": \"blah\"\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: invalid plugin conf \\\"blah\\\" for plugin [basic-auth]\"}\n\n\n\n=== TEST 15: get the default schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/basic-auth',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"properties\":{},\"title\":\"work with route or service object\",\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 16: get the schema by schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/basic-auth?schema_type=consumer',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"title\":\"work with consumer object\",\"required\":[\"username\",\"password\"],\"properties\":{\"username\":{\"type\":\"string\"},\"password\":{\"type\":\"string\"}},\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 17: get the schema by error schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/basic-auth?schema_type=consumer123123',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"properties\":{},\"title\":\"work with route or service object\",\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 18: enable basic auth plugin using admin api, set hide_credentials = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 19: verify Authorization request header is hidden\n--- request\nGET /echo\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_headers\n!Authorization\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 20: enable basic auth plugin using admin api, hide_credentials = false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 21: verify Authorization request header should not hidden\n--- request\nGET /echo\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 22: set basic-auth conf: password uses secret ref\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"$secret://vault/test1/foo/passwd\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo passwd=bar\n--- response_body\nSuccess! Data written to: kv/apisix/foo\n\n\n\n=== TEST 24: verify Authorization with foo/bar, request header should not hidden\n--- request\nGET /echo\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_headers\nAuthorization: Basic Zm9vOmJhcg==\n\n\n\n=== TEST 25: set basic-auth conf with the token in an env var: password uses secret ref\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"$ENV://VAULT_TOKEN\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"$secret://vault/test1/foo/passwd\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 26: verify Authorization with foo/bar, request header should not hidden\n--- request\nGET /echo\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_headers\nAuthorization: Basic Zm9vOmJhcg==\n\n\n\n=== TEST 27: configure the route to verify the basic scheme\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"basic-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: verify lowercase scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: basic Zm9vOmJhcg==\n--- response_body\nhello world\n--- error_log\nfind consumer foo\n--- no_error_log\n\"bar\"\n\n\n\n=== TEST 29: verify uppercase scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: BASIC Zm9vOmJhcg==\n--- response_body\nhello world\n--- error_log\nfind consumer foo\n\n\n\n=== TEST 30: verify mixed case scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: bASiC Zm9vOmJhcg==\n--- response_body\nhello world\n--- error_log\nfind consumer foo\n"
  },
  {
    "path": "t/plugin/batch-requests-grpc.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - grpc-transcode\n    - public-api\n    - batch-requests\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/batch-requests\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: set proto(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: set routes(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route\n--- request\nGET /grpctest?name=world\n--- response_body eval\nqr/\\{\"message\":\"Hello world\"\\}/\n\n\n\n=== TEST 5: successful batch-requests for both grpc and http\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"headers\": {\n                        \"Content-Type\":\"application/json\"\n                    },\n                    \"pipeline\":[\n                        {\n                            \"method\":\"GET\",\n                            \"path\":\"/grpctest\"\n                        },\n                        {\n                            \"method\":\"GET\",\n                            \"path\":\"/get\"\n                        }\n                    ]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200,\n                    \"body\":\"{\\\"message\\\":\\\"Hello \\\"}\"\n                },\n                {\n                    \"status\": 200,\n                    \"body\":\"hello\"\n                }\n                ]]=])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /get {\n        content_by_lua_block {\n            ngx.print(\"hello\")\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/batch-requests.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - public-api\n    - batch-requests\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/batch-requests\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: sanity\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"query\": {\n                        \"base\": \"base_query\",\n                        \"conflict\": \"query_value\"\n                    },\n                    \"headers\": {\n                        \"Base-Header\": \"base\",\n                        \"ConflictHeader\": \"header_value\",\n                        \"OuterConflict\": \"common_value\"\n                    },\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\",\n                            \"ConflictHeader\": \"b-header-value\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\",\n                        \"query\": {\n                            \"one\": \"thing\",\n                            \"conflict\": \"d_value\"\n                        }\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200,\n                    \"body\":\"B\",\n                    \"headers\": {\n                        \"Client-IP\": \"127.0.0.1\",\n                        \"Base-Header\": \"base\",\n                        \"Base-Query\": \"base_query\",\n                        \"X-Res\": \"B\",\n                        \"X-Header1\": \"hello\",\n                        \"X-Header2\": \"world\",\n                        \"X-Conflict-Header\": \"b-header-value\",\n                        \"X-OuterConflict\": \"common_value\"\n                    }\n                },\n                {\n                    \"status\": 201,\n                    \"body\":\"C\",\n                    \"headers\": {\n                        \"Client-IP-From-Hdr\": \"127.0.0.1\",\n                        \"Base-Header\": \"base\",\n                        \"Base-Query\": \"base_query\",\n                        \"X-Res\": \"C\",\n                        \"X-Method\": \"PUT\"\n                    }\n                },\n                {\n                    \"status\": 202,\n                    \"body\":\"D\",\n                    \"headers\": {\n                        \"Base-Header\": \"base\",\n                        \"Base-Query\": \"base_query\",\n                        \"X-Res\": \"D\",\n                        \"X-Query-One\": \"thing\",\n                        \"X-Query-Conflict\": \"d_value\"\n                    }\n                }\n                ]]=],\n                {\n                    ConflictHeader = \"outer_header\",\n                    OuterConflict = \"outer_conflict\"\n                })\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.status = 200\n            ngx.header[\"Client-IP\"] = ngx.var.remote_addr\n            ngx.header[\"Base-Header\"] = ngx.req.get_headers()[\"Base-Header\"]\n            ngx.header[\"Base-Query\"] = ngx.var.arg_base\n            ngx.header[\"X-Header1\"] = ngx.req.get_headers()[\"Header1\"]\n            ngx.header[\"X-Header2\"] = ngx.req.get_headers()[\"Header2\"]\n            ngx.header[\"X-Conflict-Header\"] = ngx.req.get_headers()[\"ConflictHeader\"]\n            ngx.header[\"X-OuterConflict\"] = ngx.req.get_headers()[\"OuterConflict\"]\n            ngx.header[\"X-Res\"] = \"B\"\n            ngx.print(\"B\")\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n            ngx.header[\"Client-IP-From-Hdr\"] = ngx.req.get_headers()[\"X-Real-IP\"]\n            ngx.header[\"Base-Header\"] = ngx.req.get_headers()[\"Base-Header\"]\n            ngx.header[\"Base-Query\"] = ngx.var.arg_base\n            ngx.header[\"X-Res\"] = \"C\"\n            ngx.header[\"X-Method\"] = ngx.req.get_method()\n            ngx.print(\"C\")\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.status = 202\n            ngx.header[\"Base-Header\"] = ngx.req.get_headers()[\"Base-Header\"]\n            ngx.header[\"Base-Query\"] = ngx.var.arg_base\n            ngx.header[\"X-Query-One\"] = ngx.var.arg_one\n            ngx.header[\"X-Query-Conflict\"] = ngx.var.arg_conflict\n            ngx.header[\"X-Res\"] = \"D\"\n            ngx.print(\"D\")\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 3: missing pipeline\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"pipeline1\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: object matches none of the required: [\\\"pipeline\\\"]\"}\n\n\n\n=== TEST 4: timeout is not number\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": \"200\",\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: property \\\"timeout\\\" validation failed: wrong type: expected integer, got string\"}\n\n\n\n=== TEST 5: different response time\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 2000,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200\n                },\n                {\n                    \"status\": 201\n                },\n                {\n                    \"status\": 202\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.sleep(0.02)\n            ngx.status = 200\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.sleep(0.05)\n            ngx.status = 201\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.status = 202\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 6: last request timeout\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200\n                },\n                {\n                    \"status\": 201\n                },\n                {\n                    \"status\": 504,\n                    \"reason\": \"upstream timeout\"\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.status = 200\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.status = 202\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n--- error_log\ntimeout\n\n\n\n=== TEST 7: first request timeout\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 504,\n                    \"reason\": \"upstream timeout\"\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.status = 200\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.status = 202\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n--- error_log\ntimeout\n\n\n\n=== TEST 8: no body in request\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                nil,\n                nil\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"no request body, you should give at least one pipeline setting\"}\n\n\n\n=== TEST 9: invalid body\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                \"invalid json string\"\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid request body: invalid json string, err: Expected value but found invalid token at character 1\"}\n\n\n\n=== TEST 10: invalid pipeline's path\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"pipeline\":[\n                    {\n                        \"path\": \"\"\n                    }]\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: property \\\"pipeline\\\" validation failed: failed to validate item 1: property \\\"path\\\" validation failed: string too short, expected at least 1, got 0\"}\n\n\n\n=== TEST 11: invalid pipeline's method\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"pipeline\":[{\n                        \"path\": \"/c\",\n                        \"method\": \"put\"\n                    }]\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: property \\\"pipeline\\\" validation failed: failed to validate item 1: property \\\"method\\\" validation failed: matches none of the enum values\"}\n\n\n\n=== TEST 12: invalid pipeline's version\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"pipeline\":[{\n                        \"path\": \"/d\",\n                        \"version\":1.2\n                    }]\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: property \\\"pipeline\\\" validation failed: failed to validate item 1: property \\\"version\\\" validation failed: matches none of the enum values\"}\n\n\n\n=== TEST 13: invalid pipeline's ssl\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"pipeline\":[{\n                        \"path\": \"/d\",\n                        \"ssl_verify\":1.2\n                    }]\n                }]=]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: property \\\"pipeline\\\" validation failed: failed to validate item 1: property \\\"ssl_verify\\\" validation failed: wrong type: expected boolean, got number\"}\n\n\n\n=== TEST 14: invalid pipeline's number\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"pipeline\":[]\n                }]=]\n                )\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /aggregate\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"bad request body: property \\\"pipeline\\\" validation failed: expect array to have at least 1 items\"}\n\n\n\n=== TEST 15: when client body has been wrote to temp file\n--- config\n    client_body_in_file_only on;\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=],\n                [=[[\n                    {\n                        \"status\": 200\n                    },\n                    {\n                        \"status\": 201\n                    },\n                    {\n                        \"status\": 202\n                    }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.status = 200\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.status = 202\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 16: copy all header to every request except content\n--- config\n    client_body_in_file_only on;\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 1000,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=],\n                [=[[\n                    {\n                        \"status\": 200,\n                        \"headers\": {\n                            \"X-Cookie\": \"request-cookies-b\",\n                            \"X-HeaderB\": \"request-header-b\"\n                        }\n                    },\n                    {\n                        \"status\": 201,\n                        \"headers\": {\n                            \"X-Cookie\": \"request-cookies-c\",\n                            \"X-HeaderC\": \"request-header-c\"\n                        }\n                    },\n                    {\n                        \"status\": 202,\n                        \"headers\": {\n                            \"X-Cookie\": \"request-cookies-d\",\n                            \"X-HeaderD\": \"request-header-d\"\n                        }\n                    }\n                ]]=],\n                {\n                    Cookie = \"request-cookies\",\n                    OuterHeader = \"request-header\"\n                })\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.status = 200\n            ngx.header[\"X-Cookie\"] = ngx.req.get_headers()[\"Cookie\"] .. \"-b\"\n            ngx.header[\"X-HeaderB\"] = ngx.req.get_headers()[\"OuterHeader\"] .. \"-b\"\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n            ngx.header[\"X-Cookie\"] = ngx.req.get_headers()[\"Cookie\"] .. \"-c\"\n            ngx.header[\"X-HeaderC\"] = ngx.req.get_headers()[\"OuterHeader\"] .. \"-c\"\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.status = 202\n            ngx.header[\"X-Cookie\"] = ngx.req.get_headers()[\"Cookie\"] .. \"-d\"\n            ngx.header[\"X-HeaderD\"] = ngx.req.get_headers()[\"OuterHeader\"] .. \"-d\"\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 17: exceed default body limit size (check header)\n--- config\n    location = /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 (\"1234\"):rep(1024 * 1024)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 413\n--- response_body eval\nqr/\\{\"error_msg\":\"request size 4194304 is greater than the maximum size 1048576 allowed\"\\}/\n\n\n\n=== TEST 18: exceed default body limit size (check file size)\n--- request eval\n\"POST /apisix/batch-requests\n\" . (\"1000\\r\n\" . (\"11111111\" x 512) . \"\\r\n\") x 257 . \"0\\r\n\\r\n\"\n--- more_headers\nTransfer-Encoding: chunked\n--- error_code: 413\n--- response_body eval\nqr/\\{\"error_msg\":\"request size 1052672 is greater than the maximum size 1048576 allowed\"\\}/\n--- error_log\nattempt to read body from file\n\n\n\n=== TEST 19: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/batch-requests',\n                ngx.HTTP_PUT,\n                [[{\n                    \"max_body_size\": 2048\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: exceed body limit size\n--- config\n    location = /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 (\"1234\"):rep(1024)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 413\n--- response_body eval\nqr/\\{\"error_msg\":\"request size 4096 is greater than the maximum size 2048 allowed\"\\}/\n\n\n\n=== TEST 21: exceed body limit size (expected)\n--- config\n    location = /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, res_data = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 (\"1234\"):rep(1024),\n                 nil,\n                 {EXPECT = \"100-CONTINUE\", [\"content-length\"] = 4096}\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 413\n--- response_body eval\nqr/\\{\"error_msg\":\"request size 4096 is greater than the maximum size 2048 allowed\"\\}/\n\n\n\n=== TEST 22: don't exceed body limit size\n--- config\n    location = /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"headers\": {\n                        \"Base-Header\": \"base\"\n                    },\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/a\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    }\n                    ]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200,\n                    \"body\":\"A\",\n                    \"headers\": {\n                        \"Base-Header\": \"base\",\n                        \"X-Res\": \"a\",\n                        \"X-Header1\": \"hello\",\n                        \"X-Header2\": \"world\"\n                    }\n                }\n                ]]=])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n    location = /a {\n        content_by_lua_block {\n            ngx.status = 200\n            ngx.header[\"Base-Header\"] = ngx.req.get_headers()[\"Base-Header\"]\n            ngx.header[\"X-Header1\"] = ngx.req.get_headers()[\"Header1\"]\n            ngx.header[\"X-Header2\"] = ngx.req.get_headers()[\"Header2\"]\n            ngx.header[\"X-Res\"] = \"a\"\n            ngx.print(\"A\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: invalid body size\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/batch-requests',\n                ngx.HTTP_PUT,\n                [[{\n                    \"max_body_size\": 0\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid configuration: property \\\\\"max_body_size\\\\\" validation failed: expected 0 to be greater than 0\"\\}/\n\n\n\n=== TEST 24: keep environment clean\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/batch-requests',\n                ngx.HTTP_PUT,\n                [[{\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/batch-requests2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - public-api\n    - batch-requests\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/batch-requests\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: customize uri, not found\n--- yaml_config\nplugin_attr:\n    batch-requests:\n        uri: \"/foo/bar\"\n--- config\n    location = /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/a\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /a {\n        content_by_lua_block {\n            ngx.status = 200\n        }\n    }\n--- error_code: 404\n\n\n\n=== TEST 3: create public API route for custom uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/foo/bar\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: customize uri, found\n--- yaml_config\nplugin_attr:\n    batch-requests:\n        uri: \"/foo/bar\"\n--- config\n    location = /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/foo/bar',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/b\",\n                        \"headers\": {\n                            \"Header1\": \"hello\",\n                            \"Header2\": \"world\"\n                        }\n                    },{\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    },{\n                        \"path\": \"/d\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200\n                },\n                {\n                    \"status\": 201\n                },\n                {\n                    \"status\": 202\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /b {\n        content_by_lua_block {\n            ngx.status = 200\n        }\n    }\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n        }\n    }\n    location = /d {\n        content_by_lua_block {\n            ngx.status = 202\n        }\n    }\n\n\n\n=== TEST 5: customize uri, missing plugin, use default\n--- yaml_config\nplugin_attr:\n    x:\n      uri: \"/foo/bar\"\n--- config\n    location = /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/a\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /a {\n        content_by_lua_block {\n            ngx.status = 200\n        }\n    }\n\n\n\n=== TEST 6: customize uri, missing attr, use default\n--- yaml_config\nplugin_attr:\n    batch-requests:\n        xyz: \"/foo/bar\"\n--- config\n    location = /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                ngx.HTTP_POST,\n                [=[{\n                    \"timeout\": 100,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/a\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 200\n                }\n                ]]=]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /a {\n        content_by_lua_block {\n            ngx.status = 200\n        }\n    }\n\n\n\n=== TEST 7: ensure real ip header is overridden\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"headers\": {\n                        \"x-real-ip\": \"127.0.0.2\"\n                    },\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/c\",\n                        \"method\": \"PUT\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 201,\n                    \"body\":\"C\",\n                    \"headers\": {\n                        \"Client-IP\": \"127.0.0.1\",\n                        \"Client-IP-From-Hdr\": \"127.0.0.1\"\n                    }\n                }\n                ]]=])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n            ngx.header[\"Client-IP\"] = ngx.var.remote_addr\n            ngx.header[\"Client-IP-From-Hdr\"] = ngx.req.get_headers()[\"x-real-ip\"]\n            ngx.print(\"C\")\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 8: ensure real ip header is overridden, header from the pipeline\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"headers\": {\n                    },\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/c\",\n                        \"headers\": {\n                            \"x-real-ip\": \"127.0.0.2\"\n                        },\n                        \"method\": \"PUT\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 201,\n                    \"body\":\"C\",\n                    \"headers\": {\n                        \"Client-IP\": \"127.0.0.1\",\n                        \"Client-IP-From-Hdr\": \"127.0.0.1\"\n                    }\n                }\n                ]]=])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n            ngx.header[\"Client-IP\"] = ngx.var.remote_addr\n            ngx.header[\"Client-IP-From-Hdr\"] = ngx.req.get_headers()[\"x-real-ip\"]\n            ngx.print(\"C\")\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 9: ensure real ip header is overridden, header has underscore\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"headers\": {\n                    },\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/c\",\n                        \"headers\": {\n                            \"x_real-ip\": \"127.0.0.2\"\n                        },\n                        \"method\": \"PUT\"\n                    }]\n                }]=],\n                [=[[\n                {\n                    \"status\": 201,\n                    \"body\":\"C\",\n                    \"headers\": {\n                        \"Client-IP\": \"127.0.0.1\",\n                        \"Client-IP-From-Hdr\": \"127.0.0.1\"\n                    }\n                }\n                ]]=])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n\n    location = /c {\n        content_by_lua_block {\n            ngx.status = 201\n            ngx.header[\"Client-IP\"] = ngx.var.remote_addr\n            ngx.header[\"Client-IP-From-Hdr\"] = ngx.req.get_headers()[\"x-real-ip\"]\n            ngx.print(\"C\")\n        }\n    }\n--- request\nGET /aggregate\n--- response_body\npassed\n\n\n\n=== TEST 10: ensure the content-type is correct\n--- request\nPOST /apisix/batch-requests\n{\n    \"headers\": {\n    },\n    \"pipeline\":[\n        {\n            \"path\": \"/c\",\n            \"method\": \"PUT\"\n        }\n    ]\n}\n--- response_headers\nContent-Type: application/json\n\n\n\n=== TEST 11: Ensure sub_responses count matches sub_requests on timed out sub_request (contains no empty json object like '{}' in batch response)\n--- config\n    location = /aggregate {\n        content_by_lua_block {\n            local cjson = require(\"cjson.safe\")\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local _, _, batch_responses_str = t('/apisix/batch-requests',\n                 ngx.HTTP_POST,\n                 [=[{\n                    \"headers\": {\n                    },\n                    \"timeout\": 200,\n                    \"pipeline\":[\n                    {\n                        \"path\": \"/ok\",\n                        \"method\": \"GET\"\n                    },{\n                      \"path\": \"/timeout\",\n                      \"method\": \"GET\"\n                    }]\n                }]=])\n            local batch_responses = cjson.decode(batch_responses_str)\n            -- there are expected to be only 2 responses in batch_responses\n            for idx, response in ipairs(batch_responses) do\n                ngx.say(idx, \"th response code: \", response.status)\n            end\n        }\n    }\n\n    location = /ok {\n        content_by_lua_block {\n            ngx.print(\"ok\")\n        }\n    }\n    location = /timeout {\n        content_by_lua_block {\n            ngx.sleep(1)\n            ngx.print(\"timeout\")\n        }\n    }\n--- request\nGET /aggregate\n--- error_log\ntimeout\n--- response_body\n1th response code: 200\n2th response code: 504\n"
  },
  {
    "path": "t/plugin/body-transformer-multipart.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: multipart request body to json request body conversion\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"{\\\"foo\\\":\\\"{{name .. \\\" world\\\"}}\\\",\\\"bar\\\":{{age+10}}}\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local body = ([[\n--AaB03x\nContent-Disposition: form-data; name=\"name\"\n\nLarry\n--AaB03x\nContent-Disposition: form-data; name=\"age\"\n\n10\n--AaB03x--]])\n\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"multipart/related; boundary=AaB03x\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n\n            ngx.status = res.status\n            ngx.say(res.body or res.reason)\n        }\n    }\n--- response_body\n{\"foo\":\"Larry world\",\"bar\":20}\n\n\n\n=== TEST 2: multipart response body to json response body conversion\n--- config\n    location /demo {\n        content_by_lua_block {\n            ngx.header[\"Content-Type\"] = \"multipart/related; boundary=AaB03x\"\n            ngx.say([[\n--AaB03x\nContent-Disposition: form-data; name=\"name\"\n\nLarry\n--AaB03x\nContent-Disposition: form-data; name=\"age\"\n\n10\n--AaB03x--]])\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"response\": {\n                                \"template\": \"{\\\"foo\\\":\\\"{{name .. \\\" world\\\"}}\\\",\\\"bar\\\":{{age+10}}}\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1984\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local opt = {method = \"GET\"}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n\n            ngx.status = res.status\n            ngx.say(res.body or res.reason)\n        }\n    }\n--- response_body\n{\"foo\":\"Larry world\",\"bar\":20}\n\n\n\n=== TEST 3: multipart parse result accessible to template renderer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n\n            local req_template = ngx.encode_base64[[\n                {%\n                    local core = require 'apisix.core'\n                    local cjson = require 'cjson'\n                    if tonumber(context.age) > 18 then\n                        context._multipart:set_simple(\"status\", \"major\")\n                    else\n                        context._multipart:set_simple(\"status\", \"minor\")\n                    end\n                    local body = context._multipart:tostring()\n                %}{* body *}\n            ]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"body-transformer\": {\n                            \"response\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]], req_template)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            ------------------------#######################-------------------\n\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local body_minor = ([[\n--AaB03x\nContent-Disposition: form-data; name=\"name\"\n\nLarry\n--AaB03x\nContent-Disposition: form-data; name=\"age\"\n\n10\n--AaB03x--]])\n\n\n            local opt = {method = \"POST\", body = body_minor, headers = {[\"Content-Type\"] = \"multipart/related; boundary=AaB03x\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n\n            ngx.say(res.body)\n\n        }\n    }\n--- response_body eval\nqr/.*Content-Disposition: form-data; name=\\\"status\\\"\\r\\n\\r\\nminor.*/\n\n\n\n=== TEST 4: multipart parse response accessible to template renderer (test with age == 19)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local body_major = ([[\n--AaB03x\nContent-Disposition: form-data; name=\"name\"\n\nLarry\n--AaB03x\nContent-Disposition: form-data; name=\"age\"\n\n19\n--AaB03x--]])\n\n\n            local opt = {method = \"POST\", body = body_major, headers = {[\"Content-Type\"] = \"multipart/related; boundary=AaB03x\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n\n            ngx.say(res.body)\n\n        }\n    }\n--- response_body eval\nqr/.*Content-Disposition: form-data; name=\\\"status\\\"\\r\\n\\r\\nmajor.*/\n"
  },
  {
    "path": "t/plugin/body-transformer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: simulate simple SOAP proxy\n--- config\nlocation /demo {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local body = core.request.get_body()\n        local xml2lua = require(\"xml2lua\")\n        local xmlhandler = require(\"xmlhandler.tree\")\n        local handler = xmlhandler:new()\n        local parser = xml2lua.parser(handler)\n        parser:parse(body)\n\n        ngx.print(string.format([[\n<SOAP-ENV:Envelope\n    xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <SOAP-ENV:Header/>\n    <SOAP-ENV:Body>\n        <ns2:getCountryResponse\n            xmlns:ns2=\"http://spring.io/guides/gs-producing-web-service\">\n            <ns2:country>\n                <ns2:name>%s</ns2:name>\n                <ns2:population>46704314</ns2:population>\n                <ns2:capital>Madrid</ns2:capital>\n                <ns2:currency>EUR</ns2:currency>\n            </ns2:country>\n        </ns2:getCountryResponse>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n        ]], handler.root[\"soap-env:Envelope\"][\"soap-env:Body\"][\"ns0:getCountryRequest\"][\"ns0:name\"]))\n    }\n}\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local req_template = ngx.encode_base64[[\n<?xml version=\"1.0\"?>\n<soap-env:Envelope xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <soap-env:Body>\n  <ns0:getCountryRequest xmlns:ns0=\"http://spring.io/guides/gs-producing-web-service\">\n   <ns0:name>{{_escape_xml(name)}}</ns0:name>\n  </ns0:getCountryRequest>\n </soap-env:Body>\n</soap-env:Envelope>\n            ]]\n\n            local rsp_template = ngx.encode_base64[[\n{% if Envelope.Body.Fault == nil then %}\n{\n   \"status\":\"{{_ctx.var.status}}\",\n   \"currency\":\"{{Envelope.Body.getCountryResponse.country.currency}}\",\n   \"population\":{{Envelope.Body.getCountryResponse.country.population}},\n   \"capital\":\"{{Envelope.Body.getCountryResponse.country.capital}}\",\n   \"name\":\"{{Envelope.Body.getCountryResponse.country.name}}\"\n}\n{% else %}\n{\n   \"message\":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},\n   \"code\":\"{{Envelope.Body.Fault.faultcode}}\"\n   {% if Envelope.Body.Fault.faultactor ~= nil then %}\n   , \"actor\":\"{{Envelope.Body.Fault.faultactor}}\"\n   {% end %}\n}\n{% end %}\n            ]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/ws\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            },\n                            \"response\": {\n                                \"input_format\": \"xml\",\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template, rsp_template, ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ws\"\n            local body = [[{\"name\": \"Spain\"}]]\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/json\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n            local data1 = core.json.decode(res.body)\n            local data2 = core.json.decode[[{\"status\":\"200\",\"currency\":\"EUR\",\"population\":46704314,\"capital\":\"Madrid\",\"name\":\"Spain\"}]]\n            assert(core.json.stably_encode(data1) == core.json.stably_encode(data2))\n        }\n    }\n\n\n\n=== TEST 2: test JSON-to-JSON\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local data = core.json.decode(body)\n            assert(data.foo == \"hello world\" and data.bar == 30)\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local body = [[{\"name\":\"hello\",\"age\":20}]]\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/json\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 3: specify wrong input_format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"input_format\": \"xml\",\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local body = [[{\"name\":\"hello\",\"age\":20}]]\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/json\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 400)\n        }\n    }\n--- error_log\nError Parsing XML\n\n\n\n=== TEST 4: invalid reference in template\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\"foo\":\"{{name() .. \" world\"}}\",\"bar\":{{age+10}}}]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local body = [[{\"name\":\"hello\",\"age\":20}]]\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/json\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 503)\n        }\n    }\n--- grep_error_log eval\nqr/transform\\(\\): request template rendering:.*/\n--- grep_error_log_out eval\nqr/attempt to call global 'name' \\(a string value\\)/\n\n\n\n=== TEST 5: generate request body from scratch\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local data = core.json.decode(body)\n            assert(data.foo == \"hello world\")\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\n                \"foo\":\"{{_ctx.var.arg_name .. \" world\"}}\"\n            }]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\",\n                            \"method\": \"POST\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar?name=hello\"\n            local opt = {method = \"GET\"}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 6: html escape in template\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local data = core.json.decode(body)\n            if (data == nil) or (data.agent:find(\"ngx_lua/\", 0, true) == nil) then\n                return ngx.exit(400)\n            end\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            -- html escape would escape '/' to '&#47' in string, which may be unexpected.\n            -- 'lua-resty-http/0.16.1 (Lua) ngx_lua/10021'\n            -- would be escaped into\n            -- 'lua-resty-http&#47;0.16.1 (Lua) ngx_lua&#47;10021'\n            local req_template = [[{\n                \"agent\":\"{{_ctx.var.http_user_agent}}\"\n            }]]\n            local admin_body = [[{\n                \"uri\": \"/foobar\",\n                \"plugins\": {\n                    \"proxy-rewrite\": {\n                        \"uri\": \"/demo\",\n                        \"method\": \"POST\"\n                    },\n                    \"body-transformer\": {\n                        \"request\": {\n                            \"template\": \"%s\"\n                        }\n                    }\n                },\n                \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:%d\": 1\n                    }\n                }\n            }]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format(admin_body, req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar?name=hello\"\n            local opt = {method = \"GET\"}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 400)\n\n            -- disable html escape, now it's ok\n            local req_template = [[{\"agent\":\"{*_ctx.var.http_user_agent*}\"}]]\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format(admin_body, req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 7: parse body in yaml format\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local data = core.json.decode(body)\n            if data == nil or data.foobar ~= \"hello world\" then\n                return ngx.exit(400)\n            end\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[\n            {%\n                local yaml = require(\"lyaml\")\n                local body = yaml.load(_body)\n            %}\n            {\"foobar\":\"{{body.foobar.foo .. \" \" .. body.foobar.bar}}\"}\n            ]]\n            local admin_body = [[{\n                \"uri\": \"/foobar\",\n                \"plugins\": {\n                    \"proxy-rewrite\": {\n                        \"uri\": \"/demo\",\n                        \"method\": \"POST\"\n                    },\n                    \"body-transformer\": {\n                        \"request\": {\n                            \"template\": \"%s\"\n                        }\n                    }\n                },\n                \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:%d\": 1\n                    }\n                }\n            }]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format(admin_body, req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local body = [[\nfoobar:\n  foo: hello\n  bar: world\n            ]]\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local opt = {method = \"POST\", body = body}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 8: test _escape_json\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local data = core.json.decode(body)\n            if data == nil or data.foobar ~= [[hello \"world\"]] then\n                return ngx.exit(400)\n            end\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\"foobar\":{*_escape_json(name)*}}]]\n            local admin_body = [[{\n                \"uri\": \"/foobar\",\n                \"plugins\": {\n                    \"proxy-rewrite\": {\n                        \"uri\": \"/demo\",\n                        \"method\": \"POST\"\n                    },\n                    \"body-transformer\": {\n                        \"request\": {\n                            \"input_format\": \"json\",\n                            \"template\": \"%s\"\n                        }\n                    }\n                },\n                \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:%d\": 1\n                    }\n                }\n            }]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format(admin_body, req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local body = [[{\"name\":\"hello \\\"world\\\"\"}]]\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local opt = {method = \"POST\", body = body}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 9: test _escape_xml\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local xml2lua = require(\"xml2lua\")\n            local xmlhandler = require(\"xmlhandler.tree\")\n            local handler = xmlhandler:new()\n            local parser = xml2lua.parser(handler)\n            parser:parse(body)\n            assert(handler.root.foobar == \"<nil>\")\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[<foobar>{*_escape_xml(name)*}</foobar>]]\n            local admin_body = [[{\n                \"uri\": \"/foobar\",\n                \"plugins\": {\n                    \"proxy-rewrite\": {\n                        \"uri\": \"/demo\",\n                        \"method\": \"POST\"\n                    },\n                    \"body-transformer\": {\n                        \"request\": {\n                            \"input_format\": \"json\",\n                            \"template\": \"%s\"\n                        }\n                    }\n                },\n                \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:%d\": 1\n                    }\n                }\n            }]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format(admin_body, req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local body = [[{\"name\":\"<nil>\"}]]\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local opt = {method = \"POST\", body = body}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 10: cooperation of proxy-cache plugin\n--- http_config\nlua_shared_dict memory_cache 50m;\n--- config\nlocation /demo {\n    content_by_lua_block {\n        ngx.say([[\n    <SOAP-ENV:Envelope\n        xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <SOAP-ENV:Header/>\n        <SOAP-ENV:Body>\n            <ns2:CapitalCityResponse\n                xmlns:ns2=\"http://spring.io/guides/gs-producing-web-service\">\n                <ns2:CapitalCityResult>hello</ns2:CapitalCityResult>\n            </ns2:CapitalCityResponse>\n        </SOAP-ENV:Body>\n    </SOAP-ENV:Envelope>\n        ]])\n    }\n}\n\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local req_template = ngx.encode_base64[[\n                <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:web=\"http://www.oorsprong.org/websamples.countryinfo\">\n                <soapenv:Header/>\n                <soapenv:Body>\n                    <web:CapitalCity>\n                    <web:sCountryISOCode>{{_escape_xml(country)}}</web:sCountryISOCode>\n                    </web:CapitalCity>\n                </soapenv:Body>\n                </soapenv:Envelope>\n            ]]\n\n            local rsp_template = ngx.encode_base64[[\n                {\"result\": {*_escape_json(Envelope.Body.CapitalCityResponse.CapitalCityResult)*}}\n                ]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/capital\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"set\": {\n                                \"Accept-Encoding\": \"identity\",\n                                \"Content-Type\": \"text/xml\"\n                            },\n                            \"uri\": \"/demo\"\n                        },\n                        \"proxy-cache\":{\n                            \"cache_strategy\": \"memory\",\n                            \"cache_bypass\": [\"$arg_bypass\"],\n                            \"cache_http_status\": [200],\n                            \"cache_key\": [\"$uri\", \"-cache-id\"],\n                            \"cache_method\": [\"POST\"],\n                            \"hide_cache_headers\": true,\n                            \"no_cache\": [\"$arg_test\"],\n                            \"cache_zone\": \"memory_cache\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"input_format\": \"json\",\n                                \"template\": \"%s\"\n                            },\n                            \"response\": {\n                                \"input_format\": \"xml\",\n                                \"template\": \"%s\"\n                            }\n                        },\n                        \"response-rewrite\":{\n                            \"headers\": {\n                                \"set\": {\n                                    \"Content-Type\": \"application/json\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template, rsp_template, ngx.var.server_port)\n            )\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/capital\"\n            local body = [[{\"country\": \"foo\"}]]\n            local opt = {method = \"POST\", body = body}\n            local httpc = http.new()\n\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n            local data = core.json.decode(res.body)\n            assert(data.result == \"hello\")\n            assert(res.headers[\"Apisix-Cache-Status\"] == \"MISS\")\n\n            local res2 = httpc:request_uri(uri, opt)\n            assert(res2.status == 200)\n            local data2 = core.json.decode(res2.body)\n            assert(data2.result == \"hello\")\n            assert(res2.headers[\"Apisix-Cache-Status\"] == \"HIT\")\n        }\n    }\n\n\n\n=== TEST 11: return raw body with _body anytime\n--- http_config\n--- config\n    location /demo {\n        content_by_lua_block {\n            ngx.header.content_type = \"application/json\"\n            ngx.print('{\"result\": \"hello world\"}')\n        }\n    }\n\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local rsp_template = ngx.encode_base64[[\n                {\"raw_body\": {*_escape_json(_body)*}, \"result\": {*_escape_json(result)*}}\n                ]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/capital\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"response\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], rsp_template, ngx.var.server_port)\n            )\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/capital\"\n            local opt = {method = \"GET\", headers = {[\"Content-Type\"] = \"application/json\"}}\n            local httpc = http.new()\n\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n            local data = core.json.decode(res.body)\n            assert(data.result == \"hello world\")\n            assert(data.raw_body == '{\"result\": \"hello world\"}')\n        }\n    }\n\n\n\n=== TEST 12: empty xml value should be rendered as empty string\n--- config\n    location /demo {\n        content_by_lua_block {\n            ngx.print([[\n    <SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xrd=\"http://x-road.eu/xsd/xroad.xsd\" xmlns:prod=\"http://rr.x-road.eu/producer\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:id=\"http://x-road.eu/xsd/identifiers\" xmlns:repr=\"http://x-road.eu/xsd/representation.xsd\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\">\n      <SOAP-ENV:Body>\n        <prod:RR58isikEpiletResponse>\n          <request><Isikukood>33333333333</Isikukood></request>\n          <response>\n            <Isikukood>33333333333</Isikukood>\n            <KOVKood></KOVKood>\n          </response>\n        </prod:RR58isikEpiletResponse>\n      </SOAP-ENV:Body>\n    </SOAP-ENV:Envelope>\n            ]])\n        }\n    }\n\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local rsp_template = ngx.encode_base64[[\n{ \"KOVKood\":\"{{Envelope.Body.RR58isikEpiletResponse.response.KOVKood}}\" }\n            ]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/ws\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"response\": {\n                                \"input_format\": \"xml\",\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], rsp_template, ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/ws\"\n            local opt = {method = \"GET\"}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n            local data1 = core.json.decode(res.body)\n            local data2 = core.json.decode[[{\"KOVKood\":\"\"}]]\n            assert(core.json.stably_encode(data1) == core.json.stably_encode(data2))\n        }\n    }\n\n\n\n=== TEST 13: test x-www-form-urlencoded to JSON\n--- config\n    location /demo {\n      content_by_lua_block {\n          local core = require(\"apisix.core\")\n          local body = core.request.get_body()\n          local data = core.json.decode(body)\n          assert(data.foo == \"hello world\" and data.bar == 30)\n      }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}]]\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local data = {name = \"hello\", age = 20}\n            local body = ngx.encode_args(data)\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/x-www-form-urlencoded\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 14: test get request  to JSON\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            local data = core.json.decode(body)\n            assert(data.foo == \"hello world\" and data.bar == 30)\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = [[{\"foo\":\"{{name .. \" world\"}}\",\"bar\":{{age+10}}}]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\" .. \"?name=hello&age=20\"\n            local opt = {method = \"GET\"}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 15: test input is in base64-encoded urlencoded format\n--- config\n    location /demo {\n      content_by_lua_block {\n          local core = require(\"apisix.core\")\n          local body = core.request.get_body()\n          local data = ngx.decode_args(body)\n          assert(data.foo == \"hello world\" and data.bar == \"30\")\n      }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local req_template = ngx.encode_base64[[foo={{name .. \" world\"}}&bar={{age+10}}]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template_is_base64\": true,\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], req_template:gsub('\"', '\\\\\"'), ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local data = {name = \"hello\", age = 20}\n            local body = ngx.encode_args(data)\n            local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/x-www-form-urlencoded\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n        }\n    }\n\n\n\n=== TEST 16: test for missing Content-Type and skip body parsing\n--- config\n    location /demo {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local body = core.request.get_body()\n            assert(body == \"{\\\"message\\\": \\\"actually json\\\"}\")\n        }\n    }\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/demo\"\n                        },\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"input_format\": \"plain\",\n                                \"template\": \"{\\\"message\\\": \\\"{* string.gsub(_body, 'not ', '') *}\\\"}\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:%d\": 1\n                        }\n                    }\n                }]], ngx.var.server_port)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\", {\n                method = \"POST\",\n                body = \"not actually json\",\n            })\n            assert(res.status == 200)\n        }\n    }\n--- no_error_log\nno input format to parse\n"
  },
  {
    "path": "t/plugin/body-transformer2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: body transformer with decoded body (keyword: context)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n\n            local req_template = ngx.encode_base64[[\n                {%\n                    local core = require 'apisix.core'\n                    local cjson = require 'cjson'\n                    context.name = \"bar\"\n                    context.address = nil\n                    context.age = context.age + 1\n                    local body = core.json.encode(context)\n                %}{* body *}\n            ]]\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                string.format([[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"%s\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]], req_template)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: verify the transformed body\n--- request\nPOST /echo\n{\"name\": \"foo\", \"address\":\"LA\", \"age\": 18}\n-- response_body\n{\"name\": \"bar\", \"age\": 19}\n\n\n\n=== TEST 3: body transformer plugin with key-auth that fails\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local core = require(\"apisix.core\")\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/foobar\",\n                    \"plugins\": {\n                        \"body-transformer\": {\n                            \"request\": {\n                                \"template\": \"some-template\"\n                            }\n                        },\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.sleep(0.5)\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/foobar\"\n            local opt = {method = \"POST\", body = \"body\", headers = {[\"Content-Type\"] = \"application/json\"}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 401)\n            ngx.say(res.reason)\n        }\n    }\n--- response_body\nUnauthorized\n"
  },
  {
    "path": "t/plugin/brotli.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - brotli\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: hit, single Accept-Encoding\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary:\n\n\n\n=== TEST 3: hit, single wildcard Accept-Encoding\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: *\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary:\n\n\n\n=== TEST 4: not hit, single Accept-Encoding\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/html\n--- response_headers\nVary:\n\n\n\n=== TEST 5: hit, br in multi Accept-Encoding\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip, br\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary:\n\n\n\n=== TEST 6: hit, no br in multi Accept-Encoding, but wildcard\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip, *\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary:\n\n\n\n=== TEST 7: not hit, no br in multi Accept-Encoding\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip, deflate\nContent-Type: text/html\n--- response_headers\nVary:\n\n\n\n=== TEST 8: hit, multi Accept-Encoding with quality\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip;q=0.5, br;q=0.6\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary:\n\n\n\n=== TEST 9: not hit, multi Accept-Encoding with quality and disable br\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip;q=0.5, br;q=0\nContent-Type: text/html\n--- response_headers\nVary:\n\n\n\n=== TEST 10: hit, multi Accept-Encoding with quality and wildcard\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip;q=0.8, deflate, sdch;q=0.6, *;q=0.1\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary:\n\n\n\n=== TEST 11: default buffers and compress level\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.brotli\")\n            local core = require(\"apisix.core\")\n            local json = require(\"toolkit.json\")\n\n            for _, conf in ipairs({\n                {},\n                {mode = 1},\n                {comp_level = 5},\n                {comp_level = 5, lgwin = 12},\n                {comp_level = 5, lgwin = 12, vary = true},\n                {comp_level = 5, lgwin = 12, lgblock = 16, vary = true},\n                {mode = 2, comp_level = 5, lgwin = 12, lgblock = 16, vary = true},\n            }) do\n                local ok, err = plugin.check_schema(conf)\n                if not ok then\n                    ngx.say(err)\n                    return\n                end\n                ngx.say(json.encode(conf))\n            end\n        }\n    }\n--- response_body\n{\"comp_level\":6,\"http_version\":1.1,\"lgblock\":0,\"lgwin\":19,\"min_length\":20,\"mode\":0,\"types\":[\"text/html\"]}\n{\"comp_level\":6,\"http_version\":1.1,\"lgblock\":0,\"lgwin\":19,\"min_length\":20,\"mode\":1,\"types\":[\"text/html\"]}\n{\"comp_level\":5,\"http_version\":1.1,\"lgblock\":0,\"lgwin\":19,\"min_length\":20,\"mode\":0,\"types\":[\"text/html\"]}\n{\"comp_level\":5,\"http_version\":1.1,\"lgblock\":0,\"lgwin\":12,\"min_length\":20,\"mode\":0,\"types\":[\"text/html\"]}\n{\"comp_level\":5,\"http_version\":1.1,\"lgblock\":0,\"lgwin\":12,\"min_length\":20,\"mode\":0,\"types\":[\"text/html\"],\"vary\":true}\n{\"comp_level\":5,\"http_version\":1.1,\"lgblock\":16,\"lgwin\":12,\"min_length\":20,\"mode\":0,\"types\":[\"text/html\"],\"vary\":true}\n{\"comp_level\":5,\"http_version\":1.1,\"lgblock\":16,\"lgwin\":12,\"min_length\":20,\"mode\":2,\"types\":[\"text/html\"],\"vary\":true}\n\n\n\n=== TEST 12: compress level\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/echo\",\n                    \"vars\": [[\"http_x\", \"==\", \"1\"]],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"comp_level\": 0\n                        }\n                    }\n                }]=]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            return\n        end\n\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [=[{\n                \"uri\": \"/echo\",\n                \"vars\": [[\"http_x\", \"==\", \"2\"]],\n                \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    }\n                },\n                \"plugins\": {\n                    \"brotli\": {\n                        \"comp_level\": 11\n                    }\n                }\n            }]=]\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            return\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 13: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/echo\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {method = \"POST\", headers = {x = \"1\"}, body = (\"0123\"):rep(1024)})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local less_compressed = res.body\n            local res, err = httpc:request_uri(uri,\n                {method = \"POST\", headers = {x = \"2\"}, body = (\"0123\"):rep(1024)})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            if #less_compressed < 4096 and #less_compressed < #res.body then\n                ngx.say(\"ok\")\n            end\n        }\n    }\n--- response_body\nok\n\n\n\n=== TEST 14: min length\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"min_length\": 21\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 15: not hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/html\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 16: http version\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"http_version\": 1.1\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 17: not hit\n--- request\nPOST /echo HTTP/1.0\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/html\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 18: hit again\n--- request\nPOST /echo HTTP/1.1\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\n\n\n\n=== TEST 19: types\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"types\": [\"text/plain\", \"text/xml\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 20: not hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/html\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 21: hit again\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/xml\n--- response_headers\nContent-Encoding: br\n\n\n\n=== TEST 22: hit with charset\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: text/plain; charset=UTF-8\n--- response_headers\nContent-Encoding: br\n\n\n\n=== TEST 23: match all types\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"types\": \"*\"\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 24: hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nContent-Type: video/3gpp\n--- response_headers\nContent-Encoding: br\n\n\n\n=== TEST 25: vary\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"vary\": true\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 26: hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: br\nVary: upstream\nContent-Type: text/html\n--- response_headers\nContent-Encoding: br\nVary: upstream, Accept-Encoding\n\n\n\n=== TEST 27: schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {input = {\n                    types = {}\n                }},\n                {input = {\n                    min_length = 0\n                }},\n                {input = {\n                    mode = 4\n                }},\n                {input = {\n                    comp_level = 12\n                }},\n                {input = {\n                    http_version = 2\n                }},\n                {input = {\n                    lgwin = 100\n                }},\n                {input = {\n                    lgblock = 8\n                }},\n                {input = {\n                    vary = 0\n                }}\n            }) do\n                local code, body = t('/apisix/admin/global_rules/1',\n                    ngx.HTTP_PUT,\n                    {\n                        id = \"1\",\n                        plugins = {\n                            [\"brotli\"] = case.input\n                        }\n                    }\n                )\n                ngx.print(body)\n            end\n    }\n}\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"types\\\" validation failed: object matches none of the required\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"min_length\\\" validation failed: expected 0 to be at least 1\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"mode\\\" validation failed: expected 4 to be at most 2\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"comp_level\\\" validation failed: expected 12 to be at most 11\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"http_version\\\" validation failed: matches none of the enum values\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"lgwin\\\" validation failed: matches none of the enum values\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"lgblock\\\" validation failed: matches none of the enum values\"}\n{\"error_msg\":\"failed to check the configuration of plugin brotli err: property \\\"vary\\\" validation failed: wrong type: expected boolean, got number\"}\n\n\n\n=== TEST 28: body checksum\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"types\": \"*\"\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 29: hit - decompressed respone body same as requset body\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/echo\"\n            local httpc = http.new()\n            local req_body = (\"abcdf01234\"):rep(1024)\n            local res, err = httpc:request_uri(uri,\n                {method = \"POST\", headers = {[\"Accept-Encoding\"] = \"br\"}, body = req_body})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            local brotli = require \"brotli\"\n            local decompressor = brotli.decompressor:new()\n            local chunk = decompressor:decompress(res.body)\n            local chunk_fin = decompressor:finish()\n            local chunks = chunk .. chunk_fin\n            if #chunks == #req_body then\n                ngx.say(\"ok\")\n            end\n        }\n    }\n--- response_body\nok\n\n\n\n=== TEST 30: mock upstream compressed response\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/mock_compressed_upstream_response\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"brotli\": {\n                            \"types\": \"*\"\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 31: hit - skip brotli compression of compressed upstream response\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/mock_compressed_upstream_response\"\n            local httpc = http.new()\n            local req_body = (\"abcdf01234\"):rep(1024)\n            local res, err = httpc:request_uri(uri,\n                {method = \"POST\", headers = {[\"Accept-Encoding\"] = \"gzip, br\"}, body = req_body})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            if res.headers[\"Content-Encoding\"] == 'gzip' then\n                ngx.say(\"ok\")\n            end\n        }\n    }\n--- request\nGET /t\n--- more_headers\nAccept-Encoding: gzip, br\nVary: upstream\nContent-Type: text/html\n--- response_body\nok\n"
  },
  {
    "path": "t/plugin/cas-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('warn');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: Add route for sp1\n--- config\n    location /t {\n        content_by_lua_block {\n            local kc = require(\"lib.keycloak_cas\")\n            local core = require(\"apisix.core\")\n\n            local default_opts = kc.get_default_opts()\n            local opts = core.table.deepcopy(default_opts)\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/cas1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\", \"POST\"],\n                        \"host\" : \"127.0.0.1\",\n                        \"plugins\": {\n                            \"cas-auth\": ]] .. core.json.encode(opts) .. [[\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: login and logout ok\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local kc = require \"lib.keycloak_cas\"\n\n            local path = \"/uri\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            local username = \"test\"\n            local password = \"test\"\n\n            local res, err, cas_cookie, keycloak_cookie = kc.login_keycloak(uri .. path, username, password)\n            if err or res.headers['Location'] ~= path then\n                ngx.log(ngx.ERR, err)\n                ngx.exit(500)\n            end\n            res, err = httpc:request_uri(uri .. res.headers['Location'], {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = cas_cookie\n                }\n            })\n            assert(res.status == 200)\n            ngx.say(res.body)\n\n            res, err = kc.logout_keycloak(uri .. \"/logout\", cas_cookie, keycloak_cookie)\n            assert(res.status == 200)\n        }\n    }\n--- response_body_like\nuri: /uri\ncookie: .*\nhost: 127.0.0.1:1984\nuser-agent: .*\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 3: Add route for sp2\n--- config\n    location /t {\n        content_by_lua_block {\n            local kc = require(\"lib.keycloak_cas\")\n            local core = require(\"apisix.core\")\n\n            local default_opts = kc.get_default_opts()\n            local opts = core.table.deepcopy(default_opts)\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/cas2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\", \"POST\"],\n                        \"host\" : \"127.0.0.2\",\n                        \"plugins\": {\n                            \"cas-auth\": ]] .. core.json.encode(opts) .. [[\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: login sp1 and sp2, then do single logout\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local kc = require \"lib.keycloak_cas\"\n\n            local path = \"/uri\"\n\n            -- login to sp1\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            local username = \"test\"\n            local password = \"test\"\n\n            local res, err, cas_cookie, keycloak_cookie = kc.login_keycloak(uri .. path, username, password)\n            if err or res.headers['Location'] ~= path then\n                ngx.log(ngx.ERR, err)\n                ngx.exit(500)\n            end\n            res, err = httpc:request_uri(uri .. res.headers['Location'], {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = cas_cookie\n                }\n            })\n            assert(res.status == 200)\n\n            -- login to sp2, which would skip login at keycloak side\n            local uri2 = \"http://127.0.0.2:\" .. ngx.var.server_port\n\n            local res, err, cas_cookie2 = kc.login_keycloak_for_second_sp(uri2 .. path, keycloak_cookie)\n            if err or res.headers['Location'] ~= path then\n                ngx.log(ngx.ERR, err)\n                ngx.exit(500)\n            end\n            res, err = httpc:request_uri(uri2 .. res.headers['Location'], {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = cas_cookie2\n                }\n            })\n            assert(res.status == 200)\n\n            -- SLO (single logout)\n            res, err = kc.logout_keycloak(uri .. \"/logout\", cas_cookie, keycloak_cookie)\n            assert(res.status == 200)\n\n            -- login to sp2, which would do normal login process at keycloak side\n            local res, err, cas_cookie2, keycloak_cookie = kc.login_keycloak(uri2 .. path, username, password)\n            if err or res.headers['Location'] ~= path then\n                ngx.log(ngx.ERR, err)\n                ngx.exit(500)\n            end\n            res, err = httpc:request_uri(uri .. res.headers['Location'], {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = cas_cookie2\n                }\n            })\n            assert(res.status == 200)\n\n            -- logout sp2\n            res, err = kc.logout_keycloak(uri2 .. \"/logout\", cas_cookie2, keycloak_cookie)\n            assert(res.status == 200)\n        }\n    }\n"
  },
  {
    "path": "t/plugin/chaitin-waf-reject.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $stream_default_server = <<_EOC_;\n    server {\n        listen 8088;\n        listen 8089;\n        content_by_lua_block {\n            require(\"lib.chaitin_waf_server\").reject()\n        }\n    }\n_EOC_\n\n    $block->set_value(\"extra_stream_config\", $stream_default_server);\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    # setup default conf.yaml\n    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;\napisix:\n  stream_proxy:                 # TCP/UDP L4 proxy\n   only: true                  # Enable L4 proxy only without L7 proxy.\n   tcp:\n     - addr: 9100              # Set the TCP proxy listening ports.\n       tls: true\n     - addr: \"127.0.0.1:9101\"\n   udp:                        # Set the UDP proxy listening ports.\n     - 9200\n     - \"127.0.0.1:9201\"\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        # use /do instead of /t because stream server will inject a default /t location\n        $block->set_value(\"request\", \"GET /do\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"mode\": \"block\",\n                    \"nodes\": [\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8088\n                        },\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8089\n                        }\n                    ]\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"chaitin-waf\": {\n                                \"upstream\": {\n                                   \"servers\": [\"httpbun.org\"]\n                               }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: pass\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"code\": 403, \"success\":false, \"message\": \"blocked by Chaitin SafeLine Web Application Firewall\", \"event_id\": \"b3c6ce574dc24f09a01f634a39dca83b\"}\n--- error_log\n--- response_headers\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-STATUS: 403\nX-APISIX-CHAITIN-WAF-ACTION: reject\n--- response_headers_like\nX-APISIX-CHAITIN-WAF-TIME:\n\n\n\n=== TEST 3: plugin mode monitor prepare\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8089\n                        }\n                    ]\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                    [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"chaitin-waf\": {\n                                \"mode\": \"monitor\",\n                                \"match\": [\n                                    {\n                                        \"vars\": [\n                                            [\"http_waf\", \"==\", \"true\"]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"uri\": \"/*\",\n                        \"upstream\": {\n                            \"nodes\": { \"127.0.0.1:1980\": 1 },\n                            \"type\": \"roundrobin\"\n                        }\n                    }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: plugin mode monitor\n--- request\nGET /hello\n--- more_headers\nwaf: true\ntrigger: block\n--- error_code: 200\n--- response_body\nhello world\n--- response_headers\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-STATUS: 403\nX-APISIX-CHAITIN-WAF-ACTION: reject\n"
  },
  {
    "path": "t/plugin/chaitin-waf-timeout.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $stream_default_server = <<_EOC_;\n    server {\n        listen 8088;\n        listen 8089;\n        content_by_lua_block {\n            require(\"lib.chaitin_waf_server\").timeout()\n        }\n    }\n_EOC_\n\n    $block->set_value(\"extra_stream_config\", $stream_default_server);\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    # setup default conf.yaml\n    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;\napisix:\n  stream_proxy:                 # TCP/UDP L4 proxy\n   only: true                  # Enable L4 proxy only without L7 proxy.\n   tcp:\n     - addr: 9100              # Set the TCP proxy listening ports.\n       tls: true\n     - addr: \"127.0.0.1:9101\"\n   udp:                        # Set the UDP proxy listening ports.\n     - 9200\n     - \"127.0.0.1:9201\"\nplugins:\n    - chaitin-waf\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        # use /do instead of /t because stream server will inject a default /t location\n        $block->set_value(\"request\", \"GET /do\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8088\n                        },\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8089\n                        }\n                    ]\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"chaitin-waf\": {\n                                \"upstream\": {\n                                   \"servers\": [\"httpbun.org\"]\n                               }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: timeout\n--- request\nGET /hello\n--- error_code: 200\n--- response_body\nhello world\n--- error_log\n--- response_headers\nX-APISIX-CHAITIN-WAF: timeout\n--- response_headers_like\nX-APISIX-CHAITIN-WAF-TIME:\n"
  },
  {
    "path": "t/plugin/chaitin-waf.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $stream_default_server = <<_EOC_;\n    server {\n        listen 8088;\n        listen 8089;\n        listen unix:/tmp/safeline-snserver.sock;\n        content_by_lua_block {\n            require(\"lib.chaitin_waf_server\").pass()\n        }\n    }\n_EOC_\n\n    $block->set_value(\"extra_stream_config\", $stream_default_server);\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    # setup default conf.yaml\n    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;\napisix:\n  stream_proxy:                 # TCP/UDP L4 proxy\n   only: true                  # Enable L4 proxy only without L7 proxy.\n   tcp:\n     - addr: 9100              # Set the TCP proxy listening ports.\n       tls: true\n     - addr: \"127.0.0.1:9101\"\n   udp:                        # Set the UDP proxy listening ports.\n     - 9200\n     - \"127.0.0.1:9201\"\nplugins:\n    - chaitin-waf\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        # use /do instead of /t because stream server will inject a default /t location\n        $block->set_value(\"request\", \"GET /do\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: wrong schema: nodes empty\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": []\n                    }\n                 ]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: expect array to have at least 1 items\"}\n\n\n\n=== TEST 2: wrong schema: nodes misses host\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {}\n                    ]\n                    }\n                 ]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"nodes\\\" validation failed: failed to validate item 1: property \\\"host\\\" is required\"}\n\n\n\n=== TEST 3: sanity\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8088\n                        },\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8089\n                        },\n                        {\n                            \"host\": \"unix:/tmp/safeline-snserver.sock\",\n                            \"port\": 8000\n                        }\n                    ]\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"chaitin-waf\": {\n                                \"upstream\": {\n                                   \"servers\": [\"httpbun.org\"]\n                               }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: pass\n--- request\nGET /hello\n--- error_code: 200\n--- response_body\nhello world\n--- error_log\n--- response_headers\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-STATUS: 200\nX-APISIX-CHAITIN-WAF-ACTION: pass\n--- response_headers_like\nX-APISIX-CHAITIN-WAF-TIME:\n\n\n\n=== TEST 5: match condition\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"chaitin-waf\": {\n                               \"upstream\": {\n                                   \"servers\": [\"httpbun.org\"]\n                               },\n                               \"match\": [\n                                    {\n                                        \"vars\": [\n                                            [\"http_waf\",\"==\",\"true\"]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: no match\n--- request\nGET /hello\n--- error_code: 200\n--- response_body\nhello world\n--- error_log\n--- response_headers\nX-APISIX-CHAITIN-WAF: no\n\n\n\n=== TEST 7: matched\n--- request\nGET /hello\n--- more_headers\nwaf: true\n--- error_code: 200\n--- response_body\nhello world\n--- error_log\n--- response_headers\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-STATUS: 200\nX-APISIX-CHAITIN-WAF-ACTION: pass\n--- response_headers_like\nX-APISIX-CHAITIN-WAF-TIME:\n\n\n\n=== TEST 8: plugin mode off prepare\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"chaitin-waf\": {\n                               \"mode\": \"off\",\n                               \"upstream\": {\n                                   \"servers\": [\"httpbun.org\"]\n                               },\n                               \"match\": [\n                                    {\n                                        \"vars\": [\n                                            [\"http_waf\",\"==\",\"true\"]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: plugin mode off\n--- request\nGET /hello\n--- more_headers\ntrigger: true\n--- error_code: 200\n--- response_body\nhello world\n--- response_headers\nX-APISIX-CHAITIN-WAF: off\n\n\n\n=== TEST 10: real_client_ip = false prepare\n--- config\n    location /do {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/plugin_metadata/chaitin-waf',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": [\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 8088\n                        }\n                    ]\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"plugins\": {\n                        \"chaitin-waf\": {\n                            \"match\": [\n                                {\n                                    \"vars\": [\n                                        [\"http_trigger\", \"==\", \"true\"]\n                                    ]\n                                }\n                            ],\n                            \"config\": {\n                                \"real_client_ip\": false\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/*\"\n                 }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.print(body)\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: real_client_ip = false\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: 1.2.3.4\ntrigger: true\n--- error_code: 200\n--- response_body\nhello world\n--- response_headers\nX-APISIX-CHAITIN-WAF: yes\nX-APISIX-CHAITIN-WAF-ACTION: pass\nX-APISIX-CHAITIN-WAF-STATUS: 200\n"
  },
  {
    "path": "t/plugin/clickhouse-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    server {\n        listen 10420;\n\n        location /clickhouse-logger/test1 {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local data = ngx.req.get_body_data()\n                local headers = ngx.req.get_headers()\n                ngx.log(ngx.WARN, \"clickhouse body: \", data)\n                for k, v in pairs(headers) do\n                    ngx.log(ngx.WARN, \"clickhouse headers: \" .. k .. \":\" .. v)\n                end\n                ngx.say(\"ok\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Full configuration verification\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.clickhouse-logger\")\n            local ok, err = plugin.check_schema({timeout = 3,\n                                                 retry_delay = 1,\n                                                 batch_max_size = 500,\n                                                 user = \"default\",\n                                                 password = \"a\",\n                                                 database = \"default\",\n                                                 logtable = \"t\",\n                                                 endpoint_addr = \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                                 max_retry_count = 1,\n                                                 name = \"clickhouse logger\",\n                                                 ssl_verify = false\n                                                 })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Basic configuration verification\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.clickhouse-logger\")\n            local ok, err = plugin.check_schema({user = \"default\",\n                                                 password = \"a\",\n                                                 database = \"default\",\n                                                 logtable = \"t\",\n                                                 endpoint_addr = \"http://127.0.0.1:1980/clickhouse_logger_server\"\n                                                 })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: auth configure undefined\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.clickhouse-logger\")\n            local ok, err = plugin.check_schema({user = \"default\",\n                                                 password = \"a\",\n                                                 database = \"default\",\n                                                 logtable = \"t\"\n                                                 })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nvalue should match only one schema, but matches none\n\n\n\n=== TEST 4: add plugin on routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"a\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"batch_max_size\":1,\n                                \"inactive_timeout\":1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: add plugin on routes using multi clickhouse-logger\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"\",\n                                \"database\": \"default\",\n                                \"logtable\": \"test\",\n                                \"endpoint_addrs\": [\"http://127.0.0.1:8123\",\n                                                  \"http://127.0.0.1:8124\"],\n                                \"batch_max_size\":1,\n                                \"inactive_timeout\":1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route\n--- request\nGET /opentracing\n--- error_code: 200\n--- wait: 5\n\n\n\n=== TEST 7: get log\n--- exec\necho \"select * from default.test\" | curl 'http://localhost:8123/' --data-binary @-\necho \"select * from default.test\" | curl 'http://localhost:8124/' --data-binary @-\n--- response_body_like\n.*127.0.0.1.*1.*\n\n\n\n=== TEST 8: to show that different endpoints will be chosen randomly\n--- config\n    location /t {\n        content_by_lua_block {\n            local code_count = {}\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 12 do\n                local code, body = t('/opentracing', ngx.HTTP_GET)\n                if code ~= 200 then\n                    ngx.say(\"code: \", code, \" body: \", body)\n                end\n                code_count[code] = (code_count[code] or 0) + 1\n            end\n\n            local code_arr = {}\n            for code, count in pairs(code_count) do\n                table.insert(code_arr, {code = code, count = count})\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(code_arr))\n            ngx.exit(200)\n        }\n    }\n--- response_body\n[{\"code\":200,\"count\":12}]\n--- error_log\nsending a batch logs to http://127.0.0.1:8123\nsending a batch logs to http://127.0.0.1:8124\n\n\n\n=== TEST 9: use single clickhouse server\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"\",\n                                \"database\": \"default\",\n                                \"logtable\": \"test\",\n                                \"endpoint_addr\": \"http://127.0.0.1:8123\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route\n--- request\nGET /opentracing\n--- error_code: 200\n--- wait: 5\n\n\n\n=== TEST 11: get log\n--- exec\necho \"select * from default.test\" | curl 'http://localhost:8123/' --data-binary @-\n--- response_body_like\n.*127.0.0.1.*1.*\n\n\n\n=== TEST 12: should drop entries when max_pending_entries is exceeded\n--- extra_yaml_config\nplugins:\n  - clickhouse-logger\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"clickhouse-logger\"] = {\n                            user = \"default\",\n                            password = \"a\",\n                            database = \"default\",\n                            logtable = \"t\",\n                            endpoint_addr = \"http://127.0.0.1:1234/clickhouse-logger/test1\",\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/clickhouse-logger', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/clickhouse-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: collect response body option\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"a\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"inactive_timeout\":1,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: collect response log\n--- request\nGET /hello\n--- error_log eval\nqr/clickhouse body: .*\\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 3\n\n\n\n=== TEST 3: collect response body with eval option\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"a\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"inactive_timeout\":1,\n                                \"include_resp_body\": true,\n                                \"include_resp_body_expr\": [\n                                    [\n                                      \"arg_foo\",\n                                      \"==\",\n                                      \"bar\"\n                                    ]\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: skip collect response log for condition\n--- request\nGET /hello?foo=unknown\n--- no_error_log eval\nqr/clickhouse body: .*\\{.*response\":\\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 3\n\n\n\n=== TEST 5: collect request body log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"a\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"inactive_timeout\":1,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: collect request body with eval option\n--- request\nPOST /hello?foo=bar\n{\"sample\":\"hello\"}\n--- error_log eval\nqr/clickhouse body: .*\\{.*request\":\\{.*\"body\":\"\\{\\\\\"sample\\\\\":\\\\\"hello\\\\\"/\n--- wait: 3\n\n\n\n=== TEST 7: collect request body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"default\",\n                                \"password\": \"a\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                                \"inactive_timeout\":1,\n                                \"include_req_body\": true,\n                                \"include_req_body_expr\": [\n                                    [\n                                      \"arg_foo\",\n                                      \"==\",\n                                      \"bar\"\n                                    ]\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: skip collect request body log for condition\n--- request\nPOST /hello?foo=unknown\n{\"sample\":\"hello\"}\n--- no_error_log eval\nqr/clickhouse body: .*\\{.*request\":\\{.*\"body\":\"\\{\\\\\"sample\\\\\":\\\\\"hello\\\\\"/\n--- wait: 3\n"
  },
  {
    "path": "t/plugin/clickhouse-logger3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nBEGIN {\n    $ENV{CLICK_HOUSE_USER} = \"default\";\n}\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: pass clickhouse user via environment variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"clickhouse-logger\": {\n                                \"user\": \"$ENV://CLICK_HOUSE_USER\",\n                                \"password\": \"\",\n                                \"database\": \"default\",\n                                \"logtable\": \"test\",\n                                \"endpoint_addr\": \"http://127.0.0.1:8123\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- request\nGET /opentracing\n--- error_code: 200\n--- wait: 5\n\n\n\n=== TEST 3: get log\n--- exec\necho \"select * from default.test\" | curl 'http://localhost:8123/' --data-binary @-\n--- response_body_like\n.*127.0.0.1.*1.*\n"
  },
  {
    "path": "t/plugin/client-control.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"client-control\": {\n                            \"max_body_size\": 5\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit, failed\n--- request\nPOST /hello\n123456\n--- error_code: 413\n\n\n\n=== TEST 3: hit, failed with chunked\n--- more_headers\nTransfer-Encoding: chunked\n--- request eval\nqq{POST /hello\n6\\r\nHello \\r\n0\\r\n\\r\n}\n--- error_code: 413\n--- error_log\nclient intended to send too large chunked body\n\n\n\n=== TEST 4: hit\n--- request\nPOST /hello\n12345\n\n\n\n=== TEST 5: bad body size\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"client-control\": {\n                            \"max_body_size\": -1\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.print(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin client-control err: property \\\"max_body_size\\\" validation failed: expected -1 to be at least 0\"}\n\n\n\n=== TEST 6: 0 means no limit\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"client-control\": {\n                            \"max_body_size\": 0\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- request\nPOST /hello\n1\n"
  },
  {
    "path": "t/plugin/consumer-bug-fix.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer jack1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        },\n                        \"echo\":{\"body\": \"before change\"}\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"key-auth\": {}\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: verify 20 times\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-one\n--- response_body eval\n[\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\",\"before change\"]\n\n\n\n=== TEST 4: modify consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        },\n                        \"echo\":{\"body\": \"after change\"}\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: verify 20 times\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-one\n--- response_body eval\n[\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\",\"after change\"]\n"
  },
  {
    "path": "t/plugin/consumer-restriction.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.consumer-restriction\")\n            local conf = {\n        title = \"whitelist\",\n        whitelist = {\n                    \"jack1\",\n                    \"jack2\"\n                }\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"rejected_code\":403,\"title\":\"whitelist\",\"type\":\"consumer_name\",\"whitelist\":[\"jack1\",\"jack2\"]}\n\n\n\n=== TEST 2: blacklist > whitelist > allowed_by_methods\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.consumer-restriction\")\n            local ok, err = plugin.check_schema({whitelist={\"jack1\"}, blacklist={\"jack2\"}, allowed_by_methods={}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 3: add consumer jack1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2019\",\n                            \"password\": \"123456\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: add consumer jack2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack2\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2020\",\n                            \"password\": \"123456\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: set whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"whitelist\": [\n                                     \"jack1\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: verify unauthorized\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing authorization in request\"}\n\n\n\n=== TEST 7: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 8: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"The consumer_name is forbidden.\"}\n\n\n\n=== TEST 9: set blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"blacklist\": [\n                                     \"jack1\"\n                                 ],\n                                 \"rejected_msg\": \"request is forbidden\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: verify unauthorized\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing authorization in request\"}\n\n\n\n=== TEST 11: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"request is forbidden\"}\n\n\n\n=== TEST 12: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 13: set whitelist without authorization\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"consumer-restriction\": {\n                                 \"whitelist\": [\n                                     \"jack1\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: verify unauthorized\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_name for this request\"}\n\n\n\n=== TEST 15: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_name for this request\"}\n\n\n\n=== TEST 16: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_name for this request\"}\n\n\n\n=== TEST 17: set blacklist without authorization\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"consumer-restriction\": {\n                                 \"blacklist\": [\n                                     \"jack1\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: verify unauthorized\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_name for this request\"}\n\n\n\n=== TEST 19: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_name for this request\"}\n\n\n\n=== TEST 20: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_name for this request\"}\n\n\n\n=== TEST 21: set allowed_by_methods\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"allowed_by_methods\":[{\n                                    \"user\":\"jack1\",\n                                    \"methods\":[\"POST\"]\n                                 }]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"The consumer_name is forbidden.\"}\n\n\n\n=== TEST 23: set allowed_by_methods\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"allowed_by_methods\":[{\n                                    \"user\": \"jack1\",\n                                    \"methods\": [\"POST\",\"GET\"]\n                                }]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 25: test blacklist priority\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"blacklist\": [\n                                     \"jack1\"\n                                 ],\n                                 \"allowed_by_methods\":[{\n                                    \"user\": \"jack1\",\n                                    \"methods\": [\"POST\",\"GET\"]\n                                }]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"The consumer_name is forbidden.\"}\n\n\n\n=== TEST 27: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 28: whitelist blacklist priority\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"whitelist\": [\"jack1\"],\n                                 \"allowed_by_methods\":[{\n                                    \"user\":\"jack1\",\n                                    \"methods\":[\"POST\"]\n                                }]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 30: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"The consumer_name is forbidden.\"}\n\n\n\n=== TEST 31: remove consumer-restriction\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 32: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 33: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 34: verify unauthorized\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 35: create service (id:1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service 001\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 36: add consumer with plugin hmac-auth and consumer-restriction, and set whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        },\n                        \"consumer-restriction\": {\n                            \"type\": \"service_id\",\n                            \"whitelist\": [ \"1\" ],\n                            \"rejected_code\": 401\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 37: Route binding `hmac-auth` plug-in and whitelist `service_id`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"service_id\": 1,\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    }\n\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 38: verify: valid whitelist `service_id`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 39: create service (id:2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"desc\": \"new service 002\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 40: Route binding `hmac-auth` plug-in and invalid whitelist `service_id`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"service_id\": 2,\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    }\n\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 41: verify: invalid whitelist `service_id`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time   = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n        if code >= 300 then\n            ngx.status = code\n        end\n\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/\\{\"message\":\"The service_id is forbidden.\"\\}/\n\n\n\n=== TEST 42: add consumer with plugin hmac-auth and consumer-restriction, and set blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        },\n                        \"consumer-restriction\": {\n                            \"type\": \"service_id\",\n                            \"blacklist\": [ \"1\" ],\n                            \"rejected_code\": 401\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 43: Route binding `hmac-auth` plug-in and blacklist `service_id`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"service_id\": 1,\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    }\n\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 44: verify: valid blacklist `service_id`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time   = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/\\{\"message\":\"The service_id is forbidden.\"\\}/\n\n\n\n=== TEST 45: Route binding `hmac-auth` plug-in and invalid blacklist `service_id`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"service_id\": 2,\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    }\n\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 46: verify: invalid blacklist `service_id`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time   = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 47: update consumer with plugin hmac-auth and consumer-restriction, and set whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        },\n                        \"consumer-restriction\": {\n                            \"type\": \"route_id\",\n                            \"whitelist\": [ \"1\" ],\n                            \"rejected_code\": 401\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 48: verify: valid whitelist `route_id`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time()\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n        local secret_key = \"my-secret-key\"\n        local gmt = ngx_http_time(ngx_time)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 49: update consumer with plugin hmac-auth and consumer-restriction, and set blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        },\n                        \"consumer-restriction\": {\n                            \"type\": \"route_id\",\n                            \"blacklist\": [ \"1\" ],\n                            \"rejected_code\": 401\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 50: verify: valid blacklist `route_id`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time   = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/\\{\"message\":\"The route_id is forbidden.\"\\}/\n\n\n\n=== TEST 51: delete: route (id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t( '/apisix/admin/routes/1', ngx.HTTP_DELETE )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 52: delete: `service_id` is 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t( '/apisix/admin/services/1', ngx.HTTP_DELETE )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 53: delete: `service_id` is 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t( '/apisix/admin/services/2', ngx.HTTP_DELETE )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/consumer-restriction2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create consumer group(group1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/group1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {}\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create consumer group(group2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/group2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {}\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: consumer jack1 with consumer group(group1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2019\",\n                            \"password\": \"123456\"\n                        }\n                    },\n                    \"group_id\": \"group1\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: consumer jack2 with consumer group(group2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack2\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2020\",\n                            \"password\": \"123456\"\n                        }\n                    },\n                    \"group_id\": \"group2\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: consumer jack3 with no consumer group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack3\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2021\",\n                            \"password\": \"123456\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: set whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                \"type\": \"consumer_group_id\",\n                                 \"whitelist\": [\n                                     \"group1\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: verify unauthorized\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing authorization in request\"}\n\n\n\n=== TEST 8: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 9: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"The consumer_group_id is forbidden.\"}\n\n\n\n=== TEST 10: set blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"basic-auth\": {},\n                            \"consumer-restriction\": {\n                                 \"type\": \"consumer_group_id\",\n                                 \"blacklist\": [\n                                     \"group1\"\n                                 ],\n                                 \"rejected_msg\": \"request is forbidden\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: verify unauthorized\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing authorization in request\"}\n\n\n\n=== TEST 12: verify jack1\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- error_code: 403\n--- response_body\n{\"message\":\"request is forbidden\"}\n\n\n\n=== TEST 13: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 14: verify jack2\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjE6MTIzNDU2\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the consumer_group_id for this request\"}\n\n\n\n=== TEST 15: set blacklist with service_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"consumer-restriction\": {\n                                 \"type\": \"service_id\",\n                                 \"blacklist\": [\n                                     \"1\"\n                                 ],\n                                 \"rejected_msg\": \"request is forbidden\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the service_id for this request\"}\n\n\n\n=== TEST 17: set whitelist with service_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"consumer-restriction\": {\n                                 \"type\": \"service_id\",\n                                 \"whitelist\": [\n                                     \"1\"\n                                 ],\n                                 \"rejected_msg\": \"request is forbidden\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"The request is rejected, please check the service_id for this request\"}\n"
  },
  {
    "path": "t/plugin/cors.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cors\")\n            local ok, err = plugin.check_schema({\n                allow_origins = 'http://test.com',\n                allow_methods = '',\n                allow_headers = '',\n                expose_headers = '',\n                max_age = 600,\n                allow_credential = true\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong value of key\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cors\")\n            local ok, err = plugin.check_schema({\n                allow_origins = 'http://test.com',\n                allow_methods = '',\n                allow_headers = '',\n                expose_headers = '',\n                max_age = '600',\n                allow_credential = true\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"max_age\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 3: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"**\",\n                            \"allow_methods\": \"**\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"madx_age\": 5,\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: set route(default)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"cors\": {\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: cors default\n--- request\nGET /hello HTTP/1.1\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: *\nVary:\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 8: set route (cors specified)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://sub.domain.com,http://sub2.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"headr1,headr2\",\n                            \"expose_headers\": \"ex-headr1,ex-headr2\",\n                            \"max_age\": 50,\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: cors specified\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub2.domain.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: http://sub2.domain.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: headr1,headr2\nAccess-Control-Expose-Headers: ex-headr1,ex-headr2\nAccess-Control-Max-Age: 50\nAccess-Control-Allow-Credentials: true\n\n\n\n=== TEST 10: cors specified no match origin\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub3.domain.com\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 11: set route(force wildcard)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"**\",\n                            \"allow_methods\": \"**\",\n                            \"allow_headers\": \"**\",\n                            \"expose_headers\": \"*\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: cors force wildcard\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\nExternalHeader1: val\nExternalHeader2: val\nExternalHeader3: val\nAccess-Control-Request-Headers: req-header1,req-header2\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: https://sub.domain.com\nVary: Origin\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\nAccess-Control-Allow-Headers: req-header1,req-header2\nAccess-Control-Expose-Headers: *\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 13: cors force wildcard no origin\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nExternalHeader1: val\nExternalHeader2: val\nExternalHeader3: val\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers: *\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 14: options return directly\n--- request\nOPTIONS /hello HTTP/1.1\n--- response_body\n\n\n\n=== TEST 15: set route(auth plugins fails)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"cors\": {\n                            \"allow_origins\": \"**\",\n                            \"allow_methods\": \"**\",\n                            \"allow_headers\": \"*\",\n                            \"expose_headers\": \"*\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: auth failed still work\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\nExternalHeader1: val\nExternalHeader2: val\nExternalHeader3: val\n--- response_body\n{\"message\":\"Missing API key in request\"}\n--- error_code: 401\n--- response_headers\nAccess-Control-Allow-Origin: https://sub.domain.com\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers: *\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 17: set route(overwrite upstream)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"**\",\n                            \"allow_methods\": \"**\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/headers\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: overwrite upstream\n--- request\nGET /headers?Access-Control-Allow-Origin=https://sub.domain.com HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\n--- response_body\n/headers\n--- response_headers\nAccess-Control-Allow-Origin: https://sub.domain.com\n\n\n\n=== TEST 19: overwrite upstream(Access-Control-Allow-Methods)\n--- request\nGET /headers?Access-Control-Allow-Methods=methods HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\n--- response_body\n/headers\n--- response_headers\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\n\n\n\n=== TEST 20: overwrite upstream(Access-Control-Allow-Headers)\n--- request\nGET /headers?Access-Control-Allow-Headers=a-headers HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\n--- response_body\n/headers\n--- response_headers\nAccess-Control-Allow-Headers: request-h\n\n\n\n=== TEST 21: overwrite upstream(Access-Control-Expose-Headers)\n--- request\nGET /headers?Access-Control-Expose-Headers=e-headers HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\n--- response_body\n/headers\n--- response_headers\nAccess-Control-Expose-Headers: expose-h\n\n\n\n=== TEST 22: overwrite upstream(Access-Control-Max-Age)\n--- request\nGET /headers?Access-Control-Max-Age=10 HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\n--- response_body\n/headers\n--- response_headers\nAccess-Control-Max-Age: 5\n\n\n\n=== TEST 23: not overwrite upstream(Access-Control-Allow-Credentials)\n--- request\nGET /headers?Access-Control-Allow-Credentials=false HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\n--- response_body\n/headers\n--- response_headers\nAccess-Control-Allow-Credentials: true\n\n\n\n=== TEST 24: should not set * to allow_origins when allow_credential is true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"*\",\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin cors err: you can not/\n\n\n\n=== TEST 25: should not set * to allow_methods when allow_credential is true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_methods\": \"*\",\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin cors err: you can not/\n\n\n\n=== TEST 26: should not set * to allow_headers when allow_credential is true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_headers\": \"*\",\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin cors err: you can not/\n\n\n\n=== TEST 27: should not set * to expose_headers when allow_credential is true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"expose_headers\": \"*\",\n                            \"allow_credential\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin cors err: you can not/\n--- no_error_log\n\n\n\n=== TEST 28: set route (regex specified)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://sub.domain.com,http://sub2.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"headr1,headr2\",\n                            \"expose_headers\": \"ex-headr1,ex-headr2\",\n                            \"max_age\": 50,\n                            \"allow_credential\": true,\n                            \"allow_origins_by_regex\":[\".*\\\\.test.com$\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: regex specified\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://a.test.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: http://a.test.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: headr1,headr2\nAccess-Control-Expose-Headers: ex-headr1,ex-headr2\nAccess-Control-Max-Age: 50\nAccess-Control-Allow-Credentials: true\n\n\n\n=== TEST 30: regex specified not match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://a.test2.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 31: set route (multiple regex specified )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://sub.domain.com,http://sub2.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"headr1,headr2\",\n                            \"expose_headers\": \"ex-headr1,ex-headr2\",\n                            \"max_age\": 50,\n                            \"allow_credential\": true,\n                            \"allow_origins_by_regex\":[\".*\\\\.test.com$\",\".*\\\\.example.org$\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 32: multiple regex specified match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://foo.example.org\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: http://foo.example.org\nVary: Via, Origin\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: headr1,headr2\nAccess-Control-Expose-Headers: ex-headr1,ex-headr2\nAccess-Control-Max-Age: 50\nAccess-Control-Allow-Credentials: true\n\n\n\n=== TEST 33: multiple regex specified not match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://foo.example.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 34: origin was modified by the proxy_rewrite plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://sub.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"headr1,headr2\",\n                            \"expose_headers\": \"ex-headr1,ex-headr2\",\n                            \"max_age\": 50,\n                            \"allow_credential\": true\n                        },\n                        \"proxy-rewrite\": {\n                            \"headers\": {\n                                  \"Origin\": \"http://example.com\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 35: origin is not affected by proxy_rewrite plugins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: http://sub.domain.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: headr1,headr2\nAccess-Control-Expose-Headers: ex-headr1,ex-headr2\nAccess-Control-Max-Age: 50\nAccess-Control-Allow-Credentials: true\n"
  },
  {
    "path": "t/plugin/cors2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: validate allow_origins\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cors\")\n            local function validate(val)\n                local conf = {}\n                conf.allow_origins = val\n                return plugin.check_schema(conf)\n            end\n\n            local good = {\n                \"*\",\n                \"**\",\n                \"null\",\n                \"http://y.com.uk\",\n                \"https://x.com\",\n                \"https://x.com,http://y.com.uk\",\n                \"https://x.com,http://y.com.uk,http://c.tv\",\n                \"https://x.com,http://y.com.uk:12000,http://c.tv\",\n            }\n            for _, g in ipairs(good) do\n                local ok, err = validate(g)\n                if not ok then\n                    ngx.say(\"failed to validate \", g, \", \", err)\n                end\n            end\n\n            local bad = {\n                \"\",\n                \"*a\",\n                \"*,http://y.com\",\n                \"nulll\",\n                \"http//y.com.uk\",\n                \"x.com\",\n                \"https://x.com,y.com.uk\",\n                \"https://x.com,*,https://y.com.uk\",\n                \"https://x.com,http://y.com.uk,http:c.tv\",\n            }\n            for _, b in ipairs(bad) do\n                local ok, err = validate(b)\n                if ok then\n                    ngx.say(\"failed to reject \", b)\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: set route ( regex specified and allow_origins is default value )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"*\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"allow_origins_by_regex\":[\".*\\\\.domain.com$\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: regex specified not match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.test.com\nresp-vary: Via\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 4: regex no origin specified\n--- request\nGET /hello HTTP/1.1\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 5: regex specified match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\nresp-vary: Via\n--- response_headers\nAccess-Control-Allow-Origin: http://sub.domain.com\nVary: Via\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\n"
  },
  {
    "path": "t/plugin/cors3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: validate metadata allow_origins\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cors\")\n            local schema_type = require(\"apisix.core\").schema.TYPE_METADATA\n            local function validate(val)\n                local conf = {}\n                conf.allow_origins = val\n                return plugin.check_schema(conf, schema_type)\n            end\n\n            local good = {\n                key_1 = \"*\",\n                key_2 = \"**\",\n                key_3 = \"null\",\n                key_4 = \"http://y.com.uk\",\n                key_5 = \"https://x.com\",\n                key_6 = \"https://x.com,http://y.com.uk\",\n                key_7 = \"https://x.com,http://y.com.uk,http://c.tv\",\n                key_8 = \"https://x.com,http://y.com.uk:12000,http://c.tv\",\n            }\n            local ok, err = validate(good)\n            if not ok then\n                ngx.say(\"failed to validate \", g, \", \", err)\n            end\n\n            local bad = {\n                \"\",\n                \"*a\",\n                \"*,http://y.com\",\n                \"nulll\",\n                \"http//y.com.uk\",\n                \"x.com\",\n                \"https://x.com,y.com.uk\",\n                \"https://x.com,*,https://y.com.uk\",\n                \"https://x.com,http://y.com.uk,http:c.tv\",\n            }\n            for _, b in ipairs(bad) do\n                local ok, err = validate({key = b})\n                if ok then\n                    ngx.say(\"failed to reject \", b)\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: set plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/cors',\n                ngx.HTTP_PUT,\n                [[{\n                    \"allow_origins\": {\n                        \"key_1\": \"https://domain.com\",\n                        \"key_2\": \"https://sub.domain.com,https://sub2.domain.com\",\n                        \"key_3\": \"*\"\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: set route (allow_origins_by_metadata specified)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"https://test.com\",\n                            \"allow_origins_by_metadata\": [\"key_1\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: origin not match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://foo.example.org\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin:\nVary: Origin\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 5: origin matches with allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://test.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: https://test.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 6: origin matches with allow_origins_by_metadata\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://domain.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: https://domain.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 7: set route (multiple allow_origins_by_metadata specified)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"https://test.com\",\n                            \"allow_origins_by_metadata\": [\"key_1\", \"key_2\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: origin not match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://foo.example.org\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin:\nVary: Origin\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 9: origin matches with first allow_origins_by_metadata\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://domain.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: https://domain.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 10: origin matches with second allow_origins_by_metadata\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://sub.domain.com\nresp-vary: Via\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: https://sub.domain.com\nVary: Via, Origin\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 11: set route (wildcard in allow_origins_by_metadata)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"https://test.com\",\n                            \"allow_origins_by_metadata\": [\"key_3\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: origin matches by wildcard\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://foo.example.org\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: http://foo.example.org\nVary: Origin\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 13: set route (allow_origins_by_metadata specified and allow_origins * is invalid while set allow_origins_by_metadata)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"*\",\n                            \"allow_origins_by_metadata\": [\"key_1\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: origin not match because allow_origins * invalid\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://foo.example.org\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\n\n\n\n=== TEST 15: origin matches with first allow_origins_by_metadata\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: https://domain.com\n--- response_body\nhello world\n--- response_headers\nAccess-Control-Allow-Origin: https://domain.com\nAccess-Control-Allow-Methods: *\nAccess-Control-Allow-Headers: *\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age: 5\nAccess-Control-Allow-Credentials:\n"
  },
  {
    "path": "t/plugin/cors4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: validate timing_allow_origins\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cors\")\n            local function validate(val)\n                local conf = {}\n                conf.timing_allow_origins = val\n                return plugin.check_schema(conf)\n            end\n\n            local good = {\n                \"*\",\n                \"**\",\n                \"null\",\n                \"http://y.com.uk\",\n                \"https://x.com\",\n                \"https://x.com,http://y.com.uk\",\n                \"https://x.com,http://y.com.uk,http://c.tv\",\n                \"https://x.com,http://y.com.uk:12000,http://c.tv\",\n            }\n            for _, g in ipairs(good) do\n                local ok, err = validate(g)\n                if not ok then\n                    ngx.say(\"failed to validate \", g, \", \", err)\n                end\n            end\n\n            local bad = {\n                \"\",\n                \"*a\",\n                \"*,http://y.com\",\n                \"nulll\",\n                \"http//y.com.uk\",\n                \"x.com\",\n                \"https://x.com,y.com.uk\",\n                \"https://x.com,*,https://y.com.uk\",\n                \"https://x.com,http://y.com.uk,http:c.tv\",\n            }\n            for _, b in ipairs(bad) do\n                local ok, err = validate(b)\n                if ok then\n                    ngx.say(\"failed to reject \", b)\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: set route ( allow_origins default, timing_allow_origins specified )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"*\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins\": \"http://sub.domain.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: origin matching\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin: http://sub.domain.com\n\n\n\n=== TEST 4: origin not matching timing_allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://other.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 5: set route ( allow_origins same as timing_allow_origins )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://sub.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins\": \"http://sub.domain.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: origin matching\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://sub.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin: http://sub.domain.com\n\n\n\n=== TEST 7: origin not matching\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://other.domain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 8: set route ( allow_origins differs from timing_allow_origins )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://one.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins\": \"http://another.domain.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: origin matching allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://one.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://one.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 10: origin matching timing_allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://another.domain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin: http://another.domain.com\n\n\n\n=== TEST 11: origin not matching\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://notexistent.domain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 12: set route ( allow_origins superset of timing_allow_origins )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://one.domain.com,http://two.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins\": \"http://one.domain.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: origin matching allow_origins and timing_allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://one.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://one.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin: http://one.domain.com\n\n\n\n=== TEST 14: origin matching only allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://two.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://two.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 15: origin not matching\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://notexistent.domain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 16: set route ( allow_origins and timing_allow_origins are two different sets with intersection )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins\": \"http://one.domain.com,http://two.domain.com\",\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins\": \"http://one.domain.com,http://three.domain.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: origin matching allow_origins and timing_allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://one.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://one.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin: http://one.domain.com\n\n\n\n=== TEST 18: origin matching only allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://two.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://two.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 19: origin matching only timing_allow_origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://three.domain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin: http://three.domain.com\n\n\n\n=== TEST 20: origin not matching\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://notexistent.domain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nAccess-Control-Allow-Credentials:\nTiming-Allow-Origin:\n\n\n\n=== TEST 21: set route ( allow_origins and timing_allow_origins specified with regex )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins_by_regex\": [\"http://.*?\\\\.domain\\\\.com\"],\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins_by_regex\": [\"http://.*?\\\\.domain\\\\.com\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: regex specified match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://sub.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nTiming-Allow-Origin: http://sub.domain.com\n\n\n\n=== TEST 23: regex no match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://other.newdomain.com\n--- response_headers\nAccess-Control-Allow-Origin:\nAccess-Control-Allow-Methods:\nAccess-Control-Allow-Headers:\nAccess-Control-Expose-Headers:\nAccess-Control-Max-Age:\nTiming-Allow-Origin:\n\n\n\n=== TEST 24: set route ( allow_origins and timing_allow_origins specified with different regex )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_origins_by_regex\": [\"http://.*?\\\\.domain\\\\.com\"],\n                            \"allow_methods\": \"GET,POST\",\n                            \"allow_headers\": \"request-h\",\n                            \"expose_headers\": \"expose-h\",\n                            \"max_age\": 10,\n                            \"timing_allow_origins_by_regex\": [\"http://test.*?\\\\.domain\\\\.com\"],\n                            \"timing_allow_origins\": \"http://nonexistent.newdomain.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 25: regex specified match, test priority of regex over list of origins\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://testurl.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://testurl.domain.com\nAccess-Control-Allow-Methods: GET,POST\nAccess-Control-Allow-Headers: request-h\nAccess-Control-Expose-Headers: expose-h\nAccess-Control-Max-Age: 10\nTiming-Allow-Origin: http://testurl.domain.com\n\n\n\n=== TEST 26: set route ( expose_headers not specified )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_credential\": true,\n                            \"allow_headers\": \"**\",\n                            \"allow_methods\": \"**\",\n                            \"allow_origins\": \"**\",\n                            \"expose_headers\": \"\",\n                            \"max_age\": 3500\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: remove Access-Control-Expose-Headers match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://sub.domain.com\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\nAccess-Control-Expose-Headers:\nAccess-Control-Allow-Headers:\nAccess-Control-Max-Age: 3500\nAccess-Control-Allow-Credentials: true\n\n\n\n=== TEST 28: set route ( expose_headers set value )\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"cors\": {\n                            \"allow_credential\": true,\n                            \"allow_headers\": \"**\",\n                            \"allow_methods\": \"**\",\n                            \"allow_origins\": \"**\",\n                            \"expose_headers\": \"ex-headr1,ex-headr2\",\n                            \"max_age\": 3500\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: Access-Control-Expose-Headers match\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nOrigin: http://sub.domain.com\n--- response_headers\nAccess-Control-Allow-Origin: http://sub.domain.com\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACE\nAccess-Control-Expose-Headers: ex-headr1,ex-headr2\nAccess-Control-Allow-Headers:\nAccess-Control-Max-Age: 3500\nAccess-Control-Allow-Credentials: true\n"
  },
  {
    "path": "t/plugin/csrf.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.csrf\")\n            local ok, err = plugin.check_schema({name = '_csrf', expires = 3600, key = 'testkey'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: set csrf plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"csrf\": {\n                            \"key\": \"userkey\",\n                            \"expires\": 1000000000\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: have csrf cookie\n--- request\nGET /hello\n--- response_headers_like\nSet-Cookie: apisix-csrf-token\\s*=\\s*[^;]+(.*)?$\n\n\n\n=== TEST 4: block request\n--- request\nPOST /hello\n--- error_code: 401\n--- response_body\n{\"error_msg\":\"no csrf token in headers\"}\n\n\n\n=== TEST 5: only header\n--- request\nPOST /hello\n--- more_headers\napisix-csrf-token: wrongtoken\n--- error_code: 401\n--- response_body\n{\"error_msg\":\"no csrf cookie\"}\n\n\n\n=== TEST 6: only cookie\n--- request\nPOST /hello\n--- more_headers\nCookie: apisix-csrf-token=testcookie\n--- error_code: 401\n--- response_body\n{\"error_msg\":\"no csrf token in headers\"}\n\n\n\n=== TEST 7: header and cookie mismatch\n--- request\nPOST /hello\n--- more_headers\napisix-csrf-token: wrongtoken\nCookie: apisix-csrf-token=testcookie\n--- error_code: 401\n--- response_body\n{\"error_msg\":\"csrf token mismatch\"}\n\n\n\n=== TEST 8: invalid csrf token\n--- request\nPOST /hello\n--- more_headers\napisix-csrf-token: eyJyYW5kb20iOjAuMTYwOTgzMDYwMTg0NDksInNpZ24iOiI2YTEyYmViYTI4MzAyNDg4MDRmNGU0N2VkZDY5MWFmNjg5N2IyNzQ4YTY1YWMwMDJiMGFjMzFlN2NlMDdlZTViIiwiZXhwaXJlcyI6MTc0MzExOTkxMX0=\nCookie: apisix-csrf-token=eyJyYW5kb20iOjAuMTYwOTgzMDYwMTg0NDksInNpZ24iOiI2YTEyYmViYTI4MzAyNDg4MDRmNGU0N2VkZDY5MWFmNjg5N2IyNzQ4YTY1YWMwMDJiMGFjMzFlN2NlMDdlZTViIiwiZXhwaXJlcyI6MTc0MzExOTkxMX0=\n--- error_code: 401\n--- error_log: Invalid signatures\n--- response_body\n{\"error_msg\":\"Failed to verify the csrf token signature\"}\n\n\n\n=== TEST 9: valid csrf token\n--- request\nPOST /hello\n--- more_headers\napisix-csrf-token: eyJyYW5kb20iOjAuNDI5ODYzMTk3MTYxMzksInNpZ24iOiI0ODRlMDY4NTkxMWQ5NmJhMDc5YzQ1ZGI0OTE2NmZkYjQ0ODhjODVkNWQ0NmE1Y2FhM2UwMmFhZDliNjE5OTQ2IiwiZXhwaXJlcyI6MjY0MzExOTYyNH0=\nCookie: apisix-csrf-token=eyJyYW5kb20iOjAuNDI5ODYzMTk3MTYxMzksInNpZ24iOiI0ODRlMDY4NTkxMWQ5NmJhMDc5YzQ1ZGI0OTE2NmZkYjQ0ODhjODVkNWQ0NmE1Y2FhM2UwMmFhZDliNjE5OTQ2IiwiZXhwaXJlcyI6MjY0MzExOTYyNH0=\n\n\n\n=== TEST 10: change expired\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"csrf\": {\n                            \"key\": \"userkey\",\n                            \"expires\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: expired csrf token\n--- request\nPOST /hello\n--- more_headers\napisix-csrf-token: eyJyYW5kb20iOjAuMDY3NjAxMDQwMDM5MzI4LCJzaWduIjoiOTE1Yjg2MjBhNTg1N2FjZmIzNjIxOTNhYWVlN2RkYjY5NmM0NWYwZjE5YjY5Zjg3NjM4ZTllNGNjNjYxYjQwNiIsImV4cGlyZXMiOjE2NDMxMjAxOTN9\nCookie: apisix-csrf-token=eyJyYW5kb20iOjAuMDY3NjAxMDQwMDM5MzI4LCJzaWduIjoiOTE1Yjg2MjBhNTg1N2FjZmIzNjIxOTNhYWVlN2RkYjY5NmM0NWYwZjE5YjY5Zjg3NjM4ZTllNGNjNjYxYjQwNiIsImV4cGlyZXMiOjE2NDMxMjAxOTN9\n--- error_code: 401\n--- error_log: token has expired\n\n\n\n=== TEST 12: token has expired after sleep 2s\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local cookie = res.headers[\"Set-Cookie\"]\n            local token = cookie:match(\"=([^;]+)\")\n\n            ngx.sleep(2)\n\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = {\n                    [\"apisix-csrf-token\"] = token,\n                    [\"Cookie\"] = cookie,\n                }\n            })\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            if res.status >= 300 then\n                ngx.status = res.status\n            end\n        }\n    }\n--- error_code: 401\n--- error_log: token has expired\n\n\n\n=== TEST 13: set expires 0\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"csrf\": {\n                            \"key\": \"userkey\",\n                            \"expires\": 0\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: token no expired after sleep 1s\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.sleep(1)\n\n            local cookie = res.headers[\"Set-Cookie\"]\n            local token = cookie:match(\"=([^;]+)\")\n\n            local res, err = httpc:request_uri(uri, {\n                method = \"POST\",\n                headers = {\n                    [\"apisix-csrf-token\"] = token,\n                    [\"Cookie\"] = cookie,\n                }\n            })\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            if res.status >= 300 then\n                ngx.status = res.status\n            end\n            ngx.status = res.status\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 15: data encryption for key\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"csrf\": {\n                            \"key\": \"userkey\",\n                            \"expires\": 1000000000\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"csrf\"].key)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"csrf\"].key)\n        }\n    }\n--- response_body\nuserkey\nmt39FazQccyMqt4ctoRV7w==\n"
  },
  {
    "path": "t/plugin/custom_sort_plugins.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nno_long_string();\nno_root_location();\nlog_level(\"info\");\nrun_tests;\n\n__DATA__\n\n=== TEST 1: custom priority and default priority on different routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"_meta\": {\n                                \"priority\": 10000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        },\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        },\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: verify order\n--- request\nGET /hello\n--- response_body\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 3: routing without custom plugin order is not affected\n--- request\nGET /hello1\n--- response_body\nserverless-pre-function\nserverless-post-function\n\n\n\n=== TEST 4: custom priority and default priority on same route\n# the priority of serverless-post-function is -2000, execute serverless-post-function first\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        },\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2001\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify order\n--- request\nGET /hello\n--- response_body\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 6: merge plugins from consumer and route, execute the rewrite phase\n# in the rewrite phase, the plugins on the route must be executed first,\n# and then executed the rewrite phase of the plugins on the consumer,\n# and the custom plugin order fails for this case.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        },\n                        \"serverless-post-function\": {\n                            \"_meta\": {\n                                \"priority\": 10000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: verify order(more requests)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local httpc = http.new()\n            local headers = {}\n            headers[\"apikey\"] = \"auth-one\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\", headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n\n            local res, err = httpc:request_uri(uri, {method = \"GET\", headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nserverless-pre-function\nserverless-post-function\nserverless-pre-function\nserverless-post-function\n\n\n\n=== TEST 8: merge plugins form custom and route, execute the access phase\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        },\n                        \"serverless-post-function\": {\n                            \"_meta\": {\n                                \"priority\": 10000\n                            },\n                            \"phase\": \"access\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2000\n                            },\n                            \"phase\": \"access\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: verify order\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\n--- response_body\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 10: merge plugins form service and route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"_meta\": {\n                                \"priority\": 10000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"service_id\": \"1\",\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: verify order\n--- request\nGET /hello\n--- response_body\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 12: custom plugins sort is not affected by plugins reload\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri)\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n\n            local t = require(\"lib.test_admin\").test\n            local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                        ngx.HTTP_PUT)\n\n            ngx.say(org_body)\n\n            ngx.sleep(0.2)\n\n            local res, err = httpc:request_uri(uri)\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nserverless-post-function\nserverless-pre-function\ndone\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 13: merge plugins form plugin_configs and route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"_meta\": {\n                                \"priority\": 10000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function\\\");\n                                        end\"]\n                        }\n                    }\n                }]]\n            )\n            if code > 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function\\\");\n                                        end\"]\n                        }\n                    },\n                    \"plugin_config_id\": 1,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: verify order\n--- request\nGET /hello\n--- response_body\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 15: custom plugins sort on global_rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"_meta\": {\n                                \"priority\": 10000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-post-function on global rule\\\");\n                                        end\"]\n                        },\n                        \"serverless-pre-function\": {\n                            \"_meta\": {\n                                \"priority\": -2000\n                            },\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\"return function(conf, ctx)\n                                        ngx.say(\\\"serverless-pre-function on global rule\\\");\n                                        end\"]\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: verify order\n--- request\nGET /hello\n--- response_body\nserverless-post-function on global rule\nserverless-pre-function on global rule\nserverless-post-function\nserverless-pre-function\n\n\n\n=== TEST 17: delete global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/datadog.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen 8125 udp;\n        content_by_lua_block {\n            require(\"lib.mock_layer4\").dogstatsd()\n        }\n    }\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity check metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.datadog\")\n            local ok, err = plugin.check_schema({host = \"127.0.0.1\", port = 8125}, core.schema.TYPE_METADATA)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- setting the metadata\n            local code, meta_body = t('/apisix/admin/plugin_metadata/datadog',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8125\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"datadog\": {\n                                \"batch_max_size\" : 1,\n                                \"max_retry_count\": 0\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"datadog\",\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n\n            ngx.say(meta_body)\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\npassed\n\n\n\n=== TEST 3: testing behaviour with mock suite\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 4: testing behaviour with multiple requests\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n            -- request 2\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\nopentracing\nopentracing\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 5: testing behaviour with different namespace\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- Change the metadata\n            local code, meta_body = t('/apisix/admin/plugin_metadata/datadog',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8125,\n                        \"namespace\": \"mycompany\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(meta_body)\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: mycompany(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: mycompany\\.request\\.counter:1\\|c\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: mycompany\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: mycompany\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: mycompany\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: mycompany\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: mycompany\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 6: testing behaviour with different constant tags\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- Change the metadata\n            local code, meta_body = t('/apisix/admin/plugin_metadata/datadog',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8125,\n                        \"constant_tags\": [\n                                \"source:apisix\",\n                                \"new_tag:must\"\n                            ]\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(meta_body)\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 7: testing behaviour when route_name is missing - must fallback to route_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"datadog\": {\n                                \"batch_max_size\" : 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            -- making a request to the route\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- response_body\npassed\nopentracing\n--- wait: 0.5\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 8: testing behaviour with service id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"name\": \"service-1\",\n                        \"plugins\": {\n                            \"datadog\": {\n                                \"batch_max_size\" : 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            -- create a route with service level abstraction\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                       \"name\": \"route-1\",\n                       \"uri\": \"/opentracing\",\n                       \"service_id\": \"1\"\n\n                 }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            -- making a request to the route\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- response_body\npassed\npassed\nopentracing\n--- wait: 0.5\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 9: testing behaviour with prefer_name is false and service name is nil\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"datadog\": {\n                                \"batch_max_size\" : 1,\n                                \"prefer_name\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(body)\n\n            -- making a request to the route\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body)\n        }\n    }\n--- response_body\npassed\nopentracing\n--- wait: 0.5\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 10: testing behaviour with consumer\n--- apisix_yaml\nconsumers:\n  - username: user0\n    plugins:\n      key-auth:\n        key: user0\nroutes:\n  - uri: /opentracing\n    name: datadog\n    upstream:\n      nodes:\n        \"127.0.0.1:1982\": 1\n    plugins:\n      datadog:\n        batch_max_size: 1\n        max_retry_count: 0\n      key-auth: {}\n#END\n--- request\nGET /opentracing?apikey=user0\n--- response_body\nopentracing\n--- wait: 0.5\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,route_name:datadog,consumer:user0,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,consumer:user0,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,consumer:user0,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,route_name:datadog,consumer:user0,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,consumer:user0,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,route_name:datadog,consumer:user0,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 11: testing behaviour with include_path, include_method, and route level constant_tags\n--- apisix_yaml\nroutes:\n  - uri: /articles/*/comments\n    name: datadog\n    upstream:\n      nodes:\n        \"127.0.0.1:1982\": 1\n    plugins:\n      datadog:\n        batch_max_size: 1\n        max_retry_count: 0\n        include_path: true\n        include_method: true\n        constant_tags:\n          - route_tag1:foo\n          - route_tag2:bar\n      proxy-rewrite:\n        uri: /opentracing\n#END\n--- request\nGET /articles/12345/comments?foo=bar\n--- response_body\nopentracing\n--- wait: 0.5\n--- grep_error_log eval\nqr/message received: apisix(.+?(?=, ))/\n--- grep_error_log_out eval\nqr/message received: apisix\\.request\\.counter:1\\|c\\|#source:apisix,route_tag1:foo,route_tag2:bar,route_name:datadog,path:\\/articles\\/\\*\\/comments,method:GET,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.request\\.latency:[\\d.]+\\|h\\|#source:apisix,route_tag1:foo,route_tag2:bar,route_name:datadog,path:\\/articles\\/\\*\\/comments,method:GET,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.upstream\\.latency:[\\d.]+\\|h\\|#source:apisix,route_tag1:foo,route_tag2:bar,route_name:datadog,path:\\/articles\\/\\*\\/comments,method:GET,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.apisix\\.latency:[\\d.]+\\|h\\|#source:apisix,route_tag1:foo,route_tag2:bar,route_name:datadog,path:\\/articles\\/\\*\\/comments,method:GET,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.ingress\\.size:[\\d]+\\|ms\\|#source:apisix,route_tag1:foo,route_tag2:bar,route_name:datadog,path:\\/articles\\/\\*\\/comments,method:GET,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\nmessage received: apisix\\.egress\\.size:[\\d]+\\|ms\\|#source:apisix,route_tag1:foo,route_tag2:bar,route_name:datadog,path:\\/articles\\/\\*\\/comments,method:GET,balancer_ip:[\\d.]+,response_status:200,response_status_class:2xx,scheme:http\n/\n\n\n\n=== TEST 12: fails on invalid constant_tags value\n--- apisix_yaml\nroutes:\n  - uri: /articles/*/comments\n    name: datadog\n    upstream:\n      nodes:\n        \"127.0.0.1:1982\": 1\n    plugins:\n      datadog:\n        batch_max_size: 1\n        max_retry_count: 0\n        constant_tags:\n          - \"1 invalid tag\"\n      proxy-rewrite:\n        uri: /opentracing\n#END\n--- request\nGET /articles/12345/comments?foo=bar\n--- error_code: 404\n--- wait: 0.5\n--- error_log\nproperty \"constant_tags\" validation failed\n"
  },
  {
    "path": "t/plugin/degraphql.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: query list\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/graphql\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8888\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"degraphql\": {\n                            \"query\": \"{\\n  persons {\\n    id\\n    name\\n  }\\n}\\n\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/graphql\"\n            local headers = {\n                [\"Content-Type\"] = \"application/json\"\n            }\n            local res, err = httpc:request_uri(uri, {headers = headers, method = \"POST\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            local json = require(\"toolkit.json\")\n            ngx.say(json.encode(res.body))\n        }\n    }\n--- response_body\n\"{\\\"data\\\":{\\\"persons\\\":[{\\\"id\\\":\\\"7\\\",\\\"name\\\":\\\"Niek\\\"},{\\\"id\\\":\\\"8\\\",\\\"name\\\":\\\"Josh\\\"},{\\\"id\\\":\\\"9\\\",\\\"name\\\":\\\"Simon\\\"},{\\\"id\\\":\\\"10\\\",\\\"name\\\":\\\"Audun\\\"},{\\\"id\\\":\\\"11\\\",\\\"name\\\":\\\"Truls\\\"},{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Maria\\\"},{\\\"id\\\":\\\"13\\\",\\\"name\\\":\\\"Zahin\\\"},{\\\"id\\\":\\\"14\\\",\\\"name\\\":\\\"Roberto\\\"},{\\\"id\\\":\\\"15\\\",\\\"name\\\":\\\"Susanne\\\"},{\\\"id\\\":\\\"16\\\",\\\"name\\\":\\\"Live JS\\\"},{\\\"id\\\":\\\"17\\\",\\\"name\\\":\\\"Dave\\\"},{\\\"id\\\":\\\"18\\\",\\\"name\\\":\\\"Matt\\\"}]}}\"\n\n\n\n=== TEST 2: query with variables\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/graphql\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8888\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"degraphql\": {\n                            \"query\": \"query($name: String!) {\\n  persons(filter: { name: $name }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\",\n                            \"variables\": [\n                                \"name\"\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: hit\n--- request\nPOST /graphql\n{\n    \"name\": \"Josh\",\n    \"githubAccount\":\"npalm\"\n}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"data\":{\"persons\":[{\"id\":\"8\",\"name\":\"Josh\",\"blog\":\"\",\"githubAccount\":\"joshlong\",\"talks\":[]}]}}\n\n\n\n=== TEST 4: query with more variables\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/graphql\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8888\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"degraphql\": {\n                            \"query\": \"query($name: String!, $githubAccount: String!) {\\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\",\n                            \"variables\": [\n                                \"name\",\n                                \"githubAccount\"\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit\n--- request\nPOST /graphql\n{\n    \"name\":\"Niek\",\n    \"githubAccount\":\"npalm\"\n}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"data\":{\"persons\":[{\"id\":\"7\",\"name\":\"Niek\",\"blog\":\"https://040code.github.io\",\"githubAccount\":\"npalm\",\"talks\":[{\"id\":\"19\",\"title\":\"GraphQL - The Next API Language\"},{\"id\":\"20\",\"title\":\"Immutable Infrastructure\"}]}]}}\n\n\n\n=== TEST 6: without body\n--- request\nPOST /graphql\n--- error_log\nmissing request body\n--- error_code: 400\n\n\n\n=== TEST 7: invalid body\n--- request\nPOST /graphql\n\"AA\"\n--- more_headers\nContent-Type: application/json\n--- error_log\ninvalid request body can't be decoded\n--- error_code: 400\n\n\n\n=== TEST 8: proxy should ensure the Content-Type is correct\n--- request\nPOST /graphql\n{\n    \"name\":\"Niek\",\n    \"githubAccount\":\"npalm\"\n}\n--- response_body chomp\n{\"data\":{\"persons\":[{\"id\":\"7\",\"name\":\"Niek\",\"blog\":\"https://040code.github.io\",\"githubAccount\":\"npalm\",\"talks\":[{\"id\":\"19\",\"title\":\"GraphQL - The Next API Language\"},{\"id\":\"20\",\"title\":\"Immutable Infrastructure\"}]}]}}\n\n\n\n=== TEST 9: schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local query1 = \"query persons($name: String!) {\\n  persons(filter: { name: $name }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\"\n            local query2 = \"query githubAccount($name: String!, $githubAccount: String!) {\\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\"\n            for _, case in ipairs({\n                {input = {\n                }},\n                {input = {\n                    query = \"uery {}\",\n                }},\n                {input = {\n                    query = \"query {}\",\n                    variables = {},\n                }},\n                {input = {\n                    query = query1 .. query2,\n                }},\n            }) do\n                local code, body = t('/apisix/admin/global_rules/1',\n                    ngx.HTTP_PUT,\n                    {\n                        id = \"1\",\n                        plugins = {\n                            [\"degraphql\"] = case.input\n                        }\n                    }\n                )\n                ngx.print(body)\n            end\n    }\n}\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin degraphql err: property \\\"query\\\" is required\"}\n{\"error_msg\":\"failed to check the configuration of plugin degraphql err: failed to parse query: Syntax error near line 1\"}\n{\"error_msg\":\"failed to check the configuration of plugin degraphql err: property \\\"variables\\\" validation failed: expect array to have at least 1 items\"}\n{\"error_msg\":\"failed to check the configuration of plugin degraphql err: operation_name is required if multiple operations are present in the query\"}\n\n\n\n=== TEST 10: check operation_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"apisix.core.json\")\n            local query1 = \"query persons($name: String!) {\\n  persons(filter: { name: $name }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\"\n            local query2 = \"query githubAccount($name: String!, $githubAccount: String!) {\\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\"\n            local query = json.encode(query1 .. query2)\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/graphql\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8888\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"degraphql\": {\n                            \"query\": ]] .. query .. [[,\n                            \"operation_name\": \"persons\",\n                            \"variables\": [\n                                \"name\"\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit\n--- request\nPOST /graphql\n{\n    \"name\": \"Josh\",\n    \"githubAccount\":\"npalm\"\n}\n--- response_body chomp\n{\"data\":{\"persons\":[{\"id\":\"8\",\"name\":\"Josh\",\"blog\":\"\",\"githubAccount\":\"joshlong\",\"talks\":[]}]}}\n\n\n\n=== TEST 12: GET with variables\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/graphql\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8888\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"degraphql\": {\n                            \"query\": \"query($name: String!, $githubAccount: String!) {\\n  persons(filter: { name: $name, githubAccount: $githubAccount }) {\\n    id\\n    name\\n    blog\\n    githubAccount\\n    talks {\\n      id\\n      title\\n    }\\n  }\\n}\",\n                            \"variables\": [\n                                \"name\",\n                                \"githubAccount\"\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit\n--- request\nGET /graphql?name=Niek&githubAccount=npalm\n--- response_body chomp\n{\"data\":{\"persons\":[{\"id\":\"7\",\"name\":\"Niek\",\"blog\":\"https://040code.github.io\",\"githubAccount\":\"npalm\",\"talks\":[{\"id\":\"19\",\"title\":\"GraphQL - The Next API Language\"},{\"id\":\"20\",\"title\":\"Immutable Infrastructure\"}]}]}}\n\n\n\n=== TEST 14: GET without variables\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/graphql\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:8888\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"degraphql\": {\n                            \"query\": \"{\\n  persons {\\n    id\\n    name\\n  }\\n}\\n\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: hit\n--- request\nGET /graphql\n--- response_body chomp\n{\"data\":{\"persons\":[{\"id\":\"7\",\"name\":\"Niek\"},{\"id\":\"8\",\"name\":\"Josh\"},{\"id\":\"9\",\"name\":\"Simon\"},{\"id\":\"10\",\"name\":\"Audun\"},{\"id\":\"11\",\"name\":\"Truls\"},{\"id\":\"12\",\"name\":\"Maria\"},{\"id\":\"13\",\"name\":\"Zahin\"},{\"id\":\"14\",\"name\":\"Roberto\"},{\"id\":\"15\",\"name\":\"Susanne\"},{\"id\":\"16\",\"name\":\"Live JS\"},{\"id\":\"17\",\"name\":\"Dave\"},{\"id\":\"18\",\"name\":\"Matt\"}]}}\n"
  },
  {
    "path": "t/plugin/dubbo-proxy/route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/mod_dubbo/) {\n    plan(skip_all => \"mod_dubbo not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->disable_dubbo) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - dubbo-proxy\n    - response-rewrite\n    - proxy-rewrite\n    - key-auth\n_EOC_\n\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if ($block->apisix_yaml) {\n        my $upstream = <<_EOC_;\nupstreams:\n  - nodes:\n        \"127.0.0.1:20880\": 1\n    type: roundrobin\n    id: 1\n#END\n_EOC_\n\n        $block->set_value(\"apisix_yaml\", $block->apisix_yaml . $upstream);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: ignore route's dubbo configuration if dubbo is disable globally\n--- disable_dubbo\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n--- response_body\nhello world\n\n\n\n=== TEST 2: check schema\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            method: hello\n    upstream_id: 1\n--- error_log\nproperty \"service_version\" is required\n--- error_code: 404\n\n\n\n=== TEST 3: sanity\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: hello\n    upstream_id: 1\n--- more_headers\nExtra-Arg-K: V\n--- response_headers\nGot-extra-arg-k: V\n--- response_body\ndubbo success\n\n\n\n=== TEST 4: enabled in service\n--- apisix_yaml\nroutes:\n  - uri: /hello\n    service_id: 1\n\nservices:\n    -\n        plugins:\n            dubbo-proxy:\n                service_name: org.apache.dubbo.backend.DemoService\n                service_version: 1.0.0\n                method: hello\n        id: 1\n        upstream_id: 1\n--- response_body\ndubbo success\n\n\n\n=== TEST 5: work with consumer\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: true\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, message = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                     \"username\":\"jack\",\n                     \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"jack\"\n                        }\n                     }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            local code, message = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\":{\n                        \"nodes\": {\n                            \"127.0.0.1:20880\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"dubbo-proxy\": {\n                            \"service_name\": \"org.apache.dubbo.backend.DemoService\",\n                            \"service_version\": \"1.0.0\",\n                            \"method\": \"hello\"\n                        },\n                        \"key-auth\": {}\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(message)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: blocked\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: true\n--- error_code: 401\n\n\n\n=== TEST 7: passed\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: true\n--- more_headers\napikey: jack\n--- response_body\ndubbo success\n\n\n\n=== TEST 8: rewrite response\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        response-rewrite:\n            headers:\n                fruit: banana\n            body: \"hello world\\n\"\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: hello\n    upstream_id: 1\n\n--- response_body\nhello world\n--- response_headers\nfruit: banana\n\n\n\n=== TEST 9: rewrite request\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        proxy-rewrite:\n            headers:\n                extra-arg-fruit: banana\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: hello\n    upstream_id: 1\n\n--- response_body\ndubbo success\n--- response_headers\nGot-extra-arg-fruit: banana\n\n\n\n=== TEST 10: use uri as default method\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n    upstream_id: 1\n\n--- response_body\ndubbo success\n\n\n\n=== TEST 11: version mismatch\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 0.1.0\n            method: hello\n    upstream_id: 1\n--- more_headers\nExtra-Arg-K: V\n--- error_code: 502\n--- error_log\nmay be version or group mismatch\n"
  },
  {
    "path": "t/plugin/dubbo-proxy/upstream.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/mod_dubbo/) {\n    plan(skip_all => \"mod_dubbo not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\nworker_connections(256);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n\n    if (!defined $block->disable_dubbo) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - dubbo-proxy\n_EOC_\n\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: retry\n--- apisix_yaml\nupstreams:\n    - nodes:\n        - host: 127.0.0.1\n          port: 20881\n          weight: 1\n        - host: 127.0.0.1\n          port: 20880\n          weight: 1\n      type: roundrobin\n      id: 1\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: hello\n    upstream_id: 1\n#END\n--- response_body\ndubbo success\n--- ignore_error_log\n\n\n\n=== TEST 2: upstream return error\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: fail\n    upstream_id: 1\nupstreams:\n  - nodes:\n        \"127.0.0.1:20880\": 1\n    type: roundrobin\n    id: 1\n#END\n--- response_body\ndubbo fail\n--- error_code: 503\n\n\n\n=== TEST 3: upstream timeout\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: timeout\n    upstream_id: 1\nupstreams:\n  - nodes:\n        \"127.0.0.1:20880\": 1\n    type: roundrobin\n    timeout:\n        connect: 0.1\n        read: 0.1\n        send: 0.1\n    id: 1\n#END\n--- error_log\nupstream timed out\n--- error_code: 504\n\n\n\n=== TEST 4: upstream return non-string status code\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    plugins:\n        dubbo-proxy:\n            service_name: org.apache.dubbo.backend.DemoService\n            service_version: 1.0.0\n            method: badStatus\n    upstream_id: 1\nupstreams:\n  - nodes:\n        \"127.0.0.1:20880\": 1\n    type: roundrobin\n    id: 1\n#END\n--- response_body\nok\n"
  },
  {
    "path": "t/plugin/echo.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.echo\")\n            local ok, err = plugin.check_schema({before_body = \"body before\", body = \"body to attach\",\n            after_body = \"body to attach\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong type of integer\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.echo\")\n            local ok, err = plugin.check_schema({before_body = \"body before\", body = \"body to attach\",\n            after_body = 10})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"after_body\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 3: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"echo\": {\n                                \"before_body\": \"before the body modification \",\n                                \"body\":\"hello upstream\",\n                                \"after_body\": \" after the body modification.\",\n                                \"headers\": {\n                                    \"Location\":\"https://www.iresty.com\",\n                                    \"Authorization\": \"userpass\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: access\n--- request\nGET /hello\n--- response_body chomp\nbefore the body modification hello upstream after the body modification.\n--- response_headers\nLocation: https://www.iresty.com\nAuthorization: userpass\n--- wait: 0.2\n\n\n\n=== TEST 5: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"echo\": {\n                                \"before_body\": \"before the body modification \",\n                                \"headers\": {\n                                    \"Location\":\"https://www.iresty.com\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: access without upstream body change\n--- request\nGET /hello\n--- response_body\nbefore the body modification hello world\n--- response_headers\nLocation: https://www.iresty.com\n--- wait: 0.2\n--- wait: 0.2\n\n\n\n=== TEST 7: print the `conf` in etcd, no dirty data\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local encode_with_keys_sorted = require(\"toolkit.json\").encode\n\n            local code, _, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"echo\": {\n                            \"before_body\": \"before the body modification \",\n                            \"headers\": {\n                                \"Location\":\"https://www.iresty.com\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local resp_data = core.json.decode(body)\n            ngx.say(encode_with_keys_sorted(resp_data.value.plugins))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"echo\":{\"before_body\":\"before the body modification \",\"headers\":{\"Location\":\"https://www.iresty.com\"}}}\n\n\n\n=== TEST 8: set body with chunked upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"echo\": {\n                                \"body\":\"hello upstream\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello_chunked\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: access\n--- request\nGET /hello_chunked\n--- response_body chomp\nhello upstream\n\n\n\n=== TEST 10: add before/after body with chunked upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"echo\": {\n                                \"before_body\": \"before the body modification \",\n                                \"after_body\": \" after the body modification.\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello_chunked\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: access\n--- request\nGET /hello_chunked\n--- response_body chomp\nbefore the body modification hello world\n after the body modification.\n"
  },
  {
    "path": "t/plugin/elasticsearch-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local ok, err\n            local configs = {\n                -- full configuration\n                {\n                    endpoint_addr = \"http://127.0.0.1:9200\",\n                    field = {\n                        index = \"services\"\n                    },\n                    auth = {\n                        username = \"elastic\",\n                        password = \"123456\"\n                    },\n                    ssl_verify = false,\n                    timeout = 60,\n                    max_retry_count = 0,\n                    retry_delay = 1,\n                    buffer_duration = 60,\n                    inactive_timeout = 2,\n                    batch_max_size = 10,\n                },\n                -- minimize configuration\n                {\n                    endpoint_addr = \"http://127.0.0.1:9200\",\n                    field = {\n                        index = \"services\"\n                    }\n                },\n                -- property \"endpoint_addr\" is required\n                {\n                    field = {\n                        index = \"services\"\n                    }\n                },\n                -- property \"field\" is required\n                {\n                    endpoint_addr = \"http://127.0.0.1:9200\",\n                },\n                -- property \"index\" is required\n                {\n                    endpoint_addr = \"http://127.0.0.1:9200\",\n                    field = {}\n                },\n                -- property \"endpoint\" must not end with \"/\"\n                {\n                    endpoint_addr = \"http://127.0.0.1:9200/\",\n                    field = {\n                        index = \"services\"\n                    }\n                }\n            }\n\n            local plugin = require(\"apisix.plugins.elasticsearch-logger\")\n            for i = 1, #configs do\n                ok, err = plugin.check_schema(configs[i])\n                if err then\n                    ngx.say(err)\n                else\n                    ngx.say(\"passed\")\n                end\n            end\n        }\n    }\n--- response_body_like\npassed\npassed\nvalue should match only one schema, but matches none\nvalue should match only one schema, but matches none\nproperty \"field\" validation failed: property \"index\" is required\nproperty \"endpoint_addr\" validation failed: failed to match pattern \"\\[\\^/\\]\\$\" with \"http://127.0.0.1:9200/\"\n\n\n\n=== TEST 2: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/elasticsearch-logger',\n                               ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9200\",\n                        field = {\n                            index = \"services\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: test route (success write)\n--- extra_init_by_lua\n    local core = require(\"apisix.core\")\n    local http = require(\"resty.http\")\n    local ngx_re = require(\"ngx.re\")\n    local log_util = require(\"apisix.utils.log-util\")\n    log_util.inject_get_full_log(function(ngx, conf)\n        return {\n            test = \"test\"\n        }\n    end)\n\n    http.request_uri = function(self, uri, params)\n        if params.method == \"GET\" then\n            return {\n                status = 200,\n                body = [[\n                {\n                    \"version\": {\n                        \"number\": \"8.10.2\"\n                    }\n                }\n                ]]\n            }\n        end\n        if not params.body or type(params.body) ~= \"string\" then\n            return nil, \"invalid params body\"\n        end\n\n        local arr = ngx_re.split(params.body, \"\\n\")\n        if not arr or #arr ~= 2 then\n            return nil, \"invalid params body\"\n        end\n\n        local entry = core.json.decode(arr[2])\n        local origin_entry = log_util.get_full_log(ngx, {})\n        for k, v in pairs(origin_entry) do\n            local vv = entry[k]\n            if not vv or vv ~= v then\n                return nil, \"invalid params body\"\n            end\n        end\n\n        core.log.error(\"check elasticsearch full log body success\")\n        return {\n            status = 200,\n            body = \"success\"\n        }, nil\n    end\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\ncheck elasticsearch full log body success\n\n\n\n=== TEST 4: set route (auth)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: test route (auth success)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[elasticsearch-logger] successfully processed the entries\n\n\n\n=== TEST 6: set route (no auth)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: test route (no auth, failed)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nfailed to process entries: elasticsearch server returned status: 401\n\n\n\n=== TEST 8: set route (error auth)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"111111\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: test route (error auth failed)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[elasticsearch-logger] failed to process entries\nBatch Processor[elasticsearch-logger] exceeded the max_retry_count\n\n\n\n=== TEST 10: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/elasticsearch-logger',\n                ngx.HTTP_PUT, [[{\n                    \"log_format\": {\n                        \"custom_host\": \"$host\",\n                        \"custom_timestamp\": \"$time_iso8601\",\n                        \"custom_client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body_like\npassed\npassed\n\n\n\n=== TEST 11: hit route and check custom elasticsearch logger\n--- extra_init_by_lua\n    local core = require(\"apisix.core\")\n    local http = require(\"resty.http\")\n    local ngx_re = require(\"ngx.re\")\n    local log_util = require(\"apisix.utils.log-util\")\n    log_util.inject_get_custom_format_log(function(ctx, format)\n        return {\n            test = \"test\"\n        }\n    end)\n\n    http.request_uri = function(self, uri, params)\n        if params.method == \"GET\" then\n            return {\n                status = 200,\n                body = [[\n                {\n                    \"version\": {\n                        \"number\": \"8.10.2\"\n                    }\n                }\n                ]]\n            }\n        end\n        if not params.body or type(params.body) ~= \"string\" then\n            return nil, \"invalid params body\"\n        end\n\n        local arr = ngx_re.split(params.body, \"\\n\")\n        if not arr or #arr ~= 2 then\n            return nil, \"invalid params body\"\n        end\n\n        local entry = core.json.decode(arr[2])\n        local origin_entry = log_util.get_custom_format_log(nil, nil)\n        for k, v in pairs(origin_entry) do\n            local vv = entry[k]\n            if not vv or vv ~= v then\n                return nil, \"invalid params body\"\n            end\n        end\n\n        core.log.error(\"check elasticsearch custom body success\")\n        return {\n            status = 200,\n            body = \"success\"\n        }, nil\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 2\n--- error_log\ncheck elasticsearch custom body success\n\n\n\n=== TEST 12: data encryption for auth.password\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"elasticsearch-logger\"].auth.password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"elasticsearch-logger\"].auth.password)\n        }\n    }\n--- response_body\n123456\nPTQvJEaPcNOXcOHeErC0XQ==\n\n\n\n=== TEST 13: add plugin on routes using multi elasticsearch-logger\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addrs = {\"http://127.0.0.1:9200\", \"http://127.0.0.1:9201\"},\n                        field = {\n                            index = \"services\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: to show that different endpoints will be chosen randomly\n--- config\n    location /t {\n        content_by_lua_block {\n            local code_count = {}\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 12 do\n                local code, body = t('/hello', ngx.HTTP_GET)\n                if code ~= 200 then\n                    ngx.say(\"code: \", code, \" body: \", body)\n                end\n                code_count[code] = (code_count[code] or 0) + 1\n            end\n\n            local code_arr = {}\n            for code, count in pairs(code_count) do\n                table.insert(code_arr, {code = code, count = count})\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(code_arr))\n            ngx.exit(200)\n        }\n    }\n--- response_body\n[{\"code\":200,\"count\":12}]\n--- error_log\nhttp://127.0.0.1:9200/_bulk\nhttp://127.0.0.1:9201/_bulk\n\n\n\n=== TEST 15: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        log_format = {\n                            custom_host = \"$host\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route and check custom elasticsearch logger\n--- extra_init_by_lua\n    local core = require(\"apisix.core\")\n    local http = require(\"resty.http\")\n    local ngx_re = require(\"ngx.re\")\n    local log_util = require(\"apisix.utils.log-util\")\n    log_util.inject_get_custom_format_log(function(ctx, format)\n        return {\n            test = \"test\"\n        }\n    end)\n\n    http.request_uri = function(self, uri, params)\n        if params.method == \"GET\" then\n            return {\n                status = 200,\n                body = [[\n                {\n                    \"version\": {\n                        \"number\": \"8.10.2\"\n                    }\n                }\n                ]]\n            }\n        end\n        if not params.body or type(params.body) ~= \"string\" then\n            return nil, \"invalid params body\"\n        end\n\n        local arr = ngx_re.split(params.body, \"\\n\")\n        if not arr or #arr ~= 2 then\n            return nil, \"invalid params body\"\n        end\n\n        local entry = core.json.decode(arr[2])\n        local origin_entry = log_util.get_custom_format_log(nil, nil)\n        for k, v in pairs(origin_entry) do\n            local vv = entry[k]\n            if not vv or vv ~= v then\n                return nil, \"invalid params body\"\n            end\n        end\n\n        core.log.error(\"check elasticsearch custom body success\")\n        return {\n            status = 200,\n            body = \"success\"\n        }, nil\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 2\n--- error_log\ncheck elasticsearch custom body success\n\n\n\n=== TEST 17: using unsupported field (type) for elasticsearch v8 should work normally\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\",\n                            type = \"collector\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: test route (auth success)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- no_error_log\nAction/metadata line [1] contains an unknown parameter [_type]\n\n\n\n=== TEST 19: add plugin with 'include_req_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/elasticsearch-logger', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1,\n                        include_req_body = true\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- error_log\n\"body\":\"{\\\"sample_payload\\\":\\\"hello\\\"}\"\n\n\n\n=== TEST 20: add plugin with 'include_resp_body' setting, collect response log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/elasticsearch-logger', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1,\n                        include_req_body = true,\n                        include_resp_body = true\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- error_log\n\"body\":\"hello world\\n\"\n\n\n\n=== TEST 21: set route (auth) - check compat with version 9\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9301\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: test route (auth success)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[elasticsearch-logger] successfully processed the entries\n\n\n\n=== TEST 23: set route (auth) - check compat with version 7\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9401\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 24: test route (auth success)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[elasticsearch-logger] successfully processed the entries\n\n\n\n=== TEST 25: set route (auth) - check compat with version 6\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9501\",\n                        field = {\n                            index = \"services\"\n                        },\n                        auth = {\n                            username = \"elastic\",\n                            password = \"123456\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 26: test route (auth success)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[elasticsearch-logger] successfully processed the entries\n"
  },
  {
    "path": "t/plugin/elasticsearch-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: should drop entries when max_pending_entries is exceededA\n--- extra_yaml_config\nplugins:\n  - elasticsearch-logger\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"elasticsearch-logger\"] = {\n                            endpoint_addr = \"http://127.0.0.1:1234\",\n                            field = {\n                                index = \"services\"\n                            },\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/elasticsearch-logger', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n\n\n\n=== TEST 2: set route with header auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"elasticsearch-logger\"] = {\n                        endpoint_addr = \"http://127.0.0.1:9201\",\n                        field = {\n                            index = \"services\"\n                        },\n                        headers = {\n                            Authorization = \"Basic ZWxhc3RpYzoxMjM0NTY=\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: test route (auth success)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[elasticsearch-logger] successfully processed the entries\n"
  },
  {
    "path": "t/plugin/error-log-logger-clickhouse.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - error-log-logger\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: test schema checker\n--- config\n    location /t {\n        content_by_lua_block {\n        local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.error-log-logger\")\n            local ok, err = plugin.check_schema(\n                {\n                    clickhouse = {\n                        user = \"default\",\n                        password = \"a\",\n                        database = \"default\",\n                        logtable = \"t\",\n                        endpoint_addr = \"http://127.0.0.1:1980/clickhouse_logger_server\"\n                    }\n                },\n                core.schema.TYPE_METADATA\n            )\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: test unreachable server\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"clickhouse\": {\n                                \"user\": \"default\",\n                                \"password\": \"a\",\n                                \"database\": \"default\",\n                                \"logtable\": \"t\",\n                                \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\"\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.warn(\"this is a warning message for test2.\")\n        }\n    }\n--- response_body\n--- error_log\nthis is a warning message for test2\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:a\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 3\n\n\n\n=== TEST 3: put plugin metadata and log an error level message\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"clickhouse\": {\n                        \"user\": \"default\",\n                        \"password\": \"a\",\n                        \"database\": \"default\",\n                        \"logtable\": \"t\",\n                        \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\"\n                    },\n                    \"batch_max_size\": 15,\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.warn(\"this is a warning message for test3.\")\n        }\n    }\n--- response_body\n--- error_log\nthis is a warning message for test3\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:a\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 5\n\n\n\n=== TEST 4: log a warn level message\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is a warning message for test4.\")\n        }\n    }\n--- response_body\n--- error_log\nthis is a warning message for test4\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:a\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 5\n\n\n\n=== TEST 5: log some messages\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is a warning message for test5.\")\n        }\n    }\n--- response_body\n--- error_log\nthis is a warning message for test5\nclickhouse body: INSERT INTO t FORMAT JSONEachRow\nclickhouse headers: x-clickhouse-key:a\nclickhouse headers: x-clickhouse-user:default\nclickhouse headers: x-clickhouse-database:default\n--- wait: 5\n\n\n\n=== TEST 6: log an info level message\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.info(\"this is an info message for test6.\")\n        }\n    }\n--- response_body\n--- error_log\nthis is an info message for test6\n--- wait: 5\n\n\n\n=== TEST 7: delete metadata for the plugin, recover to the default\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: data encryption for clickhouse.password\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"clickhouse\": {\n                        \"user\": \"default\",\n                        \"password\": \"bar\",\n                        \"database\": \"default\",\n                        \"logtable\": \"t\",\n                        \"endpoint_addr\": \"http://127.0.0.1:1980/clickhouse_logger_server\"\n                    },\n                    \"batch_max_size\": 15,\n                    \"inactive_timeout\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value[\"clickhouse\"].password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/plugin_metadata/error-log-logger'))\n\n            ngx.say(res.body.node.value[\"clickhouse\"].password)\n        }\n    }\n--- response_body\nbar\n77+NmbYqNfN+oLm0aX5akg==\n\n\n\n=== TEST 9: verify use the decrypted password to connect to clickhouse\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is a warning message for test9\")\n        }\n    }\n--- response_body\n--- error_log\nthis is a warning message for test9\nclickhouse headers: x-clickhouse-key:bar\n--- wait: 5\n"
  },
  {
    "path": "t/plugin/error-log-logger-kafka.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - error-log-logger\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: test schema checker\n--- config\n    location /t {\n        content_by_lua_block {\n        local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.error-log-logger\")\n            local ok, err = plugin.check_schema(\n                {\n                    kafka = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = 9092\n                            }\n                        },\n                        kafka_topic = \"test2\"\n                    }\n                },\n                core.schema.TYPE_METADATA\n            )\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: put plugin metadata and log an error level message - no auth kafka\n--- extra_init_by_lua\n    local core = require(\"apisix.core\")\n    local producer = require(\"resty.kafka.producer\")\n    local old_producer_new = producer.new\n    producer.new = function(self, broker_list, producer_config, cluster_name)\n        core.log.info(\"broker_config is: \", core.json.delay_encode(producer_config))\n        return old_producer_new(self, broker_list, producer_config, cluster_name)\n    end\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"kafka\": {\n                        \"brokers\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 9092\n                        }],\n                        \"kafka_topic\": \"test2\",\n                        \"meta_refresh_interval\": 1\n                    },\n                    \"level\": \"ERROR\",\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.error(\"this is a error message for test2.\")\n        }\n    }\n--- error_log eval\n[qr/this is a error message for test2/,\nqr/send data to kafka: .*test2/,\nqr/broker_config is: \\{.*\"refresh_interval\":1000/,\n]\n--- wait: 3\n\n\n\n=== TEST 3: log a error level message\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"this is a error message for test3.\")\n        }\n    }\n--- error_log eval\n[qr/this is a error message for test3/,\nqr/send data to kafka: .*test3/]\n--- wait: 5\n\n\n\n=== TEST 4: log an warning level message - will not send to kafka brokers\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is an warning message for test4.\")\n        }\n    }\n--- error_log\nthis is an warning message for test4\n--- no_error_log eval\nqr/send data to kafka: .*test4/\n--- wait: 5\n\n\n\n=== TEST 5: put plugin metadata and log an error level message - auth kafka\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"kafka\": {\n                        \"brokers\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 19094,\n                            \"sasl_config\": {\n                                \"mechanism\": \"PLAIN\",\n                                \"user\": \"admin\",\n                                \"password\": \"admin-secret\"\n                            }\n                        }],\n                        \"producer_type\": \"sync\",\n                        \"kafka_topic\": \"test4\"\n                    },\n                    \"level\": \"ERROR\",\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.error(\"this is a error message for test5.\")\n        }\n    }\n--- error_log eval\n[qr/this is a error message for test5/,\nqr/send data to kafka: .*test5/]\n--- wait: 3\n\n\n\n=== TEST 6: delete metadata for the plugin, recover to the default\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/error-log-logger-skywalking.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nworker_connections(128);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - error-log-logger\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: test schema checker\n--- config\n    location /t {\n        content_by_lua_block {\n        local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.error-log-logger\")\n            local ok, err = plugin.check_schema(\n                {\n                    skywalking = {\n                        endpoint_addr = \"http://127.0.0.1\"\n                    }\n                },\n                core.schema.TYPE_METADATA\n            )\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: test unreachable server\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skywalking\": {\n                        \"endpoint_addr\": \"http://127.0.0.1:1988/log\"\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/Batch Processor\\[error-log-logger\\] failed to process entries: error while sending data to skywalking\\[http:\\/\\/127.0.0.1:1988\\/log\\] connection refused, context: ngx.timer/\n--- wait: 3\n\n\n\n=== TEST 3: put plugin metadata and log an error level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skywalking\": {\n                        \"endpoint_addr\": \"http://127.0.0.1:1982/log\",\n                        \"service_instance_name\": \"instance\"\n                    },\n                    \"batch_max_size\": 15,\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.error(\"this is an error message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log\nthis is an error message for test\n--- wait: 5\n\n\n\n=== TEST 4: log a warn level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/.*\\[\\{\\\"body\\\":\\{\\\"text\\\":\\{\\\"text\\\":\\\".*this is a warning message for test.*\\\"\\}\\},\\\"endpoint\\\":\\\"\\\",\\\"service\\\":\\\"APISIX\\\",\\\"serviceInstance\\\":\\\"instance\\\".*/\n--- wait: 5\n\n\n\n=== TEST 5: log some messages\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.error(\"this is an error message for test.\")\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/.*\\[\\{\\\"body\\\":\\{\\\"text\\\":\\{\\\"text\\\":\\\".*this is an error message for test.*\\\"\\}\\},\\\"endpoint\\\":\\\"\\\",\\\"service\\\":\\\"APISIX\\\",\\\"serviceInstance\\\":\\\"instance\\\".*\\},\\{\\\"body\\\":\\{\\\"text\\\":\\{\\\"text\\\":\\\".*this is a warning message for test.*\\\"\\}\\}.*/\n--- wait: 5\n\n\n\n=== TEST 6: log an info level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.info(\"this is an info message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log eval\nqr/.*\\[\\{\\\"body\\\":\\{\\\"text\\\":\\{\\\"text\\\":\\\".*this is an info message for test.*\\\"\\}\\},\\\"endpoint\\\":\\\"\\\",\\\"service\\\":\\\"APISIX\\\",\\\"serviceInstance\\\":\\\"instance\\\".*/\n--- wait: 5\n\n\n\n=== TEST 7: delete metadata for the plugin, recover to the default\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /tg\n--- response_body\npassed\n\n\n\n=== TEST 8: put plugin metadata with $hostname and log an error level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"skywalking\": {\n                        \"endpoint_addr\": \"http://127.0.0.1:1982/log\",\n                        \"service_instance_name\": \"$hostname\"\n                    },\n                    \"batch_max_size\": 15,\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.error(\"this is an error message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log eval\nqr/\\\\\\\"serviceInstance\\\\\\\":\\\\\\\"\\$hostname\\\\\\\"/\nqr/\\\\\\\"serviceInstance\\\\\\\":\\\\\\\"\\\\\\\"/\n--- wait: 0.5\n"
  },
  {
    "path": "t/plugin/error-log-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $stream_single_server = <<_EOC_;\n    # fake server, only for test\n    server {\n        listen 1999;\n\n        content_by_lua_block {\n            local exiting = ngx.worker.exiting\n            local sock, err = ngx.req.socket(true)\n            if not sock then\n                ngx.log(ngx.WARN, \"socket error:\", err)\n                return\n            end\n\n            sock:settimeout(30 * 1000)\n            while(not exiting())\n            do\n                local data, err =  sock:receive()\n                if (data) then\n                    ngx.log(ngx.INFO, \"[Server] receive data:\", data)\n                else\n                    if err ~= \"timeout\" then\n                        ngx.log(ngx.WARN, \"socket error:\", err)\n                        return\n                    end\n                end\n            end\n\n        }\n    }\n_EOC_\n\n    $block->set_value(\"stream_config\", $stream_single_server);\n\n    my $stream_default_server = <<_EOC_;\n        content_by_lua_block {\n            ngx.log(ngx.INFO, \"a stream server\")\n        }\n_EOC_\n\n    $block->set_value(\"stream_server_config\", $stream_default_server);\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - error-log-logger\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: not enable the plugin\n--- extra_yaml_config\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log\nerror-log-logger\n--- wait: 2\n\n\n\n=== TEST 2: enable the plugin, but not init the metadata\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/please set the correct plugin_metadata for error-log-logger/\n--- wait: 2\n\n\n\n=== TEST 3: set a wrong metadata\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"tcp\": {\n                        \"port\": 1999\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n\n            -- ensure the request is rejected even this plugin doesn't\n            -- have check_schema method\n            ngx.status = code\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- error_code: 400\n--- response_body\n--- error_log eval\nqr/please set the correct plugin_metadata for error-log-logger/\n--- wait: 2\n\n\n\n=== TEST 4: test unreachable server\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"tcp\": {\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 2999\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log eval\nqr/\\[Server\\] receive data:.*this is a warning message for test./\n--- wait: 3\n\n\n\n=== TEST 5: log a warn level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"tcp\": {\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 1999\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/\\[Server\\] receive data:.*this is a warning message for test./\n--- wait: 5\n\n\n\n=== TEST 6: log an error level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.sleep(2)\n            core.log.error(\"this is an error message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/\\[Server\\] receive data:.*this is an error message for test./\n--- wait: 5\n\n\n\n=== TEST 7: log an info level message\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.sleep(2)\n            core.log.info(\"this is an info message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log eval\nqr/\\[Server\\] receive data:.*this is an info message for test./\n--- wait: 5\n\n\n\n=== TEST 8: delete metadata for the plugin, recover to the default\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /tg\n--- response_body\npassed\n\n\n\n=== TEST 9: want to reload the plugin by route\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"error-log-logger\": {\n                            \"tcp\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1999\n                            },\n                            \"inactive_timeout\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n                )\n            -- reload\n            code, body = t('/apisix/admin/plugins/reload',\n                                    ngx.HTTP_PUT)\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/please set the correct plugin_metadata for error-log-logger/\n--- wait: 2\n\n\n\n=== TEST 10: avoid sending stale error log\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            core.log.warn(\"this is a warning message for test.\")\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"tcp\": {\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 1999\n                    },\n                    \"level\": \"ERROR\",\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.error(\"this is an error message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log eval\nqr/\\[Server\\] receive data:.*this is a warning message for test./\n--- error_log eval\nqr/\\[Server\\] receive data:.*this is an error message for test./\n--- wait: 5\n\n\n\n=== TEST 11: delete the route\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /tg\n--- response_body\npassed\n\n\n\n=== TEST 12: log a warn level message (schema compatibility testing)\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"tcp\": {\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 1999\n                    },\n                    \"inactive_timeout\": 1\n                }]]\n                )\n            ngx.sleep(2)\n            core.log.warn(\"this is a warning message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/\\[Server\\] receive data:.*this is a warning message for test./\n--- wait: 5\n\n\n\n=== TEST 13: log an error level message (schema compatibility testing)\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.sleep(2)\n            core.log.error(\"this is an error message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- error_log eval\nqr/\\[Server\\] receive data:.*this is an error message for test./\n--- wait: 5\n\n\n\n=== TEST 14: log an info level message (schema compatibility testing)\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.sleep(2)\n            core.log.info(\"this is an info message for test.\")\n        }\n    }\n--- request\nGET /tg\n--- response_body\n--- no_error_log eval\nqr/\\[Server\\] receive data:.*this is an info message for test./\n--- wait: 5\n\n\n\n=== TEST 15: delete metadata for the plugin, recover to the default (schema compatibility testing)\n--- config\n    location /tg {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /tg\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/example.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n            local ok, err = plugin.check_schema({i = 1, s = \"s\", t = {1}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: missing args\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n\n            local ok, err = plugin.check_schema({s = \"s\", t = {1}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"i\" is required\ndone\n\n\n\n=== TEST 3: small then minimum\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n            local ok, err = plugin.check_schema({i = -1, s = \"s\", t = {1, 2}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"i\" validation failed: expected -1 to be at least 0\ndone\n\n\n\n=== TEST 4: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n            local ok, err = plugin.check_schema({i = 1, s = 123, t = {1}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"s\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 5: the size of array < minItems\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n            local ok, err = plugin.check_schema({i = 1, s = '123', t = {}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"t\" validation failed: expect array to have at least 1 items\ndone\n\n\n\n=== TEST 6: load plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugins, err = require(\"apisix.plugin\").load()\n            if not plugins then\n                ngx.say(\"failed to load plugins: \", err)\n            end\n\n            local encode_json = require(\"toolkit.json\").encode\n            local conf = {}\n            local ctx = {}\n            for _, plugin in ipairs(plugins) do\n                ngx.say(\"plugin name: \", plugin.name,\n                        \" priority: \", plugin.priority)\n\n                plugin.rewrite(conf, ctx)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nplugin name: example-plugin priority: 0\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  etcd:\n    host:\n      - \"http://127.0.0.1:2379\" # etcd address\n    prefix: \"/apisix\"           # apisix configurations prefix\n    timeout: 1\nplugins:\n  - example-plugin\n  - not-exist-plugin\n--- grep_error_log eval\nqr/\\[error\\].*/\n--- grep_error_log_out eval\nqr/module 'apisix.plugins.not-exist-plugin' not found/\n\n\n\n=== TEST 7: filter plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugin\")\n\n            local all_plugins, err = plugin.load()\n            if not all_plugins then\n                ngx.say(\"failed to load plugins: \", err)\n            end\n\n            local filter_plugins = plugin.filter(nil, {\n                value = {\n                    plugins = {\n                        [\"example-plugin\"] = {i = 1, s = \"s\", t = {1, 2}},\n                        [\"new-plugin\"] = {a = \"a\"},\n                    }\n                },\n                modifiedIndex = 1,\n            })\n\n            local encode_json = require(\"toolkit.json\").encode\n            for i = 1, #filter_plugins, 2 do\n                local plugin = filter_plugins[i]\n                local plugin_conf = filter_plugins[i + 1]\n                ngx.say(\"plugin [\", plugin.name, \"] config: \",\n                        encode_json(plugin_conf))\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nplugin [example-plugin] config: {\"i\":1,\"s\":\"s\",\"t\":[1,2]}\n\n\n\n=== TEST 8: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"example-plugin\": {\n                                \"i\": 11,\n                                \"ip\": \"127.0.0.1\",\n                                \"port\": 1981\n                            }\n                        },\n                        \"uri\": \"/server_port\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route\n--- request\nGET /server_port\n--- response_body_like eval\nqr/1981/\n\n\n\n=== TEST 10: set disable = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n            local ok, err = plugin.check_schema({\n                i = 1, s = \"s\", t = {1},\n                disable = true,\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 11: set disable = false\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.example-plugin\")\n            local ok, err = plugin.check_schema({\n                i = 1, s = \"s\", t = {1},\n                disable = true,\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 12: body filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"example-plugin\": {\n                                \"i\": 11,\n                                \"ip\": \"127.0.0.1\",\n                                \"port\": 1981\n                            }\n                        },\n                        \"uri\": \"/server_port\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route\n--- request\nGET /server_port\n--- grep_error_log eval\nqr/plugin (body_filter|delayed_body_filter) phase, eof: (false|true)/\n--- grep_error_log_out\nplugin body_filter phase, eof: false\nplugin delayed_body_filter phase, eof: false\nplugin body_filter phase, eof: true\nplugin delayed_body_filter phase, eof: true\n"
  },
  {
    "path": "t/plugin/ext-plugin/conf_token.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nworkers(3);\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\nworker_connections(1024);\n\n$ENV{\"PATH\"} = $ENV{PATH} . \":\" . $ENV{TEST_NGINX_HTML_DIR};\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen unix:\\$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $unix_socket_path = $ENV{\"TEST_NGINX_HTML_DIR\"} . \"/nginx.sock\";\n    my $orig_extra_yaml_config = $block->extra_yaml_config // \"\";\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    path_for_test: $unix_socket_path\n    cmd: $cmd\n_EOC_\n    $extra_yaml_config = $extra_yaml_config . $orig_extra_yaml_config;\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\"a\":\"b\"}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: share conf token in different workers\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local t = {}\n            for i = 1, 16 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local httpc = http.new()\n                    local res, err = httpc:request_uri(uri)\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- grep_error_log eval\nqr/fetch token from shared dict, token: 233/\n--- grep_error_log_out eval\nqr/(fetch token from shared dict, token: 233){1,}/\n"
  },
  {
    "path": "t/plugin/ext-plugin/extra-info.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen unix:\\$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $unix_socket_path = $ENV{\"TEST_NGINX_HTML_DIR\"} . \"/nginx.sock\";\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    path_for_test: $unix_socket_path\n    cmd: $cmd\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: var\n--- request\nGET /hello?x=\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"var\", name = \"server_addr\", result = \"127.0.0.1\"},\n                {type = \"var\", name = \"remote_addr\", result = \"127.0.0.1\"},\n                {type = \"var\", name = \"route_id\", result = \"1\"},\n                {type = \"var\", name = \"arg_x\", result = \"\"},\n            }\n            ext.go({extra_info = actions, stop = true})\n        }\n    }\n--- error_code: 405\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\nsend extra info req successfully\nsend extra info req successfully\nsend extra info req successfully\n\n\n\n=== TEST 3: ask nonexistent var\n--- request\nGET /hello\n--- more_headers\nX-Change: foo\nX-Delete: foo\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"var\", name = \"erver_addr\"},\n            }\n            ext.go({extra_info = actions, rewrite = true})\n        }\n    }\n--- response_body\nuri: /uri\nhost: localhost\nx-add: bar\nx-change: bar\nx-real-ip: 127.0.0.1\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n\n\n\n=== TEST 4: network is down in the middle\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"var\", name = \"server_addr\", result = \"127.0.0.1\"},\n                {type = \"closed\"},\n            }\n            ext.go({extra_info = actions, stop = true})\n        }\n    }\n--- error_code: 503\n--- error_log\nfailed to receive RPC_HTTP_REQ_CALL: closed\n\n\n\n=== TEST 5: ask response body (not exist)\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"respbody\", result = nil}\n            }\n            ext.go({extra_info = actions})\n        }\n    }\n--- error_log: failed to read response body: not exits\n\n\n\n=== TEST 6: add route with ext-plugin-post-resp\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/*\",\n                    \"plugins\": {\n                        \"ext-plugin-post-resp\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: ask var\n--- request\nGET /hello?x=\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"var\", name = \"server_addr\", result = \"127.0.0.1\"},\n                {type = \"var\", name = \"remote_addr\", result = \"127.0.0.1\"},\n                {type = \"var\", name = \"route_id\", result = \"1\"},\n                {type = \"var\", name = \"arg_x\", result = \"\"},\n            }\n            ext.go({extra_info = actions})\n        }\n    }\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\nsend extra info req successfully\nsend extra info req successfully\nsend extra info req successfully\n--- response_body\nhello world\n\n\n\n=== TEST 8: ask response body\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"respbody\", result = \"hello world\\n\"},\n            }\n            ext.go({extra_info = actions})\n        }\n    }\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n--- response_body\nhello world\n\n\n\n=== TEST 9: ask response body (chunked)\n--- request\nGET /hello_chunked\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"respbody\", result = \"hello world\\n\"},\n            }\n            ext.go({extra_info = actions})\n        }\n    }\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n--- response_body\nhello world\n\n\n\n=== TEST 10: ask request body (empty)\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"reqbody\", result = nil}\n            }\n            ext.go({extra_info = actions})\n        }\n    }\n\n\n\n=== TEST 11: ask request body\n--- request\nPOST /hello\n123\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"reqbody\", result = \"123\"}\n            }\n            ext.go({extra_info = actions})\n        }\n    }\n"
  },
  {
    "path": "t/plugin/ext-plugin/http-req-call.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen unix:\\$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $unix_socket_path = $ENV{\"TEST_NGINX_HTML_DIR\"} . \"/nginx.sock\";\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    path_for_test: $unix_socket_path\n    cmd: $cmd\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: stop\n--- request\nGET /hello\n--- response_body chomp\ncat\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({stop = true})\n        }\n    }\n--- error_code: 405\n--- response_headers\nX-Resp: foo\nX-Req: bar\n\n\n\n=== TEST 3: check input\n--- request\nPUT /hello?xx=y&xx=z&&y=&&z\n--- more_headers\nX-Req: foo\nX-Req: bar\nX-Resp: cat\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({check_input = true})\n        }\n    }\n\n\n\n=== TEST 4: check input (ipv6)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test_ipv6\n        t('/hello')\n    }\n}\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({check_input_ipv6 = true})\n        }\n    }\n--- listen_ipv6\n\n\n\n=== TEST 5: rewrite\n--- request\nGET /hello\n--- more_headers\nX-Change: foo\nX-Delete: foo\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite = true})\n        }\n    }\n--- response_body\nuri: /uri\nhost: localhost\nx-add: bar\nx-change: bar\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 6: rewrite host\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_host = true})\n        }\n    }\n--- response_body\nuri: /uri\nhost: 127.0.0.1\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 7: rewrite args\n--- request\nGET /hello?c=foo&d=bar\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_args = true})\n        }\n    }\n--- response_body\nuri: /plugin_proxy_rewrite_args\na: foo,bar\nc: bar\n\n\n\n=== TEST 8: proxy-rewrite + rewrite host\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"host\": \"test.com\"\n                        },\n                        \"ext-plugin-post-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_host = true, check_input_rewrite_host = true})\n        }\n    }\n--- response_body\nuri: /uri\nhost: 127.0.0.1\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 10: proxy-rewrite + rewrite path\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/xxx\"\n                        },\n                        \"ext-plugin-post-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_host = true, check_input_rewrite_path = true})\n        }\n    }\n--- response_body\nuri: /uri\nhost: 127.0.0.1\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 12: proxy-rewrite + rewrite path with args\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/xxx?x=z\"\n                        },\n                        \"ext-plugin-post-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_args = true, check_input_rewrite_args = true})\n        }\n    }\n--- response_body\nuri: /plugin_proxy_rewrite_args\na: foo,bar\nc: bar\nx: z\n\n\n\n=== TEST 14: rewrite args only\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/plugin_proxy_rewrite_args\",\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: hit\n--- request\nGET /plugin_proxy_rewrite_args\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_args_only = true})\n        }\n    }\n--- response_body\nuri: /plugin_proxy_rewrite_args\na: foo,bar\nc: bar\n\n\n\n=== TEST 16: rewrite, bad path\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: hit\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_bad_path = true})\n        }\n    }\n--- error_log\nundefined path in test server, uri: /plugin_proxy_rewrite_args%3Fa=2\n--- error_code: 404\n\n\n\n=== TEST 18: stop without setting status code\n--- request\nGET /hello\n--- response_body chomp\ncat\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({stop = true, check_default_status = true})\n        }\n    }\n--- response_headers\nX-Resp: foo\nX-Req: bar\n\n\n\n=== TEST 19: rewrite response header and call the upstream service\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_resp_header = true})\n        }\n    }\n--- response_body\nplugin_proxy_rewrite_resp_header\n--- response_headers\nX-Resp: foo\nX-Req: bar\n\n\n\n=== TEST 20: rewrite non-important response headers and call the upstream service\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_vital_resp_header = true})\n        }\n    }\n--- response_body\nplugin_proxy_rewrite_resp_header\n--- response_headers\nX-Resp: foo\nX-Req: bar\nContent-Type: text/plain\nContent-Encoding:\n\n\n\n=== TEST 21: trace stopped request\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"zipkin\": {\n                            \"endpoint\": \"http://127.0.0.1:1980/mock_zipkin\",\n                            \"sample_ratio\": 1\n                        },\n                        \"ext-plugin-pre-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: hit\n--- extra_init_by_lua\n    local prev_new = require(\"opentracing.tracer\").new\n    local function new(...)\n        ngx.log(ngx.WARN, \"tracer attached to stopped request\")\n        return prev_new(...)\n    end\n    require(\"opentracing.tracer\").new = new\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({stop = true})\n        }\n    }\n--- error_code: 405\n--- error_log\ntracer attached to stopped request\n\n\n\n=== TEST 23: set header with OpenResty API should invalidate the cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"functions\" : [\"return function(conf, ctx)\n                                            require('apisix.core').request.headers();\n                                            ngx.req.set_header('X-Req', 'foo');\n                                            require('ngx.req').add_header('X-Req', 'bar');\n                                            ngx.req.set_header('X-Resp', 'cat');\n                                            end\"]\n                        },\n                        \"ext-plugin-post-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 24: check input\n--- request\nPUT /hello?xx=y&xx=z&&y=&&z\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({check_input = true})\n        }\n    }\n\n\n\n=== TEST 25: rewrite same response headers and call the upstream service\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_same_resp_header = true})\n        }\n    }\n--- response_body\nplugin_proxy_rewrite_resp_header\n--- response_headers\nX-Resp: foo\nX-Req: bar\nX-Same: one, two\n\n\n\n=== TEST 26: stop with modify same response headers\n--- request\nGET /hello\n--- response_body chomp\ncat\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({stop = true})\n        }\n    }\n--- error_code: 405\n--- response_headers\nX-Resp: foo\nX-Req: bar\nX-Same: one, two\n\n\n\n=== TEST 27: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/echo\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 28: test rewrite request body\n--- request\nGET /echo\n--- response_body chomp\ncat\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_request_body = true})\n        }\n    }\n--- response_body\nabc\n\n\n\n=== TEST 29: rewrite path only (preserve original query args)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 30: hit (original query args should be preserved when only path is rewritten)\n--- request\nGET /hello?c=foo&d=bar\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_path_only = true})\n        }\n    }\n--- response_body\nuri: /plugin_proxy_rewrite_args\nc: foo\nd: bar\n\n\n\n=== TEST 31: hit (when no uri args are passed, there are no failures)\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({rewrite_path_only = true})\n        }\n    }\n--- response_body\nuri: /plugin_proxy_rewrite_args\n"
  },
  {
    "path": "t/plugin/ext-plugin/request-body.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen unix:\\$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $unix_socket_path = $ENV{\"TEST_NGINX_HTML_DIR\"} . \"/nginx.sock\";\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    path_for_test: $unix_socket_path\n    cmd: $cmd\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: request body(text)\n--- request\nPOST /hello\n123\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"reqbody\", result = \"123\"},\n            }\n            ext.go({extra_info = actions, stop = true, get_request_body = true})\n        }\n    }\n--- error_code: 405\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n\n\n\n=== TEST 3: request body(x-www-form-urlencoded)\n--- request\nPOST /hello\nfoo=bar\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"reqbody\", result = \"foo=bar\"},\n            }\n            ext.go({extra_info = actions, stop = true, get_request_body = true})\n        }\n    }\n--- error_code: 405\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n\n\n\n=== TEST 4: request body(json)\n--- request\nPOST /hello\n{\"foo\":\"bar\"}\n--- more_headers\nContent-Type: application/json\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"reqbody\", result = \"{\\\"foo\\\":\\\"bar\\\"}\"},\n            }\n            ext.go({extra_info = actions, stop = true, get_request_body = true})\n        }\n    }\n--- error_code: 405\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n\n\n\n=== TEST 5: request body(nil)\n--- request\nPOST /hello\n--- extra_stream_config\n    server {\n\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            local actions = {\n                {type = \"reqbody\", result = nil},\n            }\n            ext.go({extra_info = actions, stop = true, get_request_body = true})\n        }\n    }\n--- error_code: 405\n--- grep_error_log eval\nqr/send extra info req successfully/\n--- grep_error_log_out\nsend extra info req successfully\n"
  },
  {
    "path": "t/plugin/ext-plugin/response.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen unix:\\$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $unix_socket_path = $ENV{\"TEST_NGINX_HTML_DIR\"} . \"/nginx.sock\";\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    path_for_test: $unix_socket_path\n    cmd: $cmd\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/*\",\n                    \"plugins\": {\n                        \"ext-plugin-post-resp\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: check input\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({check_input = true})\n        }\n    }\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 3: modify body\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({modify_body = true})\n        }\n    }\n--- error_code: 200\n--- response_body chomp\ncat\n\n\n\n=== TEST 4: modify header\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({modify_header = true})\n        }\n    }\n--- more_headers\nresp-X-Runner: Go-runner\n--- error_code: 200\n--- response_headers\nX-Runner: Test-Runner\nContent-Type: application/json\n--- response_body\nhello world\n\n\n\n=== TEST 5: modify same response headers\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({modify_header = true, same_header = true})\n        }\n    }\n--- error_code: 200\n--- response_headers\nX-Same: one, two\n--- response_body\nhello world\n\n\n\n=== TEST 6: modify status\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({modify_status = true})\n        }\n    }\n--- error_code: 304\n\n\n\n=== TEST 7: default allow_degradation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-post-resp\": {\n                            \"conf\": [\n                                {\"name\":\"foo\", \"value\":\"bar\"}\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: ext-plugin-resp wrong, req reject\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock1;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n--- error_code: 503\n--- error_log eval\nqr/failed to connect to the unix socket/\n\n\n\n=== TEST 9: open allow_degradation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                            \"conf\": [\n                                {\"name\":\"foo\", \"value\":\"bar\"}\n                            ],\n                            \"allow_degradation\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: ext-plugin-resp wrong, req access\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock1;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n--- response_body\nhello world\n--- error_log eval\nqr/Plugin Runner.*allow degradation/\n\n\n\n=== TEST 11: add route: wrong upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/*\",\n                    \"plugins\": {\n                        \"ext-plugin-post-resp\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:3980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: request upstream failed\n--- request\nGET /hello\n--- error_code: 502\n--- error_log eval\nqr/failed to request/\n\n\n\n=== TEST 13: add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/*\",\n                    \"plugins\": {\n                        \"ext-plugin-post-resp\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: body_reader error\n--- request\nGET /hello1\n--- more_headers\nresp-Content-Length: 14\n--- error_code: 502\n--- error_log eval\nqr/read response failed/\n\n\n\n=== TEST 15: response chunked\n--- request\nGET /hello_chunked\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 16: check upstream uri with args\n--- request\nGET /plugin_proxy_rewrite_args?aaa=bbb&ccc=ddd\n--- error_code: 200\n--- response_body\nuri: /plugin_proxy_rewrite_args\naaa: bbb\nccc: ddd\n"
  },
  {
    "path": "t/plugin/ext-plugin/runner.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\necho \"LISTEN $APISIX_LISTEN_ADDRESS\"\necho \"EXPIRE $APISIX_CONF_EXPIRE_TIME\"\necho \"MY_ENV_VAR $MY_ENV_VAR\"\nsleep \"$1\"\nexit 111\n"
  },
  {
    "path": "t/plugin/ext-plugin/runner_can_not_terminated.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nterm() {\n    eval sleep 1800\n}\ntrap term SIGTERM\nsleep 1800\n"
  },
  {
    "path": "t/plugin/ext-plugin/sanity.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\n$ENV{\"PATH\"} = $ENV{PATH} . \":\" . $ENV{TEST_NGINX_HTML_DIR};\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen unix:\\$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $unix_socket_path = $ENV{\"TEST_NGINX_HTML_DIR\"} . \"/nginx.sock\";\n    my $orig_extra_yaml_config = $block->extra_yaml_config // \"\";\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    path_for_test: $unix_socket_path\n    cmd: $cmd\n_EOC_\n    $extra_yaml_config = $extra_yaml_config . $orig_extra_yaml_config;\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\"a\":\"b\"},\n                        \"ext-plugin-post-req\": {\"c\":\"d\"},\n                        \"ext-plugin-post-resp\": {\"e\":\"f\"}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nget conf token: 233\n--- no_error_log\nflush conf token lrucache\n[error]\n--- grep_error_log eval\nqr/(sending|receiving) rpc type: \\d data length:/\n--- grep_error_log_out\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 2 data length:\nreceiving rpc type: 2 data length:\nsending rpc type: 2 data length:\nreceiving rpc type: 2 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 2 data length:\nreceiving rpc type: 2 data length:\nsending rpc type: 2 data length:\nreceiving rpc type: 2 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 4 data length:\nreceiving rpc type: 4 data length:\nsending rpc type: 4 data length:\nreceiving rpc type: 4 data length:\n\n\n\n=== TEST 3: header too short\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.header_too_short()\n        }\n    }\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nfailed to receive RPC_PREPARE_CONF\n\n\n\n=== TEST 4: data too short\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.data_too_short()\n        }\n    }\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nfailed to receive RPC_PREPARE_CONF\n\n\n\n=== TEST 5: not listen\n--- extra_stream_config\n--- request\nGET /hello\n--- error_code: 503\n--- error_log\nfailed to connect to the unix socket\n\n\n\n=== TEST 6: spawn runner\n--- ext_plugin_cmd\n[\"t/plugin/ext-plugin/runner.sh\", \"3600\"]\n--- config\n    location /t {\n        access_by_lua_block {\n            -- ensure the runner is spawned before the request finishes\n            ngx.sleep(0.1)\n            ngx.exit(200)\n        }\n    }\n--- grep_error_log eval\nqr/LISTEN unix:\\S+/\n--- grep_error_log_out eval\nqr/LISTEN unix:.+\\/nginx.sock/\n--- error_log\nEXPIRE 3600\n\n\n\n=== TEST 7: respawn runner when it exited\n--- ext_plugin_cmd\n[\"t/plugin/ext-plugin/runner.sh\", \"0.1\"]\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.2)\n        }\n    }\n--- error_log\nrunner exited with reason: exit, status: 111\nrespawn runner 3 seconds later with cmd: [\"t/plugin/ext-plugin/runner.sh\",\"0.1\"]\n\n\n\n=== TEST 8: flush cache when runner exited\n--- ext_plugin_cmd\n[\"t/plugin/ext-plugin/runner.sh\", \"0.4\"]\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local function r()\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.log(ngx.ERR, err)\n                    return\n                else\n                    ngx.print(res.body)\n                end\n            end\n\n            r()\n            r()\n            ngx.sleep(0.5)\n            r()\n        }\n    }\n--- response_body\nhello world\nhello world\nhello world\n--- grep_error_log eval\nqr/(sending|receiving) rpc type: 1 data length:/\n--- grep_error_log_out\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\nsending rpc type: 1 data length:\nreceiving rpc type: 1 data length:\n--- error_log\nflush conf token lrucache\nflush conf token in shared dict\n\n\n\n=== TEST 9: prepare conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                            \"conf\": [\n                                {\"name\":\"foo\", \"value\":\"bar\"},\n                                {\"name\":\"cat\", \"value\":\"dog\"}\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit\n--- request\nGET /hello\n--- response_body\nhello world\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({with_conf = true, expect_key_pattern = [[^route#1#ext-plugin-pre-req#]]})\n        }\n    }\n--- error_log eval\nqr/get conf token: 233 conf: \\[(\\{\"value\":\"bar\",\"name\":\"foo\"\\}|\\{\"name\":\"foo\",\"value\":\"bar\"\\}),(\\{\"value\":\"dog\",\"name\":\"cat\"\\}|\\{\"name\":\"cat\",\"value\":\"dog\"\\})\\]/\n\n\n\n=== TEST 11: handle error code\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({inject_error = true})\n        }\n    }\n--- error_code: 503\n--- error_log\nfailed to receive RPC_PREPARE_CONF: bad request\n\n\n\n=== TEST 12: refresh token\n--- request\nGET /hello\n--- response_body\nhello world\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            if not package.loaded.count then\n                package.loaded.count = 1\n            else\n                package.loaded.count = package.loaded.count + 1\n            end\n\n            if package.loaded.count == 1 then\n                ext.go({no_token = true})\n            else\n                ext.go({with_conf = true})\n            end\n        }\n    }\n--- error_log\nrefresh cache and try again\nflush conf token in shared dict\n\n\n\n=== TEST 13: runner can access the environment variable\n--- main_config\nenv MY_ENV_VAR=foo;\n--- ext_plugin_cmd\n[\"t/plugin/ext-plugin/runner.sh\", \"3600\"]\n--- config\n    location /t {\n        access_by_lua_block {\n            -- ensure the runner is spawned before the request finishes\n            ngx.sleep(0.1)\n            ngx.exit(200)\n        }\n    }\n--- error_log\nMY_ENV_VAR foo\n\n\n\n=== TEST 14: bad conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-pre-req\": {\n                            \"conf\": [\n                                {\"value\":\"bar\"}\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.say(message)\n            end\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                            \"conf\": [\n                                {\"name\":\"bar\"}\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.print(message)\n            end\n        }\n    }\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin ext-plugin-pre-req err: property \\\"conf\\\" validation failed: failed to validate item 1: property \\\"name\\\" is required\"}\n\n{\"error_msg\":\"failed to check the configuration of plugin ext-plugin-post-req err: property \\\"conf\\\" validation failed: failed to validate item 1: property \\\"value\\\" is required\"}\n\n\n\n=== TEST 15: spawn runner which can't be terminated, ensure APISIX won't be blocked\n--- ext_plugin_cmd\n[\"t/plugin/ext-plugin/runner_can_not_terminated.sh\"]\n--- config\n    location /t {\n        return 200;\n    }\n\n\n\n=== TEST 16: prepare conf with global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.say(message)\n                return\n            end\n\n            local code, message, res = t.test('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                            \"conf\": [\n                                {\"name\":\"foo\", \"value\":\"bar\"},\n                                {\"name\":\"cat\", \"value\":\"dog\"}\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: hit\n--- request\nGET /hello\n--- response_body\nhello world\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({with_conf = true, expect_key_pattern = [[^global_rule#1#ext-plugin-post-req#]]})\n        }\n    }\n--- error_log eval\nqr/get conf token: 233 conf: \\[(\\{\"value\":\"bar\",\"name\":\"foo\"\\}|\\{\"name\":\"foo\",\"value\":\"bar\"\\}),(\\{\"value\":\"dog\",\"name\":\"cat\"\\}|\\{\"name\":\"cat\",\"value\":\"dog\"\\})\\]/\n\n\n\n=== TEST 18: clean global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n        }\n    }\n\n\n\n=== TEST 19: default allow_degradation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                            \"conf\": [\n                                {\"name\":\"foo\", \"value\":\"bar\"},\n                                {\"name\":\"cat\", \"value\":\"dog\"}\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: ext-plugin wrong, req reject\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock1;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n--- error_code: 503\n--- error_log eval\nqr/failed to connect to the unix socket/\n\n\n\n=== TEST 21: open allow_degradation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local code, message, res = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ext-plugin-post-req\": {\n                            \"conf\": [\n                                {\"name\":\"foo\", \"value\":\"bar\"},\n                                {\"name\":\"cat\", \"value\":\"dog\"}\n                            ],\n                            \"allow_degradation\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(message)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: ext-plugin wrong, req access\n--- request\nGET /hello\n--- extra_stream_config\n    server {\n        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock1;\n\n        content_by_lua_block {\n            local ext = require(\"lib.ext-plugin\")\n            ext.go({})\n        }\n    }\n--- response_body\nhello world\n--- error_log eval\nqr/Plugin Runner.*allow degradation/\n"
  },
  {
    "path": "t/plugin/ext-plugin/sanity2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $cmd = $block->ext_plugin_cmd // \"['sleep', '5s']\";\n    my $extra_yaml_config = <<_EOC_;\next-plugin:\n    cmd: $cmd\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: terminate spawn runner\n--- ext_plugin_cmd\n[\"t/plugin/ext-plugin/runner.sh\", \"3600\"]\n--- config\n    location /t {\n        return 200;\n    }\n--- shutdown_error_log eval\nqr/terminate runner \\d+ with SIGTERM/\n"
  },
  {
    "path": "t/plugin/fault-injection.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(invalid http_status in the abort property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                       \"http_status\": 100,\n                                       \"body\": \"Fault Injection!\\n\"\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/validation failed/\n\n\n\n=== TEST 2: set route(without http_status in the abort property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/validation failed/\n\n\n\n=== TEST 3: set route(without abort & delay properties)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/expect object to have at least 1 properties/\n\n\n\n=== TEST 4: set route(without duration in the delay property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"delay\": {\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/validation failed/\n\n\n\n=== TEST 5: set route(invalid duration with string in the delay property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"delay\": {\n                                       \"duration\": \"test\"\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/wrong type: expected number, got string/\n\n\n\n=== TEST 6: set route(invalid duration with double dot in the delay property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"delay\": {\n                                       \"duration\": 0.1.1\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid request body/\n--- error_log eval\nqr/invalid request body/\n\n\n\n=== TEST 7: set route(invalid duration with whitespace in the delay property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"delay\": {\n                                       \"duration\": 0. 1\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid request body/\n--- error_log eval\nqr/invalid request body/\n\n\n\n=== TEST 8: set route(invalid vars in the delay property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                \"delay\": {\n                                    \"duration\": 0.1,\n                                    \"vars\": {\n                                        \"a\",\n                                        \"b\"\n                                    }\n                                },\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid request body/\n--- error_log eval\nqr/invalid request body/\n\n\n\n=== TEST 9: set route(invalid vars in in the abort property)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                    \"abort\": {\n                                        \"http_status\": 200,\n                                        \"vars\": {\n                                            \"a\",\n                                            \"b\"\n                                        }\n                                    }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid request body/\n--- error_log eval\nqr/invalid request body/\n\n\n\n=== TEST 10: set route(delay 1 seconds)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"delay\": {\n                                       \"duration\": 1\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route(delay 1 seconds and return hello world)\n--- request\nGET /hello HTTP/1.1\n--- response_body\nhello world\n\n\n\n=== TEST 12: set route(abort with http status 200 and return \"Fault Injection!\\n\")\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                      \"http_status\": 200,\n                                      \"body\": \"Fault Injection!\\n\"\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route(abort with http code 200 and return \"Fault Injection!\\n\")\n--- request\nGET /hello HTTP/1.1\n--- error_code: 200\n--- response_body\nFault Injection!\n\n\n\n=== TEST 14: set route(abort with http status 405 and return \"Fault Injection!\\n\")\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                      \"http_status\": 405,\n                                      \"body\": \"Fault Injection!\\n\"\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello\"\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route(abort with http status 405 and return \"Fault Injection!\\n\")\n--- request\nGET /hello HTTP/1.1\n--- error_code: 405\n--- response_body\nFault Injection!\n\n\n\n=== TEST 16: set route(play with redirect plugin)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                      \"http_status\": 200,\n                                      \"body\": \"Fault Injection!\\n\"\n                                   }\n                               },\n                               \"redirect\": {\n                                   \"uri\": \"/hello/world\",\n                                   \"ret_code\": 302\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 17: hit route(abort with http status 200 and return \"Fault Injection!\\n\")\n--- request\nGET /hello HTTP/1.1\n--- error_code: 200\n--- response_body\nFault Injection!\n\n\n\n=== TEST 18: set route (abort injection but with zero percentage)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                      \"http_status\": 200,\n                                      \"body\": \"Fault Injection!\\n\",\n                                      \"percentage\": 0\n                                   }\n                               },\n                               \"redirect\": {\n                                   \"uri\": \"/hello/world\",\n                                   \"ret_code\": 302\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 19: hit route (redirect)\n--- request\nGET /hello HTTP/1.1\n--- error_code: 302\n\n\n\n=== TEST 20: set route (delay injection but with zero percentage)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"delay\": {\n                                       \"duration\": 1,\n                                       \"percentage\": 0\n                                   }\n                               },\n                               \"proxy-rewrite\": {\n                                   \"uri\": \"/hello1\"\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 21: hit route (no wait and return hello1 world)\n--- request\nGET /hello HTTP/1.1\n--- error_code: 200\n--- response_body\nhello1 world\n\n\n\n=== TEST 22: set route(body with var)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 200,\n                                \"body\": \"client addr: $remote_addr\\n\"\n                            }\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                            }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: hit route(body with var)\n--- request\nGET /hello\n--- response_body\nclient addr: 127.0.0.1\n\n\n\n=== TEST 24: set route(abort without body)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                    \"plugins\": {\n                        \"fault-injection\": {\n                            \"abort\": {\n                                \"http_status\": 200\n                            }\n                        },\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/hello\"\n                            }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 25: hit route(abort without body)\n--- request\nGET /hello\n--- response_body\n\n\n\n=== TEST 26: vars schema validation passed\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.fault-injection\")\n            local ok, err = plugin.check_schema({\n                abort = {\n                    http_status = 403,\n                    body = \"Fault Injection!\\n\",\n                    vars = {\n                        {\n                            {\"arg_name\",\"==\",\"jack\"},\n                            {\"arg_age\",\"!\",\"<\",18}\n                        },\n                        {\n                            {\"http_apikey\",\"==\",\"api-key\"}\n                        }\n                    }\n                },\n                delay = {\n                    duration = 2,\n                    vars = {\n                        {\n                            {\"arg_name\",\"==\",\"jack\"},\n                            {\"arg_age\",\"!\",\"<\",18}\n                        },\n                        {\n                            {\"http_apikey\",\"==\",\"api-key\"}\n                        }\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 27: vars schema validation failed(abort failed)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.fault-injection\")\n            local ok, err = plugin.check_schema({\n                abort = {\n                    http_status = 403,\n                    body = \"Fault Injection!\\n\",\n                    vars = {\n                        {\"arg_name\",\"!=\",\"jack\"}\n                    }\n                },\n                delay = {\n                    duration = 2,\n                    vars = {\n                        {\n                            {\"arg_name\",\"==\",\"jack\"},\n                            {\"arg_age\",\"!\",\"<\",18}\n                        },\n                        {\n                            {\"http_apikey\",\"==\",\"api-key\"}\n                        }\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nrule should be wrapped inside brackets\ndone\n--- error_log eval\nqr/failed to create vars expression:.*/\n\n\n\n=== TEST 28: set route and configure the vars rule in abort\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [=[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                        \"http_status\": 403,\n                                        \"body\": \"Fault Injection!\\n\",\n                                        \"vars\": [\n                                            [\n                                                [\"arg_name\",\"==\",\"jack\"],\n                                                [ \"arg_age\",\"!\",\"<\",18 ]\n                                            ],\n                                            [\n                                                [ \"http_apikey\",\"==\",\"api-key\" ]\n                                            ]\n                                        ]\n                                   }\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]=]\n                   )\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 29: hit the route (all vars rules pass), execute abort\n--- request\nGET /hello?name=jack&age=18\n--- more_headers\napikey: api-key\n--- error_code: 403\n--- response_body\nFault Injection!\n\n\n\n=== TEST 30: hit the route (missing apikey), execute abort\n--- request\nGET /hello?name=jack&age=20\n--- error_code: 403\n--- response_body\nFault Injection!\n\n\n\n=== TEST 31: hit the route (missing request parameters), execute abort\n--- request\nGET /hello\n--- more_headers\napikey:api-key\n--- error_code: 403\n--- response_body\nFault Injection!\n\n\n\n=== TEST 32: hit route(`vars` do not match, `age` is missing)\n--- request\nGET /hello?name=allen\n--- response_body\nhello world\n\n\n\n=== TEST 33: hit route(all `vars` do not match)\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 34: set route and configure the vars rule in delay\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [=[{\n                        \"uri\": \"/hello\",\n                        \"plugins\": {\n                            \"fault-injection\": {\n                                \"delay\": {\n                                    \"duration\": 2,\n                                    \"vars\": [\n                                        [\n                                            [\"arg_name\",\"==\",\"jack\"],\n                                            [ \"arg_age\",\"!\",\"<\",18 ]\n                                        ]\n                                    ]\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                   }]=]\n                   )\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 35: hit route(delay 2 seconds and return hello world)\n--- request\nGET /hello?name=jack&age=22\n--- response_body\nhello world\n\n\n\n=== TEST 36: hit route (no wait and return hello1 world)\n--- request\nGET /hello HTTP/1.1\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 37: set route and configure the vars rule in abort and delay\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [=[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                        \"http_status\": 403,\n                                        \"body\": \"Fault Injection!\\n\",\n                                        \"vars\": [\n                                            [\n                                                [\"arg_name\",\"==\",\"jack\"],\n                                                [\"arg_age\",\"!\",\"<\",18]\n                                            ]\n                                        ]\n                                   },\n                                   \"delay\": {\n                                    \"duration\": 2,\n                                    \"vars\": [\n                                        [\n                                            [\"http_apikey\",\"==\",\"api-key\"]\n                                        ]\n                                    ]\n                                }\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]=]\n                   )\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 38: hit the route (all vars rules are passed), execute abort and delay\n--- request\nGET /hello?name=jack&age=18\n--- more_headers\napikey: api-key\n--- error_code: 403\n--- response_body\nFault Injection!\n\n\n\n=== TEST 39: hit the route (abort rule does not match), only execute delay\n--- request\nGET /hello?name=jack&age=16\n--- more_headers\napikey: api-key\n--- response_body\nhello world\n"
  },
  {
    "path": "t/plugin/fault-injection2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: vars rule with ! (set)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [=[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                        \"http_status\": 403,\n                                        \"body\": \"Fault Injection!\\n\",\n                                        \"vars\": [\n                                            [\n                                                \"!AND\",\n                                                [\"arg_name\",\"==\",\"jack\"],\n                                                [\"arg_age\",\"!\",\"<\",18]\n                                            ]\n                                        ]\n                                    }\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]=]\n                   )\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 2: vars rule with ! (hit)\n--- request\nGET /hello?name=jack&age=17\n--- error_code: 403\n--- response_body\nFault Injection!\n\n\n\n=== TEST 3: vars rule with ! (miss)\n--- request\nGET /hello?name=jack&age=18\n--- response_body\nhello world\n\n\n\n=== TEST 4: inject header config\n--- config\n location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [=[{\n                           \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                        \"http_status\": 200,\n                                        \"headers\" : {\n                                            \"h1\": \"v1\",\n                                            \"h2\": 2,\n                                            \"h3\": \"$uri\"\n                                        }\n                                    }\n                               }\n                           },\n                           \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                           },\n                           \"uri\": \"/hello\"\n                   }]=]\n                   )\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 5: inject header\n--- request\nGET /hello\n--- response_headers\nh1: v1\nh2: 2\nh3: /hello\n\n\n\n=== TEST 6: closing curly brace not should not be a part of variable\n--- config\n location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [=[{\n                            \"plugins\": {\n                               \"fault-injection\": {\n                                   \"abort\": {\n                                      \"http_status\": 200,\n                                      \"body\": \"{\\\"count\\\": $arg_count}\"\n                                   }\n                               }\n                            },\n                            \"upstream\": {\n                               \"nodes\": {\n                                   \"127.0.0.1:1980\": 1\n                               },\n                               \"type\": \"roundrobin\"\n                            },\n                            \"uri\": \"/hello\"\n                        }]=]\n                   )\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 7: test route\n--- request\nGET /hello?count=2\n--- response_body chomp\n{\"count\": 2}\n"
  },
  {
    "path": "t/plugin/file-logger-reopen.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (! $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: prepare\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file.log\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: cache file\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            assert(io.open(\"file.log\", 'r'))\n            os.remove(\"file.log\")\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local _, err = io.open(\"file.log\", 'r')\n            ngx.say(err)\n        }\n    }\n--- response_body\nfile.log: No such file or directory\n\n\n\n=== TEST 3: reopen file\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            assert(io.open(\"file.log\", 'r'))\n            os.remove(\"file.log\")\n            ngx.sleep(0.01) -- make sure last reopen file is expired\n\n            local process = require \"ngx.process\"\n            local resty_signal = require \"resty.signal\"\n            local pid = process.get_master_pid()\n\n            local ok, err = resty_signal.kill(pid, \"USR1\")\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to kill process of pid \", pid, \": \", err)\n                return\n            end\n\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            assert(code == 200)\n\n            -- file is reopened\n            local fd, err = io.open(\"file.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file.log, error info: \", err)\n                return\n            end\n\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.client_ip == '127.0.0.1' and new_msg.route_id == '1'\n                and new_msg.host == '127.0.0.1'\n            then\n                msg = \"write file log success\"\n                ngx.status = code\n                ngx.say(msg)\n            end\n\n            os.remove(\"file.log\")\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local _, err = io.open(\"file.log\", 'r')\n            ngx.say(err)\n        }\n    }\n--- response_body\nwrite file log success\nfile.log: No such file or directory\n--- grep_error_log eval\nqr/reopen cached log file: file.log/\n--- grep_error_log_out\nreopen cached log file: file.log\n"
  },
  {
    "path": "t/plugin/file-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (! $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local configs = {\n                -- full configuration\n                {\n                    path = \"file.log\"\n                },\n                -- property \"path\" is not set in either the plugin conf or the metadata\n                {\n                    path = nil\n                }\n            }\n\n            local plugin = require(\"apisix.plugins.file-logger\")\n\n            for i = 1, #configs do\n                ok, err = plugin.check_schema(configs[i])\n                if err then\n                    ngx.say(err)\n                else\n                    ngx.say(\"done\")\n                end\n            end\n        }\n    }\n--- response_body_like\ndone\nproperty \"path\" is not set in either the plugin conf or the metadata\n\n\n\n=== TEST 2: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file.log\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: verify plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file.log, error info: \", err)\n                return\n            end\n\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.client_ip == '127.0.0.1' and new_msg.route_id == '1'\n                and new_msg.host == '127.0.0.1'\n            then\n                msg = \"write file log success\"\n                ngx.status = code\n                ngx.say(msg)\n            end\n\n            --- a new request is logged\n            t(\"/hello\", ngx.HTTP_GET)\n            msg = fd:read(\"*l\")\n            local new_msg = core.json.decode(msg)\n            if new_msg.client_ip == '127.0.0.1' and new_msg.route_id == '1'\n                and new_msg.host == '127.0.0.1'\n            then\n                msg = \"write file log success\"\n                ngx.say(msg)\n            end\n        }\n    }\n--- response_body\nwrite file log success\nwrite file log success\n\n\n\n=== TEST 5: failed to open the path\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"/log/file.log\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local code, messages = t(\"/hello\", GET)\n            core.log.warn(\"messages: \", messages)\n            if code >= 300 then\n                ngx.status = code\n            end\n        }\n    }\n--- error_log\nfailed to open file: /log/file.log, error info: /log/file.log: No such file or directory\n\n\n\n=== TEST 6: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- ensure the format is not set\n            t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_DELETE\n            )\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file.log\",\n                                \"log_format\": {\n                                    \"host\": \"$host\",\n                                    \"client_ip\": \"$remote_addr\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: verify plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file.log, error info: \", err)\n                return\n            end\n\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.client_ip == '127.0.0.1' and new_msg.route_id == '1'\n                and new_msg.host == '127.0.0.1'\n            then\n                msg = \"write file log success\"\n                ngx.status = code\n                ngx.say(msg)\n            end\n        }\n    }\n--- response_body\nwrite file log success\n\n\n\n=== TEST 8: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: ensure config in plugin is prior to the one in plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file.log, error info: \", err)\n                return\n            end\n\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.client_ip == '127.0.0.1' and new_msg.route_id == '1'\n                and new_msg.host == '127.0.0.1'\n            then\n                msg = \"write file log success\"\n                ngx.status = code\n                ngx.say(msg)\n            end\n        }\n    }\n--- response_body\nwrite file log success\n\n\n\n=== TEST 10: nested log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-logger-nested.log\",\n                                \"log_format\": {\n                                    \"host\": \"$host\",\n                                    \"client_ip\": \"$remote_addr\",\n                                    \"request\": {\n                                        \"method\": \"$request_method\",\n                                        \"uri\": \"$request_uri\",\n                                        \"headers\": {\n                                            \"user_agent\": \"$http_user_agent\"\n                                        }\n                                    },\n                                    \"response\": {\n                                        \"status\": \"$status\"\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: verify nested log format structure\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-logger-nested.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file-logger-nested.log, error info: \", err)\n                return\n            end\n\n            msg = fd:read()\n            fd:close()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.host == '127.0.0.1' and\n               new_msg.client_ip == '127.0.0.1' and\n               type(new_msg.request) == \"table\" and\n               new_msg.request.method == 'GET' and\n               new_msg.request.uri == '/hello' and\n               type(new_msg.request.headers) == \"table\" and\n               new_msg.request.headers.user_agent and\n               type(new_msg.response) == \"table\" and\n               new_msg.response.status == 200 and\n               new_msg.route_id == '1'\n            then\n                msg = \"nested log format success\"\n                ngx.status = code\n                ngx.say(msg)\n            else\n                ngx.say(\"nested log format failed\")\n            end\n        }\n    }\n--- response_body\nnested log format success\n\n\n\n=== TEST 12: deep nested log_format is truncated and warns\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            -- configure deep nested log_format\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-logger-depth.log\",\n                                \"log_format\": {\n                                    \"a\": {\"b\": {\"c\": {\"d\": {\"e\": {\"f\": {\"g\": \"$host\"}}}}}}\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            -- trigger logging\n            local code2 = t(\"/hello\", ngx.HTTP_GET)\n\n            -- read and verify depth truncation\n            local fd, err = io.open(\"file-logger-depth.log\", 'r')\n            if not fd then\n                core.log.error(\"failed to open file: file-logger-depth.log, error info: \", err)\n                return\n            end\n\n            local msg = fd:read()\n            fd:close()\n\n            local new_msg = core.json.decode(msg)\n            local ok = type(new_msg.a) == \"table\" and\n                       type(new_msg.a.b) == \"table\" and\n                       type(new_msg.a.b.c) == \"table\" and\n                       type(new_msg.a.b.c.d) == \"table\" and\n                       type(new_msg.a.b.c.d.e) == \"table\" and\n                       new_msg.a.b.c.d.e.f == nil\n\n            if ok then\n                ngx.status = code2\n                ngx.say(\"depth limit enforced\")\n            else\n                ngx.say(\"depth limit not enforced\")\n            end\n        }\n    }\n--- response_body\ndepth limit enforced\n--- error_log\nlog_format nesting exceeds max depth 5, truncating\n\n\n\n=== TEST 13: configure metadata path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"path\": \"file-from-metadata.log\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: use metadata path when plugin config does not set it\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"file-logger\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local res_code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-from-metadata.log\", 'r')\n            if not fd then\n                core.log.error(\"failed to open file: file-from-metadata.log, error info: \", err)\n                return\n            end\n\n            local msg = fd:read()\n            fd:close()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg and new_msg.route_id == '1' then\n                ngx.status = res_code\n                ngx.say(\"write file log success\")\n            end\n        }\n    }\n--- response_body\nwrite file log success\n"
  },
  {
    "path": "t/plugin/file-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (! $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n        if (!$block->response_body) {\n            $block->set_value(\"response_body\", \"passed\\n\");\n        }\n    }\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin with 'include_resp_body' setting\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- delete plugin metadata for response body format\n            t('/apisix/admin/plugin_metadata/file-logger', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-with-resp-body.log\",\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 2: verify plugin for file-logger with response\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-with-resp-body.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file-resp-check.log, error info: \", err)\n                return\n            end\n\n            -- note only for first line\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            ngx.status = code\n\n            if new_msg.response ~= nil and new_msg.response.body == \"hello world\\n\" then\n                ngx.status = code\n                ngx.say('contain with target')\n            end\n        }\n    }\n--- response_body\ncontain with target\n\n\n\n=== TEST 3: check file-logger 'include_resp_body' with 'expr'\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-with-resp-expr-body.log\",\n                                \"include_resp_body\": true,\n                                \"include_resp_body_expr\": [\n                                    [\n                                      \"arg_foo\",\n                                      \"==\",\n                                      \"bar\"\n                                    ]\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 4: verify file-logger resp with expression of concern\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello?foo=bar\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-with-resp-expr-body.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file-with-resp-expr-body.log, error info: \", err)\n                return\n            end\n\n            -- note only for first line\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            ngx.status = code\n\n            if new_msg.response ~= nil and new_msg.response.body == \"hello world\\n\" then\n                ngx.status = code\n                ngx.say('contain target body hits with expr')\n            end\n\n            --- a new request is logged\n            t(\"/hello?name=pix\", ngx.HTTP_GET)\n            msg = fd:read(\"*l\")\n            local new_msg = core.json.decode(msg)\n            if new_msg.response.body == nil then\n                ngx.say('skip unconcern body')\n            end\n        }\n    }\n--- response_body\ncontain target body hits with expr\nskip unconcern body\n\n\n\n=== TEST 5: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/file-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"client_ip\": \"$remote_addr\",\n                        \"resp_body\": \"$resp_body\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-with-resp-body2.log\",\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: verify plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-with-resp-body2.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file.log, error info: \", err)\n                return\n            end\n\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.resp_body == 'hello world\\n'\n            then\n                msg = \"write file log success\"\n                ngx.status = code\n                ngx.say(msg)\n            end\n        }\n    }\n--- response_body\nwrite file log success\n\n\n\n=== TEST 8: Add new configuration with match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-with-match.log\",\n                                \"match\": [\n                                    [\n                                        [ \"arg_name\",\"==\",\"jack\" ]\n                                    ]\n                                ],\n                                \"log_format\": {\n                                    \"request\": \"$request\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: Request match\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello?name=jack\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-with-match.log\", 'r')\n            if not fd then\n                core.log.error(\"failed to open file: file-with-match.log, error info: \", err)\n                return\n            end\n            local msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            if new_msg.request == 'GET /hello?name=jack HTTP/1.1'\n                and new_msg.route_id == '1'\n            then\n                msg = \"write file log success\"\n                ngx.status = code\n                ngx.say(msg)\n            end\n\n            os.remove(\"file-with-match.log\")\n        }\n    }\n--- response_body\nwrite file log success\n\n\n\n=== TEST 10: Request not match\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello?name=tony\", ngx.HTTP_GET)\n            local fd, err = io.open(\"file-with-match.log\", 'r')\n            if not fd then\n                local msg = \"not write file log\"\n                ngx.say(msg)\n                return\n            end\n        }\n    }\n--- response_body\nnot write file log\n\n\n\n=== TEST 11: add plugin with 'include_req_body' setting\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/file-logger', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-with-req-body.log\",\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 12: verify plugin for file-logger with request\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello\", ngx.HTTP_POST, \"body-data\")\n            local fd, err = io.open(\"file-with-req-body.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file-with-req-body.log, error info: \", err)\n                return\n            end\n\n            -- note only for first line\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            ngx.status = code\n            if new_msg.request ~= nil and new_msg.request.body == \"body-data\" then\n                ngx.status = code\n                ngx.say('contain with target')\n            end\n        }\n    }\n--- response_body\ncontain with target\n\n\n\n=== TEST 13: check file-logger 'include_req_body' with 'expr'\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"file-logger\": {\n                                \"path\": \"file-with-req-expr-body.log\",\n                                \"include_req_body\": true,\n                                \"include_req_body_expr\": [\n                                    [\n                                      \"arg_log_body\",\n                                      \"==\",\n                                      \"yes\"\n                                    ]\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 14: verify file-logger req with expression of concern\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local code = t(\"/hello?log_body=yes\",\n                ngx.HTTP_POST,\n                [[{\"foo\": \"bar\"}]]\n                )\n            local fd, err = io.open(\"file-with-req-expr-body.log\", 'r')\n            local msg\n\n            if not fd then\n                core.log.error(\"failed to open file: file-with-req-expr-body.log, error info: \", err)\n                return\n            end\n\n            -- note only for first line\n            msg = fd:read()\n\n            local new_msg = core.json.decode(msg)\n            ngx.status = code\n            if new_msg.request ~= nil and new_msg.request.body ~= nil then\n                ngx.status = code\n                ngx.say('contain target body hits with expr')\n            end\n\n            --- a new request is logged\n            t(\"/hello?log_body=no\", ngx.HTTP_POST, [[{\"foo\": \"b\"}]])\n            msg = fd:read(\"*l\")\n            local new_msg = core.json.decode(msg)\n            if new_msg.request.body == nil then\n                ngx.say('skip unconcern body')\n            end\n        }\n    }\n--- response_body\ncontain target body hits with expr\nskip unconcern body\n"
  },
  {
    "path": "t/plugin/forward-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {uri = \"http://127.0.0.1:8199\"},\n                {request_headers = {\"test\"}},\n                {uri = 3233},\n                {uri = \"http://127.0.0.1:8199\", request_headers = \"test\"},\n                {uri = \"http://127.0.0.1:8199\", request_method = \"POST\"},\n                {uri = \"http://127.0.0.1:8199\", request_method = \"PUT\"}\n            }\n            local plugin = require(\"apisix.plugins.forward-auth\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"uri\" is required\nproperty \"uri\" validation failed: wrong type: expected string, got number\nproperty \"request_headers\" validation failed: wrong type: expected array, got string\ndone\nproperty \"request_method\" validation failed: matches none of the enum values\n\n\n\n=== TEST 2: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/upstreams/u1\",\n                    data = [[{\n                        \"nodes\": {\n                            \"127.0.0.1:1984\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/auth\",\n                    data = {\n                        plugins = {\n                            [\"serverless-pre-function\"] = {\n                                phase = \"rewrite\",\n                                functions =  {\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        if core.request.header(ctx, \"Authorization\") == \"111\" then\n                                            core.response.exit(200);\n                                        end\n                                    end]],\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        if core.request.header(ctx, \"Authorization\") == \"222\" then\n                                            core.response.set_header(\"X-User-ID\", \"i-am-an-user\");\n                                            core.response.exit(200);\n                                        end\n                                    end]],\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        if core.request.header(ctx, \"Authorization\") == \"333\" then\n                                            core.response.set_header(\"Location\", \"http://example.com/auth\");\n                                            core.response.exit(403);\n                                        end\n                                    end]],\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        if core.request.header(ctx, \"Authorization\") == \"444\" then\n                                            core.response.exit(403, core.request.headers(ctx));\n                                        end\n                                    end]],\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        if core.request.header(ctx, \"Authorization\") == \"777\" then\n                                            local tenant_id = core.request.header(ctx, \"tenant_id\")\n                                            if tenant_id == \"123\" then\n                                                core.response.set_header(\"X-User-ID\", \"i-am-an-user\");\n                                                core.response.exit(200);\n                                            else\n                                                core.response.exit(400, \"tenant_id is \"..tenant_id);\n                                            end\n                                        end\n                                    end]],\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        if core.request.header(ctx, \"Authorization\") == \"888\" then\n                                            local tenant_id = core.request.header(ctx, \"tenant_id\")\n                                            if tenant_id == \"abcd\" then\n                                                core.response.set_header(\"X-User-ID\", \"i-am-an-user\");\n                                                core.response.exit(200);\n                                            else\n                                                core.response.exit(400, \"tenant_id is \"..tenant_id);\n                                            end\n                                        end\n                                    end]],\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\")\n                                        if core.request.get_method() == \"POST\" then\n                                           local req_body, err = core.request.get_body()\n                                           if err then\n                                               core.response.exit(400)\n                                           end\n                                           if req_body then\n                                               local data, err = core.json.decode(req_body)\n                                               if err then\n                                                   core.response.exit(400)\n                                               end\n                                               if data[\"authorization\"] == \"555\" then\n                                                   core.response.set_header(\"X-User-ID\", \"i-am-an-user\")\n                                                   core.response.exit(200)\n                                               elseif data[\"authorization\"] == \"666\" then\n                                                   core.response.set_header(\"Location\", \"http://example.com/auth\")\n                                                   core.response.exit(403)\n                                               end\n                                           end\n                                        end\n                                    end]]\n                                }\n                            }\n                        },\n                        uri = \"/auth\"\n                    },\n                },\n                {\n                    url = \"/apisix/admin/routes/echo\",\n                    data = [[{\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\": [\n                                    \"return function (conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        core.response.exit(200, core.request.headers(ctx));\n                                    end\"\n                                ]\n                            }\n                        },\n                        \"uri\": \"/echo\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"]\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/hello\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/2\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_headers\": [\"Authorization\"]\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/empty\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/3\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_method\": \"POST\",\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"]\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/ping\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/4\",\n                    data = [[{\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\" : [\"return function() require(\\\"apisix.core\\\").response.exit(444); end\"]\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/crashed-auth\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/5\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/crashed-auth\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"]\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/nodegr\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/6\",\n                    data = [[{\n                        \"uri\": \"/hello\",\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/crashed-auth\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"],\n                                \"allow_degradation\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"test.com:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/8\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.39.40.1:9999/auth\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"],\n                                \"status_on_error\": 503,\n                                \"allow_degradation\": false\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/onerror\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/9\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_method\": \"GET\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"],\n                                \"extra_headers\": {\"tenant_id\": \"$post_arg.tenant_id\"}\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/ping2\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/10\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_method\": \"GET\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"upstream_headers\": [\"X-User-ID\"],\n                                \"client_headers\": [\"Location\"],\n                                \"extra_headers\": {\"tenant_id\": \"abcd\"}\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/ping3\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/11\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_method\": \"GET\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"extra_headers\": {\"X-User\": \"$arg_user\"}\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/crlf\"\n                    }]]\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\" x 13\n\n\n\n=== TEST 3: hit route (test request_headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 111\n--- response_body_like eval\nqr/\\\"authorization\\\":\\\"111\\\"/\n\n\n\n=== TEST 4: hit route (test upstream_headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 222\n--- response_body_like eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 5: hit route (test client_headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 333\n--- error_code: 403\n--- response_headers\nLocation: http://example.com/auth\n\n\n\n=== TEST 6: hit route (check APISIX generated headers and ignore client headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 444\nX-Forwarded-Host: apisix.apache.org\n--- error_code: 403\n--- response_body eval\nqr/\\\"x-forwarded-proto\\\":\\\"http\\\"/     and qr/\\\"x-forwarded-method\\\":\\\"GET\\\"/    and\nqr/\\\"x-forwarded-host\\\":\\\"localhost\\\"/ and qr/\\\"x-forwarded-uri\\\":\\\"\\\\\\/hello\\\"/ and\nqr/\\\"x-forwarded-for\\\":\\\"127.0.0.1\\\"/\n--- response_body_unlike eval\nqr/\\\"x-forwarded-host\\\":\\\"apisix.apache.org\\\"/\n\n\n\n=== TEST 7: hit route (not send upstream headers)\n--- request\nGET /empty\n--- more_headers\nAuthorization: 222\n--- response_body_unlike eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 8: hit route (not send client headers)\n--- request\nGET /empty\n--- more_headers\nAuthorization: 333\n--- error_code: 403\n--- response_headers\n!Location\n\n\n\n=== TEST 9: hit route (test upstream_headers when use post method)\n--- request\nPOST /ping\n{\"authorization\": \"555\"}\n--- response_body_like eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 10: hit route (test client_headers when use post method)\n--- request\nPOST /ping\n{\"authorization\": \"666\"}\n--- error_code: 403\n--- response_headers\nLocation: http://example.com/auth\n\n\n\n=== TEST 11: hit route (unavailable auth server, expect failure)\n--- request\nGET /nodegr\n--- more_headers\nAuthorization: 111\n--- error_code: 403\n--- error_log\nfailed to process forward auth, err: closed\n\n\n\n=== TEST 12: hit route (unavailable auth server, allow degradation)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 111\n--- error_code: 200\n\n\n\n=== TEST 13: Verify status_on_error\n--- request\nGET /onerror\n--- more_headers\nAuthorization: 333\n--- error_code: 503\n\n\n\n=== TEST 14: hit route (test extra_headers when use post method)\n--- request\nPOST /ping2\n{\"tenant_id\": 123}\n--- more_headers\nAuthorization: 777\nContent-Type: application/json\n--- response_body_like eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 15: hit route (test extra_headers when extra headers has fixed value)\n--- request\nGET /ping3\n--- more_headers\nAuthorization: 888\n--- response_body_like eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 16: block CRLF header injection\n--- request\nGET /crlf?user=guest%0d%0ax-user1:%20admin\n--- more_headers\nAuthorization: 111\n--- error_code: 403\n--- error_log\nfailed to process forward auth, err: invalid characters found in header value,\n"
  },
  {
    "path": "t/plugin/forward-auth2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/upstreams/u1\",\n                    data = [[{\n                        \"nodes\": {\n                            \"127.0.0.1:1984\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/auth\",\n                    data = {\n                        plugins = {\n                            [\"serverless-pre-function\"] = {\n                                phase = \"rewrite\",\n                                functions =  {\n                                    [[return function(conf, ctx)\n                                        local core = require(\"apisix.core\");\n                                        local token = \"token-headers-test\";\n                                        if core.request.header(ctx, \"Authorization\") == token then\n                                            if core.request.get_method() == \"POST\" then\n                                                if core.request.header(ctx, \"Content-Length\") or\n                                                core.request.header(ctx, \"Transfer-Encoding\") or\n                                                core.request.header(ctx, \"Content-Encoding\") then\n                                                    core.response.exit(200)\n                                                else\n                                                    core.response.exit(403)\n                                                end\n                                            else\n                                                if core.request.header(ctx, \"Content-Length\") or\n                                                core.request.header(ctx, \"Transfer-Encoding\") or\n                                                core.request.header(ctx, \"Content-Encoding\") then\n                                                    core.response.exit(403)\n                                                else\n                                                    core.response.exit(200)\n                                                end\n                                            end\n                                        end\n                                    end]]\n                                }\n                            }\n                        },\n                        uri = \"/auth\"\n                    },\n                },\n                {\n                    url = \"/apisix/admin/routes/echo\",\n                    data = [[{\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\": [\n                                    \"return function (conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        core.response.exit(200, core.request.headers(ctx));\n                                    end\"\n                                ]\n                            }\n                        },\n                        \"uri\": \"/echo\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"request_method\": \"POST\"\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/verify-auth-post\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/2\",\n                    data = [[{\n                        \"plugins\": {\n                            \"forward-auth\": {\n                                \"uri\": \"http://127.0.0.1:1984/auth\",\n                                \"request_headers\": [\"Authorization\"],\n                                \"request_method\": \"GET\"\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/verify-auth-get\"\n                    }]],\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\" x 5\n\n\n\n=== TEST 2: verify auth server forward headers for request_method=GET\n--- request\nGET /verify-auth-get\n--- more_headers\nAuthorization: token-headers-test\n--- error_code: 200\n\n\n\n=== TEST 3: verify auth server forward headers for request_method=POST for GET upstream\n--- request\nGET /verify-auth-post\n--- more_headers\nAuthorization: token-headers-test\n--- error_code: 200\n\n\n\n=== TEST 4: verify auth server forward headers for request_method=POST\n--- request\nPOST /verify-auth-post\n{\"authorization\": \"token-headers-test\"}\n--- more_headers\nAuthorization: token-headers-test\n--- error_code: 200\n\n\n\n=== TEST 5: verify auth server forward headers for request_method=GET for POST upstream\n--- request\nPOST /verify-auth-get\n{\"authorization\": \"token-headers-test\"}\n--- more_headers\nAuthorization: token-headers-test\n--- error_code: 200\n"
  },
  {
    "path": "t/plugin/google-cloud-logging/config-https-domain.json",
    "content": "{\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\\nkEJQcmfVew5mFXyxuEn3zA==\\n-----END PRIVATE KEY-----\",\n  \"project_id\": \"apisix\",\n  \"token_uri\": \"https://test.com:1983/google/logging/token\",\n  \"scope\": [\n    \"https://apisix.apache.org/logs:admin\"\n  ],\n  \"entries_uri\": \"https://test.com:1983/google/logging/entries\"\n}\n"
  },
  {
    "path": "t/plugin/google-cloud-logging/config-https-ip.json",
    "content": "{\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\\nkEJQcmfVew5mFXyxuEn3zA==\\n-----END PRIVATE KEY-----\",\n  \"project_id\": \"apisix\",\n  \"token_uri\": \"https://127.0.0.1:1983/google/logging/token\",\n  \"scope\": [\n    \"https://apisix.apache.org/logs:admin\"\n  ],\n  \"entries_uri\": \"https://127.0.0.1:1983/google/logging/entries\"\n}\n"
  },
  {
    "path": "t/plugin/google-cloud-logging/config.json",
    "content": "{\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\\nkEJQcmfVew5mFXyxuEn3zA==\\n-----END PRIVATE KEY-----\",\n  \"project_id\": \"apisix\",\n  \"token_uri\": \"http://127.0.0.1:1980/google/logging/token\",\n  \"scope\": [\n    \"https://apisix.apache.org/logs:admin\"\n  ],\n  \"entries_uri\": \"http://127.0.0.1:1980/google/logging/entries\"\n}\n"
  },
  {
    "path": "t/plugin/google-cloud-logging.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Full configuration verification (Auth File)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.google-cloud-logging\")\n            local ok, err = plugin.check_schema({\n                auth_file = \"/path/to/apache/apisix/auth.json\",\n                resource = {\n                    type = \"global\"\n                },\n                scope = {\n                    \"https://www.googleapis.com/auth/logging.admin\"\n                },\n                log_id = \"syslog\",\n                max_retry_count = 0,\n                retry_delay = 1,\n                buffer_duration = 60,\n                inactive_timeout = 10,\n                batch_max_size = 100,\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Full configuration verification (Auth Config)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.google-cloud-logging\")\n            local ok, err = plugin.check_schema({\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = \"private_key\",\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/token\",\n                },\n                resource = {\n                    type = \"global\"\n                },\n                scope = {\n                    \"https://www.googleapis.com/auth/logging.admin\"\n                },\n                log_id = \"syslog\",\n                max_retry_count = 0,\n                retry_delay = 1,\n                buffer_duration = 60,\n                inactive_timeout = 10,\n                batch_max_size = 100,\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: Basic configuration verification (Auth File)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.google-cloud-logging\")\n            local ok, err = plugin.check_schema({\n                auth_file = \"/path/to/apache/apisix/auth.json\",\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: Basic configuration verification (Auth Config)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.google-cloud-logging\")\n            local ok, err = plugin.check_schema({\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = \"private_key\",\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/token\",\n                },\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: auth configure undefined\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.google-cloud-logging\")\n            local ok, err = plugin.check_schema({\n                log_id = \"syslog\",\n                max_retry_count = 0,\n                retry_delay = 1,\n                buffer_duration = 60,\n                inactive_timeout = 10,\n                batch_max_size = 100,\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\nvalue should match only one schema, but matches none\n\n\n\n=== TEST 6: set route (identity authentication failed)\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAKeXgPvU/dAfVhOPk5BTBXCaOXy/0S3mY9VHyqvWZBJ97g6tGbLZ\npsn6Gw0wC4mxDfEY5ER4YwU1NWCVtIr1XxcCAwEAAQJADkoowVBD4/8IA9r2JhQu\nHo/H3w8r8tH2KTVZ3pUFK15WGJf8vCF9LznVNKCP0X1NMLGvf4yRELx8jjpwJztI\ngQIhANdWaJ3AGftJNaF5qXWwniFP1BcyCPSzn3q0rn19NhyHAiEAxz0HN8Yd+7vR\npi0w/L2I/2nLqgPFtqSGpL2KkJYcXPECIQCdM/PD1k4haNzCOXNA++M1JnYLSPfI\nzKkMh4MrEZHDWQIhAKasRiKBaUnTCIJ04bs9L6NDtO4Ic9jj8ANW0Nk9yoJxAiAA\ntBXLQH7fw5H8RaxBN91yQUZombw6JnRBXKKohWHZ3Q==\n-----END RSA PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: test route (identity authentication failed)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- grep_error_log eval\nqr/\\{\\\"error\\\"\\:\\\"[\\w+\\s+]*\\\"\\}/\n--- grep_error_log_out\n{\"error\":\"identity authentication failed\"}\n--- error_log\nBatch Processor[google-cloud-logging] failed to process entries\nBatch Processor[google-cloud-logging] exceeded the max_retry_count\n\n\n\n=== TEST 8: set route (no access to this scopes)\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token\",\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: test route (no access to this scopes)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- grep_error_log eval\nqr/\\{\\\"error\\\"\\:\\\"[\\w+\\s+]*\\\"\\}/\n--- grep_error_log_out\n{\"error\":\"no access to this scopes\"}\n--- error_log\nBatch Processor[google-cloud-logging] failed to process entries\nBatch Processor[google-cloud-logging] exceeded the max_retry_count\n\n\n\n=== TEST 10: set route (succeed write)\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: test route(succeed write)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n\n\n\n=== TEST 12: set route (customize auth type)\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token?token_type=Basic\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries?token_type=Basic\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: test route(customize auth type)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n\n\n\n=== TEST 14: set route (customize auth type error)\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token?token_type=Basic\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: test route(customize auth type error)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- grep_error_log eval\nqr/\\{\\\"error\\\"\\:\\\"[\\w+\\s+]*\\\"\\}/\n--- grep_error_log_out\n{\"error\":\"identity authentication failed\"}\n--- error_log\nBatch Processor[google-cloud-logging] failed to process entries\nBatch Processor[google-cloud-logging] exceeded the max_retry_count\n\n\n\n=== TEST 16: set route (file configuration is successful)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_file = \"t/plugin/google-cloud-logging/config.json\",\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: test route(file configuration is successful)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n\n\n\n=== TEST 18: set route (file configuration is failed)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_file = \"google-cloud-logging/config.json\",\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: test route(file configuration is failed)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nconfig.json: No such file or directory\n\n\n\n=== TEST 20: set route (https file configuration is successful)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_file = \"t/plugin/google-cloud-logging/config-https-domain.json\",\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                        ssl_verify = true,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: test route(https file configuration is successful)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n\n\n\n=== TEST 22: set route (https file configuration SSL authentication failed: ssl_verify = true)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_file = \"t/plugin/google-cloud-logging/config-https-ip.json\",\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                        ssl_verify = true,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: test route(https file configuration SSL authentication failed: ssl_verify = true)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nfailed to refresh google oauth access token, certificate host mismatch\n\n\n\n=== TEST 24: set route (https file configuration SSL authentication succeed: ssl_verify = false)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_file = \"t/plugin/google-cloud-logging/config-https-ip.json\",\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                        ssl_verify = false,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: test route(https file configuration SSL authentication succeed: ssl_verify = false)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n"
  },
  {
    "path": "t/plugin/google-cloud-logging2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route (verify batch queue default params)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_file = \"t/plugin/google-cloud-logging/config.json\",\n                    }\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: data encryption for auth_config.private_key\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"google-cloud-logging\"].auth_config.private_key)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"google-cloud-logging\"].auth_config.private_key)\n        }\n    }\n--- response_body\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----\nYnwwDKc5vNzo0OU4StTRQbwgCnTZ3dmYiBFm8aGnvTxlE86D2nT07Q3BWhUdky6OGIox4MRLbiHz13NZjyUao/Nudh4PeTj5wMldPD5YvNWtbTG4ig/TNSdBncmIQPLPaUqSweE61pnASxodpTlBJ5k9yxfTmwBTOkzZevoKy9D2E4wF9vGCdkcPK/tAkvRoJTj6xD3xVuAbkcap/81oHplUZZ+ghlEnBZgOH8UMa73UfeNbOQVHD2mlU0LxkTXtwFhHWl50adrt890VDHev0+FUUDjv5Ysl8r/nnnlyq3SV4oqJfs/IVRKROe93e8sJ2/49o7kEv2XT1/6DjM/VsSLKfAi5rLNobcSaSzztSSLkrBFKQvvy2rRA7GFWKbIk+rPZhYmTMItDJv23XP6uzaLRPoq2f/AnRTKpWmA8Dk9TfFHsZLupKi1bmjCdtK8lpMCf9Au1rezt7+2BybQrtbbDbwPzC5bKHmKhc0GPTUzLAWQBin3tuZxSfk/MqRtG+AemwnFTHivJrfRwmc3db+b9W6WX09mV488f2M4qbqBmkiFU5VARWCGZ5vbop2KGhmB2fQPXTmj8QSYk6fBxFDnfzTfnYMIu2cQsbSBPCnoPinQNpBfFD3RQkkCiNtJ8GA8DWsivWsnW4jWyPmkIN/P1eLW1DSsU6V4cbhTQJs6/LzOCGAZB/ewu3mr1SDLWJPlIWW6atC/g0uiXkZ3VLUsS0BQffITf8sVXyz/BEbflLlT777zERDKyz/qS2JyR6U8s2h3Yg+GncPUCEF6Lx5Veb1lL+zs+Stvv+5/t2GfDlNYiwTU8HeffhEGgAv1s86OPo3CfWe7lEnu/MFHIm0czVenYdEVy449xj66DHqXUQVzVc+3NelW15FrKhcvU0Cxwqfk+xEOE185ssD06L+tOGjxPPvADjlcQQ1crH+tEcTTLnZZ/e16I10kcc5rBJwDy4COoeY6DZ0dFwtAdbjoR/KaSTGLK6n/u9Ow7OGDPZog4LhrzMOn2T7hk/oaMOKhlDvKroiSijhhkrQf5ZDhhh3GQn/ZRXjyiPWBqKEQiBJGyZ/iRONzJLsF8U8vsBzBToxmTe9prlwHusgAEIBUFrZRSvsVgsPCFOyJ6XJXDTdcCInHUGI9LsxWdlojYvvNuSvavkw1I4K+VBmlEG5FCMx56eX2X49hfXwRcM1ZyRRrmq6cRh+33aMeMLAtpKgTsQgmB/I01mGNZlstvU0XEFnCPuWcks50BTnvPEbU7GZJLE3HFmGb3vyC57E8oTR2FjhDrevPlLkxMPrLvXhwbmV+3YiZYq+8k6oBKfrrq41JRKr+SJDb7m6xL8AuZccMrNhDrkByQLi6zn95dIYc3+vNU4XBzvhpb7HMj2wvorxEW2HpQ+OVSZiZSCU6m4Fx6juj1D5pGs1nr68ybihqMrXuZBKP4b9Y6sw99kNmnWBdwNiY95sWy1qUe0MJq3r44hhVHvCUmzOVyO4aBmhMwgkaSQWpEeQwyIWENM1IMU6WUrKCSuLuKJAl5bM++ThBaLvIIMCyXl39136jHp97aVmHRXbSMPcSAb8l/YQ6SLK0HBxmFTXvroxHmPxPqrJ5jz65C72+uArgOZxJN6tyimIcTMyoJoN7N+QKxDLjgmqnJyEcthycEK3gikyloWsLppzEmHLHBDXlKpJLflvUujYrNsKf2xohx31gIlxBWCHP/1KL3QAehn+FEWUWsXn2hWAR0KAtmIOM7gZuCY8yKNDfXrAZJs14rwDlTbnhJvyijt1Tr6gleehmJDKSm2vM/NbznVTKwJDyMRner+vvc4zD06az/Y6Y4oM0e0IWM2fMaiiwjNAaKhhwJzqvM1c8+ZOfuRajmHFECEkYgXCKZiQxQihFG2wWp2i+xEGGwP2e+FbDdY9Ygyvw5SUvahyoX36AYbbTBOFY6E9aYUIM/Et8ZuXoWs1QaxGfJwcVvueqke45y3GKkp54sHXhrqfKX0TTiw6DCUs6dRTybxOjmjJCKp6Yw4KGWY0t3J0xbK08KTUMeHNxgtfYcz1/Wg/Q61CkUJkRNBninAAkEz8rV2olBHy1GZFFjCQySAyPH4PtWm1S4sBzdsui5wT+m2pC/DsCcQW++TGH9LdaHeT8B9u32lYToVN1/L2j5kjkhN13sNKfb6I9yYTnUqweQFU79toBfDt+6KNNfIA1TcmvZw8RcuMOArEqJQ6OPOhgUQBwsZaGeqFmAE4q64n5raS4OCdWtasFtItW3c5QHxkKoEEER04glVsCoxOvc80U=\n\n\n\n=== TEST 3: set route to test custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_metadata/google-cloud-logging',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"client_ip\": \"$remote_addr\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- extra_init_by_lua\n    local decode = require(\"toolkit.json\").decode\n    local up = require(\"lib.server\")\n    up.google_logging_entries = function()\n        ngx.log(ngx.WARN, \"the mock backend is hit\")\n\n        ngx.req.read_body()\n        local data = ngx.req.get_body_data()\n        data = decode(data)\n        assert(data.entries[1].jsonPayload.client_ip == \"127.0.0.1\")\n        assert(data.entries[1].resource.type == \"global\")\n        ngx.say('{}')\n    end\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nthe mock backend is hit\n--- no_error_log\n[error]\n\n\n\n=== TEST 5: bad custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/google-cloud-logging',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": \"'$host' '$time_iso8601'\"\n                 }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"log_format\\\" validation failed: wrong type: expected object, got string\"}\n\n\n\n=== TEST 6: set route to test custom log format in route\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"google-cloud-logging\"] = {\n                        auth_config = {\n                            client_email = \"email@apisix.iam.gserviceaccount.com\",\n                            private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                            project_id = \"apisix\",\n                            token_uri = \"http://127.0.0.1:1980/google/logging/token\",\n                            scope = {\n                                \"https://apisix.apache.org/logs:admin\"\n                            },\n                            entries_uri = \"http://127.0.0.1:1980/google/logging/entries\",\n                        },\n                        log_format = {\n                            host = \"$host\",\n                            [\"@timestamp\"] = \"$time_iso8601\",\n                            vip = \"$remote_addr\"\n                        },\n                        inactive_timeout = 1,\n                        batch_max_size = 1,\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- extra_init_by_lua\n    local decode = require(\"toolkit.json\").decode\n    local up = require(\"lib.server\")\n    up.google_logging_entries = function()\n        ngx.log(ngx.WARN, \"the mock backend is hit\")\n\n        ngx.req.read_body()\n        local data = ngx.req.get_body_data()\n        data = decode(data)\n        assert(data.entries[1].jsonPayload.vip == \"127.0.0.1\")\n        assert(data.entries[1].resource.type == \"global\")\n        ngx.say('{}')\n    end\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nthe mock backend is hit\n--- no_error_log\n[error]\n"
  },
  {
    "path": "t/plugin/google-cloud-logging3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: should drop entries when max_pending_entries is exceeded\n--- extra_yaml_config\nplugins:\n  - google-cloud-logging\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"google-cloud-logging\"] = {\n                            auth_config = {\n                                client_email = \"email@apisix.iam.gserviceaccount.com\",\n                                private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                                project_id = \"apisix\",\n                                token_uri = \"http://127.0.0.1:1234/google/logging/token\",\n                                scope = {\n                                    \"https://apisix.apache.org/logs:admin\"\n                                },\n                                entries_uri = \"http://127.0.0.1:1234/google/logging/entries\",\n                            },\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/google-cloud-logging', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/grpc-transcode-reload-bugfix.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('warn');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_init_by_lua = <<_EOC_;\n    local core = require(\"apisix.core\")\n    local orig_new = core.config.new\n    close_cnt = 0\n    core.config.new = function(key, opts)\n        local obj, err = orig_new(key, opts)\n        if key == \"/protos\" then\n            local orig_close = obj.close\n            obj.close = function(...)\n                core.log.warn(\"call config close\")\n                close_cnt = close_cnt + 1\n                return orig_close(...)\n            end\n        end\n        return obj, err\n    end\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: close protos when grpc-transcode plugin reload\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/plugins/reload',\n                ngx.HTTP_PUT)\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.sleep(2)\n            if close_cnt ~= 1 then\n                ngx.status = 500\n            end\n        }\n    }\n--- error_log\ncall config close\n"
  },
  {
    "path": "t/plugin/grpc-transcode.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('debug');\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set proto(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                         }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local res = assert(etcd.get('/protos/1'))\n            local create_time = res.body.node.value.create_time\n            assert(create_time ~= nil, \"create_time is nil\")\n            local update_time = res.body.node.value.update_time\n            assert(update_time ~= nil, \"update_time is nil\")\n\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set proto(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                         }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: delete proto(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/2',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: set routes(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route\n--- request\nGET /grpctest?name=world\n--- response_body eval\nqr/\\{\"message\":\"Hello world\"\\}/\n\n\n\n=== TEST 6: hit route by post\n--- request\nPOST /grpctest\nname=world\n--- response_body eval\nqr/\\{\"message\":\"Hello world\"\\}/\n\n\n\n=== TEST 7: hit route by post json\n--- request\nPOST /grpctest\n{\"name\": \"world\"}\n--- more_headers\nContent-Type: application/json\n--- response_body eval\nqr/\\{\"message\":\"Hello world\"\\}/\n\n\n\n=== TEST 8: wrong upstream scheme\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"asf\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 9: wrong upstream address\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1970\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route (Connection refused)\n--- request\nGET /grpctest\n--- response_body eval\nqr/502 Bad Gateway/\n--- error_log\nConnection refused) while connecting to upstream\n--- error_code: 502\n\n\n\n=== TEST 11: update proto(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                          rpc Plus (PlusRequest) returns (PlusReply) {}\n                          rpc SayHelloAfterDelay (HelloRequest) returns (HelloReply) {}\n                      }\n\n                      message HelloRequest {\n                          string name = 1;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                         }\n                      message PlusRequest {\n                          int64 a = 1;\n                          int64 b = 2;\n                      }\n                      message PlusReply {\n                          int64 result = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: set routes(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\",\n                            \"pb_option\":[\"int64_as_string\", \"enum_as_name\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route\n--- request\nGET /grpc_plus?a=1&b=2\n--- response_body eval\nqr/\\{\"result\":3\\}/\n\n\n\n=== TEST 14: hit route\n--- request\nGET /grpc_plus?a=1&b=2251799813685260\n--- response_body eval\nqr/\\{\"result\":\"#2251799813685261\"\\}/\n\n\n\n=== TEST 15: set route3 deadline nodelay\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_deadline\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\",\n                            \"deadline\": 500\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route\n--- request\nGET /grpc_deadline?name=apisix\n--- response_body eval\nqr/\\{\"message\":\"Hello apisix\"\\}/\n\n\n\n=== TEST 17: set route4 deadline delay\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/4',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_delay\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHelloAfterDelay\",\n                            \"deadline\": 500\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route\n--- request\nGET /grpc_delay?name=apisix\n--- error_code: 504\n\n\n\n=== TEST 19: set routes: missing method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin grpc-transcode err: property \\\"method\\\" is required\"}\n\n\n\n=== TEST 20: set proto(id: 1, with array parameter)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                          repeated string items = 2;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                          repeated string items = 2;\n                         }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: set routes(id: 1, with array parameter)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: hit route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/grpctest',\n                ngx.HTTP_POST,\n                [[\n                {\"name\":\"apisix\", \"items\": [\"a\",\"b\",\"c\"]}\n                ]],\n                [[\n                {\"message\":\"Hello apisix\", \"items\": [\"a\",\"b\",\"c\"]}\n                ]],\n                {[\"Content-Type\"] = \"application/json\"}\n                )\n            ngx.status = code\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 23: set proto with enum\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      enum Gender {\n                        GENDER_UNKNOWN = 0;\n                        GENDER_MALE = 1;\n                        GENDER_FEMALE = 2;\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                          repeated string items = 2;\n                          Gender gender = 3;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                          repeated string items = 2;\n                          Gender gender = 3;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: hit route, no gender\n--- request\nPOST /grpctest\n{\"name\":\"world\"}\n--- more_headers\nContent-Type: application/json\n--- response_body eval\nqr/\"gender\":\"GENDER_UNKNOWN\"/\n\n\n\n=== TEST 25: hit route, gender is a value\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"gender\":2}\n--- more_headers\nContent-Type: application/json\n--- response_body eval\nqr/\"gender\":\"GENDER_FEMALE\"/\n\n\n\n=== TEST 26: hit route, gender is a name\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"gender\":\"GENDER_MALE\"}\n--- more_headers\nContent-Type: application/json\n--- response_body eval\nqr/\"gender\":\"GENDER_MALE\"/\n\n\n\n=== TEST 27: hit route, bad gender\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"gender\":\"GENDER_MA\"}\n--- more_headers\nContent-Type: application/json\n--- error_code: 400\n--- error_log\nfailed to encode request data to protobuf\n\n\n\n=== TEST 28: set routes(decode enum as value)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\",\n                            \"pb_option\":[\"enum_as_value\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: hit route\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"gender\":2}\n--- more_headers\nContent-Type: application/json\n--- response_body eval\nqr/\"gender\":2/\n"
  },
  {
    "path": "t/plugin/grpc-transcode2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      enum Gender {\n                        GENDER_UNKNOWN = 0;\n                        GENDER_MALE = 1;\n                        GENDER_FEMALE = 2;\n                      }\n                      message Person {\n                          string name = 1;\n                          int32 age = 2;\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                          repeated string items = 2;\n                          Gender gender = 3;\n                          Person person = 4;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"person\":{\"name\":\"Joe\",\"age\":1}}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"message\":\"Hello world, name: Joe, age: 1\"}\n\n\n\n=== TEST 3: hit route, missing some fields\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"person\":{\"name\":\"Joe\"}}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"message\":\"Hello world, name: Joe\"}\n\n\n\n=== TEST 4: set rule to check if each proto is separate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      // same message, different fields. use to pollute the type info\n                      message HelloRequest {\n                          string name = 1;\n                          string person = 2;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/fail\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"2\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayHello\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n        local body = [[{\"name\":\"world\",\"person\":{\"name\":\"John\"}}]]\n        local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/json\"}}\n\n        local function access(path)\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri .. path, opt)\n            if not res then\n                ngx.say(err)\n                return\n            end\n            if res.status > 300 then\n                ngx.say(res.status)\n            else\n                ngx.say(res.body)\n            end\n        end\n\n        access(\"/fail\")\n        access(\"/grpctest\")\n        access(\"/fail\")\n        access(\"/grpctest\")\n    }\n}\n--- response_body\n400\n{\"message\":\"Hello world, name: John\"}\n400\n{\"message\":\"Hello world, name: John\"}\n--- error_log\nfailed to encode request data to protobuf\n\n\n\n=== TEST 6: set binary rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n\n            local content = t.read_file(\"t/grpc_server_example/proto.pb\")\n            local data = {content = ngx.encode_base64(content)}\n            local code, body = t.test('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.TestImport\",\n                            \"method\": \"Run\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route\n--- request\nPOST /grpctest\n{\"body\":\"world\",\"user\":{\"name\":\"Hello\"}}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"body\":\"Hello world\"}\n\n\n\n=== TEST 8: service/method not found\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/service_not_found\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.TestImportx\",\n                            \"method\": \"Run\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/method_not_found\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.TestImport\",\n                            \"method\": \"Runx\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route\n--- request\nPOST /service_not_found\n{\"body\":\"world\",\"user\":{\"name\":\"Hello\"}}\n--- more_headers\nContent-Type: application/json\n--- error_log\nUndefined service method\n--- error_code: 503\n\n\n\n=== TEST 10: hit route\n--- request\nPOST /method_not_found\n{\"body\":\"world\",\"user\":{\"name\":\"Hello\"}}\n--- more_headers\nContent-Type: application/json\n--- error_log\nUndefined service method\n--- error_code: 503\n\n\n\n=== TEST 11: set proto(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                          rpc Plus (PlusRequest) returns (PlusReply) {}\n                          rpc SayHelloAfterDelay (HelloRequest) returns (HelloReply) {}\n                      }\n\n                      message HelloRequest {\n                          string name = 1;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                         }\n                      message PlusRequest {\n                          int64 a = 1;\n                          int64 b = 2;\n                      }\n                      message PlusReply {\n                          int64 result = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: work with logger plugin which on global rule and read response body (logger plugins store undecoded body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\",\n                            \"pb_option\":[\"int64_as_string\", \"enum_as_name\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"include_resp_body\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route\n--- request\nGET /grpc_plus?a=1&b=2\n--- response_body eval\nqr/\\{\"result\":3\\}/\n--- error_log eval\nqr/request log: \\{.*body\":\\\"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0002\\\\b\\\\u0003\"/\n\n\n\n=== TEST 14: delete global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, message = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n            )\n            ngx.say(\"[delete] code: \", code, \" message: \", message)\n        }\n    }\n--- response_body\n[delete] code: 200 message: passed\n\n\n\n=== TEST 15: work with logger plugin which on route and read response body (logger plugins store decoded body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\",\n                            \"pb_option\":[\"int64_as_string\", \"enum_as_name\"]\n                        },\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 1,\n                            \"include_resp_body\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route\n--- request\nGET /grpc_plus?a=1&b=2\n--- response_body eval\nqr/\\{\"result\":3\\}/\n--- error_log eval\nqr/request log: \\{.*body\":\\\"\\{\\\\\"result\\\\\":3}/\n\n\n\n=== TEST 17: pb_option should be be set on the route level\n--- extra_init_by_lua\n    local pb = require(\"pb\")\n    local old_f = pb.option\n    pb.option = function(o)\n        if o ~= \"int64_as_string\" and o ~= \"int64_as_number\" then\n            -- filter out options set by other components.\n            -- we can still test some options like enum_as_name\n            ngx.log(ngx.WARN, \"set protobuf option: \", o)\n        end\n        return old_f(o)\n    end\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc Plus (PlusRequest) returns (PlusReply) {}\n                      }\n\n                      message PlusRequest {\n                          int64 a = 1;\n                          int64 b = 2;\n                      }\n                      message PlusReply {\n                          int64 result = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus2\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\",\n                            \"pb_option\":[\"int64_as_string\", \"enum_as_name\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n\n            for i = 1, 3 do\n                local uri = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                    (i == 2 and \"/grpc_plus2\" or \"/grpc_plus\") ..\n                    \"?a=1&b=2251799813685260\"\n                local httpc = http.new()\n                local res = assert(httpc:request_uri(uri, {keepalive = false}))\n                ngx.say(res.body)\n            end\n        }\n    }\n--- response_body\n{\"result\":\"#2251799813685261\"}\n{\"result\":2.2517998136853e+15}\n{\"result\":\"#2251799813685261\"}\n--- grep_error_log eval\nqr/set protobuf option: \\w+/\n--- grep_error_log_out\nset protobuf option: enum_as_name\nset protobuf option: auto_default_values\nset protobuf option: disable_hooks\nset protobuf option: enum_as_name\nset protobuf option: enum_as_name\n\n\n\n=== TEST 18: pb_option should be be set on the route level, two route have the same options\n--- extra_init_by_lua\n    local pb = require(\"pb\")\n    local old_f = pb.option\n    pb.option = function(o)\n        if o ~= \"int64_as_string\" and o ~= \"int64_as_number\" then\n            -- filter out options set by other components\n            -- we can still test some options like enum_as_name\n            ngx.log(ngx.WARN, \"set protobuf option: \", o)\n        end\n        return old_f(o)\n    end\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc Plus (PlusRequest) returns (PlusReply) {}\n                      }\n\n                      message PlusRequest {\n                          int64 a = 1;\n                          int64 b = 2;\n                      }\n                      message PlusReply {\n                          int64 result = 1;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus2\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/grpc_plus\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"Plus\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n\n            for i = 1, 3 do\n                local uri = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                    (i == 2 and \"/grpc_plus2\" or \"/grpc_plus\") ..\n                    \"?a=1&b=2251799813685260\"\n                local httpc = http.new()\n                local res = assert(httpc:request_uri(uri, {keepalive = false}))\n                ngx.say(res.body)\n            end\n        }\n    }\n--- response_body\n{\"result\":2.2517998136853e+15}\n{\"result\":2.2517998136853e+15}\n{\"result\":2.2517998136853e+15}\n--- grep_error_log eval\nqr/set protobuf option: \\w+/\n--- grep_error_log_out\nset protobuf option: auto_default_values\nset protobuf option: disable_hooks\nset protobuf option: enum_as_name\n"
  },
  {
    "path": "t/plugin/grpc-transcode3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set rule\n--- config\n    location /t {\n       content_by_lua_block {\n          local http = require \"resty.http\"\n          local t = require(\"lib.test_admin\").test\n          local code, body = t('/apisix/admin/protos/1',\n                ngx.HTTP_PUT,\n                [[{\n                   \"content\" : \"syntax = \\\"proto3\\\";\n                    package helloworld;\n                    service Greeter {\n                         rpc SayMultipleHello(MultipleHelloRequest) returns (MultipleHelloReply) {}\n                     }\n\n                     enum Gender {\n                           GENDER_UNKNOWN = 0;\n                           GENDER_MALE = 1;\n                           GENDER_FEMALE = 2;\n                      }\n\n                       message Person {\n                           string name = 1;\n                           int32 age = 2;\n                       }\n\n                      message MultipleHelloRequest {\n                          string name = 1;\n                          repeated string items = 2;\n                          repeated Gender genders = 3;\n                          repeated Person persons = 4;\n                    }\n\n                    message MultipleHelloReply{\n                          string message = 1;\n                    }\"\n                }]]\n              )\n\n             if code >= 300 then\n                 ngx.say(body)\n                 return\n              end\n\n             local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                   \"methods\": [\"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"SayMultipleHello\"\n                       }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n             )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- request\nPOST /grpctest\n{\"name\":\"world\",\"persons\":[{\"name\":\"Joe\",\"age\":1},{\"name\":\"Jake\",\"age\":2}]}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"message\":\"Hello world, name: Joe, age: 1, name: Jake, age: 2\"}\n\n\n\n=== TEST 3: set proto (id: 1, get error response from rpc)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc GetErrResp (HelloRequest) returns (HelloReply) {}\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                          repeated string items = 2;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                          repeated string items = 2;\n                      }\n                      message ErrorDetail {\n                          int64 code = 1;\n                          string message = 2;\n                          string type = 3;\n                      }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"GetErrResp\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route (error response in header)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, headers = t('/grpctest?name=world',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n\n            ngx.header['grpc-status'] = headers['grpc-status']\n            ngx.header['grpc-message'] = headers['grpc-message']\n            ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin']\n\n            body = json.encode(body)\n            ngx.say(body)\n        }\n    }\n--- response_headers\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\n--- response_body_unlike eval\nqr/error/\n--- error_code: 503\n\n\n\n=== TEST 5: set routes (id: 1, show error response in body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"GetErrResp\",\n                            \"show_status_in_body\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route (show error status in body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, headers = t('/grpctest?name=world',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n\n            ngx.header['grpc-status'] = headers['grpc-status']\n            ngx.header['grpc-message'] = headers['grpc-message']\n            ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin']\n\n            body = json.decode(body)\n            body = json.encode(body)\n            ngx.say(body)\n        }\n    }\n--- response_headers\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\n--- response_body\n{\"error\":{\"code\":14,\"details\":[{\"type_url\":\"type.googleapis.com/helloworld.ErrorDetail\",\"value\":\"\\b\\u0001\\u0012\\u001cThe server is out of service\\u001a\\u0007service\"}],\"message\":\"Out of service\"}}\n--- error_code: 503\n\n\n\n=== TEST 7: set routes (id: 1, show error details in body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"GetErrResp\",\n                            \"show_status_in_body\": true,\n                            \"status_detail_type\": \"helloworld.ErrorDetail\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route (show error details in body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, headers = t('/grpctest?name=world',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n\n            ngx.header['grpc-status'] = headers['grpc-status']\n            ngx.header['grpc-message'] = headers['grpc-message']\n            ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin']\n\n            body = json.decode(body)\n            body = json.encode(body)\n            ngx.say(body)\n        }\n    }\n--- response_headers\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\n--- response_body\n{\"error\":{\"code\":14,\"details\":[{\"code\":1,\"message\":\"The server is out of service\",\"type\":\"service\"}],\"message\":\"Out of service\"}}\n--- error_code: 503\n\n\n\n=== TEST 9: set routes (id: 1, show error details in body and wrong status_detail_type)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\": [\"GET\", \"POST\"],\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"helloworld.Greeter\",\n                            \"method\": \"GetErrResp\",\n                            \"show_status_in_body\": true,\n                            \"status_detail_type\": \"helloworld.error\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route (show error details in body and wrong status_detail_type)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local code, body, headers = t('/grpctest?name=world',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n\n            ngx.header['grpc-status'] = headers['grpc-status']\n            ngx.header['grpc-message'] = headers['grpc-message']\n            ngx.header['grpc-status-details-bin'] = headers['grpc-status-details-bin']\n\n            ngx.say(body)\n        }\n    }\n--- response_headers\ngrpc-status: 14\ngrpc-message: Out of service\ngrpc-status-details-bin: CA4SDk91dCBvZiBzZXJ2aWNlGlcKKnR5cGUuZ29vZ2xlYXBpcy5jb20vaGVsbG93b3JsZC5FcnJvckRldGFpbBIpCAESHFRoZSBzZXJ2ZXIgaXMgb3V0IG9mIHNlcnZpY2UaB3NlcnZpY2U\n--- response_body\nfailed to call pb.decode to decode details in grpc-status-details-bin\n--- error_log\ntransform response error: failed to call pb.decode to decode details in grpc-status-details-bin, err:\n--- error_code: 503\n\n\n\n=== TEST 11: set binary rule for EchoStruct\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n\n            local content = t.read_file(\"t/grpc_server_example/echo.pb\")\n            local data = {content = ngx.encode_base64(content)}\n            local code, body = t.test('/apisix/admin/protos/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/grpctest\",\n                    \"plugins\": {\n                        \"grpc-transcode\": {\n                            \"proto_id\": \"1\",\n                            \"service\": \"echo.Echo\",\n                            \"method\": \"EchoStruct\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"scheme\": \"grpc\",\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:10051\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route to test EchoStruct\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require \"apisix.core\"\n        local http = require \"resty.http\"\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/grpctest\"\n        local body = [[{\"data\":{\"fields\":{\"foo\":{\"string_value\":\"xxx\"},\"bar\":{\"number_value\":666}}}}]]\n        local opt = {method = \"POST\", body = body, headers = {[\"Content-Type\"] = \"application/json\"}, keepalive = false}\n        local httpc = http.new()\n        local res, err = httpc:request_uri(uri, opt)\n        if not res then\n            ngx.log(ngx.ERR, err)\n            return ngx.exit(500)\n        end\n        if res.status > 300 then\n            return ngx.exit(res.status)\n        else\n            local req = core.json.decode(body)\n            local rsp = core.json.decode(res.body)\n            for k, v in pairs(req.data.fields) do\n                if rsp.data.fields[k] == nil then\n                    ngx.log(ngx.ERR, \"rsp missing field=\", k, \", rsp: \", res.body)\n                else\n                    for k1, v1 in pairs(v) do\n                        if v1 ~= rsp.data.fields[k][k1] then\n                            ngx.log(ngx.ERR, \"rsp mismatch: k=\", k1,\n                                \", req=\", v1, \", rsp=\", rsp.data.fields[k][k1])\n                        end\n                    end\n                end\n            end\n        end\n    }\n}\n\n\n\n=== TEST 13: bugfix - filter out illegal INT(string) formats\n--- config\nlocation /t {\n    content_by_lua_block {\n        local pcall = pcall\n        local require = require\n        local protoc = require(\"protoc\")\n        local pb = require(\"pb\")\n        local pb_encode = pb.encode\n\n        assert(protoc:load [[\n        syntax = \"proto3\";\n        message IntStringPattern {\n            int64 value = 1;\n        }]])\n\n        local patterns\n        do\n            local function G(pattern)\n                return {pattern, true}\n            end\n\n            local function B(pattern)\n                return {pattern, [[bad argument #2 to '?' (number/'#number' expected for field 'value', got string)]]}\n            end\n\n            patterns = {\n                G(1), G(2), G(-3), G(\"#123\"), G(\"0xabF\"), G(\"#-0x123abcdef\"), G(\"-#0x123abcdef\"), G(\"#0x123abcdef\"), G(\"123\"),\n                B(\"#a\"), B(\"+aaa\"), B(\"#aaaa\"), B(\"#-aa\"),\n            }\n        end\n\n        for _, p in pairs(patterns) do\n            local pattern = {\n                value = p[1],\n            }\n            local status, err = pcall(pb_encode, \"IntStringPattern\", pattern)\n            local res = status\n            if not res then\n                res = err\n            end\n            assert(res == p[2])\n        end\n        ngx.say(\"passed\")\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 14: pb_option - check the matchings between enum and category\n--- config\n    location /t {\n        content_by_lua_block {\n            local ngx_re_match = ngx.re.match\n            local plugin = require(\"apisix.plugins.grpc-transcode\")\n\n            local pb_option_def = plugin.schema.properties.pb_option.items.anyOf\n\n            local patterns = {\n                [[^enum_as_.+$]],\n                [[^int64_as_.+$]],\n                [[^.+_default_.+$]],\n                [[^.+_hooks$]],\n            }\n\n            local function check_pb_option_enum_category()\n                for i, category in ipairs(pb_option_def) do\n                    for _, enum in ipairs(category.enum) do\n                        if not ngx_re_match(enum, patterns[i], \"jo\") then\n                            return ([[mismatch between enum(\"%s\") and category(\"%s\")]]):format(\n                                enum, category.description)\n                        end\n                    end\n                end\n            end\n\n            local err = check_pb_option_enum_category()\n            if err then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n"
  },
  {
    "path": "t/plugin/grpc-web/a6/buf.gen.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nversion: v2\nplugins:\n  - remote: buf.build/protocolbuffers/go:v1.36.6\n    out: .\n    opt:\n      - paths=source_relative\n  - remote: buf.build/grpc/go:v1.5.1\n    out: .\n    opt:\n      - paths=source_relative\n  - remote: buf.build/protocolbuffers/js:v3.21.4\n    out: .\n    opt:\n      - import_style=commonjs\n  - remote: buf.build/grpc/web:v1.5.0\n    out: .\n    opt:\n      - mode=grpcweb\n      - import_style=commonjs+dts\n"
  },
  {
    "path": "t/plugin/grpc-web/a6/buf.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nversion: v2\nlint:\n  use:\n    - STANDARD\nbreaking:\n  use:\n    - FILE\n"
  },
  {
    "path": "t/plugin/grpc-web/a6/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "t/plugin/grpc-web/a6/route.proto",
    "content": "//\n// Licensed to the Apache Software Foundation (ASF) under one or more\n// contributor license agreements.  See the NOTICE file distributed with\n// this work for additional information regarding copyright ownership.\n// The ASF licenses this file to You under the Apache License, Version 2.0\n// (the \"License\"); you may not use this file except in compliance with\n// the License.  You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nsyntax = \"proto3\";\n\npackage a6;\n\noption go_package = \"./;a6\";\n\nservice RouteService {\n  rpc GetRoute(Query) returns (Route) {}\n  rpc GetRoutes(Query) returns (stream Route) {}\n  rpc GetError(Query) returns (Route) {}\n}\n\nmessage Query {\n  string name = 1;\n}\n\nmessage Route {\n  string name = 1;\n  string path = 2;\n}\n"
  },
  {
    "path": "t/plugin/grpc-web/client.cts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n//@ts-expect-error no typing for xhr2\nimport XMLHttpRequest from 'xhr2';\n\nimport route_grpc_web_pb from './a6/route_grpc_web_pb';\nimport route_pb from './a6/route_pb';\nconst { Query: RouteServiceQuery } = route_pb;\n\n// inject xhr polyfill for grpc-web\n(global as any).XMLHttpRequest = XMLHttpRequest;\n\nconst RPC_CALL_FORMAT = {\n  TEXT: 'TEXT',\n  BIN: 'BIN',\n} as const;\ntype RPC_CALL_FORMAT = keyof typeof RPC_CALL_FORMAT;\nconst formats = [RPC_CALL_FORMAT.TEXT, RPC_CALL_FORMAT.BIN];\n\nconst RPC_CALL_TYPE = {\n  UNARY: 'UNARY',\n  STREAM: 'STREAM',\n  EXPECT_ERROR: 'EXPECT_ERROR',\n} as const;\ntype RPC_CALL_TYPE = keyof typeof RPC_CALL_TYPE;\nconst types = [\n  RPC_CALL_TYPE.UNARY,\n  RPC_CALL_TYPE.STREAM,\n  RPC_CALL_TYPE.EXPECT_ERROR,\n];\n\nclass gRPCWebClient {\n  private clients = {\n    [RPC_CALL_FORMAT.BIN]: new route_grpc_web_pb.RouteServiceClient(\n      'http://127.0.0.1:1984/grpc/web',\n      null,\n      { format: 'binary' },\n    ),\n    [RPC_CALL_FORMAT.TEXT]: new route_grpc_web_pb.RouteServiceClient(\n      'http://127.0.0.1:1984/grpc/web',\n      null,\n      { format: 'text' },\n    ),\n  };\n\n  [RPC_CALL_TYPE.UNARY](format: RPC_CALL_FORMAT) {\n    let query = new RouteServiceQuery().setName('hello');\n    this.clients[format]\n      .getRoute(query, {}, (error, response) => {\n        if (error) {\n          console.log(error);\n          return;\n        }\n        console.log(JSON.stringify(response.toObject()));\n      })\n      .on('status', (status) => console.log('Status:', status));\n  }\n\n  [RPC_CALL_TYPE.STREAM](format: RPC_CALL_FORMAT) {\n    let query = new RouteServiceQuery();\n    let stream = this.clients[format].getRoutes(query, {});\n\n    stream.on('data', (response) =>\n      console.log(JSON.stringify(response.toObject())),\n    );\n\n    stream.on('end', () => stream.cancel());\n\n    stream.on('status', (status) => console.log('Status:', status));\n  }\n\n  [RPC_CALL_TYPE.EXPECT_ERROR](format: RPC_CALL_FORMAT) {\n    this.clients[format]\n      .getError(new RouteServiceQuery(), undefined, () => {})\n      .on('status', (status) => {\n        console.log(`Status: ${status.code}, Details: ${status.details}`);\n      });\n  }\n}\n\n(async () => {\n  const args = process.argv.splice(2);\n\n  if (args.length !== 2) {\n    console.log(\n      'please input dispatch function, e.g: node client.js [format] [type]',\n    );\n    return;\n  }\n\n  const format = args[0].toUpperCase() as RPC_CALL_FORMAT;\n  if (!formats.includes(format)) {\n    console.log('dispatch format not found');\n    return;\n  }\n\n  const type = args[1].toUpperCase() as RPC_CALL_TYPE;\n  if (!types.includes(type)) {\n    console.log('dispatch type not found');\n    return;\n  }\n\n  let grpc = new gRPCWebClient();\n\n  grpc[type](format);\n})();\n"
  },
  {
    "path": "t/plugin/grpc-web/go.mod",
    "content": "module apisix.apache.org/plugin/grpc-web\n\ngo 1.24\n\nrequire (\n\tgoogle.golang.org/grpc v1.74.2\n\tgoogle.golang.org/protobuf v1.36.6\n)\n\nrequire (\n\tgolang.org/x/net v0.40.0 // indirect\n\tgolang.org/x/sys v0.33.0 // indirect\n\tgolang.org/x/text v0.25.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect\n)\n"
  },
  {
    "path": "t/plugin/grpc-web/go.sum",
    "content": "github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=\ngo.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=\ngo.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=\ngo.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=\ngo.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=\ngo.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=\ngo.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=\ngo.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=\ngo.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=\ngo.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=\ngolang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=\ngolang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=\ngolang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=\ngoogle.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=\ngoogle.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=\ngoogle.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=\ngoogle.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=\n"
  },
  {
    "path": "t/plugin/grpc-web/server.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\n\tpb \"apisix.apache.org/plugin/grpc-web/a6\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\ntype routeServiceServer struct {\n\tpb.UnimplementedRouteServiceServer\n\tsavedRoutes []*pb.Route\n}\n\nfunc (rss *routeServiceServer) GetRoute(ctx context.Context, req *pb.Query) (*pb.Route, error) {\n\tvar r *pb.Route\n\tif len(req.Name) <= 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"query params invalid\")\n\t}\n\n\tfor _, savedRoute := range rss.savedRoutes {\n\t\tif savedRoute.Name == req.Name {\n\t\t\tr = savedRoute\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif r == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"route not found\")\n\t}\n\n\treturn r, nil\n}\n\nfunc (rss *routeServiceServer) GetRoutes(req *pb.Query, srv pb.RouteService_GetRoutesServer) error {\n\tif len(rss.savedRoutes) <= 0 {\n\t\treturn status.Errorf(codes.NotFound, \"routes data is empty\")\n\t}\n\tfor _, savedRoute := range rss.savedRoutes {\n\t\tif err := srv.Send(savedRoute); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (rss *routeServiceServer) GetError(ctx context.Context, req *pb.Query) (*pb.Route, error) {\n\treturn nil, status.Errorf(codes.Internal, \"execpted error\")\n}\n\nfunc (rss *routeServiceServer) LoadRoutes() {\n\tif err := json.Unmarshal(exampleData, &rss.savedRoutes); err != nil {\n\t\tlog.Fatalf(\"Failed to load default routes: %v\", err)\n\t}\n}\n\nvar exampleData = []byte(`[\n{\n\t\"name\":\"hello\",\n\t\"path\":\"/hello\"\n},\n{\n\t\"name\":\"world\",\n\t\"path\":\"/world\"\n}]`)\n\nvar ServerPort = \":50001\"\n\nfunc main() {\n\tflag.Parse()\n\n\tlis, err := net.Listen(\"tcp\", ServerPort)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to listen gRPC-Web Test Server: %v\", err)\n\t} else {\n\t\tlog.Printf(\"successful to listen gRPC-Web Test Server, address %s\", ServerPort)\n\t}\n\n\ts := routeServiceServer{}\n\ts.LoadRoutes()\n\tvar opts []grpc.ServerOption\n\tgrpcServer := grpc.NewServer(opts...)\n\tpb.RegisterRouteServiceServer(grpcServer, &s)\n\n\tif err = grpcServer.Serve(lis); err != nil {\n\t\tlog.Fatalf(\"failed to serve: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "t/plugin/grpc-web/setup.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\npnpm install -g tsx\n\npushd a6\n\npnpx @bufbuild/buf generate --debug\n\npopd\n\nCGO_ENABLED=0 go build -o grpc-web-server server.go\n\n./grpc-web-server > grpc-web-server.log 2>&1 || (cat grpc-web-server.log && exit 1)&\n"
  },
  {
    "path": "t/plugin/grpc-web.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route (default grpc web proxy route)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/grpc/web/*\",\n                upstream = {\n                    scheme = \"grpc\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:50001\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"grpc-web\"] = {}\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Proxy unary request using APISIX with trailers gRPC-Web plugin\nStatus should be printed at most once per request, otherwise this would be out of specification.\n--- exec\npnpx tsx ./t/plugin/grpc-web/client.cts BIN UNARY\npnpx tsx ./t/plugin/grpc-web/client.cts TEXT UNARY\n--- response_body\nStatus: { code: 0, details: '', metadata: {} }\n{\"name\":\"hello\",\"path\":\"/hello\"}\nStatus: { code: 0, details: '', metadata: {} }\n{\"name\":\"hello\",\"path\":\"/hello\"}\n\n\n\n=== TEST 3: Proxy server-side streaming request using APISIX with trailers gRPC-Web plugin\n--- exec\npnpx tsx ./t/plugin/grpc-web/client.cts BIN STREAM\npnpx tsx ./t/plugin/grpc-web/client.cts TEXT STREAM\n--- response_body\n{\"name\":\"hello\",\"path\":\"/hello\"}\n{\"name\":\"world\",\"path\":\"/world\"}\nStatus: { code: 0, details: '', metadata: {} }\n{\"name\":\"hello\",\"path\":\"/hello\"}\n{\"name\":\"world\",\"path\":\"/world\"}\nStatus: { code: 0, details: '', metadata: {} }\n\n\n\n=== TEST 4: test options request\n--- request\nOPTIONS /grpc/web/a6.RouteService/GetRoute\n--- error_code: 204\n--- response_headers\nAccess-Control-Allow-Methods: POST\nAccess-Control-Allow-Headers: content-type,x-grpc-web,x-user-agent\nAccess-Control-Allow-Origin: *\n\n\n\n=== TEST 5: test non-options request\n--- request\nGET /grpc/web/a6.RouteService/GetRoute\n--- error_code: 405\n--- response_headers\nAccess-Control-Allow-Origin: *\n--- error_log\nrequest method: `GET` invalid\n\n\n\n=== TEST 6: test non gRPC Web MIME type request\n--- request\nPOST /grpc/web/a6.RouteService/GetRoute\n--- more_headers\nContent-Type: application/json\n--- error_code: 400\n--- response_headers\nAccess-Control-Allow-Origin: *\nContent-Type: text/html\n--- error_log\nrequest Content-Type: `application/json` invalid\n\n\n\n=== TEST 7: set route (absolute match)\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local config = {\n                uri = \"/grpc/web/a6.RouteService/GetRoute\",\n                upstream = {\n                    scheme = \"grpc\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:50001\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"grpc-web\"] = {}\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: test route (absolute match)\n--- request\nPOST /grpc/web/a6.RouteService/GetRoute\nhello\n--- more_headers\nContent-Type: application/grpc-web\n--- error_code: 200\n--- response_headers\nAccess-Control-Allow-Origin: *\nContent-Type: application/grpc-web\n\n\n\n=== TEST 9: set route (with cors plugin)\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/grpc/web/*\",\n                upstream = {\n                    scheme = \"grpc\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:50001\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"grpc-web\"] = {},\n                    cors = {\n                        allow_origins = \"http://test.com\",\n                        allow_methods = \"POST,OPTIONS\",\n                        allow_headers = \"application/grpc-web\",\n                        expose_headers = \"application/grpc-web\",\n                        max_age = 5,\n                        allow_credential = true\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: don't override Access-Control-Allow-Origin header in response\n--- exec\ncurl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \\\n--header 'Origin: http://test.com' \\\n--header 'Content-Type: application/grpc-web-text' \\\n--data-raw 'AAAAAAcKBXdvcmxkCgo='\n--- response_body eval\nqr/HTTP\\/1.1 200 OK/ and qr/Access-Control-Allow-Origin: http:\\/\\/test.com/\n\n\n\n=== TEST 11: check for Access-Control-Expose-Headers header in response\n--- exec\ncurl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \\\n--header 'Origin: http://test.com' \\\n--header 'Content-Type: application/grpc-web-text' \\\n--data-raw 'AAAAAAcKBXdvcmxkCgo='\n--- response_body eval\nqr/Access-Control-Expose-Headers: grpc-message,grpc-status/ and qr/Access-Control-Allow-Origin: http:\\/\\/test.com/\n\n\n\n=== TEST 12: verify trailers in response\nAccording to the gRPC documentation, the grpc-web proxy should not retain trailers received from upstream when\nforwarding them, as the reference implementation envoy does, so the current test case is status quo rather\nthan \"correct\", which is not expected to have an impact since browsers ignore trailers.\nCurrently there is no API or hook point available in nginx/lua-nginx-module to remove specified trailers\non demand (grpc_hide_header can do it but it affects the grpc proxy), and some nginx patches may be needed\nto allow for code-controlled removal of the trailer at runtime.\nWhen we implement that, this use case will be removed.\n--- exec\ncurl -iv --location 'http://127.0.0.1:1984/grpc/web/a6.RouteService/GetRoute' \\\n--header 'Content-Type: application/grpc-web+proto' \\\n--header 'X-Grpc-Web: 1' \\\n--data-binary '@./t/plugin/grpc-web/req.bin'\n--- response_body eval\nqr/grpc-status:0\\x0d\\x0agrpc-message:/\n\n\n\n=== TEST 13: confg default response route\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/grpc/web/*\",\n                upstream = {\n                    scheme = \"grpc\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:50001\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"grpc-web\"] = {}\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: check header in default response\n--- request\nOPTIONS /grpc/web/a6.RouteService/GetRoute\n--- error_code: 204\n--- response_headers\nAccess-Control-Allow-Methods: POST\nAccess-Control-Allow-Headers: content-type,x-grpc-web,x-user-agent\nAccess-Control-Allow-Origin: *\nAccess-Control-Expose-Headers: grpc-message,grpc-status\n\n\n\n=== TEST 15: Custom configuration routing\n--- config\n    location /t {\n        content_by_lua_block {\n            local config = {\n                uri = \"/grpc/web/*\",\n                upstream = {\n                    scheme = \"grpc\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:50001\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"grpc-web\"] = {\n                        cors_allow_headers = \"grpc-accept-encoding\"\n                    }\n                }\n            }\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: check header in default response\n--- request\nOPTIONS /grpc/web/a6.RouteService/GetRoute\n--- error_code: 204\n--- response_headers\nAccess-Control-Allow-Methods: POST\nAccess-Control-Allow-Headers: grpc-accept-encoding\nAccess-Control-Allow-Origin: *\nAccess-Control-Expose-Headers: grpc-message,grpc-status\n\n\n\n=== TEST 17: check gRPC metadata when empty resp body\n--- exec\npnpx tsx ./t/plugin/grpc-web/client.cts BIN EXPECT_ERROR\npnpx tsx ./t/plugin/grpc-web/client.cts TEXT EXPECT_ERROR\n--- response_body\nStatus: 13, Details: execpted error\nStatus: 13, Details: execpted error\n\n\n\n=== TEST 18: check gRPC metadata when empty resp body (check empty resp body and metadatas in header)\n--- request\nPOST /grpc/web/a6.RouteService/GetError HTTP/1.1\nAAAAAAcKBXdvcmxkCgo=\n--- more_headers\nContent-Type: application/grpc-web-text\n--- response_headers\ngrpc-status: 13\ngrpc-message: execpted error\n--- response_body eval\nqr/^$/\n"
  },
  {
    "path": "t/plugin/gzip.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/html\n--- response_headers\nContent-Encoding: gzip\nVary:\n\n\n\n=== TEST 3: default buffers and compress level\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.gzip\")\n            local core = require(\"apisix.core\")\n            local json = require(\"toolkit.json\")\n\n            for _, conf in ipairs({\n                {},\n                {buffers = {}},\n                {buffers = {number = 1}},\n                {buffers = {size = 1}},\n            }) do\n                local ok, err = plugin.check_schema(conf)\n                if not ok then\n                    ngx.say(err)\n                    return\n                end\n                ngx.say(json.encode(conf.buffers))\n            end\n        }\n    }\n--- response_body\n{\"number\":32,\"size\":4096}\n{\"number\":32,\"size\":4096}\n{\"number\":1,\"size\":4096}\n{\"number\":32,\"size\":1}\n\n\n\n=== TEST 4: compress level\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"uri\": \"/echo\",\n                    \"vars\": [[\"http_x\", \"==\", \"1\"]],\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                            \"comp_level\": 1\n                        }\n                    }\n                }]=]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n            return\n        end\n\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [=[{\n                \"uri\": \"/echo\",\n                \"vars\": [[\"http_x\", \"==\", \"2\"]],\n                \"upstream\": {\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    }\n                },\n                \"plugins\": {\n                    \"gzip\": {\n                        \"comp_level\": 9\n                    }\n                }\n            }]=]\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            return\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 5: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/echo\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {method = \"POST\", headers = {x = \"1\"}, body = (\"0123\"):rep(1024)})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local less_compressed = res.body\n            local res, err = httpc:request_uri(uri,\n                {method = \"POST\", headers = {x = \"2\"}, body = (\"0123\"):rep(1024)})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            if #less_compressed < 4096 and #less_compressed < #res.body then\n                ngx.say(\"ok\")\n            end\n        }\n    }\n--- response_body\nok\n\n\n\n=== TEST 6: min length\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                            \"min_length\": 21\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 7: not hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/html\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 8: http version\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                            \"http_version\": 1.1\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 9: not hit\n--- request\nPOST /echo HTTP/1.0\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/html\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 10: hit again\n--- request\nPOST /echo HTTP/1.1\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/html\n--- response_headers\nContent-Encoding: gzip\n\n\n\n=== TEST 11: types\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                            \"types\": [\"text/plain\", \"text/xml\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 12: not hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/html\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 13: hit again\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/xml\n--- response_headers\nContent-Encoding: gzip\n\n\n\n=== TEST 14: hit with charset\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: text/plain; charset=UTF-8\n--- response_headers\nContent-Encoding: gzip\n\n\n\n=== TEST 15: match all types\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                            \"types\": \"*\"\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 16: hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nContent-Type: video/3gpp\n--- response_headers\nContent-Encoding: gzip\n\n\n\n=== TEST 17: vary\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"gzip\": {\n                            \"vary\": true\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 18: hit\n--- request\nPOST /echo\n0123456789\n012345678\n--- more_headers\nAccept-Encoding: gzip\nVary: upstream\nContent-Type: text/html\n--- response_headers\nContent-Encoding: gzip\nVary: upstream, Accept-Encoding\n\n\n\n=== TEST 19: schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {input = {\n                    types = {}\n                }},\n                {input = {\n                    min_length = 0\n                }},\n                {input = {\n                    comp_level = 10\n                }},\n                {input = {\n                    http_version = 2\n                }},\n                {input = {\n                    buffers = {\n                        number = 0,\n                    }\n                }},\n                {input = {\n                    buffers = {\n                        size = 0,\n                    }\n                }},\n                {input = {\n                    vary = 0\n                }}\n            }) do\n                local code, body = t('/apisix/admin/global_rules/1',\n                    ngx.HTTP_PUT,\n                    {\n                        id = \"1\",\n                        plugins = {\n                            [\"gzip\"] = case.input\n                        }\n                    }\n                )\n                ngx.print(body)\n            end\n    }\n}\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"types\\\" validation failed: object matches none of the required\"}\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"min_length\\\" validation failed: expected 0 to be at least 1\"}\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"comp_level\\\" validation failed: expected 10 to be at most 9\"}\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"http_version\\\" validation failed: matches none of the enum values\"}\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"buffers\\\" validation failed: property \\\"number\\\" validation failed: expected 0 to be at least 1\"}\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"buffers\\\" validation failed: property \\\"size\\\" validation failed: expected 0 to be at least 1\"}\n{\"error_msg\":\"failed to check the configuration of plugin gzip err: property \\\"vary\\\" validation failed: wrong type: expected boolean, got number\"}\n"
  },
  {
    "path": "t/plugin/hmac-auth-anonymous-consumer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer jack and anonymous\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"user-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 4,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"anonymous\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\npassed\n\n\n\n=== TEST 2: add hmac auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"anonymous_consumer\": \"anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: request without hmac-auth header will be from anonymous consumer and it will pass\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 4: request without hmac-auth header will be from anonymous consumer and different rate limit will apply\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 5: add hmac auth plugin with non-existent anonymous_consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"anonymous_consumer\": \"not-found-anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: anonymous-consumer configured in the route should not be found\n--- request\nGET /hello\n--- error_code: 401\n--- error_log\nfailed to get anonymous consumer not-found-anonymous\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n"
  },
  {
    "path": "t/plugin/hmac-auth-realm.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity, default realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: verify default realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: hmac realm=\"hmac\"\n\n\n\n=== TEST 3: set custom realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"realm\": \"my-hmac-realm\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: verify custom realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: hmac realm=\"my-hmac-realm\"\n\n\n\n=== TEST 5: set anonymous consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"anonymous_consumer\": \"missing-consumer\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: verify anonymous consumer missing returns realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: hmac realm=\"hmac\"\n--- error_log\nfailed to get anonymous consumer\n"
  },
  {
    "path": "t/plugin/hmac-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 2: add consumer with plugin hmac-auth - missing secret key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"user-key\"\n                        }\n                    }\n                }]])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\\\\"secret_key\\\\\" is required\"\\}/\n\n\n\n=== TEST 3: add consumer with plugin hmac-auth - missing key_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"bar\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"secret_key\": \"skey\"\n                        }\n                    }\n                }]])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\\\\"key_id\\\\\" is required\"\\}/\n\n\n\n=== TEST 4: add consumer with plugin hmac-auth - key id exceeds the length limit\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"li\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"akeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakeyakey\",\n                            \"secret_key\": \"skey\"\n                        }\n                    }\n                }]])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\\\\"key_id\\\\\" validation failed: string too long, expected at most 256, got 320\"\\}/\n\n\n\n=== TEST 5: add consumer with plugin hmac-auth - secret key exceeds the length limit\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"zhang\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"akey\",\n                            \"secret_key\": \"skeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskeyskey\"\n                        }\n                    }\n                }]])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\\\\"secret_key\\\\\" validation failed: string too long, expected at most 256, got 384\"\\}/\n\n\n\n=== TEST 6: enable hmac auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: verify,missing Authorization header\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated: missing Authorization header\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: missing Authorization header\n\n\n\n=== TEST 8: verify, missing algorithm\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",headers=\"@request-target date\" ,signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated[^,]+/\n--- grep_error_log_out\nclient request can't be validated: algorithm missing\n\n\n\n=== TEST 9: verify: invalid key_id\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"sdf\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid key_id\n\n\n\n=== TEST 10: verify: invalid algorithm\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",algorithm=\"ljlj\",headers=\"@request-target date\",signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid algorithm\n\n\n\n=== TEST 11: verify: Clock skew exceeded\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Clock skew exceeded\n\n\n\n=== TEST 12: verify: missing Date\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"asdf\"\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: Date header missing/\n--- grep_error_log_out\nclient request can't be validated: Date header missing\n\n\n\n=== TEST 13: verify: Invalid GMT format time\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"asdf\"\nDate: adfsdf\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid GMT format time\n\n\n\n=== TEST 14: verify: ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n             key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n        core.log.info(\"signing_string:\", signing_string)\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature algorithm=\\\"hmac-sha256\\\"\" .. \",keyId=\\\"\" .. key_id .. \"\\\",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 15: add route with 0 clock skew\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"clock_skew\":  0\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code == 400 then\n                ngx.say(body)\n            end\n        }\n    }\n--- request\nGET /t\n-- error_code: 400\n--- response_body eval\nqr/.*failed to check the configuration of plugin hmac-auth err.*/\n\n\n\n=== TEST 16: add route with valid clock skew\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"clock_skew\":  1000000000000\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code == 200 then\n                ngx.say(body)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: verify: invalid signature\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid signature\n\n\n\n=== TEST 18: verify: invalid signature\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"client request can't be validated\"}\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid signature\n\n\n\n=== TEST 19: add route with 1 clock skew\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"clock_skew\":  1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code == 200 then\n                ngx.say(body)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: verify: Invalid GMT format time\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        ngx.sleep(2)\n\n        local signing_string = \"GET\" .. \"/hello\" ..  \"\" ..\n            key_id .. gmt .. custom_header_a .. custom_header_b\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            core.json.encode(data),\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/{\"message\":\"client request can't be validated\"}/\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Clock skew exceeded\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 21: update route with default clock skew\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code == 200 then\n                ngx.say(body)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: verify: put ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local data = {cert = \"ssl_cert\", key = \"ssl_key\", sni = \"test.com\"}\n        local req_body = core.json.encode(data)\n        req_body = req_body or \"\"\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"PUT /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n        core.log.info(\"signing_string:\", signing_string)\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_PUT,\n            req_body,\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 23: update route with signed_headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"signed_headers\": [\"date\",\"x-custom-header-a\", \"x-custom-header-b\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: verify with invalid signed header\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_c = \"23879fmsldfk\"\n\n        local signing_string = \"GET\" .. \"/hello\" ..  \"\" ..\n            key_id .. gmt .. custom_header_a .. custom_header_c\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-c\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-c\"] = custom_header_c\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/{\"message\":\"client request can't be validated\"}/\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: expected header \"x-custom-header-b\" missing in signing\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 25: verify ok with signed headers\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"asld$%dfasf\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 26: add consumer with plugin hmac-auth - empty configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                        }\n                    }\n                }]])\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin hmac-auth err: property \\\\\"(key_id|secret_key)\\\\\" is required\"\\}/\n\n\n\n=== TEST 27: add route with no allowed algorithms\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"allowed_algorithms\": []\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/validation failed: expect array to have at least 1 items/\n\n\n\n=== TEST 28: update route with signed_headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/headers\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: verify Authorization header missing\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n\n        local signing_string = {\n            key_id,\n            \"GET /headers\",\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        local code, _, body = t.test('/headers',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        if string.find(body,\"Authorization\") then\n            ngx.say(\"failed\")\n        else\n            ngx.say(\"passed\")\n        end\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 30 : update route with signed_headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"signed_headers\": [\"date\",\"x-custom-header-a\", \"x-custom-header-b\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 31: verify error with the client only sends one in the request, but there are two in the signature\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"asld$%dfasf\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/client request can't be validated/\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid signature\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 32: verify error with the client sends two in the request, but there is only one in the signature\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"asld$%dfasf\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/client request can't be validated/\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid signature\n--- no_error_log\nmy-secret-key\n\n\n\n=== TEST 33 : update route with allowed_algorithms\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"allowed_algorithms\": [\"hmac-sha256\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 34: verify with hmac-sha1 algorithm, not part of allowed_algorithms\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"asld$%dfasf\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA1):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha1\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 401\n--- response_body eval\nqr/client request can't be validated/\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid algorithm\n--- no_error_log\nmy-secret-key\n"
  },
  {
    "path": "t/plugin/hmac-auth2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: enable the hmac auth plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/uri\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: get the default schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/hmac-auth',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"type\":\"object\",\"$comment\":\"this is a mark for our injected plugin schema\",\"title\":\"work with route or service object\",\"properties\":{\"allowed_algorithms\":{\"type\":\"array\",\"default\":[\"hmac-sha1\",\"hmac-sha256\",\"hmac-sha512\"],\"items\":{\"type\":\"string\",\"enum\":[\"hmac-sha1\",\"hmac-sha256\",\"hmac-sha512\"]},\"minItems\":1},\"_meta\":{\"type\":\"object\",\"properties\":{\"filter\":{\"description\":\"filter determines whether the plugin needs to be executed at runtime\",\"type\":\"array\"},\"error_response\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"object\"}]},\"disable\":{\"type\":\"boolean\"},\"priority\":{\"description\":\"priority of plugins by customized order\",\"type\":\"integer\"}}},\"clock_skew\":{\"type\":\"integer\",\"default\":300,\"minimum\":1},\"signed_headers\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":50}},\"hide_credentials\":{\"type\":\"boolean\",\"default\":false},\"validate_request_body\":{\"type\":\"boolean\",\"default\":false,\"title\":\"A boolean value telling the plugin to enable body validation\"}}}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 3: get the schema by schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/hmac-auth?schema_type=consumer',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"title\":\"work with consumer object\",\"required\":[\"key_id\",\"secret_key\"],\"properties\":{\"secret_key\":{\"minLength\":1,\"maxLength\":256,\"type\":\"string\"},\"key_id\":{\"minLength\":1,\"maxLength\":256,\"type\":\"string\"}},\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 4: get the schema by error schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/hmac-auth?schema_type=consumer123123',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"properties\":{},\"title\":\"work with route or service object\",\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 5: enable hmac auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/hmac-auth3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with validate_request_body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"robin\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: enable hmac auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"validate_request_body\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: missing body digest when validate_request_body is enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local ngx_time = ngx.time\n            local ngx_http_time = ngx.http_time\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local hmac = require(\"resty.hmac\")\n            local ngx_encode_base64 = ngx.encode_base64\n\n            local secret_key = \"my-secret-key\"\n            local timestamp = ngx_time()\n            local gmt = ngx_http_time(timestamp)\n            local key_id = \"my-access-key\"\n            local custom_header_a = \"asld$%dfasf\"\n            local custom_header_b = \"23879fmsldfk\"\n            local body = \"{\\\"name\\\": \\\"world\\\"}\"\n\n            local signing_string = {\n                 key_id,\n                \"POST /hello\",\n                \"date: \" .. gmt,\n                \"x-custom-header-a: \" .. custom_header_a,\n                \"x-custom-header-b: \" .. custom_header_b\n            }\n            signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n            core.log.info(\"signing_string:\", signing_string)\n\n            local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n\n            core.log.info(\"signature:\", ngx_encode_base64(signature))\n            local headers = {}\n            headers[\"Date\"] = gmt\n            headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n            headers[\"x-custom-header-a\"] = custom_header_a\n            headers[\"x-custom-header-b\"] = custom_header_b\n\n            local code, body = t.test('/hello',\n                ngx.HTTP_POST,\n                body,\n                nil,\n                headers\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 401\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid digest\n--- response_body eval\nqr/\\{\"message\":\"client request can't be validated\"\\}/\n\n\n\n=== TEST 4: verify body digest: not ok\n--- config\n    location /t {\n        content_by_lua_block {\n            local ngx_time = ngx.time\n            local ngx_http_time = ngx.http_time\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local hmac = require(\"resty.hmac\")\n            local ngx_encode_base64 = ngx.encode_base64\n\n            local secret_key = \"my-secret-key\"\n            local timestamp = ngx_time()\n            local gmt = ngx_http_time(timestamp)\n            local key_id = \"my-access-key\"\n            local custom_header_a = \"asld$%dfasf\"\n            local custom_header_b = \"23879fmsldfk\"\n            local body = \"{\\\"name\\\": \\\"world\\\"}\"\n\n            local signing_string = {\n                key_id,\n                \"POST /hello\",\n                \"date: \" .. gmt,\n                \"x-custom-header-a: \" .. custom_header_a,\n                \"x-custom-header-b: \" .. custom_header_b\n            }\n            signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n            core.log.info(\"signing_string:\", signing_string)\n\n            local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n\n            core.log.info(\"signature:\", ngx_encode_base64(signature))\n            local headers = {}\n            headers[\"Date\"] = gmt\n            headers[\"Authorization\"] =\"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n            headers[\"Digest\"] = \"hello\"\n            headers[\"x-custom-header-a\"] = custom_header_a\n            headers[\"x-custom-header-b\"] = custom_header_b\n\n            local code, body = t.test('/hello',\n                ngx.HTTP_POST,\n                body,\n                nil,\n                headers\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 401\n--- grep_error_log eval\nqr/client request can't be validated: [^,]+/\n--- grep_error_log_out\nclient request can't be validated: Invalid digest\n--- response_body eval\nqr/\\{\"message\":\"client request can't be validated\"\\}/\n\n\n\n=== TEST 5: verify body digest: ok\n--- config\n    location /t {\n        content_by_lua_block {\n            local ngx_time = ngx.time\n            local ngx_http_time = ngx.http_time\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local hmac = require(\"resty.hmac\")\n            local ngx_encode_base64 = ngx.encode_base64\n\n            local secret_key = \"my-secret-key\"\n            local timestamp = ngx_time()\n            local gmt = ngx_http_time(timestamp)\n            local key_id = \"my-access-key\"\n            local custom_header_a = \"asld$%dfasf\"\n            local custom_header_b = \"23879fmsldfk\"\n            local body = \"{\\\"name\\\": \\\"world\\\"}\"\n\n            local signing_string = {\n                key_id,\n                \"POST /hello\",\n                \"date: \" .. gmt,\n                \"x-custom-header-a: \" .. custom_header_a,\n                \"x-custom-header-b: \" .. custom_header_b\n            }\n            signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n            core.log.info(\"signing_string:\", signing_string)\n\n            local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n            local ngx_encode_base64 = ngx.encode_base64\n\n            local resty_sha256 = require(\"resty.sha256\")\n            local hash = resty_sha256:new()\n            hash:update(body)\n            local digest = hash:final()\n            local body_digest = ngx_encode_base64(digest)\n\n            core.log.info(\"signature:\", ngx_encode_base64(signature))\n            local headers = {}\n            headers[\"Date\"] = gmt\n            headers[\"Digest\"] = \"SHA-256=\" .. body_digest\n            headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n            headers[\"x-custom-header-a\"] = custom_header_a\n            headers[\"x-custom-header-b\"] = custom_header_b\n\n            local code, body = t.test('/hello',\n                ngx.HTTP_POST,\n                body,\n                nil,\n                headers\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/hmac-auth4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set hmac-auth conf: secret_key uses secret ref\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"$secret://vault/test1/jack/secret_key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jack secret_key=my-secret-key\n--- response_body\nSuccess! Data written to: kv/apisix/jack\n\n\n\n=== TEST 3: verify: ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n        core.log.info(\"signing_string:\", signing_string)\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 4: set hmac-auth conf with the token in an env var: secret_key uses secret ref\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"$ENV://VAULT_TOKEN\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"hmac-auth\": {\n                            \"key_id\": \"my-access-key\",\n                            \"secret_key\": \"$secret://vault/test1/jack/secret_key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"hmac-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify: ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local ngx_time = ngx.time\n        local ngx_http_time = ngx.http_time\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local hmac = require(\"resty.hmac\")\n        local ngx_encode_base64 = ngx.encode_base64\n\n        local secret_key = \"my-secret-key\"\n        local timestamp = ngx_time()\n        local gmt = ngx_http_time(timestamp)\n        local key_id = \"my-access-key\"\n        local custom_header_a = \"asld$%dfasf\"\n        local custom_header_b = \"23879fmsldfk\"\n\n        local signing_string = {\n            key_id,\n            \"GET /hello\",\n            \"date: \" .. gmt,\n            \"x-custom-header-a: \" .. custom_header_a,\n            \"x-custom-header-b: \" .. custom_header_b\n        }\n        signing_string = core.table.concat(signing_string, \"\\n\") .. \"\\n\"\n        core.log.info(\"signing_string:\", signing_string)\n\n        local signature = hmac:new(secret_key, hmac.ALGOS.SHA256):final(signing_string)\n        core.log.info(\"signature:\", ngx_encode_base64(signature))\n        local headers = {}\n        headers[\"Date\"] = gmt\n        headers[\"Authorization\"] = \"Signature keyId=\\\"\" .. key_id .. \"\\\",algorithm=\\\"hmac-sha256\\\"\" .. \",headers=\\\"@request-target date x-custom-header-a x-custom-header-b\\\",signature=\\\"\" .. ngx_encode_base64(signature) .. \"\\\"\"\n        headers[\"x-custom-header-a\"] = custom_header_a\n        headers[\"x-custom-header-b\"] = custom_header_b\n\n        local code, body = t.test('/hello',\n            ngx.HTTP_GET,\n            \"\",\n            nil,\n            headers\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/http-dubbo.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\n    enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1:  test_pojo\n--- apisix_yaml\nupstreams:\n    - nodes:\n        - host: 127.0.0.1\n          port: 30880\n          weight: 1\n      type: roundrobin\n      id: 1\nroutes:\n  -\n    uri: /t\n    plugins:\n        http-dubbo:\n            service_name: org.apache.dubbo.backend.DubboSerializationTestService\n            params_type_desc: Lorg/apache/dubbo/backend/PoJo;\n            serialized: true\n            method: testPoJo\n            service_version: 1.0.0\n    upstream_id: 1\n#END\n--- request\nPOST /t\n{\"aBoolean\":true,\"aByte\":1,\"aDouble\":1.1,\"aFloat\":1.2,\"aInt\":2,\"aLong\":3,\"aShort\":4,\"aString\":\"aa\",\"acharacter\":\"a\",\"stringMap\":{\"key\":\"value\"},\"strings\":[\"aa\",\"bb\"]}\n--- response_body chomp\n{\"aBoolean\":true,\"aByte\":1,\"aDouble\":1.1,\"aFloat\":1.2,\"aInt\":2,\"aLong\":3,\"aShort\":4,\"aString\":\"aa\",\"acharacter\":\"a\",\"stringMap\":{\"key\":\"value\"},\"strings\":[\"aa\",\"bb\"]}\n\n\n\n=== TEST 2:  test_pojos\n--- apisix_yaml\nupstreams:\n    - nodes:\n        - host: 127.0.0.1\n          port: 30880\n          weight: 1\n      type: roundrobin\n      id: 1\nroutes:\n  -\n    uri: /t\n    plugins:\n        http-dubbo:\n            service_name: org.apache.dubbo.backend.DubboSerializationTestService\n            params_type_desc: \"[Lorg/apache/dubbo/backend/PoJo;\"\n            serialized: true\n            method: testPoJos\n            service_version: 1.0.0\n    upstream_id: 1\n#END\n--- request\nPOST /t\n[{\"aBoolean\":true,\"aByte\":1,\"aDouble\":1.1,\"aFloat\":1.2,\"aInt\":2,\"aLong\":3,\"aShort\":4,\"aString\":\"aa\",\"acharacter\":\"a\",\"stringMap\":{\"key\":\"value\"},\"strings\":[\"aa\",\"bb\"]}]\n--- response_body chomp\n[{\"aBoolean\":true,\"aByte\":1,\"aDouble\":1.1,\"aFloat\":1.2,\"aInt\":2,\"aLong\":3,\"aShort\":4,\"aString\":\"aa\",\"acharacter\":\"a\",\"stringMap\":{\"key\":\"value\"},\"strings\":[\"aa\",\"bb\"]}]\n\n\n\n=== TEST 3:  test_timeout\n--- apisix_yaml\nupstreams:\n    - nodes:\n        - host: 127.0.0.1\n          port: 30881\n          weight: 1\n      type: roundrobin\n      id: 1\nroutes:\n  -\n    uri: /t\n    plugins:\n        http-dubbo:\n            service_name: org.apache.dubbo.backend.DubboSerializationTestService\n            params_type_desc: \"[Lorg/apache/dubbo/backend/PoJo;\"\n            serialized: true\n            method: testPoJos\n            service_version: 1.0.0\n            connect_timeout: 100\n            read_timeout: 100\n            send_timeout: 100\n    upstream_id: 1\n#END\n--- request\nGET /t\n--- error_code: 502\n--- error_log\nfailed to connect to upstream\n\n\n\n=== TEST 4:  test_void\n--- apisix_yaml\nupstreams:\n    - nodes:\n        - host: 127.0.0.1\n          port: 30880\n          weight: 1\n      type: roundrobin\n      id: 1\nroutes:\n  -\n    uri: /t\n    plugins:\n        http-dubbo:\n            service_name: org.apache.dubbo.backend.DubboSerializationTestService\n            serialized: true\n            method: testVoid\n            service_version: 1.0.0\n    upstream_id: 1\n#END\n--- request\nGET /t\n\n\n\n=== TEST 5:  test_fail\n--- apisix_yaml\nupstreams:\n    - nodes:\n        - host: 127.0.0.1\n          port: 30880\n          weight: 1\n      type: roundrobin\n      id: 1\nroutes:\n  -\n    uri: /t\n    plugins:\n        http-dubbo:\n            service_name: org.apache.dubbo.backend.DubboSerializationTestService\n            serialized: true\n            method: testFailure\n            service_version: 1.0.0\n    upstream_id: 1\n#END\n--- request\nGET /t\n--- error_code: 500\n"
  },
  {
    "path": "t/plugin/http-logger-json.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: json body with request_body\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_req_body: true\n#END\n--- request\nPOST /hello\n{\"sample_payload\":\"hello\"}\n--- error_log\n\"body\":\"{\\\"sample_payload\\\":\\\"hello\\\"}\"\n\n\n\n=== TEST 2: json body with response_body\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_resp_body: true\n#END\n--- request\nPOST /hello\n{\"sample_payload\":\"hello\"}\n--- error_log\n\"response\":{\"body\":\"hello world\\n\"\n\n\n\n=== TEST 3: json body with response_body and response_body expression\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_resp_body: true\n            include_resp_body_expr:\n                - - arg_bar\n                  - ==\n                  - foo\n#END\n--- request\nPOST /hello?bar=foo\n{\"sample_payload\":\"hello\"}\n--- error_log\n\"response\":{\"body\":\"hello world\\n\"\n\n\n\n=== TEST 4: json body with response_body, expr not hit\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_resp_body: true\n            include_resp_body_expr:\n                - - arg_bar\n                  - ==\n                  - foo\n#END\n--- request\nPOST /hello?bar=bar\n{\"sample_payload\":\"hello\"}\n--- no_error_log\n\"response\":{\"body\":\"hello world\\n\"\n\n\n\n=== TEST 5: json body with request_body and response_body\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_req_body: true\n            include_resp_body: true\n#END\n--- request\nPOST /hello\n{\"sample_payload\":\"hello\"}\n--- error_log eval\nqr/(.*\"response\":\\{.*\"body\":\"hello world\\\\n\".*|.*\\{\\\\\\\"sample_payload\\\\\\\":\\\\\\\"hello\\\\\\\"\\}.*){2}/\n\n\n\n=== TEST 6: json body without request_body or response_body\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n#END\n--- request\nPOST /hello\n{\"sample_payload\":\"hello\"}\n--- error_log eval\nqr/(.*\"response\":\\{.*\"body\":\"hello world\\\\n\".*|.*\\{\\\\\\\"sample_payload\\\\\\\":\\\\\\\"hello\\\\\\\"\\}.*){0}/\n\n\n\n=== TEST 7: json body with request_body and request_body expression\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_req_body: true\n            include_req_body_expr:\n                - - arg_bar\n                  - ==\n                  - foo\n#END\n--- request\nPOST /hello?bar=foo\n{\"test\":\"hello\"}\n--- error_log\n\"request\":{\"body\":\"{\\\"test\\\":\\\"hello\\\"}\"\n\n\n\n=== TEST 8: json body with request_body, expr not hit\n--- apisix_yaml\nroutes:\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n        http-logger:\n            batch_max_size: 1\n            uri: http://127.0.0.1:1980/log\n            include_resp_body: true\n            include_resp_body_expr:\n                - - arg_bar\n                  - ==\n                  - foo\n#END\n--- request\nPOST /hello?bar=bar\n{\"sample_payload\":\"hello\"}\n--- no_error_log\n\"request\":{\"body\":\"{\\\"test\\\":\\\"hello\\\"}\"\n"
  },
  {
    "path": "t/plugin/http-logger-large-body.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    # fake server, only for test\n    server {\n        listen 1970;\n        location /large_resp {\n            content_by_lua_block {\n                local large_body = {\n                    \"h\", \"e\", \"l\", \"l\", \"o\"\n                }\n\n                local size_in_bytes = 1024 * 1024 -- 1mb\n                for i = 1, size_in_bytes do\n                    large_body[i+5] = \"l\"\n                end\n                large_body = table.concat(large_body, \"\")\n\n                ngx.say(large_body)\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: max_body_bytes is not an integer\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({\n                uri = \"http://127.0.0.1:1980/hello\",\n                timeout = 1,\n                batch_max_size = 1,\n                max_req_body_bytes = \"10\",\n                include_req_body = true\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"max_req_body_bytes\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 2: max_resp_body_bytes is not an integer\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({\n                uri = \"http://127.0.0.1:1980/hello\",\n                timeout = 1,\n                batch_max_size = 1,\n                max_resp_body_bytes = \"10\",\n                include_resp_body = true\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"max_resp_body_bytes\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 3: set route(include_req_body = true, concat_method = json)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/hello\",\n                            \"timeout\": 1,\n                            \"batch_max_size\": 1,\n                            \"max_req_body_bytes\": 5,\n                            \"include_req_body\": true,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route(include_req_body = true, concat_method = json)\n--- request\nPOST /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log_like eval\nqr/\"body\":\"abcde\"/\n--- wait: 2\n\n\n\n=== TEST 5: set route(include_resp_body = true, concat_method = json)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"max_resp_body_bytes\": 5,\n                            \"include_resp_body\": true,\n                            \"batch_max_size\": 1,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route(include_resp_body = true, concat_method = json)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/request log:.*\"response\":\\{\"body\":\"hello\"/\n--- wait: 2\n\n\n\n=== TEST 7: set route(include_resp_body = true, include_req_body = true, concat_method = json)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"include_req_body\": true,\n                            \"max_req_body_bytes\": 5,\n                            \"include_resp_body\": true,\n                            \"max_resp_body_bytes\": 5,\n                            \"batch_max_size\": 1,\n                            \"concat_method\": \"json\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route(include_resp_body = true, include_req_body = true, concat_method = json)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/request log:.*\"response\":\\{\"body\":\"hello\"/\n--- error_log_like\nqr/\"body\":\"abcde\"/\n--- wait: 2\n\n\n\n=== TEST 9: set route(include_resp_body = false, include_req_body = false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route(include_resp_body = false, include_req_body = false)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\nqr/request log:.*\"response\":\\{\"body\":.*/\n--- wait: 2\n\n\n\n=== TEST 11: set route(large_body, include_resp_body = true, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"include_req_body\": true,\n                            \"max_req_body_bytes\": 256,\n                            \"include_resp_body\": true,\n                            \"max_resp_body_bytes\": 256,\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route(large_body, include_resp_body = true, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 10 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/request log:.*\"response\":\\{\"body\":\"hello(l{251})\"/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 13: set route(large_body, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"include_resp_body\": true,\n                            \"max_resp_body_bytes\": 256,\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: hit route(large_body, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 10 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/request log:.*\"response\":\\{\"body\":\"hello(l{251})\"/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 15: set route(large_body, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"include_req_body\": true,\n                            \"max_req_body_bytes\": 256,\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route(large_body, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 10 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/request log:.*\"body\":\"hello(l{251})\"/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 17: set route(large_body, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"include_resp_body\": true,\n                            \"max_resp_body_bytes\": 256,\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1970\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/large_resp\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: truncate upstream response body 1m to 256 bytes\n--- request\nGET /large_resp\n--- error_log eval\nqr/request log:.*\"response\":\\{\"body\":\"hello(l{251})\"/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 19: set route(large_body, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"include_req_body\": true,\n                            \"max_req_body_bytes\": 256,\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: truncate upstream request body 100kb to 256 bytes\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 100 * 1024 -- 100kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n\n            if err then\n                ngx.say(err)\n            end\n\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like\nhello world\n--- error_log eval\nqr/request log:.*\"body\":\"hello(l{251})\"/\n\n\n\n=== TEST 21: set route(include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"batch_max_size\": 1,\n                            \"max_req_body_bytes\": 5,\n                            \"include_req_body\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: empty request body\n--- request\nGET /hello?ab=cd\n--- response_body\nhello world\n--- no_error_log eval\nqr/\"body\":/\n--- wait: 2\n\n\n\n=== TEST 23: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"request_body\": \"$request_body\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: set route with plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"batch_max_size\": 1,\n                            \"max_req_body_bytes\": 5\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: hit route with custom log_format\n--- request\nPOST /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log_like eval\nqr/\"request_body\": \"abcde\"/\n--- wait: 2\n\n\n\n=== TEST 26: set route(include_req_body = true, concat_method = new_line)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"batch_max_size\": 2,\n                            \"max_req_body_bytes\": 5,\n                            \"include_req_body\": true,\n                            \"concat_method\": \"new_line\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 27: hit route(concat_method = new_line, batch_max_size = 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri,\n                    {\n                        method = \"POST\",\n                        body = \"test_body\" .. i,\n                    }\n                )\n                if err then\n                    ngx.say(err)\n                end\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log_like eval\nqr/request log:.*\"body\":\"test_\"\\}\\\\n.*\"body\":\"test_\"/\n--- wait: 2\n\n\n\n=== TEST 28: set route(include_resp_body = true, concat_method = new_line)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1982/log\",\n                            \"timeout\": 1,\n                            \"batch_max_size\": 2,\n                            \"max_resp_body_bytes\": 6,\n                            \"include_resp_body\": true,\n                            \"concat_method\": \"new_line\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 29: hit route(concat_method = new_line, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\"})\n                if err then\n                    ngx.say(err)\n                end\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log_like eval\nqr/request log:.*\"body\":\"hello \\\\n\"\\}\\\\n.*\"body\":\"hello \\\\n\"/\n--- wait: 2\n"
  },
  {
    "path": "t/plugin/http-logger-log-format.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"@timestamp\": \"$time_iso8601\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n            if code >=300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: sanity, batch_max_size=1 and concat_method is new_line\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"new_line\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: report http logger\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/.*route_id\":\"1\".*/\n\n\n\n=== TEST 4: sanity, batch_max_size=2 and concat_method is new_line\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 2,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"new_line\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: report http logger\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/\"\\@timestamp\":\"20/\n\n\n\n=== TEST 6: sanity, batch_max_size=1 and concat_method is json\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"json\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: report http logger\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/\"route_id\":\"1\"/\n\n\n\n=== TEST 8: sanity, batch_max_size=2 and concat_method is json\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 2,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"json\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: report http logger to confirm two json in array\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/\\[\\{.*?\\},\\{.*?\\}\\]/\n\n\n\n=== TEST 10: remove plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: remove route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: check default log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            },\n                            \"key-auth\": {}\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, _ = t(\"/hello\", \"GET\",null,null,{apikey = \"auth-one\"})\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: check logs\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/\"consumer\":\\{\"username\":\"jack\"\\}/\n--- wait: 0.5\n\n\n\n=== TEST 14: multi level nested expr conditions\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({\n                 uri = \"http://127.0.0.1\",\n                 include_resp_body = true,\n                 include_resp_body_expr = {\n                    {\"http_content_length\", \"<\", 1024},\n                    {\"http_content_type\", \"in\", {\"application/xml\", \"application/json\", \"text/plain\", \"text/xml\"}}\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 15: use custom variable in the logger\n--- extra_init_by_lua\n    local core = require \"apisix.core\"\n\n    core.ctx.register_var(\"a6_route_labels\", function(ctx)\n        local route = ctx.matched_route and ctx.matched_route.value\n        if route and route.labels then\n            return route.labels\n        end\n        return nil\n    end)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"labels\": \"$a6_route_labels\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return body\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"concat_method\": \"json\"\n                            }\n                        },\n                        \"labels\":{\n                            \"key\":\"testvalue\"\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route and report http logger\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/.*testvalue.*/\n\n\n\n=== TEST 17: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"new_line\",\n                                \"log_format\": {\n                                    \"x_ip\": \"$remote_addr\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route and report http logger\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/\"x_ip\":\"127.0.0.1\".*\\}/\n\n\n\n=== TEST 19: nested log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"json\",\n                                \"log_format\": {\n                                    \"host\": \"$host\",\n                                    \"client_ip\": \"$remote_addr\",\n                                    \"request\": {\n                                        \"method\": \"$request_method\",\n                                        \"uri\": \"$request_uri\",\n                                        \"headers\": {\n                                            \"user_agent\": \"$http_user_agent\"\n                                        }\n                                    },\n                                    \"response\": {\n                                        \"status\": \"$status\"\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: hit route and verify nested log format\n--- exec\ntail -n 1 ci/pod/vector/http.log\n--- response_body eval\nqr/\"client_ip\":\"127\\.0\\.0\\.1\"/ and\nqr/\"request\":\\{[^}]*\"method\":\"GET\"/ and\nqr/\"request\":\\{[^}]*\"uri\":\"\\/hello\"/ and\nqr/\"response\":\\{[^}]*\"status\":200/ and\nqr/\"host\":\"127\\.0\\.0\\.1\"/\n\n\n\n=== TEST 21: deep nested log_format is truncated and warns\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            -- configure deep nested log_format for http-logger\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:3001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 1,\n                                \"buffer_duration\": 1,\n                                \"inactive_timeout\": 1,\n                                \"concat_method\": \"json\",\n                                \"log_format\": {\n                                    \"a\": {\"b\": {\"c\": {\"d\": {\"e\": {\"f\": {\"g\": \"$host\"}}}}}}\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            -- trigger logging\n            local code2 = t(\"/hello\", ngx.HTTP_GET)\n\n            -- wait for the batch processor to flush the log entry\n            ngx.sleep(1.1)\n\n            -- read last log line from vector http log\n            local fd, err = io.open(\"ci/pod/vector/http.log\", 'r')\n            if not fd then\n                core.log.error(\"failed to open file: ci/pod/vector/http.log, error info: \", err)\n                return\n            end\n\n            local last\n            for line in fd:lines() do\n                last = line\n            end\n            fd:close()\n\n            local has_chain = last and last:find('\"a\"%s*:%s*%{\"b\"%s*:%s*%{\"c\"%s*:%s*%{\"d\"%s*:%s*%{\"e\"%s*:%s*%{')\n            local has_f = last and last:find('\\\"f\\\"%s*:')\n\n            if has_chain and not has_f then\n                ngx.status = code2\n                ngx.say(\"http depth limit enforced\")\n            else\n                ngx.say(\"http depth limit not enforced\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nhttp depth limit enforced\n--- error_log\nlog_format nesting exceeds max depth 5, truncating\n"
  },
  {
    "path": "t/plugin/http-logger-new-line.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity, batch_max_size=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1980/log\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"new_line\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route and report http logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- error_log eval\nqr/request log: .*\"upstream\":\"127.0.0.1:1982\"/\n\n\n\n=== TEST 3: sanity, batch_max_size=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1980/log\",\n                                \"batch_max_size\": 3,\n                                \"max_retry_count\": 3,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 1,\n                                \"concat_method\": \"new_line\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route, and no report log\n--- request\nGET /hello\n--- response_body\nhello world\n--- no_error_log\n[error]\nrequest log:\n\n\n\n=== TEST 5: hit route, and report log\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n\n        for i = 1, 6 do\n            t('/hello', ngx.HTTP_GET)\n        end\n\n        ngx.sleep(3)\n        ngx.say(\"done\")\n    }\n}\n--- request\nGET /t\n--- timeout: 10\n--- grep_error_log eval\nqr/request log:/\n--- grep_error_log_out\nrequest log:\nrequest log:\n\n\n\n=== TEST 6: hit route, and report log\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n\n        for i = 1, 6 do\n            t('/hello', ngx.HTTP_GET)\n        end\n\n        ngx.sleep(3)\n        ngx.say(\"done\")\n    }\n}\n--- request\nGET /t\n--- timeout: 10\n--- grep_error_log eval\nqr/\"upstream\":\"127.0.0.1:1982\"/\n--- grep_error_log_out\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\n\n\n=== TEST 7: hit route, and report log\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n\n        for i = 1, 5 do\n            t('/hello', ngx.HTTP_GET)\n        end\n\n        ngx.sleep(3)\n        ngx.say(\"done\")\n    }\n}\n--- request\nGET /t\n--- timeout: 10\n--- grep_error_log eval\nqr/\"upstream\":\"127.0.0.1:1982\"/\n--- grep_error_log_out\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\"upstream\":\"127.0.0.1:1982\"\n\n\n\n=== TEST 8: set in global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                            \"uri\": \"http://127.0.0.1:1980/log\",\n                            \"batch_max_size\": 3,\n                            \"max_retry_count\": 3,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"inactive_timeout\": 1,\n                            \"concat_method\": \"new_line\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: not hit route, and report log\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n\n        for i = 1, 5 do\n            t('/not_hit_route', ngx.HTTP_GET)\n        end\n\n        ngx.sleep(3)\n        ngx.say(\"done\")\n    }\n}\n--- request\nGET /t\n--- timeout: 10\n\n\n\n=== TEST 10: delete the global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/http-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({uri = \"http://127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: full schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({uri = \"http://127.0.0.1\",\n                                                 auth_header = \"Basic 123\",\n                                                 timeout = 3,\n                                                 name = \"http-logger\",\n                                                 max_retry_count = 2,\n                                                 retry_delay = 2,\n                                                 buffer_duration = 2,\n                                                 inactive_timeout = 2,\n                                                 batch_max_size = 500,\n                                                 ssl_verify = false,\n                                                 })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 3: uri is missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({auth_header = \"Basic 123\",\n                                                 timeout = 3,\n                                                 name = \"http-logger\",\n                                                 max_retry_count = 2,\n                                                 retry_delay = 2,\n                                                 buffer_duration = 2,\n                                                 inactive_timeout = 2,\n                                                 batch_max_size = 500,\n                                                 })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"uri\" is required\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/hello\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: access local server\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nBatch Processor[http logger] successfully processed the entries\n--- wait: 0.5\n\n\n\n=== TEST 6: set to the http external endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/echo\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: access external endpoint\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nBatch Processor[http logger] successfully processed the entries\n--- wait: 1.5\n\n\n\n=== TEST 8: set wrong https endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"https://127.0.0.1:1982/echo\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"ssl_verify\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: access wrong https endpoint\n--- request\nGET /hello1\n--- response_body\nhello1 world\n--- error_log\nfailed to perform SSL with host[127.0.0.1] port[1982] handshake failed\n--- wait: 1.5\n\n\n\n=== TEST 10: set correct https endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"https://127.0.0.1:1983/echo\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"ssl_verify\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: access correct https endpoint\n--- request\nGET /hello1\n--- response_body\nhello1 world\n--- error_log\nBatch Processor[http logger] successfully processed the entries\n--- wait: 1.5\n\n\n\n=== TEST 12: set batch max size to two\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"https://127.0.0.1:1983/echo\",\n                                \"batch_max_size\": 2,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: access route with batch max size twice\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, { method = \"GET\"})\n            res, err = httpc:request_uri(uri, { method = \"GET\"})\n            ngx.status = res.status\n            if res.status == 200 then\n                ngx.say(\"hello1 world\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\n--- error_log\nBatch Processor[http logger] batch max size has exceeded\ntransferring buffer entries to processing pipe line, buffercount[2]\nBatch Processor[http logger] successfully processed the entries\n--- wait: 1.5\n\n\n\n=== TEST 14: set wrong port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:9991/echo\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: access wrong port\n--- request\nGET /hello1\n--- response_body\nhello1 world\n--- error_log\nBatch Processor[http logger] failed to process entries: failed to connect to host[127.0.0.1] port[9991] connection refused\n--- wait: 1.5\n\n\n\n=== TEST 16: check uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local bad_uris = {\n               \"127.0.0.1\",\n               \"127.0.0.1:1024\",\n            }\n            for _, bad_uri in ipairs(bad_uris) do\n                local ok, err = plugin.check_schema({uri = bad_uri})\n                if ok then\n                    ngx.say(\"mismatched \", bad)\n                end\n            end\n\n            local good_uris = {\n               \"http://127.0.0.1:1024/x?aa=b\",\n               \"http://127.0.0.1:1024?aa=b\",\n               \"http://127.0.0.1:1024\",\n               \"http://x.con\",\n               \"https://x.con\",\n            }\n            for _, good_uri in ipairs(good_uris) do\n                local ok, err = plugin.check_schema({uri = good_uri})\n                if not ok then\n                    ngx.say(\"mismatched \", good)\n                end\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 17: check plugin configuration updating\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/hello\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body2 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, body3 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/hello1\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body4 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body1)\n            ngx.print(body2)\n            ngx.print(body3)\n            ngx.print(body4)\n        }\n    }\n--- wait: 0.5\n--- response_body\npassedopentracing\npassedopentracing\n--- grep_error_log eval\nqr/sending a batch logs to http:\\/\\/127.0.0.1:1982\\/hello\\d?/\n--- grep_error_log_out\nsending a batch logs to http://127.0.0.1:1982/hello\nsending a batch logs to http://127.0.0.1:1982/hello1\n\n\n\n=== TEST 18: check log schema(include_resp_body_expr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({uri = \"http://127.0.0.1\",\n                                                 auth_header = \"Basic 123\",\n                                                 timeout = 3,\n                                                 name = \"http-logger\",\n                                                 max_retry_count = 2,\n                                                 retry_delay = 2,\n                                                 buffer_duration = 2,\n                                                 inactive_timeout = 2,\n                                                 batch_max_size = 500,\n                                                 include_resp_body = true,\n                                                 include_resp_body_expr = {\n                                                     {\"bar\", \"<>\", \"foo\"}\n                                                 }\n                                                 })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nfailed to validate the 'include_resp_body_expr' expression: invalid operator '<>'\ndone\n\n\n\n=== TEST 19: ssl_verify default is false for comppatibaility\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/hello\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: set correct https endpoint and ssl verify true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"https://127.0.0.1:1983/echo\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"ssl_verify\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: access correct https endpoint but ssl verify failed\n--- request\nGET /hello1\n--- error_log\ncertificate host mismatch\n--- wait: 3\n\n\n\n=== TEST 22: set correct https endpoint and ssl verify false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"https://127.0.0.1:1983/echo\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"ssl_verify\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello1\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: access correct https endpoint but ssl verify ok\n--- request\nGET /hello1\n--- wait: 3\n\n\n\n=== TEST 24: set route with include_resp_body enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/log\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: test http logger with include_resp_body\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log:.*\"response\":\\{\"body\":\"hello world\\\\n\"/\n--- wait: 1.5\n\n\n\n=== TEST 26: set route with http-logger include_resp_body disabled file-logger include_resp_body enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/log\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_resp_body\": false\n                            },\n                            \"file-logger\": {\n                              \"path\": \"/var/log/apisix/file.log\",\n                              \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 27: test http logger without log response body\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/request log:(?!.*\"body\")/\n--- wait: 1.5\n"
  },
  {
    "path": "t/plugin/http-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    server {\n        listen 12001;\n\n        location /http-logger/test {\n            content_by_lua_block {\n                ngx.say(\"test-http-logger-response\")\n            }\n        }\n\n        location /http-logger/Authorization {\n            content_by_lua_block {\n                ngx.log(ngx.WARN, \"received Authorization header: [\", ngx.var.http_authorization, \"]\")\n                ngx.say(\"OK\")\n            }\n        }\n\n        location /http-logger/center {\n            content_by_lua_block {\n                local function str_split(str, reps)\n                    local str_list = {}\n                    string.gsub(str, '[^' .. reps .. ']+', function(w)\n                        table.insert(str_list, w)\n                    end)\n                    return str_list\n                end\n\n                local args = ngx.req.get_uri_args()\n                local query = args.query or nil\n                ngx.req.read_body()\n                local body = ngx.req.get_body_data()\n\n                if query then\n                    if type(query) == \"string\" then\n                        query = {query}\n                    end\n\n                    local data, err = require(\"cjson\").decode(body)\n                    if err then\n                        ngx.log(ngx.WARN, \"logs:\", body)\n                    end\n\n                    for i = 1, #query do\n                        local fields = str_split(query[i], \".\")\n                        local val\n                        for j = 1, #fields do\n                            local key = fields[j]\n                            if j == 1 then\n                                val = data[key]\n                            else\n                                val = val[key]\n                            end\n                        end\n                        ngx.log(ngx.WARN ,query[i], \":\", val)\n                    end\n                else\n                    ngx.log(ngx.WARN, \"logs:\", body)\n                end\n            }\n        }\n\n        location / {\n            content_by_lua_block {\n                ngx.log(ngx.WARN, \"test http logger for root path\")\n            }\n        }\n    }\n\n    server {\n        listen 11451;\n        gzip on;\n        gzip_types *;\n        gzip_min_length 1;\n        location /gzip_hello {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local s = \"gzip hello world\"\n                ngx.header['Content-Length'] = #s + 1\n                ngx.say(s)\n            }\n        }\n    }\n\n    server {\n        listen 11452;\n        location /brotli_hello {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local s = \"brotli hello world\"\n                ngx.header['Content-Length'] = #s + 1\n                ngx.say(s)\n            }\n            header_filter_by_lua_block {\n                local conf = {\n                    comp_level = 6,\n                    http_version = 1.1,\n                    lgblock = 0,\n                    lgwin = 19,\n                    min_length = 1,\n                    mode = 0,\n                    types = \"*\",\n                }\n                local brotli = require(\"apisix.plugins.brotli\")\n                brotli.header_filter(conf, ngx.ctx)\n            }\n            body_filter_by_lua_block {\n                local conf = {\n                    comp_level = 6,\n                    http_version = 1.1,\n                    lgblock = 0,\n                    lgwin = 19,\n                    min_length = 1,\n                    mode = 0,\n                    types = \"*\",\n                }\n                local brotli = require(\"apisix.plugins.brotli\")\n                brotli.body_filter(conf, ngx.ctx)\n            }\n        }\n    }\n\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    my $extra_init_by_lua = <<_EOC_;\n    local bpm = require(\"apisix.utils.batch-processor-manager\")\n    bpm.set_check_stale_interval(1)\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: check stale batch processor\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/hello\",\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: don't remove current processor\n--- request\nGET /opentracing\n--- error_log\nBatch Processor[http logger] successfully processed the entries\n--- no_error_log\nremoving batch processor stale object\n--- wait: 0.5\n\n\n\n=== TEST 3: remove stale processor\n--- request\nGET /opentracing\n--- error_log\nBatch Processor[http logger] successfully processed the entries\nremoving batch processor stale object\n--- wait: 1.5\n\n\n\n=== TEST 4: don't remove batch processor which is in used\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1982/hello\",\n                                \"batch_max_size\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: don't remove\n--- request\nGET /opentracing\n--- no_error_log\nremoving batch processor stale object\n--- wait: 1.5\n\n\n\n=== TEST 6: set fetch request body and response body route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"POST\"],\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:12001/http-logger/center?query[]=request.body&query[]=response.body\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_req_body\": true,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:12001\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/http-logger/test\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: test fetch request body and response body route\n--- request\nPOST /http-logger/test\ntest-http-logger-request\n--- response_body\ntest-http-logger-response\n--- error_log\nrequest.body:test-http-logger-request\nresponse.body:test-http-logger-response\n--- wait: 1.5\n\n\n\n=== TEST 8: set fetch request body and response body route - gzip\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:12001/http-logger/center?query[]=response.body\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:11451\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/gzip_hello\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: test fetch request body and response body route\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- error_log\nresponse.body:gzip hello world\n--- wait: 1.5\n\n\n\n=== TEST 10: set fetch request body and response body route - brotli\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:12001/http-logger/center?query[]=response.body\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:11452\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/brotli_hello\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: test fetch request body and response body route\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- error_log\nresponse.body:brotli hello world\n--- wait: 1.5\n\n\n\n=== TEST 12: test default Authorization header sent to the log server\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"POST\"],\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:12001/http-logger/Authorization\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:12001\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/http-logger/test\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit\n--- request\nPOST /http-logger/test\ntest-http-logger-request\n--- error_log\nreceived Authorization header: [nil]\n--- wait: 1.5\n\n\n\n=== TEST 14: add default path\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:12001\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:12001\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/http-logger/test\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: hit\n--- request\nGET /http-logger/test\n--- error_log\ntest http logger for root path\n"
  },
  {
    "path": "t/plugin/http-logger3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    my $extra_init_by_lua = <<_EOC_;\n    local bpm = require(\"apisix.utils.batch-processor-manager\")\n    bpm.set_check_stale_interval(1)\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: should drop entries when max_pending_entries is exceeded\n--- extra_yaml_config\nplugins:\n  - http-logger\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"http-logger\"] = {\n                            uri = \"http://127.0.0.1:1234/http-logger/test\",\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/http-logger', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/inspect.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('warn');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $user_yaml_config = <<_EOC_;\nplugin_attr:\n  inspect:\n    delay: 1\n    hooks_file: \"/tmp/apisix_inspect_hooks.lua\"\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\nlocal function gen_funcs_invoke(...)\n    local code = \"\"\n    for _, func in ipairs({...}) do\n        code = code .. \"test.\" .. func .. \"();\"\n    end\n    return code\nend\nfunction set_test_route(...)\n    func = func or 'run1'\n    local t = require(\"lib.test_admin\").test\n    local code = [[{\n        \"methods\": [\"GET\"],\n        \"uri\": \"/inspect\",\n        \"plugins\": {\n            \"serverless-pre-function\": {\n                \"phase\": \"rewrite\",\n                \"functions\" : [\"return function() local test = require(\\\\\"lib.test_inspect\\\\\");]]\n                .. gen_funcs_invoke(...)\n                .. [[ngx.say(\\\\\"ok\\\\\"); end\"]\n            }\n        },\n        \"upstream\": {\n            \"type\": \"roundrobin\",\n            \"nodes\": {\n                \"127.0.0.1:1980\": 1\n            }\n        }\n    }]]\n    return t('/apisix/admin/routes/inspect', ngx.HTTP_PUT, code)\nend\n\nfunction do_request()\n    local http = require \"resty.http\"\n    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/inspect\"\n\n    local httpc = http.new()\n    local res = httpc:request_uri(uri, {method = \"GET\"})\n    assert(res.body == \"ok\\\\n\")\nend\n\nfunction write_hooks(code, file)\n    local file = io.open(file or \"/tmp/apisix_inspect_hooks.lua\", \"w\")\n    file:write(code)\n    file:close()\nend\n_EOC_\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n\n    # note that it's different from APISIX.pm,\n    # here we enable no_error_log ignoreless of error_log.\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!$block->timeout) {\n        $block->set_value(\"timeout\", \"10\");\n    }\n});\n\nadd_cleanup_handler(sub {\n    unlink(\"/tmp/apisix_inspect_hooks.lua\");\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: simple hook\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 27, nil, function(info)\n                ngx.log(ngx.WARN, \"var1=\", info.vals.var1)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\nvar1=hello\n\n\n\n=== TEST 2: filename only\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"test_inspect.lua\", 27, nil, function(info)\n                ngx.log(ngx.WARN, \"var1=\", info.vals.var1)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\nvar1=hello\n\n\n\n=== TEST 3: hook lifetime\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            local hook1_times = 2\n            dbg.set_hook(\"test_inspect.lua\", 27, nil, function(info)\n                ngx.log(ngx.WARN, \"var1=\", info.vals.var1)\n                hook1_times = hook1_times - 1\n                return hook1_times == 0\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            -- request 3 times, but hook triggered 2 times\n            for _ = 1,3 do\n                do_request()\n            end\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\nvar1=hello\nvar1=hello\n\n\n\n=== TEST 4: multiple hooks\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"test_inspect.lua\", 26, nil, function(info)\n                ngx.log(ngx.WARN, \"var1=\", info.vals.var1)\n                return true\n            end)\n\n            dbg.set_hook(\"test_inspect.lua\", 27, nil, function(info)\n                ngx.log(ngx.WARN, \"var2=\", info.vals.var2)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            -- note that we don't remove the hook file,\n            -- used for next test case\n        }\n    }\n--- error_log\nvar1=hello\nvar2=world\n\n\n\n=== TEST 5: hook file not removed, re-enabled by next startup\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\nvar1=hello\n\n\n\n=== TEST 6: soft link\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 27, nil, function(info)\n                ngx.log(ngx.WARN, \"var1=\", info.vals.var1)\n                return true\n            end)\n            ]], \"/tmp/test_real_tmp_file.lua\")\n\n            os.execute(\"ln -sf /tmp/test_real_tmp_file.lua /tmp/apisix_inspect_hooks.lua\")\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n            os.remove(\"/tmp/test_real_tmp_file.lua\")\n        }\n    }\n--- error_log\nvar1=hello\n\n\n\n=== TEST 7: remove soft link would disable hooks\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 27, nil, function(info)\n                ngx.log(ngx.WARN, \"var1=\", info.vals.var1)\n                return true\n            end)\n            ]], \"/tmp/test_real_tmp_file.lua\")\n\n            os.execute(\"ln -sf /tmp/test_real_tmp_file.lua /tmp/apisix_inspect_hooks.lua\")\n\n            ngx.sleep(1.5)\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/test_real_tmp_file.lua\")\n        }\n    }\n--- no_error_log\nvar1=hello\n\n\n\n=== TEST 8: ensure we see all local variables till the hook line\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 27, nil, function(info)\n                local count = 0\n                for k,v in pairs(info.vals) do\n                    count = count + 1\n                end\n                ngx.log(ngx.WARN, \"count=\", count)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\ncount=2\n\n\n\n=== TEST 9: check upvalue of run2(), only upvalue used in function code are visible\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run2\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 33, nil, function(info)\n                ngx.log(ngx.WARN, \"upvar1=\", info.uv.upvar1)\n                ngx.log(ngx.WARN, \"upvar2=\", info.uv.upvar2)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\nupvar1=2\nupvar2=nil\n\n\n\n=== TEST 10: check upvalue of run3(), now both upvar1 and upvar2 are visible\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run3\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 37, nil, function(info)\n                ngx.log(ngx.WARN, \"upvar1=\", info.uv.upvar1)\n                ngx.log(ngx.WARN, \"upvar2=\", info.uv.upvar2)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\nupvar1=2\nupvar2=yes\n\n\n\n=== TEST 11: flush specific JIT cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local test = require(\"lib.test_inspect\")\n\n            local t1 = test.hot1()\n            local t8 = test.hot2()\n\n            write_hooks([[\n            local test = require(\"lib.test_inspect\")\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 47, test.hot1, function(info)\n                return false\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            local t2 = test.hot1()\n            local t9 = test.hot2()\n\n            assert(t2-t1 > t1, \"hot1 consumes at least double times than before\")\n            assert(t9-t8 < t8*0.8, \"hot2 not affected\")\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n\n            ngx.sleep(1.5)\n\n            local t3 = test.hot1()\n            local t4 = test.hot2()\n            assert(t3-t1 < t1*0.8, \"hot1 jit recover\")\n            assert(t4-t8 < t4*0.8, \"hot2 jit recover\")\n        }\n    }\n\n\n\n=== TEST 12: flush the whole JIT cache\n--- config\n    location /t {\n        content_by_lua_block {\n            local test = require(\"lib.test_inspect\")\n\n            local t1 = test.hot1()\n            local t8 = test.hot2()\n\n            write_hooks([[\n            local test = require(\"lib.test_inspect\")\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 47, nil, function(info)\n                return false\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            local t2 = test.hot1()\n            local t9 = test.hot2()\n\n            assert(t2-t1 > t1, \"hot1 consumes at least double times than before\")\n            assert(t9-t8 > t8, \"hot2 consumes at least double times than before\")\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n\n            ngx.sleep(1.5)\n\n            local t3 = test.hot1()\n            local t4 = test.hot2()\n            assert(t3-t1 < t1*0.8, \"hot1 jit recover\")\n            assert(t4-t8 < t4*0.8, \"hot2 jit recover\")\n        }\n    }\n\n\n\n=== TEST 13: remove hook log\n--- config\n    location /t {\n        content_by_lua_block {\n            local code = set_test_route(\"run1\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            write_hooks([[\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 27, nil, function(info)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            do_request()\n\n            os.remove(\"/tmp/apisix_inspect_hooks.lua\")\n        }\n    }\n--- error_log\ninspect: remove hook: t/lib/test_inspect.lua#27\ninspect: all hooks removed\n\n\n\n=== TEST 14: jit should be recovered after all hooks are done\n--- config\n    location /t {\n        content_by_lua_block {\n            local test = require(\"lib.test_inspect\")\n\n            local t1 = test.hot1()\n\n            write_hooks([[\n            local test = require(\"lib.test_inspect\")\n            local dbg = require \"apisix.inspect.dbg\"\n            dbg.set_hook(\"t/lib/test_inspect.lua\", 47, test.hot1, function(info)\n                return true\n            end)\n            ]])\n\n            ngx.sleep(1.5)\n\n            local t2 = test.hot1()\n            assert(t2-t1 < t1*0.8, \"hot1 consumes at least double times than before\")\n        }\n    }\n--- error_log\ninspect: remove hook: t/lib/test_inspect.lua#47\ninspect: all hooks removed\n"
  },
  {
    "path": "t/plugin/ip-restriction.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n            local conf = {\n                whitelist = {\n                    \"10.255.254.0/24\",\n                    \"192.168.0.0/16\"\n                }\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"message\":\"Your IP address is not allowed\",\"response_code\":403,\"whitelist\":[\"10.255.254.0/24\",\"192.168.0.0/16\"]}\n\n\n\n=== TEST 2: wrong CIDR v4 format\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n            local conf = {\n                whitelist = {\n                    \"10.255.256.0/24\",\n                    \"192.168.0.0/16\"\n                }\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/failed to validate item 1: object matches none of the required/\n\n\n\n=== TEST 3: wrong CIDR v4 format\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n            local conf = {\n                whitelist = {\n                    \"10.255.254.0/38\",\n                    \"192.168.0.0/16\"\n                }\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/failed to validate item 1: object matches none of the required/\n\n\n\n=== TEST 4: empty conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n\n            local ok, err = plugin.check_schema({})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches none\ndone\n\n\n\n=== TEST 5: empty CIDRs\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n\n            local ok, err = plugin.check_schema({blacklist={}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/expect array to have at least 1 items/\n\n\n\n=== TEST 6: whitelist and blacklist mutual exclusive\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n            local ok, err = plugin.check_schema({whitelist={\"172.17.40.0/24\"}, blacklist={\"10.255.0.0/16\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches both schemas 1 and 2\ndone\n\n\n\n=== TEST 7: set whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"whitelist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route and ip cidr in the whitelist\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 9: hit route and ip in the whitelist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 113.74.26.106\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 10: hit route and ip not in the whitelist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 114.114.114.114\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n--- error_log\nip-restriction exits with http status code 403\n\n\n\n=== TEST 11: hit route and IPv6 not in the whitelist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 2001:db8::2\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n\n\n\n=== TEST 12: set blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"blacklist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route and ip cidr in the blacklist\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n\n\n\n=== TEST 14: hit route and ip in the blacklist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 113.74.26.106\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n\n\n\n=== TEST 15: hit route and ip not in the blacklist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 114.114.114.114\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 16: hit route and IPv6 not in the blacklist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 2001:db8::2\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 17: remove ip-restriction\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 19: sanity(IPv6)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n            local conf = {\n                whitelist = {\n                    \"::1\",\n                    \"fe80::/32\",\n                    \"2001:DB8:0:23:8:800:200C:417A\",\n                }\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"pass\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npass\n\n\n\n=== TEST 20: set blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"blacklist\": [\n                                \"::1\",\n                                \"fe80::/32\"\n                            ]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 22: hit route and IPv6 in the blacklist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: ::1\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n\n\n\n=== TEST 23: hit route and IPv6 in the blacklist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: fe80::1:1\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Your IP address is not allowed\"}\n\n\n\n=== TEST 24: wrong IPv6 format\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ip-restriction\")\n            for i, ip in ipairs({\"::1/129\", \"::ffgg\"}) do\n                local conf = {\n                    whitelist = {\n                        ip\n                    }\n                }\n                local ok, err = plugin.check_schema(conf)\n                if not ok then\n                    ngx.say(err)\n                end\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/failed to validate item 1: object matches none of the required/\n\n\n\n=== TEST 25: set disable=true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                            \"blacklist\": [\n                                \"127.0.0.0/24\"\n                            ],\n                            \"_meta\": {\n                                \"disable\": true\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: set blacklist and user-defined message\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"blacklist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ],\n                                 \"message\": \"Do you want to do something bad?\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: hit route and ip cidr in the blacklist\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Do you want to do something bad?\"}\n\n\n\n=== TEST 28: hit route and ip in the blacklist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 113.74.26.106\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Do you want to do something bad?\"}\n\n\n\n=== TEST 29: set whitelist and user-defined message\n--- config\nlocation /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"whitelist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ],\n                                 \"message\": \"Do you want to do something bad?\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: hit route and ip not in the whitelist\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 114.114.114.114\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Do you want to do something bad?\"}\n--- error_log\nip-restriction exits with http status code 403\n\n\n\n=== TEST 31: message that do not reach the minimum range\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"whitelist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ],\n                                 \"message\": \"\"\n                            }\n                        }\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/string too short, expected at least 1, got 0/\n\n\n\n=== TEST 32: exceeds the maximum limit of message\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local data = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    }\n                },\n                plugins = {\n                    [\"ip-restriction\"] = {\n                        [\"whitelist\"] = {\n                            \"127.0.0.0/24\",\n                            \"113.74.26.106\"\n                        },\n                        message = (\"-1Aa#\"):rep(205)\n                    }\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/string too long, expected at most 1024, got 1025/\n\n\n\n=== TEST 33: set whitelist and 404 response code\n--- config\nlocation /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"whitelist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ],\n                                 \"response_code\": 404\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 34: hit route and ip not in the whitelist expect 404\n--- http_config\nset_real_ip_from 127.0.0.1;\nreal_ip_header X-Forwarded-For;\n--- more_headers\nX-Forwarded-For: 114.114.114.114\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nip-restriction exits with http status code 404\n\n\n\n=== TEST 35: set wrong response code\n--- config\nlocation /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ip-restriction\": {\n                                 \"whitelist\": [\n                                     \"127.0.0.0/24\",\n                                     \"113.74.26.106\"\n                                 ],\n                                 \"response_code\": 409\n                            }\n                        }\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/property \\\\\"response_code\\\\\" validation failed: expected 409 to be at most 404/\n"
  },
  {
    "path": "t/plugin/jwe-decrypt.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.jwe-decrypt\")\n            local core = require(\"apisix.core\")\n            local conf = {key = \"123\", secret = \"12345678901234567890123456789012\"}\n\n            local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- response_body_like eval\nqr/{\"key\":\"123\",\"secret\":\"[a-zA-Z0-9+\\\\\\/]+={0,2}\"}/\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 2: wrong type of key\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwe-decrypt\")\n            local ok, err = plugin.check_schema({key = 123, secret = \"12345678901234567890123456789012\"}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"key\" validation failed: wrong type: expected string, got number\ndone\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 3: wrong type of secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwe-decrypt\")\n            local ok, err = plugin.check_schema({key = \"123\", secret = 12345678901234567890123456789012}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"secret\" validation failed: wrong type: expected string, got number\ndone\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 4: secret length too long\n--- yaml_config\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwe-decrypt\")\n            local ok, err = plugin.check_schema({key = \"123\", secret = \"123456789012345678901234567890123\"}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nthe secret length should be 32 chars\ndone\n--- no_error_log\n123456789012345678901234567890123\n\n\n\n=== TEST 5: secret length too long(base64 encode)\n--- yaml_config\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwe-decrypt\")\n            local ok, err = plugin.check_schema({key = \"123\", secret = \"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXphYmNkZWZn\", is_base64_encoded = true}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nthe secret length after base64 decode should be 32 chars\ndone\n--- no_error_log\nYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXphYmNkZWZn\n\n\n\n=== TEST 6: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"12345678901234567890123456789012\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 7: verify encrypted field\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            -- get plugin conf from etcd, secret and key is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/jack'))\n            ngx.say(res.body.node.value.plugins[\"jwe-decrypt\"].key)\n            ngx.say(res.body.node.value.plugins[\"jwe-decrypt\"].secret)\n        }\n    }\n--- response_body\nXU29sA3FEVF68hGcdPo7sg==\nf9pGB0Dt4gYNCLKiINPfVSviKjQs2zfkBCT4+XZ3mDABZkJTr0orzYRD5CptDKMc\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 8: enable jwe-decrypt plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"header\": \"Authorization\",\n                            \"forward_header\": \"Authorization\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 9: create public API route (jwe-decrypt sign)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/jwe/encrypt\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 10: sign / verify in argument\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, err, token = t('/apisix/plugin/jwe/encrypt?key=user-key&payload=hello',\n                ngx.HTTP_GET\n            )\n\n            if code > 200 then\n                ngx.status = code\n                ngx.say(err)\n                return\n            end\n\n            code, err, body = t('/hello',\n                ngx.HTTP_GET,\n                nil,\n                nil,\n                { Authorization = token }\n            )\n\n            ngx.print(body)\n        }\n    }\n--- response_body\nhello world\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 11: test for unsupported method\n--- request\nPATCH /apisix/plugin/jwe/encrypt?key=user-key\n--- error_code: 404\n\n\n\n=== TEST 12: verify, missing token\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"missing JWE token in request\"}\n\n\n\n=== TEST 13: verify: invalid JWE token\n--- request\nGET /hello\n--- more_headers\nAuthorization: invalid-eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.6JeRgm02rpOJdg.4nkSYJgwMKYgTeacatgmRw\n--- error_code: 400\n--- response_body\n{\"message\":\"JWE token invalid\"}\n\n\n\n=== TEST 14: verify (in header)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.6JeRgm02rpOJdg.4nkSYJgwMKYgTeacatgmRw\n--- response_body\nhello world\n\n\n\n=== TEST 15: verify (in header without Bearer)\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.6JeRgm02rpOJdg.4nkSYJgwMKYgTeacatgmRw\n--- response_body\nhello world\n\n\n\n=== TEST 16: verify (header with bearer)\n--- request\nGET /hello\n--- more_headers\nAuthorization: bearer eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.6JeRgm02rpOJdg.4nkSYJgwMKYgTeacatgmRw\n--- response_body\nhello world\n\n\n\n=== TEST 17: verify (invalid bearer token)\n--- request\nGET /hello\n--- more_headers\nAuthorization: bearer invalid-eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy.6JeRgm02rpOJdg.4nkSYJgwMKYgTeacatgmRw\n--- error_code: 400\n--- response_body\n{\"message\":\"JWE token invalid\"}\n\n\n\n=== TEST 18: delete a exist consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"12345678901234567890123456789012\"\n                        }\n                    }\n                }]]\n            )\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"chen\",\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"key\": \"chen-key\",\n                            \"secret\": \"12345678901234567890123456789021\"\n                        }\n                    }\n                }]]\n            )\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n\n            code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_DELETE)\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n\n            code, body = t('/apisix/plugin/jwe/encrypt?key=chen-key&payload=hello',\n                ngx.HTTP_GET)\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n        }\n    }\n--- response_body\ncode: true body: passed\ncode: true body: passed\ncode: true body: passed\ncode: true body: passed\n--- no_error_log\n12345678901234567890123456789012\n\n\n\n=== TEST 19: add consumer with username and plugins with base64 secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\",\n                            \"is_base64_encoded\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n\n\n\n=== TEST 20: enable jwt decrypt plugin with base64 secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"header\": \"Authorization\",\n                            \"forward_header\": \"Authorization\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n\n\n\n=== TEST 21: create public API route (jwe-decrypt sign)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/jwe/encrypt\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n\n\n\n=== TEST 22: sign / verify in argument\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, err, token = t('/apisix/plugin/jwe/encrypt?key=user-key&payload=hello',\n                ngx.HTTP_GET\n            )\n\n            if code > 200 then\n                ngx.status = code\n                ngx.say(err)\n                return\n            end\n\n            ngx.log(ngx.WARN, \"dibag: \", token)\n\n            code, err, body = t('/hello',\n                ngx.HTTP_GET,\n                nil,\n                nil,\n                { Authorization = token }\n            )\n\n            ngx.print(body)\n        }\n    }\n--- response_body\nhello world\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n\n\n\n=== TEST 23: verify (in header)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy._0DrWD0.vl-ydutnNuMpkYskwNqu-Q\n--- response_body\nhello world\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n\n\n\n=== TEST 24: verify (in header without Bearer)\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy._0DrWD0.vl-ydutnNuMpkYskwNqu-Q\n--- response_body\nhello world\n\n\n\n=== TEST 25: enable jwt decrypt plugin with test upstream route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwe-decrypt\": {\n                            \"header\": \"Authorization\",\n                            \"forward_header\": \"Authorization\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"httpbin.local:8280\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/headers\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n\n\n\n=== TEST 26:  verify in upstream header\n--- request\nGET /headers\n--- more_headers\nAuthorization: eyJhbGciOiJkaXIiLCJraWQiOiJ1c2VyLWtleSIsImVuYyI6IkEyNTZHQ00ifQ..MTIzNDU2Nzg5MDEy._0DrWD0.vl-ydutnNuMpkYskwNqu-Q\n--- response_body_like\n.*\"Authorization\": \"hello\".*\n--- no_error_log\nfo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\n"
  },
  {
    "path": "t/plugin/jwt-auth-anonymous-consumer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer jack and anonymous\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 4,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"anonymous\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\npassed\n\n\n\n=== TEST 2: add jwt auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"anonymous_consumer\": \"anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: normal consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 5, 1 do\n                local code, body = t('/hello',\n                    ngx.HTTP_GET,\n                    nil,\n                    nil,\n                    {\n                        Authorization = \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\"\n                    }\n                )\n\n                if code >= 300 then\n                    ngx.say(\"failed\" .. code)\n                    return\n                end\n                ngx.say(body .. i)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed1\npassed2\npassed3\npassed4\nfailed503\n\n\n\n=== TEST 4: request without jwt-auth header will be from anonymous consumer and it will pass\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: request without jwt-auth header will be from anonymous consumer and different rate limit will apply\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 6: add jwt auth plugin with non-existent anonymous_consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"anonymous_consumer\": \"not-found-anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: anonymous-consumer configured in the route should not be found\n--- request\nGET /hello\n--- error_code: 401\n--- error_log\nfailed to get anonymous consumer not-found-anonymous\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n"
  },
  {
    "path": "t/plugin/jwt-auth-more-algo.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"debug\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\",\n                            \"algorithm\": \"HS384\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: enable jwt auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: create public API route (jwt-auth sign)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/jwt/sign\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: sign / verify in argument\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, res = t('/hello?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MjA4NTA4Nzc5Mn0.6BNfYOnGvB27uY5LIwZFgIV_g42wiqLSlITtgAXinuZA9DNcquCTiudmbaXCHj20',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- error_log\n\"alg\":\"HS384\"\n\n\n\n=== TEST 5: verify: invalid JWT token\n--- request\nGET /hello?jwt=invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n--- error_code: 401\n--- response_body\n{\"message\":\"JWT token invalid\"}\n--- error_log\nJWT token invalid: invalid header: invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n\n\n\n=== TEST 6: verify token with algorithm HS256\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n--- error_log\n\"alg\":\"HS256\"\n\n\n\n=== TEST 7: missing public key and private key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\",\n                            \"algorithm\": \"PS256\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin jwt-auth err: failed to validate dependent schema for \\\"algorithm\\\": value should match only one schema, but matches none\"}\n\n\n\n=== TEST 8: missing public key and private key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"cjson\").encode\n            local cons_tab = {\n                username = \"jack\",\n                plugins = {\n                    [\"jwt-auth\"] = {\n                        key = \"user-key2\",\n                        secret = \"my-secret-key\",\n                        algorithm = \"PS256\",\n                        public_key = \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiSpoCgu3GzeExroi2YQ+\\nxcQlXqEO8D5/5DgrlGsEb3Y9kEX+lj3ayW/G93nAob1xrtpjzBLf4chDivcmMj1q\\nOwggoAOOmC9D/EYzDNKAos/gNcgsxra1X7xdMje+jUYR8nQGLemkidD71XbOrrcy\\nLTE886t/lcrauC3dxNl55DkZc22YZWSanmizGfedMIEVtZb08uXbTi+8KyP3d+QL\\nKYQ2eSa8AQredrKmM0eREQHr6R+zz6xqgycJ/Pxp+C0UYFbV+LVnHom5u6ck2SNG\\nuGI1sBQ3V763BArbGpWlpcetQT5JB8QDhywf1ihNdaJgWhswQJVSMpJ8ZmA8R1Av\\nDQIDAQAB\\n-----END PUBLIC KEY-----\"\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                json(cons_tab)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: sign / verify in argument\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, res = t('/hello?jwt=' .. \"eyJ0eXAiOiJKV1QiLCJ4NWMiOlsiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBaVNwb0NndTNHemVFeHJvaTJZUStcbnhjUWxYcUVPOEQ1LzVEZ3JsR3NFYjNZOWtFWCtsajNheVcvRzkzbkFvYjF4cnRwanpCTGY0Y2hEaXZjbU1qMXFcbk93Z2dvQU9PbUM5RC9FWXpETktBb3MvZ05jZ3N4cmExWDd4ZE1qZStqVVlSOG5RR0xlbWtpZEQ3MVhiT3JyY3lcbkxURTg4NnQvbGNyYXVDM2R4Tmw1NURrWmMyMllaV1Nhbm1pekdmZWRNSUVWdFpiMDh1WGJUaSs4S3lQM2QrUUxcbktZUTJlU2E4QVFyZWRyS21NMGVSRVFIcjZSK3p6NnhxZ3ljSi9QeHArQzBVWUZiVitMVm5Ib201dTZjazJTTkdcbnVHSTFzQlEzVjc2M0JBcmJHcFdscGNldFFUNUpCOFFEaHl3ZjFpaE5kYUpnV2hzd1FKVlNNcEo4Wm1BOFIxQXZcbkRRSURBUUFCXG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0iXSwiYWxnIjoiUFMyNTYifQ.eyJrZXkiOiJ1c2VyLWtleTIiLCJleHAiOjIwODUwNjkxMzl9.FmtBZ-LBqyIDQV3lTiN0XaWOrl19D3s6oF3VmbZZ1xoW7gdHVtkMdOs4FrwflxUiZOyAGq7FDBVaHgbzil0LkyXwFqY8EABARUu4S9S3H0xpM6oFXvXsqoA9ygyq5Nty0L8KBI4LMm-rIL0g34pecjZG5cJEbjFhuN4bHM1ZUvJZf-VX6JMmwdueknTY0rIOM7CzComazue3u9JXrDxF1j1xkPInraUmtkUhNM90JuidAgMFVHQb8XN6U-E2Xbn6cD_kXc93Ul4WJK8H2KQNk_gwLmUXBs45wVMzZuEtJ1_nlsPHJztupSE2tSJvwX_YF1EL-v2_OYhgLBSgFsncpw\",\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- error_log\n\"alg\":\"PS256\"\n\n\n\n=== TEST 10: verify token with algorithm PS356\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImtleSI6InVzZXIta2V5MiIsImV4cCI6MjA4OTcyNDA1N30.cKaijeZ4ydKVKCC37UZObPFj_kVsdiScEuGwK_G9JBjg0dcRnL8Xvr6Ofp8kDJz16FO2vy8FHgA_9HVjVpzehNe-AbtYJ88Qopy2pAQHsottGuQe3jgAt-yBI5chf26GzpqTtyymteg-lt-cW6EoP4gVHfXEbzQaOZt0wmdNBX17jISKW70okdxrp7cJKbv4hXQXjhYwKY8h0jYnGb-RhuHXRwWFhp6TZVV57Lfpi1yUDm6GqXM42W7owOOwjUqS8-7KYv1iugQzTo7qcVjPic7X5Wug7N-4t8BRM9jZkUiNrAY2BoxxBMUUru4fd201KY23p4bZDwQFpg6MVck7XA\n--- response_body\nhello world\n\n\n\n=== TEST 11: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\",\n                            \"algorithm\": \"HS384\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: only verify nbf claim\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"claims_to_verify\": [\"nbf\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: verify success with expired token\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n--- response_body\nhello world\n\n\n\n=== TEST 14: verify failed before nbf claim\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMSwibmJmIjoyMjI5NjcxODc0fQ.RJynr34TyCesYHwvDwOwETi1vOfZXKqc_wvQJ3pijBfrx1x5IF3O1CCUCvd5lMYf\n--- error_code: 401\n--- response_body eval\nqr/failed to verify jwt/\n--- error_log\n'nbf' claim not valid until Mon, 27 Aug 2040 09:17:54 GMT\n\n\n\n=== TEST 15: verify success after nbf claim\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMSwibmJmIjoxNzI5Njc1MDQyfQ.IycpH4Lc48BHSxUBXBNDXGawvNgi_6a-qsa-xnhYFLooeWc8DyX8zLadvyEFpMPq\n--- response_body\nhello world\n\n\n\n=== TEST 16: EdDSA algorithm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\",\n                            \"algorithm\": \"EdDSA\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEA9PdGVALrrBX4oX5t9DKb5JHYx7XRb0RXU42r0FVO2sA=\\n-----END PUBLIC KEY-----\",\n                            \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMC4CAQAwBQYDK2VwBCIEIKmBJXpq9Fp0K97TpJ2X9V6jszx23j7NtKKa6gZRaAjI\\n-----END PRIVATE KEY-----\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: sign / verify in argument\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, res = t('/hello?jwt=' .. \"eyJ4NWMiOlsiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Db3dCUVlESzJWd0F5RUE5UGRHVkFMcnJCWDRvWDV0OURLYjVKSFl4N1hSYjBSWFU0MnIwRlZPMnNBPVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tIl0sInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJleHAiOjIwODUwNzA2MDQsImtleSI6InVzZXIta2V5In0.FmPpxVDubPukcnW58DICZOYMqvkikn4TuUzIQX68-s9KOBUhOgH1_TZM3gUk5Wv0L86c4joVzU7hqstsJSs0Cw\",\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- error_log\n\"alg\":\"EdDSA\"\n"
  },
  {
    "path": "t/plugin/jwt-auth-realm.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity, default realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: verify default realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Bearer realm=\"jwt\"\n\n\n\n=== TEST 3: set custom realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"realm\": \"my-jwt-realm\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: verify custom realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Bearer realm=\"my-jwt-realm\"\n\n\n\n=== TEST 5: set anonymous consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"anonymous_consumer\": \"missing-consumer\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: verify anonymous consumer missing returns realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Bearer realm=\"jwt\"\n--- error_log\nfailed to get anonymous consumer\n"
  },
  {
    "path": "t/plugin/jwt-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            local core = require(\"apisix.core\")\n            local conf = {key = \"123\", secret = \"my-secret-key\"}\n\n            local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- response_body_like eval\nqr/{\"algorithm\":\"HS256\",\"base64_secret\":false,\"exp\":86400,\"key\":\"123\",\"lifetime_grace_period\":0,\"secret\":\"my-secret-key\"}/\n\n\n\n=== TEST 2: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            local ok, err = plugin.check_schema({key = 123}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"key\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 3: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: enable jwt auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify, missing token\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing JWT token in request\"}\n\n\n\n=== TEST 6: verify: invalid JWT token\n--- request\nGET /hello?jwt=invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n--- error_code: 401\n--- response_body\n{\"message\":\"JWT token invalid\"}\n--- error_log\nJWT token invalid: invalid header: invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n\n\n\n=== TEST 7: verify: expired JWT token\n--- request\nGET /hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n--- error_code: 401\n--- response_body\n{\"message\":\"failed to verify jwt\"}\n--- error_log\nfailed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT\n\n\n\n=== TEST 8: verify (in header)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 9: verify (in cookie)\n--- request\nGET /hello\n--- more_headers\nCookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 10: verify (in header without Bearer)\n--- request\nGET /hello\n--- more_headers\nAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 11: verify (header with bearer)\n--- request\nGET /hello\n--- more_headers\nAuthorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 12: verify (invalid bearer token)\n--- request\nGET /hello\n--- more_headers\nAuthorization: bearer invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- error_code: 401\n--- response_body\n{\"message\":\"JWT token invalid\"}\n--- error_log\nJWT token invalid: invalid header: invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n\n\n\n=== TEST 13: delete a exist consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1)\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n            )\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"chen\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"chen-key\",\n                            \"secret\": \"chen-key\"\n                        }\n                    }\n                }]]\n            )\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n\n            code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_DELETE)\n            ngx.say(\"code: \", code < 300, \" body: \", body)\n        }\n    }\n--- response_body\ncode: true body: passed\ncode: true body: passed\ncode: true body: passed\n\n\n\n=== TEST 14: add consumer with username and plugins with base64 secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=\",\n                            \"base64_secret\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: enable jwt auth plugin with base64 secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: sign / verify\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- sign is generated via https://jwt.io/#debugger-io. This is the case for all other test cases and is not specified further\n            local sign = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsIm5iZiI6MTcyNzI3NDk4M30._Z8b_Asb2ROvGX4R5sNMbgJNQXB6x7aQeuVjmjY21Nw\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 17: verify: invalid JWT token\n--- request\nGET /hello?jwt=invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n--- error_code: 401\n--- response_body\n{\"message\":\"JWT token invalid\"}\n--- error_log\nJWT token invalid: invalid header: invalid-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n\n\n\n=== TEST 18: verify: invalid signature\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- error_code: 401\n--- response_body\n{\"message\":\"failed to verify jwt\"}\n--- error_log\nfailed to verify jwt: signature mismatch: fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n\n\n\n=== TEST 19: verify: happy path\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0._kNmXeH1uYVAvApFTONk2Z3Gh-a4XfGrjmqd_ahoOI0\n--- response_body\nhello world\n\n\n\n=== TEST 20: without key\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            local ok, err = plugin.check_schema({}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"key\" is required\n\n\n\n=== TEST 21: get the schema by schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, raw = t('/apisix/admin/schema/plugins/jwt-auth?schema_type=consumer',\n                ngx.HTTP_GET,\n                [[\n{\"dependencies\":{\"algorithm\":{\"oneOf\":[{\"properties\":{\"algorithm\":{\"default\":\"HS256\",\"enum\":[\"HS256\",\"HS512\"]}}},{\"required\":[\"public_key\"],\"properties\":{\"algorithm\":{\"enum\":[\"RS256\",\"ES256\"]},\"public_key\":{\"type\":\"string\"}}}]}},\"required\":[\"key\"],\"type\":\"object\",\"properties\":{\"base64_secret\":{\"default\":false,\"type\":\"boolean\"},\"secret\":{\"type\":\"string\"},\"algorithm\":{\"enum\":[\"HS256\",\"HS512\",\"RS256\",\"ES256\"],\"default\":\"HS256\",\"type\":\"string\"},\"exp\":{\"minimum\":1,\"default\":86400,\"type\":\"integer\"},\"key\":{\"type\":\"string\"}}}\n                ]]\n                )\n\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 22: get the schema by error schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/jwt-auth?schema_type=consumer123123',\n                ngx.HTTP_GET,\n                nil,\n                [[\n                {\"properties\":{},\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 23: get the schema by default schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/jwt-auth',\n                ngx.HTTP_GET,\n                nil,\n                [[\n                {\"properties\":{},\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 24: add consumer with username and plugins with public_key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-rs256\",\n                            \"algorithm\": \"RS256\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr\\n7noq/0ukiZqVQLSJPMOv0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQ==\\n-----END PUBLIC KEY-----\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 25: JWT sign and verify use RS256 algorithm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 26: sign/verify use RS256 algorithm(private_key numbits = 512)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this private_key\n            -- private_key = \"-----BEGIN RSA PRIVATE KEY-----\\nMIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv\\n0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7\\n+pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL\\nwQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF\\nIeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb\\n2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs\\nYvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG\\n-----END RSA PRIVATE KEY-----\"\n\n            local sign = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1yczI1NiIsIm5iZiI6MTcyNzI3NDk4M30.FaV6N-bWaSXkRrF2ec28hH5QENl-8I0LCONdNnQpB1YOb4akP-lKnwtABgfsQ_eKaEIf1PWNoghyByLejXaPbQ\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 27: add consumer with username and plugins with public_key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-rs256\",\n                            \"algorithm\": \"RS256\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGxOfVe/seP5T/V8pkS5YNAPRC\\n3Ffxxedi7v0pyZh/4d4p9Qx0P9wOmALwlOq4Ftgks311pxG0zL0LcTJY4ikbc3r0\\nh8SM0yhj9UV1VGtuia4YakobvpM9U+kq3lyIMO9ZPRez0cP3AJIYCt5yf8E7bNYJ\\njbJNjl8WxvM1tDHqVQIDAQAB\\n-----END PUBLIC KEY-----\"\n                            }\n                        }\n                    }\n                ]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 28: JWT sign and verify use RS256 algorithm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 29: sign/verify use RS256 algorithm(private_key numbits = 1024)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this private_key\n            -- private_key = \"-----BEGIN RSA PRIVATE KEY-----\\nMIICXQIBAAKBgQDGxOfVe/seP5T/V8pkS5YNAPRC3Ffxxedi7v0pyZh/4d4p9Qx0\\nP9wOmALwlOq4Ftgks311pxG0zL0LcTJY4ikbc3r0h8SM0yhj9UV1VGtuia4Yakob\\nvpM9U+kq3lyIMO9ZPRez0cP3AJIYCt5yf8E7bNYJjbJNjl8WxvM1tDHqVQIDAQAB\\nAoGAYFy9eAXvLC7u8QuClzT9vbgksvVXvWKQVqo+GbAeOoEpz3V5YDJFYN3ZLwFC\\n+ZQ5nTFXNV6Veu13CMEMA4NBIa8I4r3aYzSjq7X7UEBkLDBtEUge52mYakNfXD8D\\nqViHkyJqvtVnBl7jNZVqbBderQnXA0kigaeZPL3+hkYKBgECQQDmiDbUL3FBynLy\\nNX6/JdAbO4g1Nl/1RsGg8svhb6vRM8WQyIQWt5EKi7yoP/9nIRXcIgdwpVO6wZRU\\nDojL0oy1AkEA3LpjqXxIRzcy2ALsqKN3hoNPGAlkPyG3Mlph91mqSZ2jYpXCX9LW\\nhhQdf9GmfO8jZtYhYAJqEMOJrKeZHToLIQJBAJbrJbnTNTn05ztZehh5ELxDRPBR\\nIJDaOXi8emyjRsA2PGiEXLTih7l3sZIUE4fYSQ9L18MO+LmScSB2Q2fr9uECQFc7\\nIh/dCgN7ARD1Nun+kEIMqrlpHMEGZgv0RDsoqG+naOaRINwVysn6MR5OkGlXaLo/\\nbbkvuxMc88/T/GLciYECQQC4oUveCOic4Qs6TQfMUKKv/kJ09slbD70HkcBzA5nY\\nyro4RT4z/SN6T3SD+TuWn2//I5QxiQEIbOCTySci7yuh\\n-----END RSA PRIVATE KEY-----\"\n\n            local sign = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1yczI1NiIsIm5iZiI6MTcyNzI3NDk4M30.FG-PAyscR-pFyw1a5ZiRxHLxzSI1jyVyZxm-fj3-u5igjacJY7UByCUKDnieV9-Ft81X15gdHAcrumUsTbu-77F50Bp5A1sxzdL_PXVLJ1cc8UP2ltvQwf1YWdutK7CI_uNLaeCYPZd9tWPhnfpsv4AdTdaCWeFyoaZSNOdw4oA\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 30: sign/verify use RS256 algorithm(private_key numbits = 1024,with extra payload)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this private_key and payload\n            -- private_key = \"-----BEGIN RSA PRIVATE KEY-----\\nMIICXQIBAAKBgQDGxOfVe/seP5T/V8pkS5YNAPRC3Ffxxedi7v0pyZh/4d4p9Qx0\\nP9wOmALwlOq4Ftgks311pxG0zL0LcTJY4ikbc3r0h8SM0yhj9UV1VGtuia4Yakob\\nvpM9U+kq3lyIMO9ZPRez0cP3AJIYCt5yf8E7bNYJjbJNjl8WxvM1tDHqVQIDAQAB\\nAoGAYFy9eAXvLC7u8QuClzT9vbgksvVXvWKQVqo+GbAeOoEpz3V5YDJFYN3ZLwFC\\n+ZQ5nTFXNV6Veu13CMEMA4NBIa8I4r3aYzSjq7X7UEBkLDBtEUge52mYakNfXD8D\\nqViHkyJqvtVnBl7jNZVqbBderQnXA0kigaeZPL3+hkYKBgECQQDmiDbUL3FBynLy\\nNX6/JdAbO4g1Nl/1RsGg8svhb6vRM8WQyIQWt5EKi7yoP/9nIRXcIgdwpVO6wZRU\\nDojL0oy1AkEA3LpjqXxIRzcy2ALsqKN3hoNPGAlkPyG3Mlph91mqSZ2jYpXCX9LW\\nhhQdf9GmfO8jZtYhYAJqEMOJrKeZHToLIQJBAJbrJbnTNTn05ztZehh5ELxDRPBR\\nIJDaOXi8emyjRsA2PGiEXLTih7l3sZIUE4fYSQ9L18MO+LmScSB2Q2fr9uECQFc7\\nIh/dCgN7ARD1Nun+kEIMqrlpHMEGZgv0RDsoqG+naOaRINwVysn6MR5OkGlXaLo/\\nbbkvuxMc88/T/GLciYECQQC4oUveCOic4Qs6TQfMUKKv/kJ09slbD70HkcBzA5nY\\nyro4RT4z/SN6T3SD+TuWn2//I5QxiQEIbOCTySci7yuh\\n-----END RSA PRIVATE KEY-----\"\n            -- payload = {\"aaa\":\"11\",\"bb\":\"222\"}\n\n            local sign = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1yczI1NiIsIm5iZiI6MTcyNzI3NDk4M30.FG-PAyscR-pFyw1a5ZiRxHLxzSI1jyVyZxm-fj3-u5igjacJY7UByCUKDnieV9-Ft81X15gdHAcrumUsTbu-77F50Bp5A1sxzdL_PXVLJ1cc8UP2ltvQwf1YWdutK7CI_uNLaeCYPZd9tWPhnfpsv4AdTdaCWeFyoaZSNOdw4oA\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 31: add consumer with username and plugins with public_key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-rs256\",\n                            \"algorithm\": \"RS256\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5LHjZ4FxQ9jk6eQGDRt\\noRwFVkLq+dUBebs97hrzirokVr2B+RoxqdLfKAM+AsN2DadawZ2GqlCV9DL0/gz6\\nnWSqTQpWbQ8c7CrF31EkIHUYRzZvWy17K3WC9Odk/gM1FVd0HbZ2Rjuqj9ADeeqx\\nnj9npDqKrMODOENy31SqZNerWZsdgGkML5JYbX5hbI2L9LREvRU21fDgSfGL6Mw4\\nNaxnnzcvll4yqwrBELSeDZEAt0+e/p1dO7moxF+b1pFkh9vQl6zGvnvf8fOqn5Ex\\ntLHXVzgx752PHMwmuj9mO1ko6p8FOM0JHDnooI+5rwK4j3I27Ho5nnatVWUaxK4U\\n8wIDAQAB\\n-----END PUBLIC KEY-----\"\n                            }\n                        }\n                    }\n                ]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- error_code_like: ^(?:200|201)?$\n\n\n\n=== TEST 32: JWT sign and verify use RS256 algorithm(private_key numbits = 2048)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 33: sign/verify use RS256 algorithm(private_key numbits = 2048)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this private_key\n            -- private_key = \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEAv5LHjZ4FxQ9jk6eQGDRtoRwFVkLq+dUBebs97hrzirokVr2B\\n+RoxqdLfKAM+AsN2DadawZ2GqlCV9DL0/gz6nWSqTQpWbQ8c7CrF31EkIHUYRzZv\\nWy17K3WC9Odk/gM1FVd0HbZ2Rjuqj9ADeeqxnj9npDqKrMODOENy31SqZNerWZsd\\ngGkML5JYbX5hbI2L9LREvRU21fDgSfGL6Mw4Naxnnzcvll4yqwrBELSeDZEAt0+e\\n/p1dO7moxF+b1pFkh9vQl6zGvnvf8fOqn5ExtLHXVzgx752PHMwmuj9mO1ko6p8F\\nOM0JHDnooI+5rwK4j3I27Ho5nnatVWUaxK4U8wIDAQABAoIBAFsFQC73H8KrNyKW\\ngI4fit77U0XS8ZXWMKdH4XrZ71DAdDeKPtC+M05+1GxMbhAeEl8WXraTQ8J0G2s1\\nMtXqEMDrbUbBXKLghVtoTy91e/a369sZ7/qgN19Eq/30WzWdDIGhVZgwcy2Xd8hw\\nitZIPi/z7ChJcE35bsUytseJkJPsWeMJNq4mLbHqMSBQWze/vNvIeGYr2xfqXc6H\\nywGWGlk46RI28mOf7PecU0DxFoTBNcntZrpOwaIrTDsC7E6uNvhVbtsneseTlQuj\\nihS7DAH72Zx3CXc9+SL3b5QNRD1Rnp+gKM6itjW1yduOj2dS0p8YzcUYNtxnw5Gv\\nuLoHwuECgYEA58NhvnHn10YLBEMYxb30tDobdGfOjBSfih8K53+/SJhqF5mv4qZX\\nUfw3o5R+CkkrhbZ24yst7wqKFYZ+LfazOqljOPOrBsgIIry/sXBlcbGLCw9MYFfB\\nejKTt/xZjqLdDCcEbiSB0L2xNuyF/TZOu8V5Nu55LXKBqeW4yISQ5FkCgYEA05t1\\n2cq8gE1jMfGXQNFIpUDG2j4wJXAPqnJZSUF/BICa55mH/HYRKoP2uTSvAnqNrdGt\\nsnjnnMA7T+fGogB4STif1POWfj+BTKVa/qhUX9ytH6TeI4aqPXSZdTVEPRfR7bG1\\nIB/j2lyPkiNi2VijMx33xqxIaQUUsvxIT95GSisCgYAdaJFylQmSK3UiaVEvZlcy\\nt1zcfH+dDtDfueisT216TLzJmdrTq7/Qy2xT+Xe03mwDX4/ea5A8kN3MtXA1bOR5\\nQR0yENlW1vMRVVoNrfFxZ9H46UwLvZbzZo+P/RlwHAJolFrfjwpZ7ngaPBEUfFup\\nP/mNmt0Ng0YoxNmZuBiaoQKBgQCa2d4RRgpRvdAEYW41UbHetJuQZAfprarZKZrr\\nP9HKoq45I6Je/qurOCzZ9ZLItpRtic6Zl16u2AHPhKZYMQ3VT2mvdZ5AvwpI44zG\\nZLpx+FR8nrKsvsRf+q6+Ff/c0Uyfq/cHDi84wZmS8PBKa1Hqe1ix+6t1pvEx1eq4\\n/8jiRwKBgGOZzt5H5P0v3cFG9EUPXtvf2k81GmZjlDWu1gu5yWSYpqCfYr/K/1Md\\ndaQ/YCKTc12SYL7hZ2j+2/dGFXNXwknIyKNj76UxjUpJywWI5mUaXJZJDkLCRvxF\\nkk9nWvPorpjjjxaIVN+TkGgDd/60at/tI6HxzZitVyla5rB8hoPm\\n-----END RSA PRIVATE KEY-----\"\n\n            local sign = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1yczI1NiIsIm5iZiI6MTcyNzI3NDk4M30.Zvp8dXefvGXrKgeoaNsA3sbV_3fw1w6Te7a0B_UANzef7gGJwvlnD6c3-f4yAy7GPgNzP_H1-atcF-sgLHAYpUa14XKe22a9S_BJSoQszoZuqGgpnGcjSzDMK9JX3FLUtzOFMQR5C4_3d7_z0NlepNo2xdQ6IQj0SvS1jrNwydpA9L89N07id3EO739uNw339g78N9QHP-j8nWItfbjo31xefCWTHtcloGkfaJOhcr06qmSbrivBU1AuPA8T3ZVumqw6fcRJzrvQJdKEfVyP-IPUtUy8SM1yLqstaKojJtU3A2HKaeb4fycwHXxtl52xhzIshr_I3iUhX_ak-z7m0A\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 34: sign/verify use RS256 algorithm(private_key numbits = 2048,with extra payload)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this private_key and payload\n            -- private_key = \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEAv5LHjZ4FxQ9jk6eQGDRtoRwFVkLq+dUBebs97hrzirokVr2B\\n+RoxqdLfKAM+AsN2DadawZ2GqlCV9DL0/gz6nWSqTQpWbQ8c7CrF31EkIHUYRzZv\\nWy17K3WC9Odk/gM1FVd0HbZ2Rjuqj9ADeeqxnj9npDqKrMODOENy31SqZNerWZsd\\ngGkML5JYbX5hbI2L9LREvRU21fDgSfGL6Mw4Naxnnzcvll4yqwrBELSeDZEAt0+e\\n/p1dO7moxF+b1pFkh9vQl6zGvnvf8fOqn5ExtLHXVzgx752PHMwmuj9mO1ko6p8F\\nOM0JHDnooI+5rwK4j3I27Ho5nnatVWUaxK4U8wIDAQABAoIBAFsFQC73H8KrNyKW\\ngI4fit77U0XS8ZXWMKdH4XrZ71DAdDeKPtC+M05+1GxMbhAeEl8WXraTQ8J0G2s1\\nMtXqEMDrbUbBXKLghVtoTy91e/a369sZ7/qgN19Eq/30WzWdDIGhVZgwcy2Xd8hw\\nitZIPi/z7ChJcE35bsUytseJkJPsWeMJNq4mLbHqMSBQWze/vNvIeGYr2xfqXc6H\\nywGWGlk46RI28mOf7PecU0DxFoTBNcntZrpOwaIrTDsC7E6uNvhVbtsneseTlQuj\\nihS7DAH72Zx3CXc9+SL3b5QNRD1Rnp+gKM6itjW1yduOj2dS0p8YzcUYNtxnw5Gv\\nuLoHwuECgYEA58NhvnHn10YLBEMYxb30tDobdGfOjBSfih8K53+/SJhqF5mv4qZX\\nUfw3o5R+CkkrhbZ24yst7wqKFYZ+LfazOqljOPOrBsgIIry/sXBlcbGLCw9MYFfB\\nejKTt/xZjqLdDCcEbiSB0L2xNuyF/TZOu8V5Nu55LXKBqeW4yISQ5FkCgYEA05t1\\n2cq8gE1jMfGXQNFIpUDG2j4wJXAPqnJZSUF/BICa55mH/HYRKoP2uTSvAnqNrdGt\\nsnjnnMA7T+fGogB4STif1POWfj+BTKVa/qhUX9ytH6TeI4aqPXSZdTVEPRfR7bG1\\nIB/j2lyPkiNi2VijMx33xqxIaQUUsvxIT95GSisCgYAdaJFylQmSK3UiaVEvZlcy\\nt1zcfH+dDtDfueisT216TLzJmdrTq7/Qy2xT+Xe03mwDX4/ea5A8kN3MtXA1bOR5\\nQR0yENlW1vMRVVoNrfFxZ9H46UwLvZbzZo+P/RlwHAJolFrfjwpZ7ngaPBEUfFup\\nP/mNmt0Ng0YoxNmZuBiaoQKBgQCa2d4RRgpRvdAEYW41UbHetJuQZAfprarZKZrr\\nP9HKoq45I6Je/qurOCzZ9ZLItpRtic6Zl16u2AHPhKZYMQ3VT2mvdZ5AvwpI44zG\\nZLpx+FR8nrKsvsRf+q6+Ff/c0Uyfq/cHDi84wZmS8PBKa1Hqe1ix+6t1pvEx1eq4\\n/8jiRwKBgGOZzt5H5P0v3cFG9EUPXtvf2k81GmZjlDWu1gu5yWSYpqCfYr/K/1Md\\ndaQ/YCKTc12SYL7hZ2j+2/dGFXNXwknIyKNj76UxjUpJywWI5mUaXJZJDkLCRvxF\\nkk9nWvPorpjjjxaIVN+TkGgDd/60at/tI6HxzZitVyla5rB8hoPm\\n-----END RSA PRIVATE KEY-----\"\n            -- payload = {\"aaa\":\"11\",\"bb\":\"222\"}\n\n            local sign = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1yczI1NiIsIm5iZiI6MTcyNzI3NDk4M30.Zvp8dXefvGXrKgeoaNsA3sbV_3fw1w6Te7a0B_UANzef7gGJwvlnD6c3-f4yAy7GPgNzP_H1-atcF-sgLHAYpUa14XKe22a9S_BJSoQszoZuqGgpnGcjSzDMK9JX3FLUtzOFMQR5C4_3d7_z0NlepNo2xdQ6IQj0SvS1jrNwydpA9L89N07id3EO739uNw339g78N9QHP-j8nWItfbjo31xefCWTHtcloGkfaJOhcr06qmSbrivBU1AuPA8T3ZVumqw6fcRJzrvQJdKEfVyP-IPUtUy8SM1yLqstaKojJtU3A2HKaeb4fycwHXxtl52xhzIshr_I3iUhX_ak-z7m0A\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 35: JWT sign with the public key when using the RS256 algorithm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-rs256\",\n                            \"algorithm\": \"RS256\",\n                            \"public_key\": \"-----BEGIN RSA PRIVATE KEY-----\\nMIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv\\n0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7\\n+pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL\\nwQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF\\nIeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb\\n2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs\\nYvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG\\n-----END RSA PRIVATE KEY-----\"\n                        }\n                    }\n                }]]\n                )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 36: JWT sign and verify RS256\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 37: sanity(algorithm = HS512)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            local core = require(\"apisix.core\")\n            local conf = {key = \"123\", algorithm = \"HS512\", secret = \"my-secret-key\"}\n\n            local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- response_body_like eval\nqr/{\"algorithm\":\"HS512\",\"base64_secret\":false,\"exp\":86400,\"key\":\"123\",\"lifetime_grace_period\":0,\"secret\":\"my-secret-key\"}/\n\n\n\n=== TEST 38: add consumer with username and plugins use HS512 algorithm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-HS512\",\n                            \"algorithm\": \"HS512\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 39: JWT sign and verify use HS512 algorithm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 40: sign / verify (algorithm = HS512)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local sign = \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1IUzUxMiIsIm5iZiI6MTcyNzI3NDk4M30.emzmjIbFqkRAr55YW5YobdXDxYWiMuUNLPooE5G_bbme1ul19p1dKW7ESrlqvr4BPJRKThm4PnkNC4h9xSJpBQ\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 41: sign / verify (algorithm = HS512,with extra payload)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this payload\n            -- payload = {\"aaa\":\"11\",\"bb\":\"222\"}\n\n            local sign = \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhYWEiOiIxMSIsImJiIjoiMjIyIiwia2V5IjoidXNlci1rZXktSFM1MTIiLCJuYmYiOjE3MjcyNzQ5ODN9.s6E3-wNJypgJL71MxoyTTHBDeqdrGQddFjkhLlh3ZN6IZwgpFRlFT1_8suQg9dWUDHGQqgejULyLPhmBMIbw2A\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 42: wrong format of secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            local ok, err = plugin.check_schema({\n                key = \"123\",\n                secret = \"{^c0j4&]2!=J=\",\n                base64_secret = true,\n            }, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\nbase64_secret required but the secret is not in base64 format\n\n\n\n=== TEST 43: when the exp value is not set, make sure the default value(86400) works\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            local conf = {\n                key = \"exp-not-set\",\n                secret = \"my-secret-key\"\n            }\n            local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER)\n\n            if not ok then\n                ngx.say(err)\n            else\n                assert(conf.exp == 86400, \"exp should be 86400\")\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 44: RS256 without public key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"algorithm\": \"RS256\",\n                            \"key\": \"user-key\"\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body_like eval\nqr/failed to validate dependent schema for \\\\\"algorithm\\\\\"/\n\n\n\n=== TEST 45: RS256 without private key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"algorithm\": \"RS256\",\n                            \"key\": \"user-key\",\n                            \"public_key\": \"-----BEGIN RSA PRIVATE KEY-----\\nMIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv\\n0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7\\n+pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL\\nwQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF\\nIeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb\\n2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs\\nYvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG\\n-----END RSA PRIVATE KEY-----\"\n                        }\n                    }\n                }]]\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 46: add consumer with username and plugins with public_key, private_key(ES256)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-es256\",\n                            \"algorithm\": \"ES256\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\\n-----END PUBLIC KEY-----\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 47: JWT sign and verify use ES256 algorithm(private_key numbits = 512)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 48: sign/verify use ES256 algorithm(private_key numbits = 512)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- the jwt signature is encoded with this private_key\n            -- private_key = \"-----BEGIN PRIVATE KEY-----\\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2\\nOF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r\\n1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G\\n-----END PRIVATE KEY-----\"\n\n            local sign = \"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleS1lczI1NiIsIm5iZiI6MTcyNzI3NDk4M30.t-CZzJRSxIuVVjU3m8_zDtb7h9x2R2s3BJWmerh0hw-RMIklBqLJ3V9kYAWl7DIyXlp0jQCPDZ_M7mhr1Q3HPw\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n--- skip_eval\n1: $ENV{OPENSSL_FIPS} eq 'yes'\n\n\n\n=== TEST 49: add consumer missing public_key (algorithm=RS256)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-res256\",\n                            \"algorithm\": \"RS256\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin jwt-auth err: failed to validate dependent schema for \\\"algorithm\\\": value should match only one schema, but matches none\"}\n\n\n\n=== TEST 50: add consumer missing public_key (algorithm=ES256)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key-es256\",\n                            \"algorithm\": \"ES256\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin jwt-auth err: failed to validate dependent schema for \\\"algorithm\\\": value should match only one schema, but matches none\"}\n\n\n\n=== TEST 51: secret is required when algorithm is not RS256 or ES256\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.jwt-auth\")\n            -- default algorithm is HS256\n            local ok, err = plugin.check_schema({\n                key = \"123\",\n            }, core.schema.TYPE_CONSUMER)\n            assert(not ok, \"secret should be required when algorithm is HS256(default)\")\n\n            ok, err = plugin.check_schema({\n                key = \"123\",\n                algorithm = \"HS256\",\n            }, core.schema.TYPE_CONSUMER)\n            assert(not ok, \"secret should be required when algorithm is HS256\")\n\n            ok, err = plugin.check_schema({\n                key = \"123\",\n                algorithm = \"HS512\",\n            }, core.schema.TYPE_CONSUMER)\n            assert(not ok, \"secret should be required when algorithm is HS512\")\n\n            ok, err = plugin.check_schema({\n                key = \"123\",\n                algorithm = \"RS256\",\n                public_key = \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr\\n7noq/0ukiZqVQLSJPMOv0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQ==\\n-----END PUBLIC KEY-----\"\n            }, core.schema.TYPE_CONSUMER)\n            assert(ok, \"secret should not be required when algorithm is RS256\")\n\n            ok, err = plugin.check_schema({\n                key = \"123\",\n                algorithm = \"ES256\",\n                public_key = \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr\\n7noq/0ukiZqVQLSJPMOv0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQ==\\n-----END PUBLIC KEY-----\"\n            }, core.schema.TYPE_CONSUMER)\n            assert(ok, \"secret should not be required when algorithm is ES256\")\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/jwt-auth2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: enable jwt auth plugin using admin api with custom parameter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: verify (in header)\n--- request\nGET /hello\n--- more_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 4: verify (in cookie)\n--- request\nGET /hello\n--- more_headers\nCookie: jwt-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 5: verify (in query)\n--- request\nGET /hello?jwt-query=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 6: verify (in header without Bearer)\n--- request\nGET /hello\n--- more_headers\njwt-header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 7: verify (in header with bearer)\n--- request\nGET /hello\n--- more_headers\njwt-header: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n\n\n\n=== TEST 8: use lifetime_grace_period default value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- in order to modify the system_leeway in jwt-validators module\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\\n]] ..\n                                    [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\\n]] ..\n                                    [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\\n]] ..\n                                    [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\\n]] ..\n                                    [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\\n]] ..\n                                    [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\\n]] ..\n                                    [[zQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"Mysoft corp\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = [[Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A]]\n                    }\n                })\n            ngx.status = res.status\n            if res.status >= 300 then\n                ngx.status = res.status\n                ngx.say(res.body)\n                return\n            end\n\n            -- add consumer\n            local code, body, res_data = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"exp\": 1,\n                            \"algorithm\": \"HS256\",\n                            \"base64_secret\": false,\n                            \"secret\": \"test-jwt-secret\",\n                            \"key\": \"test-jwt-a\"\n                        }\n                    }\n                }]]\n             )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            -- add route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"query\": \"jwt\",\n                            \"header\": \"Mytoken\",\n                            \"cookie\": \"jwt\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local gen_token = require(\"lib.apisix.plugins.jwt-auth\").gen_token\n            local auth_conf = {\n                exp = 1,\n                algorithm = \"HS256\",\n                base64_secret = false,\n                secret = \"test-jwt-secret\",\n                key = \"test-jwt-a\"\n            }\n            local sign = gen_token(auth_conf)\n            if not sign then\n                ngx.status = 500\n                ngx.say(\"failed to gen_token\")\n            end\n\n            -- verify JWT token\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {headers={Mytoken=sign}})\n\n            -- the JWT has not expired, so it should be valid\n            if res.status >= 300 then\n                ngx.status = res.status\n                ngx.say(res.body)\n                return\n            end\n\n            -- after 1.1 seconds, the JWT should be expired, because the exp is only 1 second\n            ngx.sleep(1.1)\n            res, err = httpc:request_uri(uri, {headers={Mytoken=sign}})\n            ngx.status = res.status\n            ngx.print(res.body)\n        }\n    }\n--- error_code: 401\n--- response_body eval\nqr/failed to verify jwt/\n--- error_log eval\nqr/ailed to verify jwt: 'exp' claim expired at/\n\n\n\n=== TEST 9: lifetime_grace_period is 2 seconds\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- in order to modify the system_leeway in jwt-validators module\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\\n]] ..\n                                    [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\\n]] ..\n                                    [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\\n]] ..\n                                    [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\\n]] ..\n                                    [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\\n]] ..\n                                    [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\\n]] ..\n                                    [[zQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"Mysoft corp\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = [[Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A]]\n                    }\n                })\n            ngx.status = res.status\n            if res.status >= 300 then\n                ngx.status = res.status\n                ngx.say(res.body)\n                return\n            end\n\n            -- add consumer\n            local code, body, res_data = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"kerouac\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"exp\": 1,\n                            \"algorithm\": \"HS256\",\n                            \"base64_secret\": false,\n                            \"secret\": \"test-jwt-secret\",\n                            \"key\": \"test-jwt-a\",\n                            \"lifetime_grace_period\": 2\n                        }\n                    }\n                }]]\n             )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            -- add route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"query\": \"jwt\",\n                            \"header\": \"Mytoken\",\n                            \"cookie\": \"jwt\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            -- get JWT token\n            local gen_token = require(\"lib.apisix.plugins.jwt-auth\").gen_token\n            local auth_conf = {\n                exp = 1,\n                algorithm = \"HS256\",\n                base64_secret = false,\n                secret = \"test-jwt-secret\",\n                key = \"test-jwt-a\",\n                lifetime_grace_period = 2\n            }\n            local sign = gen_token(auth_conf)\n            if not sign then\n                ngx.status = 500\n                ngx.say(\"failed to gen_token\")\n            end\n\n            -- verify JWT token\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local httpc = http.new()\n\n            -- after 1.1 seconds, since lifetime_grace_period is 2 seconds,\n            -- so the JWT has not expired, it should be valid\n            ngx.sleep(1.1)\n            local res, err = httpc:request_uri(uri, {headers={Mytoken=sign}})\n            ngx.status = res.status\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello world\n"
  },
  {
    "path": "t/plugin/jwt-auth3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n        if (!$block->response_body) {\n            $block->set_value(\"response_body\", \"passed\\n\");\n        }\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 2: enable jwt auth plugin using admin api with custom parameter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 3: verify (in header) not hiding credentials\n--- request\nGET /echo\n--- more_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n\n\n\n=== TEST 4: verify (in cookie) not hiding credentials\n--- request\nGET /echo\n--- more_headers\nCookie: jwt-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_headers\nCookie: jwt-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n\n\n\n=== TEST 5: enable jwt auth plugin using admin api without hiding credentials\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin_proxy_rewrite_args\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 6: verify (in query) without hiding credentials\n--- request\nGET /plugin_proxy_rewrite_args?foo=bar&hello=world&jwt-query=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nuri: /plugin_proxy_rewrite_args\nfoo: bar\nhello: world\njwt-query: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n\n\n\n=== TEST 7: enable jwt auth plugin using admin api with hiding credentials\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 8: verify (in header) with hiding credentials\n--- request\nGET /echo\n--- more_headers\njwt-header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_headers\n!jwt-header\n\n\n\n=== TEST 9: enable jwt auth plugin using admin api with hiding credentials\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin_proxy_rewrite_args\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 10: verify (in query) with hiding credentials\n--- request\nGET /plugin_proxy_rewrite_args?foo=bar&hello=world&jwt-query=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nuri: /plugin_proxy_rewrite_args\nfoo: bar\nhello: world\n\n\n\n=== TEST 11: verify (in cookie) with hiding credentials\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test.com:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 12: verify (in cookie) with hiding credentials\n--- request\nGET /hello\n--- more_headers\nCookie: hello=world; jwt-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs; foo=bar\n--- response_body\nhello world\n\n\n\n=== TEST 13: delete exist consumers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- delete exist consumers\n            local code, body = t('/apisix/admin/consumers/jack', ngx.HTTP_DELETE)\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: data encryption for secret\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n            ngx.say(res.value.plugins[\"jwt-auth\"].secret)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/consumers/jack'))\n            ngx.say(res.body.node.value.plugins[\"jwt-auth\"].secret)\n        }\n    }\n--- response_body\nmy-secret-key\nIRWpPjbDq5BCgHyIllnOMA==\n\n\n\n=== TEST 15: set jwt-auth conf: secret uses secret ref\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"$secret://vault/test1/jack/secret\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jack secret=my-secret-key\n--- response_body\nSuccess! Data written to: kv/apisix/jack\n\n\n\n=== TEST 17: verify (in header) not hiding credentials\n--- request\nGET /echo\n--- more_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n\n\n\n=== TEST 18: store rsa key pairs and secret into vault from local filesystem\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/rsa1 secret=$3nsitiv3-c8d3 public_key=@t/certs/public.pem\n--- response_body\nSuccess! Data written to: kv/apisix/rsa1\n\n\n\n=== TEST 19: create consumer for RS256 algorithm with public key fetched from vault and public key in consumer schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- enable jwt auth plugin using admin api\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"john\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"rsa1\",\n                            \"algorithm\": \"RS256\",\n                            \"secret\": \"$secret://vault/test1/rsa1/secret\",\n                            \"public_key\": \"$secret://vault/test1/rsa1/public_key\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: set jwt-auth conf with the token in an env var: secret uses secret ref\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"$ENV://VAULT_TOKEN\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"$secret://vault/test1/jack/secret\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"header\": \"jwt-header\",\n                            \"query\": \"jwt-query\",\n                            \"cookie\": \"jwt-cookie\",\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: verify (in header) not hiding credentials\n--- request\nGET /echo\n--- more_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_headers\njwt-header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n"
  },
  {
    "path": "t/plugin/jwt-auth4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n        if (!$block->response_body) {\n            $block->set_value(\"response_body\", \"passed\\n\");\n        }\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: enable jwt auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\",\n                            \"key_claim_name\": \"iss\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: verify (in header)\n--- config\n    location /t {\n        content_by_lua_block {\n            local function gen_token(payload)\n                local buffer = require \"string.buffer\"\n                local openssl_mac = require \"resty.openssl.mac\"\n\n                local base64 = require \"ngx.base64\"\n                local base64_encode = base64.encode_base64url\n\n                local json = require(\"cjson\")\n\n                local function sign(data, key)\n                    return openssl_mac.new(key, \"HMAC\", nil, \"sha256\"):final(data)\n                end\n                local header = { typ = \"JWT\", alg = \"HS256\" }\n                local buf = buffer.new()\n\n                buf:put(base64_encode(json.encode(header))):put(\".\"):put(base64_encode(json.encode(payload)))\n\n                local ok, signature = pcall(sign, buf:tostring(), \"my-secret-key\")\n                if not ok then\n                    return nil, signature\n                end\n\n                buf:put(\".\"):put(base64_encode(signature))\n\n                return buf:get()\n            end\n\n            local payload = {\n                sub = \"1234567890\",\n                iss = \"user-key\",\n                exp = 9916239022\n            }\n\n            local token = gen_token(payload)\n\n            local http = require(\"resty.http\")\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local opt = {method = \"POST\", headers = {[\"Authorization\"] = \"Bearer \" .. token}}\n            local httpc = http.new()\n            local res = httpc:request_uri(uri, opt)\n            assert(res.status == 200)\n\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- more_headers\n--- response_body\nhello world\n\n\n\n=== TEST 4: ensure secret is non empty\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            -- prepare consumer with a custom key claim name\n            local csm_code, csm_body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"mike\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"custom-user-key\",\n                            \"secret\": \"\"\n                        }\n                    }\n                }]]\n            )\n            if csm_code == 200 then\n                ngx.status = 500\n                ngx.say(\"error\")\n                return\n            end\n            ngx.status = csm_code\n            ngx.say(csm_body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/\\\\\"secret\\\\\" validation failed: string too short, expected at least 1, got 0/\n\n\n\n=== TEST 5: ensure key is non empty\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            -- prepare consumer with a custom key claim name\n            local csm_code, csm_body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"mike\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"\",\n                            \"algorithm\": \"RS256\",\n                            \"public_key\": \"somekey\",\n                            \"private_key\": \"someprivkey\"\n                        }\n                    }\n                }]]\n            )\n            if csm_code == 200 then\n                ngx.status = 500\n                ngx.say(\"error\")\n                return\n            end\n            ngx.status = csm_code\n            ngx.say(csm_body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/\\\\\"key\\\\\" validation failed: string too short, expected at least 1, got 0/\n\n\n\n=== TEST 6: store_in_ctx disabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {},\n                        \"serverless-post-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\n                                \"return function(conf, ctx)\n                                if ctx.jwt_auth_payload then\n                                    ngx.status = 200\n                                    ngx.say(\\\"JWT found in ctx. Payload key: \\\" .. ctx.jwt_auth_payload.key)\n                                    return ngx.exit(200)\n                                else\n                                    ngx.status = 401\n                                    ngx.say(\\\"JWT not found in ctx.\\\")\n                                    return ngx.exit(401)\n                                end\n                                end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/jwt-auth-no-ctx\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: verify store_in_ctx disabled (header with bearer)\n--- request\nGET /jwt-auth-no-ctx\n--- more_headers\nAuthorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsIm5iZiI6MTcyNzI3NDk4M30.N6ebc4U5ms976pwKZ_iQ88w_uJKqUVNtTYZ_nXhRpWo\n--- error_code: 401\n--- response_body\nJWT not found in ctx.\n\n\n\n=== TEST 8: store_in_ctx enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"store_in_ctx\": true\n                        },\n                        \"serverless-post-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\n                                \"return function(conf, ctx)\n                                if ctx.jwt_auth_payload then\n                                    ngx.status = 200\n                                    ngx.say(\\\"JWT found in ctx. Payload key: \\\" .. ctx.jwt_auth_payload.key)\n                                    return ngx.exit(200)\n                                else\n                                    ngx.status = 401\n                                    ngx.say(\\\"JWT not found in ctx.\\\")\n                                    return ngx.exit(401)\n                                end\n                                end\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/jwt-auth-ctx\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: verify store_in_ctx enabled (header with bearer)\n--- request\nGET /jwt-auth-ctx\n--- more_headers\nAuthorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsIm5iZiI6MTcyNzI3NDk4M30.N6ebc4U5ms976pwKZ_iQ88w_uJKqUVNtTYZ_nXhRpWo\n--- error_code: 200\n--- response_body\nJWT found in ctx. Payload key: user-key\n\n\n\n=== TEST 10: Test Ed448 signature verification with lua-resty-openssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local pkey = require(\"resty.openssl.pkey\")\n            local base64 = require(\"ngx.base64\")\n\n            -- Test data for Ed448 verification\n            local header = \"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6InNjTy16dnUwWWRxOEVJSmxIb25CdWNYVmN2VjVnUm1oZ1BnZXFWSzZFdVkiLCJqa3UiOiJodHRwOi8vbG9jYWxob3N0OjkwNDIvb2lkYy9qd2tzIn0\"\n            local payload = \"eyJjbGllbnRfaWQiOiJhcHAtMDEifQ\"\n            local signature = \"kOC0UuRy3-eOSZiYWdH1izidwg1cWHsVAgvWgonOw7q1fEOXxD-AG3R1aj-heq-ENZn4hHWv3j8AabiBm6psCwrtf9C7ygDJmFT38Q2-EB3aVlbXSujXjwvWrw0o4yCZciHRVB2pNVkw36pjbQm2Lh8A\"\n            local jwk = '{\"alg\": \"EdDSA\", \"crv\": \"Ed448\", \"kty\": \"OKP\", \"use\": \"sig\", \"x\": \"XtrFWAUpoSzZd8OXZAP8LAUyfcGKVnAH7MNJZmqlmz-vz05pwP2q-8cOb14UmkY9nvbL1iBl1tUA\"}'\n\n            local raw_signature = base64.decode_base64url(signature)\n\n            -- Test JWK import\n            local ed448, err = pkey.new(jwk, { format = \"JWK\" })\n            if not ed448 then\n                ngx.say(\"FAIL: Failed to create pkey from JWK: \", err)\n                return\n            end\n\n            -- Test JWK export to verify consistency\n            local exported_jwk, export_err = ed448:tostring(\"public\", \"JWK\")\n            if not exported_jwk then\n                ngx.say(\"FAIL: Failed to export JWK: \", export_err)\n                return\n            end\n\n            -- Parse JWKs to compare\n            local original_parsed = core.json.decode(jwk)\n            local exported_parsed = core.json.decode(exported_jwk)\n\n            if not original_parsed or not exported_parsed then\n                ngx.say(\"FAIL: Failed to parse JWKs\")\n                return\n            end\n\n            -- Verify key parameters are consistent\n            local jwk_consistent = (original_parsed.crv == exported_parsed.crv) and\n                                  (original_parsed.kty == exported_parsed.kty)\n\n            if not jwk_consistent then\n                ngx.say(\"FAIL: JWK parameters inconsistent - Original crv: \", original_parsed.crv,\n                       \", Exported crv: \", exported_parsed.crv)\n                return\n            end\n\n            -- Test signature verification\n            local data_to_verify = header .. \".\" .. payload\n            local verify, verify_err = ed448:verify(raw_signature, data_to_verify)\n\n            if verify then\n                ngx.say(\"PASS: Ed448 signature verification successful\")\n                ngx.say(\"PASS: JWK import/export consistent\")\n            else\n                ngx.say(\"FAIL: Ed448 signature verification failed - Error: \", verify_err)\n                ngx.say(\"INFO: This may be expected with older lua-resty-openssl versions\")\n                ngx.say(\"INFO: Original JWK x: \", original_parsed.x)\n                ngx.say(\"INFO: Exported JWK x: \", exported_parsed.x)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body_like\n(PASS: Ed448 signature verification successful|FAIL: Ed448 signature verification failed)\n--- no_error_log\n[error]\n\n\n\n=== TEST 11: secret is required for HS algorithms\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"algorithm\": \"HS384\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.log(ngx.ERR, body)\n            ngx.say(\"failed\")\n\n        }\n    }\n--- error_code: 400\n--- response_body\nfailed\n--- error_log\nproperty \\\"secret\\\" is required when using HS based algorithms\n"
  },
  {
    "path": "t/plugin/kafka-logger-large-body.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    # fake server, only for test\n    server {\n        listen 1970;\n        location /large_resp {\n            content_by_lua_block {\n                local large_body = {\n                    \"h\", \"e\", \"l\", \"l\", \"o\"\n                }\n\n                local size_in_bytes = 1024 * 1024 -- 1mb\n                for i = 1, size_in_bytes do\n                    large_body[i+5] = \"l\"\n                end\n                large_body = table.concat(large_body, \"\")\n\n                ngx.say(large_body)\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: max_body_bytes is not an integer\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({\n                broker_list= {\n                    [\"127.0.0.1\"] = 9092\n                },\n                kafka_topic = \"test2\",\n                key = \"key1\",\n                timeout = 1,\n                batch_max_size = 1,\n                max_req_body_bytes = \"10\",\n                include_req_body = true,\n                meta_format = \"origin\"\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"max_req_body_bytes\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 2: set route(meta_format = origin, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"max_req_body_bytes\": 5,\n                                \"include_req_body\": true,\n                                \"meta_format\": \"origin\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: hit route(meta_format = origin, include_req_body = true)\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log\nsend data to kafka: GET /hello?ab=cd HTTP/1.1\nhost: localhost\ncontent-length: 6\nconnection: close\nabcde\n--- wait: 2\n\n\n\n=== TEST 4: set route(meta_format = default, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"max_req_body_bytes\": 5,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route(meta_format = default, include_req_body = true)\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log_like eval\nqr/\"body\": \"abcde\"/\n--- wait: 2\n\n\n\n=== TEST 6: set route(id: 1, meta_format = default, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"max_resp_body_bytes\": 5,\n                                \"include_resp_body\": true,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route(meta_format = default, include_resp_body = true)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello\"/\n--- wait: 2\n\n\n\n=== TEST 8: set route(id: 1, meta_format = origin, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"origin\",\n                                \"include_resp_body\": true,\n                                \"max_resp_body_bytes\": 5,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route(meta_format = origin, include_resp_body = true)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log\nsend data to kafka: POST /hello?name=qwerty HTTP/1.1\nhost: localhost\ncontent-length: 6\nconnection: close\n--- wait: 2\n\n\n\n=== TEST 10: set route(id: 1, meta_format = default, include_resp_body = true, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"include_req_body\": true,\n                                \"max_req_body_bytes\": 5,\n                                \"include_resp_body\": true,\n                                \"max_resp_body_bytes\": 5,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route(meta_format = default, include_resp_body = true, include_req_body = true)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcde\"/\n--- error_log_like\n*\"body\":\"hello\"\n--- wait: 2\n\n\n\n=== TEST 12: set route(id: 1, meta_format = default, include_resp_body = false, include_req_body = false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route(meta_format = default, include_resp_body = false, include_req_body = false)\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\nqr/send data to kafka: \\{.*\"body\":.*/\n--- wait: 2\n\n\n\n=== TEST 14: set route(large_body, meta_format = default, include_resp_body = true, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"include_req_body\": true,\n                                \"max_req_body_bytes\": 256,\n                                \"include_resp_body\": true,\n                                \"max_resp_body_bytes\": 256,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route(large_body, meta_format = default, include_resp_body = true, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t    = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 10 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello(l{251})\".*/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 16: set route(large_body, meta_format = default, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"include_resp_body\": true,\n                                \"max_resp_body_bytes\": 256,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 17: hit route(large_body, meta_format = default, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t    = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 10 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello(l{251})\".*/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 18: set route(large_body, meta_format = default, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"include_req_body\": true,\n                                \"max_req_body_bytes\": 256,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 19: hit route(large_body, meta_format = default, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t    = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 10 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/echo\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello(l{251})\".*/\n--- response_body eval\nqr/hello.*/\n\n\n\n=== TEST 20: set route(large_body, meta_format = default, include_resp_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"include_resp_body\": true,\n                                \"max_resp_body_bytes\": 256,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1970\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/large_resp\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 21: truncate upstream response body 1m to 256 bytes\n--- request\nGET /large_resp\n--- response_body eval\nqr/hello.*/\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello(l{251})\".*/\n\n\n\n=== TEST 22: set route(large_body, meta_format = default, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_format\": \"default\",\n                                \"include_req_body\": true,\n                                \"max_req_body_bytes\": 256,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 23: truncate upstream request body 1m to 256 bytes\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t    = require(\"lib.test_admin\")\n            local http = require(\"resty.http\")\n\n            local large_body = {\n                \"h\", \"e\", \"l\", \"l\", \"o\"\n            }\n\n            local size_in_bytes = 100 * 1024 -- 10kb\n            for i = 1, size_in_bytes do\n                large_body[i+5] = \"l\"\n            end\n            large_body = table.concat(large_body, \"\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = large_body,\n                }\n            )\n\n            if err then\n                ngx.say(err)\n            end\n\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello(l{251})\".*/\n\n\n\n=== TEST 24: set route(meta_format = default, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"max_req_body_bytes\": 5,\n                                \"include_req_body\": true,\n                                \"meta_format\": \"default\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: empty request body\n--- request\nGET /hello?ab=cd\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka/\n--- wait: 2\n\n\n\n=== TEST 26: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/kafka-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"request_body\": \"$request_body\"\n                    }\n                }]]\n                )\n            if code >=300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: set route(meta_format = default, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"max_req_body_bytes\": 5\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 28: hit route(meta_format = default, include_req_body = true)\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log_like eval\nqr/\"request_body\": \"abcde\"/\n"
  },
  {
    "path": "t/plugin/kafka-logger-log-format.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/kafka-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"@timestamp\": \"$time_iso8601\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1), batch_max_size=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: hit route and report kafka logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- error_log eval\nqr/send data to kafka: \\{.*\"host\":\"localhost\"/\n\n\n\n=== TEST 4: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"log_format\": {\n                                    \"x_ip\": \"$remote_addr\"\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route and report kafka logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- error_log eval\nqr/send data to kafka: \\{.*\"x_ip\":\"127.0.0.1\".*\\}/\n"
  },
  {
    "path": "t/plugin/kafka-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({\n                 kafka_topic = \"test\",\n                 key = \"key1\",\n                 broker_list = {\n                    [\"127.0.0.1\"] = 3\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: missing broker list\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({kafka_topic = \"test\", key= \"key1\"})\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nvalue should match only one schema, but matches none\ndone\n\n\n\n=== TEST 3: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({\n                broker_list = {\n                    [\"127.0.0.1\"] = 3000\n                },\n                timeout = \"10\",\n                kafka_topic =\"test\",\n                key= \"key1\"\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"timeout\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 4: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: access\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 2\n--- ignore_error_log\n\n\n\n=== TEST 6: error log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                             \"kafka-logger\": {\n                                    \"broker_list\" :\n                                      {\n                                        \"127.0.0.1\":9092,\n                                        \"127.0.0.1\":9093\n                                      },\n                                    \"kafka_topic\" : \"test2\",\n                                    \"producer_type\": \"sync\",\n                                    \"key\" : \"key1\",\n                                    \"batch_max_size\": 1\n                             }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n        }\n    }\n--- error_log\nfailed to send data to Kafka topic\n[error]\n--- wait: 1\n\n\n\n=== TEST 7: set route(meta_format = origin, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true,\n                                \"meta_format\": \"origin\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route, report log to kafka\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log\nsend data to kafka: GET /hello?ab=cd HTTP/1.1\nhost: localhost\ncontent-length: 6\nconnection: close\n\nabcdef\n--- wait: 2\n\n\n\n=== TEST 9: set route(meta_format = origin, include_req_body = false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false,\n                                \"meta_format\": \"origin\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route, report log to kafka\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log\nsend data to kafka: GET /hello?ab=cd HTTP/1.1\nhost: localhost\ncontent-length: 6\nconnection: close\n--- wait: 2\n\n\n\n=== TEST 11: set route(meta_format = default)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route, report log to kafka\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log_like eval\nqr/send data to kafka: \\{.*\"upstream\":\"127.0.0.1:1980\"/\n--- wait: 2\n\n\n\n=== TEST 13: set route(id: 1), missing key field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: access, test key field is optional\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 2\n\n\n\n=== TEST 15: set route(meta_format = default), missing key field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route, report log to kafka\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n--- error_log_like eval\nqr/send data to kafka: \\{.*\"upstream\":\"127.0.0.1:1980\"/\n--- wait: 2\n\n\n\n=== TEST 17: use the topic with 3 partitions\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\": 9092\n                                },\n                                \"kafka_topic\" : \"test3\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: report log to kafka by different partitions\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\": 9092\n                                },\n                                \"kafka_topic\" : \"test3\",\n                                \"producer_type\": \"sync\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n        }\n    }\n--- timeout: 5s\n--- ignore_response\n--- error_log eval\n[qr/partition_id: 1/,\nqr/partition_id: 0/,\nqr/partition_id: 2/]\n\n\n\n=== TEST 19: report log to kafka by different partitions in async mode\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\": 9092\n                                },\n                                \"kafka_topic\" : \"test3\",\n                                \"producer_type\": \"async\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n        }\n    }\n--- timeout: 5s\n--- ignore_response\n--- error_log eval\n[qr/partition_id: 1/,\nqr/partition_id: 0/,\nqr/partition_id: 2/]\n\n\n\n=== TEST 20: set route with incorrect sasl_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\":{\n                        \"kafka-logger\":{\n                            \"brokers\":[\n                            {\n                                \"host\":\"127.0.0.1\",\n                                \"port\":19094,\n                                \"sasl_config\":{\n                                    \"mechanism\":\"PLAIN\",\n                                    \"user\":\"admin\",\n                                    \"password\":\"admin-secret2233\"\n                            }\n                        }],\n                            \"kafka_topic\":\"test2\",\n                            \"key\":\"key1\",\n                            \"timeout\":1,\n                            \"batch_max_size\":1\n                        }\n                    },\n                    \"upstream\":{\n                        \"nodes\":{\n                            \"127.0.0.1:1980\":1\n                        },\n                        \"type\":\"roundrobin\"\n                    },\n                    \"uri\":\"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: hit route, failed to send data to kafka\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nfailed to do PLAIN auth with 127.0.0.1:19094: Authentication failed: Invalid username or password\n--- wait: 2\n\n\n\n=== TEST 22: set route with correct sasl_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\":{\n                        \"kafka-logger\":{\n                            \"brokers\":[\n                            {\n                                \"host\":\"127.0.0.1\",\n                                \"port\":19094,\n                                \"sasl_config\":{\n                                    \"mechanism\":\"PLAIN\",\n                                    \"user\":\"admin\",\n                                    \"password\":\"admin-secret\"\n                            }\n                        }],\n                            \"kafka_topic\":\"test4\",\n                            \"producer_type\":\"sync\",\n                            \"key\":\"key1\",\n                            \"timeout\":1,\n                            \"batch_max_size\":1,\n                            \"include_req_body\": true\n                        }\n                    },\n                    \"upstream\":{\n                        \"nodes\":{\n                            \"127.0.0.1:1980\":1\n                        },\n                        \"type\":\"roundrobin\"\n                    },\n                    \"uri\":\"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: hit route, send data to kafka successfully\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- no_error_log\n[error]\n--- wait: 2\n\n\n\n=== TEST 24: set route(batch_max_size = 2), check if prometheus is initialized properly\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: access\n--- extra_yaml_config\nplugins:\n  - kafka-logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 2\n\n\n\n=== TEST 26: create a service with kafka-logger and three routes bound to it\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" : {\n                                    \"127.0.0.1\":9092\n                                },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true,\n                                \"meta_format\": \"origin\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.say(\"create service failed\")\n                return\n            end\n            for i = 1, 3 do\n                local code, body = t('/apisix/admin/routes/' .. i,\n                     ngx.HTTP_PUT,\n                     string.format([[{\n                        \"uri\": \"/hello%d\",\n                        \"service_id\": \"1\"\n                     }]], i)\n                    )\n                if code >= 300 then\n                    ngx.say(\"create route failed\")\n                    return\n                end\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 27: hit three routes, should create batch processor only once\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            for i = 1, 3 do\n                local resp = httpc:request_uri(\"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\" .. i)\n                if not resp then\n                    ngx.say(\"failed to request test server\")\n                    return\n                end\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n--- grep_error_log eval\nqr/creating new batch processor with config.*/\n--- grep_error_log_out eval\nqr/creating new batch processor with config.*/\n"
  },
  {
    "path": "t/plugin/kafka-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: required_acks, matches none of the enum values\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({\n                broker_list = {\n                    [\"127.0.0.1\"] = 3000\n                },\n                required_acks = 10,\n                kafka_topic =\"test\",\n                key= \"key1\"\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"required_acks\" validation failed: matches none of the enum values\ndone\n\n\n\n=== TEST 2: report log to kafka, with required_acks(1, -1)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"kafka-logger\"] = {\n                            broker_list = {\n                                [\"127.0.0.1\"] = 9092\n                            },\n                            kafka_topic = \"test2\",\n                            producer_type = \"sync\",\n                            timeout = 1,\n                            batch_max_size = 1,\n                            required_acks = 1,\n                            meta_format = \"origin\",\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n            {\n                input = {\n                    plugins = {\n                        [\"kafka-logger\"] = {\n                            broker_list = {\n                                [\"127.0.0.1\"] = 9092\n                            },\n                            kafka_topic = \"test2\",\n                            producer_type = \"sync\",\n                            timeout = 1,\n                            batch_max_size = 1,\n                            required_acks = -1,\n                            meta_format = \"origin\",\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n        local err_count = 0\n        for i in ipairs(data) do\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[i].input)\n\n            if code >= 300 then\n                err_count = err_count + 1\n            end\n            ngx.print(body)\n\n            t('/hello', ngx.HTTP_GET)\n        end\n\n        assert(err_count == 0)\n    }\n}\n--- error_log\nsend data to kafka: GET /hello\nsend data to kafka: GET /hello\nsend data to kafka: GET /hello\n\n\n\n=== TEST 3: update the broker_list and cluster_name, generate different kafka producers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            ngx.sleep(0.5)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"kafka-logger\": {\n                            \"broker_list\" : {\n                                \"127.0.0.1\": 9092\n                            },\n                            \"kafka_topic\" : \"test2\",\n                            \"timeout\" : 1,\n                            \"batch_max_size\": 1,\n                            \"include_req_body\": false,\n                            \"cluster_name\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n\n            code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"kafka-logger\": {\n                            \"broker_list\" : {\n                                \"127.0.0.1\": 19092\n                            },\n                            \"kafka_topic\" : \"test4\",\n                            \"timeout\" : 1,\n                            \"batch_max_size\": 1,\n                            \"include_req_body\": false,\n                            \"cluster_name\": 2\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n\n            ngx.sleep(2)\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 10\n--- response\npassed\n--- wait: 5\n--- error_log\nphase_func(): kafka cluster name 1, broker_list[1] port 9092\nphase_func(): kafka cluster name 2, broker_list[1] port 19092\n--- no_error_log eval\nqr/not found topic/\n\n\n\n=== TEST 4: use the topic that does not exist on kafka(even if kafka allows auto create topics, first time push messages to kafka would got this error)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"kafka-logger\": {\n                            \"broker_list\" : {\n                                \"127.0.0.1\": 9092\n                            },\n                            \"kafka_topic\" : \"undefined_topic\",\n                            \"timeout\" : 1,\n                            \"batch_max_size\": 1,\n                            \"include_req_body\": false\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n\n            ngx.sleep(2)\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 5\n--- response\npassed\n--- error_log eval\nqr/not found topic, retryable: true, topic: undefined_topic, partition_id: -1/\n\n\n\n=== TEST 5: check broker_list via schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    input = {\n                        broker_list = {},\n                        kafka_topic = \"test\",\n                        key= \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        broker_list = {\n                            [\"127.0.0.1\"] = \"9092\"\n                        },\n                        kafka_topic = \"test\",\n                        key= \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        broker_list = {\n                            [\"127.0.0.1\"] = 0\n                        },\n                        kafka_topic = \"test\",\n                        key= \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        broker_list = {\n                            [\"127.0.0.1\"] = 65536\n                        },\n                        kafka_topic = \"test\",\n                        key= \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                            }\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                port = 9092,\n                            }\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = \"9093\",\n                            },\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = 0,\n                            },\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = 65536,\n                            },\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = 9093,\n                                sasl_config = {\n                                    mechanism = \"INVALID\",\n                                    user = \"admin\",\n                                    password = \"admin-secret\",\n                                },\n                            },\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = 9093,\n                                sasl_config = {\n                                    user = \"admin\",\n                                },\n                            },\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n                {\n                    input = {\n                        brokers = {\n                            {\n                                host = \"127.0.0.1\",\n                                port = 9093,\n                                sasl_config = {\n                                    password = \"admin-secret\",\n                                },\n                            },\n                        },\n                        kafka_topic = \"test\",\n                        key = \"key1\",\n                    },\n                },\n            }\n\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n\n            local err_count = 0\n            for i in ipairs(data) do\n                local ok, err = plugin.check_schema(data[i].input)\n                if not ok then\n                    err_count = err_count + 1\n                    ngx.say(err)\n                end\n            end\n\n            assert(err_count == #data)\n        }\n    }\n--- response_body\nproperty \"broker_list\" validation failed: expect object to have at least 1 properties\nproperty \"broker_list\" validation failed: failed to validate 127.0.0.1 (matching \".*\"): wrong type: expected integer, got string\nproperty \"broker_list\" validation failed: failed to validate 127.0.0.1 (matching \".*\"): expected 0 to be at least 1\nproperty \"broker_list\" validation failed: failed to validate 127.0.0.1 (matching \".*\"): expected 65536 to be at most 65535\nproperty \"brokers\" validation failed: expect array to have at least 1 items\nproperty \"brokers\" validation failed: failed to validate item 1: property \"port\" is required\nproperty \"brokers\" validation failed: failed to validate item 1: property \"host\" is required\nproperty \"brokers\" validation failed: failed to validate item 1: property \"port\" validation failed: wrong type: expected integer, got string\nproperty \"brokers\" validation failed: failed to validate item 1: property \"port\" validation failed: expected 0 to be at least 1\nproperty \"brokers\" validation failed: failed to validate item 1: property \"port\" validation failed: expected 65536 to be at most 65535\nproperty \"brokers\" validation failed: failed to validate item 1: property \"sasl_config\" validation failed: property \"mechanism\" validation failed: matches none of the enum values\nproperty \"brokers\" validation failed: failed to validate item 1: property \"sasl_config\" validation failed: property \"password\" is required\nproperty \"brokers\" validation failed: failed to validate item 1: property \"sasl_config\" validation failed: property \"user\" is required\n\n\n\n=== TEST 6: kafka brokers info in log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                             \"kafka-logger\": {\n                                    \"broker_list\" :\n                                      {\n                                        \"127.0.0.127\":9092\n                                      },\n                                    \"kafka_topic\" : \"test2\",\n                                    \"producer_type\": \"sync\",\n                                    \"key\" : \"key1\",\n                                    \"batch_max_size\": 1,\n                                    \"cluster_name\": 10\n                             }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n        }\n    }\n--- error_log_like eval\nqr/create new kafka producer instance, brokers: \\[\\{\"port\":9092,\"host\":\"127.0.0.127\"}]/\nqr/failed to send data to Kafka topic: .*, brokers: \\{\"127.0.0.127\":9092}/\n\n\n\n=== TEST 7: set route(id: 1,include_req_body = true,include_req_body_expr = array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_req_body\": true,\n                                \"include_req_body_expr\": [\n                                    [\n                                      \"arg_name\",\n                                      \"==\",\n                                      \"qwerty\"\n                                    ]\n                                ],\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route, expr eval success\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n\n\n\n=== TEST 9: hit route,expr eval fail\n--- request\nPOST /hello?name=zcxv\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n\n\n\n=== TEST 10: check log schema(include_req_body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({\n                 kafka_topic = \"test\",\n                 key = \"key1\",\n                 broker_list = {\n                    [\"127.0.0.1\"] = 3\n                 },\n                 include_req_body = true,\n                 include_req_body_expr = {\n                     {\"bar\", \"<>\", \"foo\"}\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nfailed to validate the 'include_req_body_expr' expression: invalid operator '<>'\ndone\n\n\n\n=== TEST 11: check log schema(include_resp_body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.kafka-logger\")\n            local ok, err = plugin.check_schema({\n                 kafka_topic = \"test\",\n                 key = \"key1\",\n                 broker_list = {\n                    [\"127.0.0.1\"] = 3\n                 },\n                 include_resp_body = true,\n                 include_resp_body_expr = {\n                     {\"bar\", \"<!>\", \"foo\"}\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nfailed to validate the 'include_resp_body_expr' expression: invalid operator '<!>'\ndone\n\n\n\n=== TEST 12: set route include_resp_body - gzip\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_resp_body\": true,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:11451\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/gzip_hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 13: hit\n--- http_config\nserver {\n    listen 11451;\n    gzip on;\n    gzip_types *;\n    gzip_min_length 1;\n    location /gzip_hello {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local s = \"gzip hello world\"\n            ngx.header['Content-Length'] = #s + 1\n            ngx.say(s)\n        }\n    }\n}\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"gzip hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 14: set route include_resp_body - brotli\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_resp_body\": true,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:11452\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/brotli_hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 15: hit\n--- http_config\nserver {\n    listen 11452;\n    location /brotli_hello {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local s = \"brotli hello world\"\n            ngx.header['Content-Length'] = #s + 1\n            ngx.say(s)\n        }\n        header_filter_by_lua_block {\n            local conf = {\n                comp_level = 6,\n                http_version = 1.1,\n                lgblock = 0,\n                lgwin = 19,\n                min_length = 1,\n                mode = 0,\n                types = \"*\",\n            }\n            local brotli = require(\"apisix.plugins.brotli\")\n            brotli.header_filter(conf, ngx.ctx)\n        }\n        body_filter_by_lua_block {\n            local conf = {\n                comp_level = 6,\n                http_version = 1.1,\n                lgblock = 0,\n                lgwin = 19,\n                min_length = 1,\n                mode = 0,\n                types = \"*\",\n            }\n            local brotli = require(\"apisix.plugins.brotli\")\n            brotli.body_filter(conf, ngx.ctx)\n        }\n    }\n}\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"brotli hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 16: set route(id: 1,include_resp_body = true,include_resp_body_expr = array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"broker_list\" :\n                                  {\n                                    \"127.0.0.1\":9092\n                                  },\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_resp_body\": true,\n                                \"include_resp_body_expr\": [\n                                    [\n                                      \"arg_name\",\n                                      \"==\",\n                                      \"qwerty\"\n                                    ]\n                                ],\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 17: hit route, expr eval success\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 18: hit route,expr eval fail\n--- request\nPOST /hello?name=zcxv\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 19: multi level nested expr conditions\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local kafka = {\n                 kafka_topic = \"test2\",\n                 key = \"key1\",\n                 batch_max_size = 1,\n                 broker_list = {\n                    [\"127.0.0.1\"] = 9092\n                 },\n                 timeout = 3,\n                 include_req_body = true,\n                 include_req_body_expr = {\n                    {\"request_length\", \"<\", 1054},\n                    {\"arg_name\", \"in\", {\"qwerty\", \"asdfgh\"}}\n                 },\n                 include_resp_body = true,\n                 include_resp_body_expr = {\n                    {\"http_content_length\", \"<\", 1054},\n                    {\"arg_name\", \"in\", {\"qwerty\", \"zxcvbn\"}}\n                 }\n            }\n            local plugins = {}\n            plugins[\"kafka-logger\"] = kafka\n            local data = {\n                plugins = plugins\n            }\n            data.upstream = {\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1980\"] = 1\n                }\n            }\n            data.uri = \"/hello\"\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 core.json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: hit route, req_body_expr and resp_body_expr both eval success\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\n[qr/send data to kafka: \\{.*\"body\":\"abcdef\"/,\nqr/send data to kafka: \\{.*\"body\":\"hello world\\\\n\"/]\n--- wait: 2\n\n\n\n=== TEST 21: hit route, req_body_expr eval success, resp_body_expr both eval failed\n--- request\nPOST /hello?name=asdfgh\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- no_error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 22: hit route, req_body_expr eval failed, resp_body_expr both eval success\n--- request\nPOST /hello?name=zxcvbn\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"hello world\\\\n\"/\n--- no_error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n\n\n\n=== TEST 23: hit route, req_body_expr eval success, resp_body_expr both eval failed\n--- request\nPOST /hello?name=xxxxxx\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\n[qr/send data to kafka: \\{.*\"body\":\"abcdef\"/,\nqr/send data to kafka: \\{.*\"body\":\"hello world\\\\n\"/]\n--- wait: 2\n\n\n\n=== TEST 24: update route(id: 1,include_req_body = true,include_req_body_expr = array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"brokers\" :\n                                  [{\n                                    \"host\":\"127.0.0.1\",\n                                    \"port\": 9092\n                                  }],\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_req_body\": true,\n                                \"include_req_body_expr\": [\n                                    [\n                                      \"arg_name\",\n                                      \"==\",\n                                      \"qwerty\"\n                                    ]\n                                ],\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 25: hit route, expr eval success\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n\n\n\n=== TEST 26: setup route with meta_refresh_interval\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"kafka-logger\": {\n                                \"brokers\" :\n                                  [{\n                                    \"host\":\"127.0.0.1\",\n                                    \"port\": 9092\n                                  }],\n                                \"kafka_topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"meta_refresh_interval\": 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 27: hit route, send data to kafka successfully\n--- request\nPOST /hello\nabcdef\n--- response_body\nhello world\n--- no_error_log\n[error]\n--- error_log eval\nqr/send data to kafka: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n"
  },
  {
    "path": "t/plugin/kafka-logger3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: should drop entries\n--- extra_yaml_config\nplugins:\n  - kafka-logger\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"kafka-logger\"] = {\n                            broker_list = {\n                                [\"127.0.0.1\"] = 1234\n                            },\n                            kafka_topic = \"test2\",\n                            producer_type = \"sync\",\n                            timeout = 1,\n                            batch_max_size = 1,\n                            required_acks = 1,\n                            meta_format = \"origin\",\n                            max_retry_count = 10,\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/kafka-logger', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/kafka-logger4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route with correct sasl_config - SCRAM-SHA-256\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\":{\n                        \"kafka-logger\":{\n                            \"brokers\":[\n                            {\n                                \"host\":\"127.0.0.1\",\n                                \"port\":29094,\n                                \"sasl_config\":{\n                                    \"mechanism\":\"SCRAM-SHA-256\",\n                                    \"user\":\"admin\",\n                                    \"password\":\"admin-secret\"\n                            }\n                        }],\n                            \"kafka_topic\":\"test-scram-256\",\n                            \"producer_type\":\"async\",\n                            \"key\":\"key1\",\n                            \"timeout\":1,\n                            \"batch_max_size\":1,\n                            \"include_req_body\": true\n                        }\n                    },\n                    \"upstream\":{\n                        \"nodes\":{\n                            \"127.0.0.1:1980\":1\n                        },\n                        \"type\":\"roundrobin\"\n                    },\n                    \"uri\":\"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route, send data to kafka successfully\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"route_id\":\"1\"/\n--- no_error_log\n[error]\n--- wait: 2\n\n\n\n=== TEST 3: set route with incorrect password - SCRAM-SHA-256\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\":{\n                        \"kafka-logger\":{\n                            \"brokers\":[\n                            {\n                                \"host\":\"127.0.0.1\",\n                                \"port\":29094,\n                                \"sasl_config\":{\n                                    \"mechanism\":\"SCRAM-SHA-256\",\n                                    \"user\":\"admin\",\n                                    \"password\":\"admin-secrets\"\n                            }\n                        }],\n                            \"kafka_topic\":\"test-scram-256\",\n                            \"producer_type\":\"async\",\n                            \"key\":\"key1\",\n                            \"timeout\":1,\n                            \"batch_max_size\":1,\n                            \"include_req_body\": true\n                        }\n                    },\n                    \"upstream\":{\n                        \"nodes\":{\n                            \"127.0.0.1:1980\":1\n                        },\n                        \"type\":\"roundrobin\"\n                    },\n                    \"uri\":\"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route, send data to kafka unsuccessfully\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log\nAuthentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256\n--- wait: 2\n\n\n\n=== TEST 5: set route with correct sasl_config - SCRAM-SHA-512\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\":{\n                        \"kafka-logger\":{\n                            \"brokers\":[\n                            {\n                                \"host\":\"127.0.0.1\",\n                                \"port\":29094,\n                                \"sasl_config\":{\n                                    \"mechanism\":\"SCRAM-SHA-512\",\n                                    \"user\":\"admin\",\n                                    \"password\":\"admin-secret\"\n                            }\n                        }],\n                            \"kafka_topic\":\"test-scram-512\",\n                            \"producer_type\":\"async\",\n                            \"key\":\"key1\",\n                            \"timeout\":1,\n                            \"batch_max_size\":1,\n                            \"include_req_body\": true\n                        }\n                    },\n                    \"upstream\":{\n                        \"nodes\":{\n                            \"127.0.0.1:1980\":1\n                        },\n                        \"type\":\"roundrobin\"\n                    },\n                    \"uri\":\"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route, send data to kafka successfully\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to kafka: \\{.*\"route_id\":\"1\"/\n--- no_error_log\n[error]\n--- wait: 2\n\n\n\n=== TEST 7: set route with incorrect password - SCRAM-SHA-512\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\":{\n                        \"kafka-logger\":{\n                            \"brokers\":[\n                            {\n                                \"host\":\"127.0.0.1\",\n                                \"port\":29094,\n                                \"sasl_config\":{\n                                    \"mechanism\":\"SCRAM-SHA-512\",\n                                    \"user\":\"admin\",\n                                    \"password\":\"admin-secrets\"\n                            }\n                        }],\n                            \"kafka_topic\":\"test-scram-256\",\n                            \"producer_type\":\"async\",\n                            \"key\":\"key1\",\n                            \"timeout\":1,\n                            \"batch_max_size\":1,\n                            \"include_req_body\": true\n                        }\n                    },\n                    \"upstream\":{\n                        \"nodes\":{\n                            \"127.0.0.1:1980\":1\n                        },\n                        \"type\":\"roundrobin\"\n                    },\n                    \"uri\":\"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route, send data to kafka unsuccessfully\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log\nAuthentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-512\n--- wait: 2\n"
  },
  {
    "path": "t/plugin/kafka-proxy.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {},\n                {sasl = {username = \"user\", password = \"pwd\"}},\n                {sasl = {username = \"user\"}},\n                {sasl = {username = \"user\", password = 1234}},\n            }\n            local plugin = require(\"apisix.plugins.kafka-proxy\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\ndone\nproperty \"sasl\" validation failed: property \"password\" is required\nproperty \"sasl\" validation failed: property \"password\" validation failed: wrong type: expected string, got number\n\n\n\n=== TEST 2: data encryption for sasl.password\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"kafka-proxy\": {\n                            \"sasl\": {\n                                \"username\": \"admin\",\n                                \"password\": \"admin-secret\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"kafka-proxy\"].sasl.password)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"kafka-proxy\"].sasl.password)\n        }\n    }\n--- response_body\nadmin-secret\ny4Z3aqo51xrt3f9UziNUrg==\n"
  },
  {
    "path": "t/plugin/key-auth-anonymous-consumer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\n\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer jack and anonymous\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 4,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"anonymous\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\npassed\n\n\n\n=== TEST 2: add key auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"anonymous_consumer\": \"anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: normal consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for i = 1, 5, 1 do\n                local code, body = t('/hello',\n                    ngx.HTTP_GET,\n                    nil,\n                    nil,\n                    {\n                        apikey = \"auth-one\"\n                    }\n                )\n\n                if code >= 300 then\n                    ngx.say(\"failed\" .. code)\n                    return\n                end\n                ngx.say(body .. i)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed1\npassed2\npassed3\npassed4\nfailed503\n\n\n\n=== TEST 4: request without key-auth header will be from anonymous consumer and it will pass\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: request without key-auth header will be from anonymous consumer and different rate limit will apply\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 6: add key auth plugin with non-existent anonymous_consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"anonymous_consumer\": \"not-found-anonymous\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: anonymous-consumer configured in the route should not be found\n--- request\nGET /hello\n--- error_code: 401\n--- error_log\nfailed to get anonymous consumer not-found-anonymous\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n"
  },
  {
    "path": "t/plugin/key-auth-realm.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity, default realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: verify default realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: apikey realm=\"key\"\n\n\n\n=== TEST 3: set custom realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"realm\": \"my-custom-realm\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: verify custom realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: apikey realm=\"my-custom-realm\"\n\n\n\n=== TEST 5: set anonymous consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"anonymous_consumer\": \"missing-consumer\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: verify anonymous consumer missing returns realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: apikey realm=\"key\"\n--- error_log\nfailed to get anonymous consumer\n"
  },
  {
    "path": "t/plugin/key-auth-upstream-domain-node.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{CUSTOM_DNS_SERVER} = \"127.0.0.1:1053\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set service and enabled plugin `key-auth`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"desc\": \"new service\"\n                }]]\n            )\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: create route with plugin `limit-count`(upstream node contains domain)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 5,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"ttl.test.local:1980\": 1\n                        },\n                        \"pass_host\": \"node\",\n                        \"type\": \"roundrobin\"\n                    },\n                    \"service_id\": 1,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route 3 times\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local headers = {\n            [\"User-Agent\"] = \"curl/7.68.0\",\n            [\"apikey\"] = \"auth-one\",\n        }\n\n        for i = 1, 3 do\n            local code, body = t.test('/index.html',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                headers\n            )\n            ngx.say(\"return: \", code)\n            ngx.sleep(1)\n        end\n    }\n}\n--- request\nGET /t\n--- response_body\nreturn: 404\nreturn: 503\nreturn: 503\n\n\n\n=== TEST 5: set upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"ttl.test.local:1980\": 1\n                    },\n                    \"pass_host\": \"node\",\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: create route with plugin `limit-count`, and bind upstream via id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 5,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream_id\": 1,\n                    \"service_id\": 1,\n                    \"uri\": \"/index.html\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route 3 times\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local headers = {\n            [\"User-Agent\"] = \"curl/7.68.0\",\n            [\"apikey\"] = \"auth-one\",\n        }\n\n        for i = 1, 3 do\n            local code, body = t.test('/index.html',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                headers\n            )\n            ngx.say(\"return: \", code)\n        end\n    }\n}\n--- request\nGET /t\n--- response_body\nreturn: 404\nreturn: 503\nreturn: 503\n"
  },
  {
    "path": "t/plugin/key-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $user_yaml_config = <<_EOC_;\napisix:\n  data_encryption:\n    enable_encrypt_fields: false\n_EOC_\n    $block->set_value(\"yaml_config\", $user_yaml_config);\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.key-auth\")\n            local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\ntest-key\n\n\n\n=== TEST 2: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.key-auth\")\n            local ok, err = plugin.check_schema({key = 123}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"key\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 3: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 4: add key auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 5: valid consumer\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\n--- response_body\nhello world\n--- no_error_log\nauth-one\n\n\n\n=== TEST 6: invalid consumer\n--- request\nGET /hello\n--- more_headers\napikey: 123\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid API key in request\"}\n--- no_error_log\nauth-one\n\n\n\n=== TEST 7: not found apikey header\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing API key in request\"}\n\n\n\n=== TEST 8: valid consumer\n--- config\n    location /add_more_consumer {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local username = \"\"\n            local key = \"\"\n            local code, body\n            for i = 1, 20 do\n                username = \"user_\" .. tostring(i)\n                key = \"auth-\" .. tostring(i)\n                code, body = t('/apisix/admin/consumers',\n                    ngx.HTTP_PUT,\n                    string.format('{\"username\":\"%s\",\"plugins\":{\"key-auth\":{\"key\":\"%s\"}}}', username, key),\n                    string.format('{\"value\":{\"username\":\"%s\",\"plugins\":{\"key-auth\":{\"key\":\"%s\"}}}}', username, key)\n                    )\n            end\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /add_more_consumer\n--- pipelined_requests eval\n[\"GET /add_more_consumer\", \"GET /hello\"]\n--- more_headers\napikey: auth-13\n--- response_body eval\n[\"passed\\n\", \"hello world\\n\"]\n--- no_error_log\nauth-one\n\n\n\n=== TEST 9: add consumer with empty key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"error\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin key-auth err: property \\\"key\\\" is required\"}\n\n\n\n=== TEST 10: customize header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"header\": \"Authorization\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 11: valid consumer\n--- request\nGET /hello\n--- more_headers\nAuthorization: auth-one\n--- response_body\nhello world\n--- no_error_log\nauth-one\n\n\n\n=== TEST 12: customize query string\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"query\": \"auth\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 13: valid consumer\n--- request\nGET /hello?auth=auth-one\n--- response_body\nhello world\n\n\n\n=== TEST 14: enable key auth plugin using admin api, set hide_credentials = false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 15: verify apikey request header should not hidden\n--- request\nGET /echo\n--- more_headers\napikey: auth-one\n--- response_headers\napikey: auth-one\n--- no_error_log\nauth-one\n\n\n\n=== TEST 16: add key auth plugin using admin api, set hide_credentials = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 17: verify apikey request header is hidden\n--- request\nGET /echo\n--- more_headers\napikey: auth-one\n--- response_headers\n!apikey\n--- no_error_log\nauth-one\n\n\n\n=== TEST 18: verify that only the keys in the title are deleted\n--- request\nGET /echo\n--- more_headers\napikey: auth-one\ntest: auth-two\n--- response_headers\n!apikey\ntest: auth-two\n\n\n\n=== TEST 19: when apikey both in header and query string, verify apikey request header is hidden but request args is not hidden\n--- request\nGET /echo?apikey=auth-one\n--- more_headers\napikey: auth-one\n--- response_headers\n!apikey\n--- response_args\napikey: auth-one\n\n\n\n=== TEST 20: customize query string, set hide_credentials = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"query\": \"auth\",\n                            \"hide_credentials\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 21: verify auth request args is hidden\n--- request\nGET /echo?auth=auth-one\n--- response_args\n!auth\n\n\n\n=== TEST 22: verify that only the keys in the query parameters are deleted\n--- request\nGET /echo?auth=auth-one&test=auth-two\n--- response_args\n!auth\ntest: auth-two\n\n\n\n=== TEST 23: when auth both in header and query string, verify auth request args is hidden but request header is not hidden\n--- request\nGET /echo?auth=auth-one\n--- more_headers\nauth: auth-one\n--- response_headers\nauth: auth-one\n--- response_args\n!auth\n\n\n\n=== TEST 24: customize query string, set hide_credentials = false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"query\": \"auth\",\n                            \"hide_credentials\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nauth-one\n\n\n\n=== TEST 25: verify auth request args should not hidden\n--- request\nGET /hello?auth=auth-one\n--- response_args\nauth: auth-one\n\n\n\n=== TEST 26: change consumer with secrets ref: env\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"$env://test_auth\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: verify auth request\n--- main_config\nenv test_auth=authone;\n--- request\nGET /hello?auth=authone\n--- response_args\nauth: authone\n\n\n\n=== TEST 28: set key-auth conf: key uses secret ref\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: vault\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"$secret://vault/test1/jack/key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jack key=authtwo\n--- response_body\nSuccess! Data written to: kv/apisix/jack\n\n\n\n=== TEST 30: verify auth request\n--- request\nGET /hello?auth=authtwo\n--- response_args\nauth: authtwo\n\n\n\n=== TEST 31: set key-auth conf with the token in an env var: key uses secret ref\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local etcd = require(\"apisix.core.etcd\")\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"$ENV://VAULT_TOKEN\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- change consumer with secrets ref: vault\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"$secret://vault/test1/jack/key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 32: verify auth request\n--- request\nGET /hello?auth=authtwo\n--- response_args\nauth: authtwo\n"
  },
  {
    "path": "t/plugin/lago.spec.mts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { generateKeyPair } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { readFile, rm, writeFile } from 'node:fs/promises';\nimport { promisify } from 'node:util';\n\nimport { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals';\nimport axios from 'axios';\nimport * as compose from 'docker-compose';\nimport { gql, request } from 'graphql-request';\nimport { Api as LagoApi, Client as LagoClient } from 'lago-javascript-client';\nimport simpleGit from 'simple-git';\nimport * as YAML from 'yaml';\n\nimport { request as requestAdminAPI } from '../ts/admin_api';\nimport { wait } from '../ts/utils';\n\nconst LAGO_VERSION = 'v1.27.0';\nconst LAGO_PATH = '/tmp/lago';\nconst LAGO_FRONT_PORT = 59999;\nconst LAGO_API_PORT = 30699;\nconst LAGO_API_URL = `http://127.0.0.1:${LAGO_API_PORT}`;\nconst LAGO_API_BASEURL = `http://127.0.0.1:${LAGO_API_PORT}/api/v1`;\nconst LAGO_API_GRAPHQL_ENDPOINT = `${LAGO_API_URL}/graphql`;\nconst LAGO_BILLABLE_METRIC_CODE = 'test';\nconst LAGO_EXTERNAL_SUBSCRIPTION_ID = 'jack_test';\n\n// The project uses AGPLv3, so we can't store the docker compose file it uses in our repository and download it during testing.\nconst downloadComposeFile = async () =>\n  simpleGit().clone('https://github.com/getlago/lago', LAGO_PATH, {\n    '--depth': '1',\n    '--branch': LAGO_VERSION,\n  });\n\nconst launchLago = async () => {\n  // patch docker-compose.yml to disable useless port\n  const composeFilePath = `${LAGO_PATH}/docker-compose.yml`;\n  const composeFile = YAML.parse(await readFile(composeFilePath, 'utf8'));\n  delete composeFile.services.front; // front-end is not needed for tests\n  delete composeFile.services['api-clock']; // clock is not needed for tests\n  delete composeFile.services['api-worker']; // worker is not needed for tests\n  delete composeFile.services['pdf']; // pdf is not needed for tests\n  delete composeFile.services.redis.ports; // prevent port conflict\n  delete composeFile.services.db.ports; // prevent port conflict\n  await writeFile(composeFilePath, YAML.stringify(composeFile), 'utf8');\n\n  // launch services\n  const { privateKey } = await promisify(generateKeyPair)('rsa', {\n    modulusLength: 2048,\n    publicKeyEncoding: { type: 'pkcs1', format: 'pem' },\n    privateKeyEncoding: { type: 'pkcs1', format: 'pem' },\n  });\n  const composeOpts: compose.IDockerComposeOptions = {\n    cwd: LAGO_PATH,\n    log: true,\n    env: {\n      ...process.env,\n      LAGO_RSA_PRIVATE_KEY: Buffer.from(privateKey).toString('base64'),\n      FRONT_PORT: `${LAGO_FRONT_PORT}`, // avoiding conflicts, tests do not require a front-end\n      API_PORT: `${LAGO_API_PORT}`,\n      LAGO_FRONT_URL: `http://127.0.0.1:${LAGO_FRONT_PORT}`,\n      LAGO_API_URL,\n    },\n  };\n\n  await compose.createAll(composeOpts);\n  await compose.upOne('api', composeOpts);\n  await compose.exec('api', 'rails db:create', composeOpts);\n  await compose.exec('api', 'rails db:migrate', composeOpts);\n  await compose.upAll(composeOpts);\n};\n\nconst provisionLago = async () => {\n  // sign up\n  const { registerUser } = await request<{\n    registerUser: { token: string; user: { organizations: { id: string }[] } };\n  }>(\n    LAGO_API_GRAPHQL_ENDPOINT,\n    gql`\n      mutation signup($input: RegisterUserInput!) {\n        registerUser(input: $input) {\n          token\n          user {\n            id\n            organizations {\n              id\n            }\n          }\n        }\n      }\n    `,\n    {\n      input: {\n        email: 'test@test.com',\n        password: 'Admin000!',\n        organizationName: 'test',\n      },\n    },\n  );\n\n  const webToken = registerUser.token;\n  const organizationId = registerUser.user.organizations[0].id;\n  const requestHeaders = {\n    Authorization: `Bearer ${webToken}`,\n    'X-Lago-Organization': organizationId,\n  };\n\n  // list api keys\n  const { apiKeys } = await request<{\n    apiKeys: { collection: { id: string }[] };\n  }>(\n    LAGO_API_GRAPHQL_ENDPOINT,\n    gql`\n      query getApiKeys {\n        apiKeys(page: 1, limit: 20) {\n          collection {\n            id\n          }\n        }\n      }\n    `,\n    {},\n    requestHeaders,\n  );\n\n  // get first api key\n  const { apiKey } = await request<{ apiKey: { value: string } }>(\n    LAGO_API_GRAPHQL_ENDPOINT,\n    gql`\n      query getApiKeyValue($id: ID!) {\n        apiKey(id: $id) {\n          id\n          value\n        }\n      }\n    `,\n    { id: apiKeys.collection[0].id },\n    requestHeaders,\n  );\n\n  const lagoClient = LagoClient(apiKey.value, { baseUrl: LAGO_API_BASEURL });\n\n  // create billable metric\n  const { data: billableMetric } =\n    await lagoClient.billableMetrics.createBillableMetric({\n      billable_metric: {\n        name: LAGO_BILLABLE_METRIC_CODE,\n        code: LAGO_BILLABLE_METRIC_CODE,\n        aggregation_type: 'count_agg',\n        filters: [\n          {\n            key: 'tier',\n            values: ['normal', 'expensive'],\n          },\n        ],\n      },\n    });\n\n  // create plan\n  const { data: plan } = await lagoClient.plans.createPlan({\n    plan: {\n      name: 'test',\n      code: 'test',\n      interval: 'monthly',\n      amount_cents: 0,\n      amount_currency: 'USD',\n      pay_in_advance: false,\n      charges: [\n        {\n          billable_metric_id: billableMetric.billable_metric.lago_id,\n          charge_model: 'standard',\n          pay_in_advance: false,\n          properties: { amount: '1' },\n          filters: [\n            {\n              properties: { amount: '10' },\n              values: { tier: ['expensive'] },\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  // create customer\n  const external_customer_id = 'jack';\n  const { data: customer } = await lagoClient.customers.createCustomer({\n    customer: {\n      external_id: external_customer_id,\n      name: 'Jack',\n      currency: 'USD',\n    },\n  });\n\n  // assign plan to customer\n  await lagoClient.subscriptions.createSubscription({\n    subscription: {\n      external_customer_id: customer.customer.external_id,\n      plan_code: plan.plan.code,\n      external_id: LAGO_EXTERNAL_SUBSCRIPTION_ID,\n    },\n  });\n\n  return { apiKey: apiKey.value, client: lagoClient };\n};\n\ndescribe('Plugin - Lago', () => {\n  const JACK_USERNAME = 'jack_test';\n  const client = axios.create({ baseURL: 'http://127.0.0.1:1984' });\n\n  let restAPIKey: string;\n  let lagoClient: LagoApi<unknown>; // prettier-ignore\n\n  // set up\n  beforeAll(async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] starting suite`)\n    if (existsSync(LAGO_PATH)) await rm(LAGO_PATH, { recursive: true });\n    console.log(`[${new Date().toLocaleTimeString()}] download compose file`)\n    await downloadComposeFile();\n    console.log(`[${new Date().toLocaleTimeString()}] launch lago`)\n    await launchLago();\n    console.log(`[${new Date().toLocaleTimeString()}] provision lago`)\n    let res = await provisionLago();\n    restAPIKey = res.apiKey;\n    lagoClient = res.client;\n  }, 120 * 1000);\n\n  // clean up\n  afterAll(async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] cleaning up`)\n    await compose.downAll({\n      cwd: LAGO_PATH,\n      commandOptions: ['--volumes'],\n    });\n    console.log(`[${new Date().toLocaleTimeString()}] cleaned up`)\n    await rm(LAGO_PATH, { recursive: true });\n  }, 30 * 1000);\n\n  it('should create route', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] creating route`)\n    await expect(\n      requestAdminAPI('/apisix/admin/routes/1', 'PUT', {\n        uri: '/hello',\n        upstream: {\n          nodes: {\n            '127.0.0.1:1980': 1,\n          },\n          type: 'roundrobin',\n        },\n        plugins: {\n          'request-id': { include_in_response: true }, // for transaction_id\n          'key-auth': {}, // for subscription_id\n          lago: {\n            endpoint_addrs: [LAGO_API_URL],\n            token: restAPIKey,\n            event_transaction_id: '${http_x_request_id}',\n            event_subscription_id: '${http_x_consumer_username}',\n            event_code: 'test',\n            batch_max_size: 1, // does not buffered usage reports\n          },\n        },\n      }),\n    ).resolves.not.toThrow();\n\n    console.log(`[${new Date().toLocaleTimeString()}] creating second route`)\n    await expect(\n      requestAdminAPI('/apisix/admin/routes/2', 'PUT', {\n        uri: '/hello1',\n        upstream: {\n          nodes: {\n            '127.0.0.1:1980': 1,\n          },\n          type: 'roundrobin',\n        },\n        plugins: {\n          'request-id': { include_in_response: true },\n          'key-auth': {},\n          lago: {\n            endpoint_addrs: [LAGO_API_URL],\n            token: restAPIKey,\n            event_transaction_id: '${http_x_request_id}',\n            event_subscription_id: '${http_x_consumer_username}',\n            event_code: 'test',\n            event_properties: { tier: 'expensive' },\n            batch_max_size: 1,\n          },\n        },\n      }),\n    ).resolves.not.toThrow();\n    console.log(`[${new Date().toLocaleTimeString()}] created routes`)\n  }, 5 * 1000);\n\n  it('should create consumer', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] creating consumer`)\n    expect(\n      requestAdminAPI(`/apisix/admin/consumers/${JACK_USERNAME}`, 'PUT', {\n        username: JACK_USERNAME,\n        plugins: {\n          'key-auth': { key: JACK_USERNAME },\n        },\n      }),\n    ).resolves.not.toThrow()\n    console.log(`[${new Date().toLocaleTimeString()}] created consumer`)\n  }, 5 * 1000);\n\n  it('call API (without key)', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] calling API without key`)\n    const res = await client.get('/hello', { validateStatus: () => true });\n    expect(res.status).toEqual(401);\n    console.log(`[${new Date().toLocaleTimeString()}] called API without key`)\n  }, 5 * 1000);\n\n  it('call normal API', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] calling normal API`)\n    for (let i = 0; i < 3; i++) {\n      await expect(\n        client.get('/hello', { headers: { apikey: JACK_USERNAME } }),\n      ).resolves.not.toThrow();\n    }\n    await wait(500);\n    console.log(`[${new Date().toLocaleTimeString()}] called normal API`)\n  }, 5 * 1000);\n\n  it('check Lago events (normal API)', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] checking Lago events (normal API)`)\n    const { data } = await lagoClient.events.findAllEvents({\n      external_subscription_id: LAGO_EXTERNAL_SUBSCRIPTION_ID,\n    });\n\n    expect(data.events).toHaveLength(3);\n    expect(data.events[0].code).toEqual(LAGO_BILLABLE_METRIC_CODE);\n    console.log(`[${new Date().toLocaleTimeString()}] checked Lago events (normal API)`)\n  }, 5 * 1000);\n\n  let expensiveStartAt: Date;\n  it('call expensive API', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] calling expensive API`)\n    expensiveStartAt = new Date();\n    for (let i = 0; i < 3; i++) {\n      await expect(\n        client.get('/hello1', { headers: { apikey: JACK_USERNAME } }),\n      ).resolves.not.toThrow();\n    }\n    await wait(500);\n    console.log(`[${new Date().toLocaleTimeString()}] called expensive API`)\n  }, 5 * 1000);\n\n  it('check Lago events (expensive API)', async () => {\n    console.log(`[${new Date().toLocaleTimeString()}] checking Lago events (expensive API)`)\n    const { data } = await lagoClient.events.findAllEvents({\n      external_subscription_id: LAGO_EXTERNAL_SUBSCRIPTION_ID,\n      timestamp_from: expensiveStartAt.toISOString(),\n    });\n\n    expect(data.events).toHaveLength(3);\n    expect(data.events[0].code).toEqual(LAGO_BILLABLE_METRIC_CODE);\n    expect(data.events[1].properties).toEqual({ tier: 'expensive' });\n    console.log(`[${new Date().toLocaleTimeString()}] checked Lago events (expensive API)`)\n  }, 5 * 1000);\n});\n"
  },
  {
    "path": "t/plugin/lago.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\n# This test cannot be executed normally at the moment, so it will be temporarily skipped and fixed in a later PR.\nplan(skip_all => 'skip test case');\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {endpoint_addrs = {\"http://127.0.0.1:3000\"}, token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\"},\n                {endpoint_addrs = \"http://127.0.0.1:3000\", token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\"},\n                {endpoint_addrs = {}, token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\"},\n                {endpoint_addrs = {\"http://127.0.0.1:3000\"}, endpoint_uri = \"/test\", token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\"},\n                {endpoint_addrs = {\"http://127.0.0.1:3000\"}, endpoint_uri = 1234, token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\"},\n                {endpoint_addrs = {\"http://127.0.0.1:3000\"}, token = 1234, event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\"},\n                {endpoint_addrs = {\"http://127.0.0.1:3000\"}, token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\", event_properties = {key = \"value\"}},\n                {endpoint_addrs = {\"http://127.0.0.1:3000\"}, token = \"token\", event_transaction_id = \"tid\", event_subscription_id = \"sid\", event_code = \"code\", event_properties = {1,2,3}},\n            }\n            local plugin = require(\"apisix.plugins.lago\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"endpoint_addrs\" validation failed: wrong type: expected array, got string\nproperty \"endpoint_addrs\" validation failed: expect array to have at least 1 items\ndone\nproperty \"endpoint_uri\" validation failed: wrong type: expected string, got number\nproperty \"token\" validation failed: wrong type: expected string, got number\ndone\nproperty \"event_properties\" validation failed: wrong type: expected object, got table\n\n\n\n=== TEST 2: test\n--- timeout: 302\n--- max_size: 2048000\n--- exec\ncd t && pnpm test plugin/lago.spec.mts 2>&1\n--- no_error_log\nfailed to execute the script with status\n--- response_body eval\nqr/PASS plugin\\/lago.spec.mts/\n"
  },
  {
    "path": "t/plugin/ldap-auth-realm.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity, default realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"dc=example,dc=com\",\n                            \"ldap_uri\": \"ldap://127.0.0.1:1389\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: verify default realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"ldap\"\n\n\n\n=== TEST 3: set custom realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"dc=example,dc=com\",\n                            \"ldap_uri\": \"ldap://127.0.0.1:1389\",\n                            \"realm\": \"my-ldap-realm\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: verify custom realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"my-ldap-realm\"\n\n\n\n=== TEST 5: ldap auth failure (missing header) returns realm\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"my-ldap-realm\"\n\n\n\n=== TEST 6: ldap auth failure (invalid credentials) returns realm\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"dc=example,dc=com\",\n                            \"ldap_uri\": \"ldap://127.0.0.1:1389\",\n                            \"realm\": \"my-ldap-realm\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: verify ldap invalid credentials returns realm\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjp3cm9uZw==\n--- error_code: 401\n--- response_headers\nWWW-Authenticate: Basic realm=\"my-ldap-realm\"\n--- error_log\nldap-auth failed\n"
  },
  {
    "path": "t/plugin/ldap-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.ldap-auth\")\n            local ok, err = plugin.check_schema({user_dn = 'foo'}, core.schema.TYPE_CONSUMER)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ldap-auth\")\n            local ok, err = plugin.check_schema({base_dn = 123, ldap_uri = \"127.0.0.1:1389\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body_like eval\nqr/wrong type: expected string, got number\ndone\n/\n\n\n\n=== TEST 3: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"user01\",\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"user_dn\": \"cn=user01,ou=users,dc=example,dc=org\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: enable basic auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"ou=users,dc=example,dc=org\",\n                            \"ldap_uri\": \"127.0.0.1:1389\",\n                            \"uid\": \"cn\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify, missing authorization\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing authorization in request\"}\n\n\n\n=== TEST 6: verify, invalid basic authorization header\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bad_header Zm9vOmZvbwo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid authorization in request\"}\n--- grep_error_log eval\nqr/Invalid authorization header format/\n--- grep_error_log_out\nInvalid authorization header format\n\n\n\n=== TEST 7: verify, invalid authorization value (bad base64 str)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic aca_a\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid authorization in request\"}\n--- grep_error_log eval\nqr/Failed to decode authentication header: aca_a/\n--- grep_error_log_out\nFailed to decode authentication header: aca_a\n\n\n\n=== TEST 8: verify, invalid authorization value (no password)\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9v\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid authorization in request\"}\n--- grep_error_log eval\nqr/Split authorization err: invalid decoded data: foo/\n--- grep_error_log_out\nSplit authorization err: invalid decoded data: foo\n\n\n\n=== TEST 9: verify, invalid password\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmZvbwo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Invalid user authorization\"}\n--- error_log\nThe supplied credential is invalid\n\n\n\n=== TEST 10: verify\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 11: enable basic auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"ou=users,dc=example,dc=org\",\n                            \"ldap_uri\": \"127.0.0.1:1389\",\n                            \"uid\": \"cn\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: verify capitalization scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 13: invalid schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {},\n                \"blah\"\n            }) do\n                local code, body = t('/apisix/admin/consumers',\n                    ngx.HTTP_PUT,\n                    {\n                        username = \"foo\",\n                        plugins = {\n                            [\"ldap-auth\"] = case\n                        }\n                    }\n                )\n                ngx.print(body)\n            end\n        }\n    }\n--- response_body\n{\"error_msg\":\"invalid plugins configuration: failed to check the configuration of plugin ldap-auth err: property \\\"user_dn\\\" is required\"}\n{\"error_msg\":\"invalid plugins configuration: invalid plugin conf \\\"blah\\\" for plugin [ldap-auth]\"}\n\n\n\n=== TEST 14: get the default schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/ldap-auth',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"title\":\"work with route or service object\",\"required\":[\"base_dn\",\"ldap_uri\"],\"properties\":{\"base_dn\":{\"type\":\"string\"},\"ldap_uri\":{\"type\":\"string\"},\"use_tls\":{\"type\":\"boolean\"},\"tls_verify\":{\"type\":\"boolean\"},\"uid\":{\"type\":\"string\"}},\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 15: get the schema by schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/ldap-auth?schema_type=consumer',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"title\":\"work with consumer object\",\"required\":[\"user_dn\"],\"properties\":{\"user_dn\":{\"type\":\"string\"}},\"type\":\"object\"}\n                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 16: get the schema by error schema_type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/schema/plugins/ldap-auth?schema_type=consumer123123',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"title\":\"work with route or service object\",\"required\":[\"base_dn\",\"ldap_uri\"],\"properties\":{\"base_dn\":{\"type\":\"string\"},\"ldap_uri\":{\"type\":\"string\"},\"use_tls\":{\"type\":\"boolean\"},\"tls_verify\":{\"type\":\"boolean\"},\"uid\":{\"type\":\"string\"}},\"type\":\"object\"}                ]]\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 17: enable ldap-auth with tls\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"ou=users,dc=example,dc=org\",\n                            \"ldap_uri\": \"test.com:1636\",\n                            \"uid\": \"cn\",\n                            \"use_tls\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: verify\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 19: enable ldap-auth with tls, verify CA\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"ou=users,dc=example,dc=org\",\n                            \"ldap_uri\": \"test.com:1636\",\n                            \"uid\": \"cn\",\n                            \"use_tls\": true,\n                            \"tls_verify\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: verify\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 21: set ldap-auth conf: user_dn uses secret ref\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"user01\",\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"user_dn\": \"$secret://vault/test1/user01/user_dn\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"ou=users,dc=example,dc=org\",\n                            \"ldap_uri\": \"127.0.0.1:1389\",\n                            \"uid\": \"cn\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/user01 user_dn=\"cn=user01,ou=users,dc=example,dc=org\"\n--- response_body\nSuccess! Data written to: kv/apisix/user01\n\n\n\n=== TEST 23: verify\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 24: set ldap-auth conf with the token in an env var: user_dn uses secret ref\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"$ENV://VAULT_TOKEN\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"user01\",\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"user_dn\": \"$secret://vault/test1/user01/user_dn\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- set route\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"ldap-auth\": {\n                            \"base_dn\": \"ou=users,dc=example,dc=org\",\n                            \"ldap_uri\": \"127.0.0.1:1389\",\n                            \"uid\": \"cn\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: verify\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 26: verify lowercase scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: basic dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 27: verify uppercase scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: BASIC dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n\n\n\n=== TEST 28: verify mixed case scheme\n--- request\nGET /hello\n--- more_headers\nAuthorization: bASiC dXNlcjAxOnBhc3N3b3JkMQ==\n--- response_body\nhello world\n--- error_log\nfind consumer user01\n"
  },
  {
    "path": "t/plugin/limit-conn-redis-cluster.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn')\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            local status_map = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                local status_key = resp.status\n                if status_map[status_key] then\n                    status_map[status_key] = status_map[status_key] + 1\n                else\n                    status_map[status_key] = 1\n                end\n            end\n            for key, value in pairs(status_map) do\n                ngx.say(\"status:\" .. key .. \", \" .. \"count:\" .. value)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local ok, err = plugin.check_schema({\n                conn = 1,\n                burst = 0,\n                default_conn_delay = 0.1,\n                rejected_code = 503,\n                key = 'remote_addr',\n                policy = \"redis-cluster\",\n                redis_cluster_nodes = {\n                    \"127.0.0.1:5000\",\n                    \"127.0.0.1:5003\",\n                    \"127.0.0.1:5002\"\n                },\n                dict_name = \"test\",\n                redis_cluster_name = \"test\"\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis-cluster\",\n                                \"redis_cluster_nodes\": [\n                                    \"127.0.0.1:5000\",\n                                    \"127.0.0.1:5003\",\n                                    \"127.0.0.1:5002\"\n                                ],\n                                \"redis_cluster_name\": \"test\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: not exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:10\n\n\n\n=== TEST 4: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 2,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis-cluster\",\n                                \"redis_cluster_nodes\": [\n                                    \"127.0.0.1:5000\",\n                                    \"127.0.0.1:5002\"\n                                ],\n                                \"redis_cluster_name\": \"redis-cluster-1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:3\nstatus:503, count:7\n\n\n\n=== TEST 6: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis-cluster\",\n                                \"redis_cluster_nodes\": [\n                                    \"127.0.0.1:5000\",\n                                    \"127.0.0.1:5002\"\n                                ],\n                                \"redis_cluster_name\": \"redis-cluster-1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:6\nstatus:503, count:4\n\n\n\n=== TEST 8: set route, with redis_cluster_nodes and redis_cluster_name redis_cluster_ssl and redis_cluster_ssl_verify\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis-cluster\",\n                                \"redis_cluster_nodes\": [\n                                    \"127.0.0.1:7001\",\n                                    \"127.0.0.1:7002\",\n                                    \"127.0.0.1:7000\"\n                                ],\n                                \"redis_cluster_name\": \"redis-cluster-2\",\n                                \"redis_cluster_ssl\": true,\n                                \"redis_cluster_ssl_verify\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:6\nstatus:503, count:4\n\n\n\n=== TEST 10: check redis cluster keepalive param\n--- config\n    location /t {\n        content_by_lua_block {\n            local lim_conn_redis_cluster = require(\"apisix.plugins.limit-conn.limit-conn-redis-cluster\")\n            local conf = {\n                conn = 5,\n                burst = 1,\n                default_conn_delay = 0.1,\n                rejected_code = 503,\n                key = \"remote_addr\",\n                policy = \"redis-cluster\",\n                redis_cluster_nodes = {\n                    \"127.0.0.1:7001\",\n                    \"127.0.0.1:7002\",\n                    \"127.0.0.1:7000\"\n                },\n                redis_keepalive_timeout = 10000,\n                redis_keepalive_pool = 100,\n                redis_cluster_name = \"redis-cluster-2\",\n                redis_cluster_ssl = true,\n                redis_cluster_ssl_verify = false\n            }\n            local lim = lim_conn_redis_cluster.new(\"limit-conn\", conf, 5, 1)\n            local redis_conf = lim.red_cli.config\n            if redis_conf.keepalive_timeout == 10000 and redis_conf.keepalive_cons == 100 then\n                ngx.say(\"keepalive set success\")\n                return\n            end\n            ngx.say(\"keepalive set abnormal, keepalive_timeout: \",\n                    redis_conf.keepalive_timeout, \", keepalive_cons: \",redis_conf.keepalive_cons)\n        }\n    }\n--- request\nGET /t\n--- response_body\nkeepalive set success\n"
  },
  {
    "path": "t/plugin/limit-conn-redis.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn')\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            local status_map = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                local status_key = resp.status\n                if status_map[status_key] then\n                    status_map[status_key] = status_map[status_key] + 1\n                else\n                    status_map[status_key] = 1\n                end\n            end\n            for key, value in pairs(status_map) do\n                ngx.say(\"status:\" .. key .. \", \" .. \"count:\" .. value)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local ok, err = plugin.check_schema({\n                conn = 1,\n                burst = 0,\n                default_conn_delay = 0.1,\n                rejected_code = 503,\n                key = 'remote_addr',\n                policy = \"redis\",\n                redis_host = 'localhost',\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\",\n                                \"redis_port\": 6379\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: not exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:10\n\n\n\n=== TEST 4: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 2,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\",\n                                \"redis_port\": 6379\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:3\nstatus:503, count:7\n\n\n\n=== TEST 6: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\",\n                                \"redis_port\": 6379\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:6\nstatus:503, count:4\n\n\n\n=== TEST 8: update plugin with username, password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\",\n                                \"redis_port\": 6379,\n                                \"redis_username\": \"alice\",\n                                \"redis_password\": \"somepassword\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:6\nstatus:503, count:4\n\n\n\n=== TEST 10: update plugin with username, wrong password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\",\n                                \"redis_port\": 6379,\n                                \"redis_username\": \"alice\",\n                                \"redis_password\": \"someerrorpassword\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: catch wrong pass\n--- request\nGET /access_root_dir\n--- error_code: 500\n--- error_log\nfailed to limit conn: WRONGPASS invalid username-password pair or user is disabled.\n\n\n\n=== TEST 12: invalid route: missing redis_host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"conn\": 1,\n                                \"policy\": \"redis\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: then clause did not match\"}\n\n\n\n=== TEST 13: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:10\n\n\n\n=== TEST 15: set route(key: server_addr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"server_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: key: http_x_real_ip\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"http_x_real_ip\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: exceeding the burst (X-Real-IP)\n--- config\nlocation /access_root_dir {\n    content_by_lua_block {\n        local port = ngx.var.server_port\n        local httpc = require \"resty.http\"\n        local hc = httpc:new()\n\n        local res, err = hc:request_uri('http://127.0.0.1:' .. port .. '/limit_conn', {\n            keepalive = false,\n            headers = {[\"X-Real-IP\"] = \"10.10.10.1\"}\n        })\n        if res then\n            ngx.exit(res.status)\n        end\n    }\n}\n\nlocation /test_concurrency {\n    content_by_lua_block {\n        local reqs = {}\n        local status_map = {}\n        for i = 1, 10 do\n            reqs[i] = { \"/access_root_dir\" }\n        end\n        local resps = { ngx.location.capture_multi(reqs) }\n        for i, resp in ipairs(resps) do\n            local status_key = resp.status\n            if status_map[status_key] then\n                status_map[status_key] = status_map[status_key] + 1\n            else\n                status_map[status_key] = 1\n            end\n        end\n        for key, value in pairs(status_map) do\n            ngx.say(\"status:\" .. key .. \", \" .. \"count:\" .. value)\n        end\n    }\n}\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:6\nstatus:503, count:4\n--- error_log\nlimit key: 10.10.10.1route\n\n\n\n=== TEST 18: key: http_x_forwarded_for\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"http_x_forwarded_for\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: exceeding the burst(X-Forwarded-For)\n--- config\nlocation /access_root_dir {\n    content_by_lua_block {\n        local port = ngx.var.server_port\n        local httpc = require \"resty.http\"\n        local hc = httpc:new()\n\n        local res, err = hc:request_uri('http://127.0.0.1:' .. port .. '/limit_conn', {\n            keepalive = false,\n            headers = {[\"X-Forwarded-For\"] = \"10.10.10.2\"}\n        })\n        if res then\n            ngx.exit(res.status)\n        end\n    }\n}\n\nlocation /test_concurrency {\n    content_by_lua_block {\n        local reqs = {}\n        local status_map = {}\n        for i = 1, 10 do\n            reqs[i] = { \"/access_root_dir\" }\n        end\n        local resps = { ngx.location.capture_multi(reqs) }\n        for i, resp in ipairs(resps) do\n            local status_key = resp.status\n            if status_map[status_key] then\n                status_map[status_key] = status_map[status_key] + 1\n            else\n                status_map[status_key] = 1\n            end\n        end\n        for key, value in pairs(status_map) do\n            ngx.say(\"status:\" .. key .. \", \" .. \"count:\" .. value)\n        end\n    }\n}\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:6\nstatus:503, count:4\n--- error_log\nlimit key: 10.10.10.2route\n\n\n\n=== TEST 20: default rejected_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 4,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis\",\n                                \"redis_host\": \"127.0.0.1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:5\nstatus:503, count:5\n\n\n\n=== TEST 22: set global rule with conn = 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: exceeding the burst of global rule\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:3\nstatus:503, count:7\n\n\n\n=== TEST 24: delete global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 25: not exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\nstatus:200, count:5\nstatus:503, count:5\n\n\n\n=== TEST 26: verify redis connection reused times in debug log\n--- log_level: debug\n--- pipelined_requests eval\n[ \"GET /limit_conn\", \"GET /limit_conn\"]\n--- error_log_like eval\n[qr/redis connection reused times: 0/, qr/redis connection reused times: 1/]\n"
  },
  {
    "path": "t/plugin/limit-conn-variable.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn', {\n                headers = ngx.req.get_headers()\n            })\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: use variable in conn and burst with default value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": \"${http_conn ?? 5}\",\n                                \"burst\": \"${http_burst ?? 2}\",\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: request without conn/burst headers\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n503\n503\n503\n\n\n\n=== TEST 3: request with conn header\n--- request\nGET /test_concurrency\n--- more_headers\nconn: 3\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n503\n503\n503\n503\n503\n\n\n\n=== TEST 4: request with conn and burst header\n--- request\nGET /test_concurrency\n--- more_headers\nconn: 3\nburst: 4\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n503\n503\n503\n\n\n\n=== TEST 5: configure conn/burst and rules at same time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 2,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.01,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\",\n                                \"rules\": [\n                                    {\n                                        \"conn\": 1,\n                                        \"burst\": 0,\n                                        \"key\": \"${http_company}\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: value should match only one schema, but matches both schemas 1 and 2\"}\n\n\n\n=== TEST 6: setup route with rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"default_conn_delay\": 0.01,\n                                \"rejected_code\": 503,\n                                \"rules\": [\n                                    {\n                                        \"conn\": 4,\n                                        \"burst\": 3,\n                                        \"key\": \"${http_user}\"\n                                    },\n                                    {\n                                        \"conn\": \"${http_project_conn ?? 3}\",\n                                        \"burst\": \"${http_project_burst ?? 2}\",\n                                        \"key\": \"${http_project}\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: request matching user rule\n--- request\nGET /test_concurrency\n--- more_headers\nuser: jack\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n503\n503\n503\n\n\n\n=== TEST 8: request matching project rule with default conn/burst\n--- request\nGET /test_concurrency\n--- more_headers\nproject: apisix\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n503\n503\n503\n503\n503\n\n\n\n=== TEST 9: request matching project rule with custom conn/burst\n--- request\nGET /test_concurrency\n--- more_headers\nproject: apisix\nproject-conn: 2\nproject-burst: 1\n--- timeout: 10s\n--- response_body\n200\n200\n200\n503\n503\n503\n503\n503\n503\n503\n\n\n\n=== TEST 10: request not matching any rule\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n500\n500\n500\n500\n500\n500\n500\n500\n500\n500\n--- error_log\nfailed to get limit conn rules\n"
  },
  {
    "path": "t/plugin/limit-conn.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn')\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local ok, err = plugin.check_schema({conn = 1, burst = 0, default_conn_delay = 0.1, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong value of key\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local ok, err = plugin.check_schema({conn = 1, default_conn_delay = 0.1, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches none\ndone\n\n\n\n=== TEST 3: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: not exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n200\n200\n200\n\n\n\n=== TEST 5: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 2,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n503\n503\n503\n503\n503\n503\n503\n\n\n\n=== TEST 7: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n503\n503\n503\n503\n\n\n\n=== TEST 9: invalid route: missing key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 10: invalid route: wrong conn\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                    \"conn\": -1,\n                                    \"burst\": 1,\n                                    \"default_conn_delay\": 0.1,\n                                    \"rejected_code\": 503,\n                                    \"key\": \"remote_addr\"\n                                }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: property \\\"conn\\\" validation failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 11: invalid service: missing key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                    \"burst\": 1,\n                                    \"default_conn_delay\": 0.1,\n                                    \"rejected_code\": 503,\n                                    \"key\": \"remote_addr\"\n                                }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 12: invalid service: wrong count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": -1,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: property \\\"conn\\\" validation failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 13: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n200\n200\n200\n\n\n\n=== TEST 15: set route(key: server_addr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"server_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: key: http_x_real_ip\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"http_x_real_ip\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: exceeding the burst (X-Real-IP)\n--- config\nlocation /access_root_dir {\n    content_by_lua_block {\n        local port = ngx.var.server_port\n        local httpc = require \"resty.http\"\n        local hc = httpc:new()\n\n        local res, err = hc:request_uri('http://127.0.0.1:' .. port .. '/limit_conn', {\n            keepalive = false,\n            headers = {[\"X-Real-IP\"] = \"10.10.10.1\"}\n        })\n        if res then\n            ngx.exit(res.status)\n        end\n    }\n}\n\nlocation /test_concurrency {\n    content_by_lua_block {\n        local reqs = {}\n        for i = 1, 10 do\n            reqs[i] = { \"/access_root_dir\" }\n        end\n        local resps = { ngx.location.capture_multi(reqs) }\n        for i, resp in ipairs(resps) do\n            ngx.say(resp.status)\n        end\n    }\n}\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n503\n503\n503\n503\n--- error_log\nlimit key: 10.10.10.1route\n\n\n\n=== TEST 18: key: http_x_forwarded_for\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 5,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"http_x_forwarded_for\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: exceeding the burst(X-Forwarded-For)\n--- config\nlocation /access_root_dir {\n    content_by_lua_block {\n        local port = ngx.var.server_port\n        local httpc = require \"resty.http\"\n        local hc = httpc:new()\n\n        local res, err = hc:request_uri('http://127.0.0.1:' .. port .. '/limit_conn', {\n            keepalive = false,\n            headers = {[\"X-Forwarded-For\"] = \"10.10.10.2\"}\n        })\n        if res then\n            ngx.exit(res.status)\n        end\n    }\n}\n\nlocation /test_concurrency {\n    content_by_lua_block {\n        local reqs = {}\n        for i = 1, 10 do\n            reqs[i] = { \"/access_root_dir\" }\n        end\n        local resps = { ngx.location.capture_multi(reqs) }\n        for i, resp in ipairs(resps) do\n            ngx.say(resp.status)\n        end\n    }\n}\n--- more_headers\nX-Real-IP: 10.0.0.1\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n503\n503\n503\n503\n--- error_log\nlimit key: 10.10.10.2route\n\n\n\n=== TEST 20: default rejected_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: set global rule with conn = 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: exceeding the burst of global rule\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n503\n503\n503\n503\n503\n503\n503\n\n\n\n=== TEST 23: delete global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: not exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n200\n200\n200\n\n\n\n=== TEST 25: invalid schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local cases = {\n                {conn = 0, burst = 0, default_conn_delay = 0.1, rejected_code = 503, key = 'remote_addr'},\n                {conn = 1, burst = 0, default_conn_delay = 0, rejected_code = 503, key = 'remote_addr'},\n            }\n            for _, c in ipairs(cases) do\n                local ok, err = plugin.check_schema(c)\n                if not ok then\n                    ngx.say(err)\n                end\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"conn\" validation failed: value should match only one schema, but matches none\nproperty \"default_conn_delay\" validation failed: expected 0 to be greater than 0\ndone\n\n\n\n=== TEST 26: create consumer and bind key-auth plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"consumer_jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: create route and enable plugin 'key-auth'\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {},\n                            \"limit-conn\": {\n                                \"conn\": 100,\n                                \"burst\": 50,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"consumer_name\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: not exceeding the burst\n--- config\n    location /access_root_dir {\n        content_by_lua_block {\n            local port = ngx.var.server_port\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:' .. port .. '/limit_conn', {\n                headers = {[\"apikey\"] = \"auth-jack\"}\n            })\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n200\n200\n200\n200\n200\n200\n200\n--- error_log_like eval\nqr/limit key: consumer_jackroute&consumer\\d+/\n\n\n\n=== TEST 29: update plugin \"limit-conn\" configuration \"conn\" and \"burst\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {},\n                            \"limit-conn\": {\n                                \"conn\": 2,\n                                \"burst\": 1,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"consumer_name\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: exceeding the burst\n--- config\n    location /access_root_dir {\n        content_by_lua_block {\n            local port = ngx.var.server_port\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:' .. port .. '/limit_conn', {\n                headers = {[\"apikey\"] = \"auth-jack\"}\n            })\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n503\n503\n503\n503\n503\n503\n503\n--- error_log_like eval\nqr/limit key: consumer_jackroute&consumer\\d+/\n\n\n\n=== TEST 31: plugin limit-conn uses the wrong value of key\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local ok, err = plugin.check_schema({\n                conn = 1,\n                default_conn_delay = 0.1,\n                rejected_code = 503,\n                key = 'consumer_name'\n                    })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches none\ndone\n\n\n\n=== TEST 32: enable plugin: conn=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 1,\n                            \"burst\": 0,\n                            \"default_conn_delay\": 0.3,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"allow_degradation\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 33: hit route and should not be limited\n--- pipelined_requests eval\n[\n    \"GET /hello\", \"GET /hello\", \"GET /hello\",\n    \"GET /hello\", \"GET /hello\", \"GET /hello\",\n]\n--- timeout: 10s\n--- error_code eval\n[\n    200, 200, 200,\n    200, 200, 200\n]\n\n\n\n=== TEST 34: invalid route: wrong allow_degradation\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                    \"conn\": 1,\n                                    \"burst\": 1,\n                                    \"default_conn_delay\": 0.1,\n                                    \"rejected_code\": 503,\n                                    \"key\": \"remote_addr\",\n                                    \"allow_degradation\": \"true1\"\n                                }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: property \\\"allow_degradation\\\" validation failed: wrong type: expected boolean, got string\"}\n"
  },
  {
    "path": "t/plugin/limit-conn2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the check leak tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn')\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 5 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: limit-conn with retry upstream, set upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"retries\": 2,\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/mysleep\",\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 1,\n                            \"burst\": 0,\n                            \"default_conn_delay\": 0.3,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- log_level: debug\n--- request\nGET /mysleep?seconds=0.1\n--- error_log\nrequest latency is 0.1\n--- response_body\n0.1\n\n\n\n=== TEST 3: set both global and route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 1,\n                            \"burst\": 0,\n                            \"default_conn_delay\": 0.3,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 1,\n                            \"burst\": 0,\n                            \"default_conn_delay\": 0.3,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route\n--- log_level: debug\n--- request\nGET /hello\n--- grep_error_log eval\nqr/request latency is/\n--- grep_error_log_out\nrequest latency is\nrequest latency is\n\n\n\n=== TEST 5: set only_use_default_delay option to true in specific route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello1\",\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 1,\n                            \"burst\": 0,\n                            \"default_conn_delay\": 0.3,\n                            \"only_use_default_delay\": true,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route\n--- log_level: debug\n--- request\nGET /hello1\n--- grep_error_log eval\nqr/request latency is nil/\n--- grep_error_log_out\nrequest latency is nil\n\n\n\n=== TEST 7: invalid route: wrong rejected_msg type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                    \"conn\": 1,\n                                    \"burst\": 1,\n                                    \"default_conn_delay\": 0.1,\n                                    \"rejected_code\": 503,\n                                    \"key\": \"remote_addr\",\n                                    \"rejected_msg\": true\n                                }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: property \\\"rejected_msg\\\" validation failed: wrong type: expected string, got boolean\"}\n\n\n\n=== TEST 8: invalid route: wrong rejected_msg length\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                    \"conn\": 1,\n                                    \"burst\": 1,\n                                    \"default_conn_delay\": 0.1,\n                                    \"rejected_code\": 503,\n                                    \"key\": \"remote_addr\",\n                                    \"rejected_msg\": \"\"\n                                }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-conn err: property \\\"rejected_msg\\\" validation failed: string too short, expected at least 1, got 0\"}\n\n\n\n=== TEST 9: update plugin to set key_type to var_combination\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 1,\n                                \"burst\": 0,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"$http_a $http_b\",\n                                \"key_type\": \"var_combination\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: Don't exceed the burst\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/limit_conn\"\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {a = i}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- timeout: 10s\n--- response_body\n[200,200]\n\n\n\n=== TEST 11: request when key is missing\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n503\n503\n503\n503\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 12: update plugin to set invalid key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 1,\n                                \"burst\": 0,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"abcdefgh\",\n                                \"key_type\": \"var_combination\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: request when key is invalid\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n503\n503\n503\n503\n--- error_log\nThe value of the configured key is empty, use client IP instead\n"
  },
  {
    "path": "t/plugin/limit-conn3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the check leak tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create route with limit-conn plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-conn\": {\n                                \"conn\": 1,\n                                \"burst\": 0,\n                                \"default_conn_delay\": 0.1,\n                                \"rejected_code\": 503,\n                                \"key\": \"$remote_addr\",\n                                \"key_type\": \"var_combination\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/limit_conn\",\n                        \"host\": \"www.test.com\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: create ssl(sni: www.test.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 3: use HTTP version 2 to request\n--- exec\ncurl --http2 --parallel -k https://www.test.com:1994/limit_conn https://www.test.com:1994/limit_conn --resolve www.test.com:1994:127.0.0.1\n--- response_body_like\n503 Service Temporarily Unavailable.*.hello world\n"
  },
  {
    "path": "t/plugin/limit-count-consumer-group-credentials.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup consumer group with limit-count plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/test_group',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 429,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"local\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: setup consumer with group_id (no direct plugins)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"group_id\": \"test_group\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: setup credentials via credentials endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/jack/credentials/cred-jack',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"cred-jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: setup route with key-auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify rate limiting works (all 3 requests in one test)\n--- pipelined_requests eval\n[\n    \"GET /hello\", \"GET /hello\", \"GET /hello\"\n]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 200, 429]\n\n\n\n=== TEST 6: setup second consumer group with higher limit\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumer_groups/premium_group',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 10,\n                            \"time_window\": 60,\n                            \"rejected_code\": 429,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"local\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: setup premium consumer with credentials\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/consumers/jane',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jane\",\n                    \"group_id\": \"premium_group\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            code, body = t('/apisix/admin/consumers/jane/credentials/cred-jane',\n                ngx.HTTP_PUT,\n                [[{\n                    \"id\": \"cred-jane\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jane\"\n                        }\n                    }\n                }]]\n            )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: verify premium consumer has higher limit\n--- pipelined_requests eval\n[\n    \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\n    \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\",\n    \"GET /hello\"\n]\n--- more_headers\napikey: auth-jane\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 429]\n"
  },
  {
    "path": "t/plugin/limit-count-consumer-isolation.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nlog_level(\"info\");\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with key-auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: add consumer jack1 with limit-count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"jack1\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: add consumer jack2 with limit-count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack2\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"jack2\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: jack1 hits limit\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n    \"GET /hello\",\n    \"GET /hello\"\n]\n--- more_headers\napikey: jack1\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 5: jack2 starts fresh (isolated from jack1)\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n    \"GET /hello\",\n    \"GET /hello\"\n]\n--- more_headers\napikey: jack2\n--- error_code eval\n[200, 200, 503, 503]\n"
  },
  {
    "path": "t/plugin/limit-count-redis-cluster.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route, missing redis_cluster_nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: else clause did not match\"}\n\n\n\n=== TEST 2: set route, with redis_cluster_nodes and redis_cluster_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_timeout\": 1001,\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: up the limit\n--- request\nGET /hello\n\n\n\n=== TEST 5: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n\n\n\n=== TEST 6: up the limit again\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 200, 404, 200, 503]\n\n\n\n=== TEST 7: set route, four redis nodes, only one is valid\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 9999,\n                            \"time_window\": 60,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:8001\",\n                                \"127.0.0.1:8002\",\n                                \"127.0.0.1:8003\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 20 do\n                local code, body = t('/hello', ngx.HTTP_GET)\n                ngx.say(\"code: \", code)\n            end\n\n        }\n    }\n--- response_body\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\ncode: 200\n--- timeout: 10\n\n\n\n=== TEST 9: update route, use new limit configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local function set_route(count)\n                t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"uri\": \"/hello\",\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": ]] .. count .. [[,\n                                \"time_window\": 69,\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"redis-cluster\",\n                                \"redis_cluster_nodes\": [\n                                    \"127.0.0.1:5000\",\n                                    \"127.0.0.1:5001\"\n                                ],\n                                \"redis_cluster_name\": \"redis-cluster-1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    }]]\n                )\n            end\n\n            set_route(2)\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 5 do\n                local code, body = t('/hello', ngx.HTTP_GET)\n                ngx.say(\"code: \", code)\n            end\n\n            set_route(3)\n            local t = require(\"lib.test_admin\").test\n            for i = 1, 5 do\n                local code, body = t('/hello', ngx.HTTP_GET)\n                ngx.say(\"code: \", code)\n            end\n        }\n    }\n--- response_body\ncode: 200\ncode: 200\ncode: 503\ncode: 503\ncode: 503\ncode: 200\ncode: 200\ncode: 200\ncode: 503\ncode: 503\n\n\n\n=== TEST 10: set route, four redis nodes, no one is valid, with enable degradation switch\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 9999,\n                            \"time_window\": 60,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"allow_degradation\": true,\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:8001\",\n                                \"127.0.0.1:8002\",\n                                \"127.0.0.1:8003\",\n                                \"127.0.0.1:8004\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: enable degradation switch for TEST 10\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nconnection refused\n\n\n\n=== TEST 12: set route, use error type for redis_cluster_ssl and redis_cluster_ssl_verify\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_timeout\": 1001,\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:7000\",\n                                \"127.0.0.1:7001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\",\n                            \"redis_cluster_ssl\": \"true\",\n                            \"redis_cluster_ssl_verify\": \"false\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: else clause did not match\"}\n\n\n\n=== TEST 13: set route, redis_cluster_ssl_verify is true(will cause ssl handshake err), with enable degradation switch\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"allow_degradation\": true,\n                            \"redis_timeout\": 1001,\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:7000\",\n                                \"127.0.0.1:7001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\",\n                            \"redis_cluster_ssl\": true,\n                            \"redis_cluster_ssl_verify\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: enable degradation switch for TEST 13\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nfailed to do ssl handshake\n\n\n\n=== TEST 15: set route, with redis_cluster_nodes and redis_cluster_name redis_cluster_ssl and redis_cluster_ssl_verify\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_timeout\": 1001,\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:7000\",\n                                \"127.0.0.1:7001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\",\n                            \"redis_cluster_ssl\": true,\n                            \"redis_cluster_ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n"
  },
  {
    "path": "t/plugin/limit-count-redis-cluster2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: update route, use new limit configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello2\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            local old_X_RateLimit_Reset = 61\n            for i = 1, 3 do\n                local _, _, headers = t('/hello2', ngx.HTTP_GET)\n                ngx.sleep(1)\n                if tonumber(headers[\"X-RateLimit-Reset\"]) < old_X_RateLimit_Reset then\n                    old_X_RateLimit_Reset = tonumber(headers[\"X-RateLimit-Reset\"])\n                    ngx.say(\"OK\")\n                else\n                   ngx.say(\"WRONG\")\n                end\n            end\n            ngx.say(\"Done\")\n        }\n    }\n--- response_body\nOK\nOK\nOK\nDone\n\n\n\n=== TEST 2: test header X-RateLimit-Reset shouldn't be set to 0 after request be rejected\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello2\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5001\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            for i = 1, 3 do\n                local _, _, headers = t('/hello2', ngx.HTTP_GET)\n                ngx.sleep(1)\n                if tonumber(headers[\"X-RateLimit-Reset\"]) > 0 then\n                    ngx.say(\"OK\")\n                else\n                   ngx.say(\"WRONG\")\n                end\n            end\n            ngx.say(\"Done\")\n        }\n    }\n--- response_body\nOK\nOK\nOK\nDone\n"
  },
  {
    "path": "t/plugin/limit-count-redis-cluster3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{REDIS_NODE_0} = \"127.0.0.1:5000\";\n    $ENV{REDIS_NODE_1} = \"127.0.0.1:5001\";\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: modified redis script, cost == 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                redis_cluster_nodes = {\"127.0.0.1:5000\", \"127.0.0.1:5001\"},\n                redis_cluster_name = \"redis-cluster-1\",\n                redis_cluster_ssl = false,\n                redis_timeout = 1000,\n                key_type = \"var\",\n                time_window = 60,\n                show_limit_quota_header = true,\n                allow_degradation = false,\n                key = \"remote_addr\",\n                rejected_code = 503,\n                count = 3,\n                policy = \"redis-cluster\",\n                redis_cluster_ssl_verify = false\n            }\n\n            local lim_count_redis_cluster = require(\"apisix.plugins.limit-count.limit-count-redis-cluster\")\n            local lim = lim_count_redis_cluster.new(\"limit-count\", 3, 60, conf)\n            local uri = ngx.var.uri\n            local _, remaining, _ = lim:incoming(uri, 2)\n\n            ngx.say(\"remaining: \", remaining)\n        }\n    }\n--- response_body\nremaining: 1\n\n\n\n=== TEST 2: set route, with single node in redis_cluster_nodes and redis_cluster_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_timeout\": 1001,\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: up the limit for single node in redis_cluster_nodes\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n\n\n\n=== TEST 4: set route, with redis_cluster_nodes as environment variables and redis_cluster_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_timeout\": 1001,\n                            \"redis_cluster_nodes\": [\n                                \"$ENV://REDIS_NODE_0\",\n                                \"$ENV://REDIS_NODE_1\"\n                            ],\n                            \"redis_cluster_name\": \"redis-cluster-1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: up the limit with environment variables for redis_cluster_nodes\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n\n\n\n=== TEST 6: check redis cluster keepalive param\n--- config\n    location /t {\n        content_by_lua_block {\n            local lim_count_redis_cluster = require(\"apisix.plugins.limit-count.limit-count-redis-cluster\")\n            local conf = {\n                count = 2,\n                time_window = 60,\n                rejected_code = 503,\n                key = \"remote_addr\",\n                policy = \"redis-cluster\",\n                redis_timeout = 1001,\n                redis_keepalive_timeout = 10000,\n                redis_keepalive_pool = 100,\n                redis_cluster_nodes = {\n                    \"127.0.0.1:5000\"\n                },\n                redis_cluster_name = \"redis-cluster-1\"\n            }\n            local lim = lim_count_redis_cluster.new(\"limit-count\", 2, 60, conf)\n            local redis_conf = lim.red_cli.config\n            if redis_conf.keepalive_timeout == 10000 and redis_conf.keepalive_cons == 100 then\n                ngx.say(\"keepalive set success\")\n                return\n            end\n            ngx.say(\"keepalive set abnormal, keepalive_timeout: \",\n                    redis_conf.keepalive_timeout, \", keepalive_cons: \",redis_conf.keepalive_cons)\n        }\n    }\n--- response_body\nkeepalive set success\n"
  },
  {
    "path": "t/plugin/limit-count-redis.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all({password = \"foobared\"})\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route, missing redis host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: then clause did not match\"}\n\n\n\n=== TEST 2: set route, with redis host and port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: set route(default value: port and timeout)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: up the limit\n--- request\nGET /hello\n\n\n\n=== TEST 5: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n\n\n\n=== TEST 6: up the limit\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 200, 404, 200, 503]\n\n\n\n=== TEST 7: set route, with redis host, port and right password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- set redis password\n            local redis = require \"resty.redis\"\n\n            local red = redis:new()\n\n            red:set_timeout(1000) -- 1 sec\n\n            local ok, err = red:connect(\"127.0.0.1\", 6379)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            -- for get_reused_times works\n            -- local ok, err = red:set_keepalive(10000, 100)\n            -- if not ok then\n            --     ngx.say(\"failed to set keepalive: \", err)\n            --     return\n            -- end\n\n            local count\n            count, err = red:get_reused_times()\n            if 0 == count then\n                local res, err = red:config('set', 'requirepass', 'foobared')\n                if not res then\n                    ngx.say(\"failed to set: \", err)\n                    return\n                end\n            elseif err then\n                -- ngx.say(\"already set requirepass done: \", err)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001,\n                            \"redis_password\": \"foobared\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 9: up the limit\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 200, 404, 200, 503]\n\n\n\n=== TEST 10: set route, with redis host, port and wrong password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001,\n                            \"redis_password\": \"WRONG_foobared\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello_new\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code eval\n200\n\n\n\n=== TEST 11: request for TEST 10\n--- request\nGET /hello_new\n--- error_code eval\n500\n--- error_log\nfailed to limit count: WRONGPASS invalid username-password pair or user is disabled\n\n\n\n=== TEST 12: multi request for TEST 10\n--- pipelined_requests eval\n[\"GET /hello_new\", \"GET /hello1\", \"GET /hello1\", \"GET /hello_new\"]\n--- no_error_log\n[alert]\n--- error_code eval\n[500, 404, 404, 500]\n\n\n\n=== TEST 13: set route, with redis host, port and bad username and good password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001,\n                            \"redis_username\": \"bob\",\n                            \"redis_password\": \"somepassword\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: request for TEST 13\n--- request\nGET /hello\n--- error_code eval\n500\n--- error_log\nfailed to limit count: WRONGPASS invalid username-password pair or user is disabled\n\n\n\n=== TEST 15: set route, with redis host, port and good username and bad password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001,\n                            \"redis_username\": \"alice\",\n                            \"redis_password\": \"badpassword\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: request for TEST 15\n--- request\nGET /hello\n--- error_code eval\n500\n--- error_log\nfailed to limit count: WRONGPASS invalid username-password pair or user is disabled\n\n\n\n=== TEST 17: set route, with redis host, port and right username and password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001,\n                            \"redis_username\": \"alice\",\n                            \"redis_password\": \"somepassword\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 19: up the limit\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 200, 404, 200, 503]\n\n\n\n=== TEST 20: restore redis password to ''\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- set redis password\n            local redis = require \"resty.redis\"\n\n            local red = redis:new()\n\n            red:set_timeout(1000) -- 1 sec\n\n            local ok, err = red:connect(\"127.0.0.1\", 6379)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            -- for get_reused_times works\n            -- local ok, err = red:set_keepalive(10000, 100)\n            -- if not ok then\n            --     ngx.say(\"failed to set keepalive: \", err)\n            --     return\n            -- end\n\n            local count\n            count, err = red:get_reused_times()\n            if 0 == count then\n                local redis_password = \"foobared\"\n                if redis_password and redis_password ~= '' then\n                    local ok, err = red:auth(redis_password)\n                    if not ok then\n                        return nil, err\n                    end\n                end\n                local res, err = red:config('set', 'requirepass', '')\n                if not res then\n                    ngx.say(\"failed to set: \", err)\n                    return\n                end\n            elseif err then\n                -- ngx.say(\"already set requirepass done: \", err)\n                return\n            end\n        }\n    }\n--- error_code eval\n200\n"
  },
  {
    "path": "t/plugin/limit-count-redis2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route, with redis host and port and default database\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: set route, with redis host and port but wrong database\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 999999,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: use wrong database\n--- request\nGET /hello\n--- error_code eval\n500\n--- error_log\nfailed to limit count: failed to change redis db, err: ERR DB index is out of range\n\n\n\n=== TEST 4: set route, with redis host and port and right database\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 6: set route, with redis host but wrong port, with enable degradation switch\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"allow_degradation\": true,\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 16379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: enable degradation switch for TEST 6\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nconnection refused\n\n\n\n=== TEST 8: set route, with don't show limit quota header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"show_limit_quota_header\": false,\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: don't show limit quota header for TEST 8\n--- request\nGET /hello\n--- raw_response_headers_unlike eval\nqr/X-RateLimit-Limit/\n\n\n\n=== TEST 10: configuration from the same group should be the same\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"show_limit_quota_header\": false,\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001,\n                            \"group\": \"redis\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"show_limit_quota_header\": false,\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 2,\n                            \"redis_timeout\": 1001,\n                            \"group\": \"redis\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- error_log\n[error]\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: group conf mismatched\"}\n"
  },
  {
    "path": "t/plugin/limit-count-redis3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route, counter will be shared\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60,\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: test X-RateLimit-Reset second number could be decline\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local old_X_RateLimit_Reset = 61\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.sleep(2)\n                if tonumber(res.headers[\"X-RateLimit-Reset\"]) < old_X_RateLimit_Reset then\n                    old_X_RateLimit_Reset = tonumber(res.headers[\"X-RateLimit-Reset\"])\n                    ngx.say(\"OK\")\n                else\n                   ngx.say(\"WRONG\")\n                end\n            end\n            ngx.say(\"Done\")\n        }\n    }\n--- response_body\nOK\nOK\nDone\n\n\n\n=== TEST 3: set router\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 10,\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test header X-RateLimit-Remaining exist when limit rejected\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.sleep(1)\n                table.insert(ress, res.headers[\"X-RateLimit-Remaining\"])\n\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[\"1\",\"0\",\"0\"]\n\n\n\n=== TEST 5: set route, with redis host, port and SSL\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6380,\n                            \"redis_timeout\": 1001,\n                            \"redis_ssl\": true,\n                            \"redis_ssl_verify\": false\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 7: up the limit\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 200, 404, 200, 503]\n\n\n\n=== TEST 8: set route, with redis host, port, SSL and SSL verify is true(will cause ssl handshake err), with enable degradation switch\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"allow_degradation\": true,\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6380,\n                            \"redis_timeout\": 1001,\n                            \"redis_ssl\": true,\n                            \"redis_ssl_verify\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: enable degradation switch for TEST 8\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nfailed to do ssl handshake\n\n\n\n=== TEST 10: set router\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_database\": 1,\n                            \"redis_timeout\": 1001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: test header X-RateLimit-Reset shouldn't be set to 0 after request be rejected\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.sleep(1)\n                local reset = res.headers[\"X-RateLimit-Reset\"]\n                if tonumber(reset) <= 0 then\n                    ngx.say(\"failed\")\n                end\n\n            end\n            ngx.say(\"success\")\n        }\n    }\n--- response_body\nsuccess\n"
  },
  {
    "path": "t/plugin/limit-count-redis4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n\n    $ENV{REDIS_HOST} = \"127.0.0.1\";\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: modified redis script, cost == 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                allow_degradation = false,\n                rejected_code = 503,\n                redis_timeout = 1000,\n                key_type = \"var\",\n                time_window = 60,\n                show_limit_quota_header = true,\n                count = 3,\n                redis_host = \"127.0.0.1\",\n                redis_port = 6379,\n                redis_database = 0,\n                policy = \"redis\",\n                key = \"remote_addr\"\n            }\n\n            local lim_count_redis = require(\"apisix.plugins.limit-count.limit-count-redis\")\n            local lim = lim_count_redis.new(\"limit-count\", 3, 60, conf)\n            local uri = ngx.var.uri\n            local _, remaining, _ = lim:incoming(uri, 2)\n\n            ngx.say(\"remaining: \", remaining)\n        }\n    }\n--- response_body\nremaining: 1\n\n\n\n=== TEST 2: set route, with redis host as environment variable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"$remote_addr testcase_4_2\",\n                            \"key_type\":\"var_combination\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"$ENV://REDIS_HOST\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: up the limit with host environment variable\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n\n\n\n=== TEST 4: set route for keepalive test\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 20,\n                            \"time_window\": 1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"show_limit_quota_header\":false,\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"$ENV://REDIS_HOST\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify redis connection reused times in debug log\n--- log_level: debug\n--- pipelined_requests eval\n[ \"GET /hello\", \"GET /hello\"]\n--- error_log_like eval\n[qr/redis connection reused times: 0/, qr/redis connection reused times: 1/]\n"
  },
  {
    "path": "t/plugin/limit-count-rules.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: configure count/time_window and rules at same time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 5,\n                                \"rejected_code\": 503,\n                                \"key_type\": \"var\",\n                                \"key\": \"remote_addr\",\n                                \"rules\": [\n                                    {\n                                        \"count\": 1,\n                                        \"time_window\": 10,\n                                        \"key\": \"${http_company}\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches both schemas 1 and 2\"}\n\n\n\n=== TEST 2: configure multiple rules with same key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rules\": [\n                                    {\n                                        \"count\": 5,\n                                        \"time_window\": 10,\n                                        \"key\": \"${http_company}\"\n                                    },\n                                    {\n                                        \"count\": 8,\n                                        \"time_window\": 20,\n                                        \"key\": \"${http_company}\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: duplicate key '${http_company}' in rules\"}\n\n\n\n=== TEST 3: setup route with rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rejected_msg\" : \"rejected\",\n                                \"rules\": [\n                                    {\n                                        \"key\": \"${http_user}\",\n                                        \"count\": \"${http_jack_count}\",\n                                        \"time_window\": 60\n                                    },\n                                    {\n                                        \"key\": \"${http_project}\",\n                                        \"count\": \"${http_apisix_count}\",\n                                        \"time_window\": 60\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: no any rule matched\n--- request\nGET /hello\n--- error_code: 500\n--- error_log\nfailed to get rate limit rules\n\n\n\n=== TEST 5: match user rule\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\nuser: jack\njack-count: 2\n--- error_code eval\n[200, 200, 503]\n--- response_body eval\n[\"hello world\\n\", \"hello world\\n\", \"{\\\"error_msg\\\":\\\"rejected\\\"}\\n\"]\n\n\n\n=== TEST 6: match project rule\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\"]\n--- more_headers\nproject: apisix\napisix-count: 3\n--- error_code eval\n[200, 200, 200, 503]\n--- response_body eval\n[\"hello world\\n\", \"hello world\\n\", \"hello world\\n\", \"{\\\"error_msg\\\":\\\"rejected\\\"}\\n\"]\n\n\n\n=== TEST 7: setup route with rules with variables with default values\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rejected_msg\" : \"rejected\",\n                                \"rules\": [\n                                    {\n                                        \"count\": \"${http_count ?? 2}\",\n                                        \"time_window\": \"${http_tw ?? 5}\",\n                                        \"key\": \"${remote_addr}\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: rules with variables in count - default value\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n--- response_body eval\n[\"hello world\\n\", \"hello world\\n\", \"{\\\"error_msg\\\":\\\"rejected\\\"}\\n\"]\n\n\n\n=== TEST 9: rules with variables in count - with header\n--- setup\n    ngx.sleep(5)\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\"]\n--- more_headers\ncount: 1\n--- error_code eval\n[200, 503]\n--- response_body eval\n[\"hello world\\n\", \"{\\\"error_msg\\\":\\\"rejected\\\"}\\n\"]\n\n\n\n=== TEST 10: rules with same key and custom headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rejected_msg\" : \"rejected\",\n                                \"show_limit_quota_header\": true,\n                                \"rules\": [\n                                    {\n                                        \"count\": 2,\n                                        \"time_window\": 2,\n                                        \"key\": \"${remote_addr}_2s\"\n                                    },\n                                    {\n                                        \"count\": 3,\n                                        \"time_window\": 5,\n                                        \"key\": \"${remote_addr}_5s\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: test rules with same key\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            for i = 1, 2, 1 do\n                local res = httpc:request_uri(uri)\n                if res.status ~= 200 then\n                    ngx.say(\"first two requests failed, status: \" .. res.status)\n                    return\n                end\n            end\n\n            -- req 3, rejected by rule 1\n            res = httpc:request_uri(uri)\n            if res.status ~= 503 then\n                ngx.say(\"req 3 should be rejected by rule 1, but got status: \", res.status)\n                return\n            end\n\n            ngx.sleep(2)\n\n            -- req 4, after sleep\n            res = httpc:request_uri(uri)\n            if res.status ~= 200 then\n                ngx.say(\"req 4 failed, status: \", res.status)\n                return\n            end\n\n            -- req 5, rejected by rule 2\n            res = httpc:request_uri(uri)\n            if res.status ~= 503 then\n                ngx.say(\"req 5 should be rejected by rule 2, but got status: \", res.status)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: setup route with header_prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rejected_msg\" : \"rejected\",\n                                \"rules\": [\n                                    {\n                                        \"key\": \"${http_user}\",\n                                        \"count\": \"${http_jack_count}\",\n                                        \"time_window\": 60,\n                                        \"header_prefix\": \"jack\"\n                                    },\n                                    {\n                                        \"key\": \"${http_project}\",\n                                        \"count\": \"${http_apisix_count}\",\n                                        \"time_window\": 60,\n                                        \"header_prefix\": \"bar\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: match jack\n--- request\nGET /hello\n--- more_headers\nuser: jack\njack-count: 2\n--- error_code: 200\n--- response_headers\nX-Jack-RateLimit-Limit: 2\nX-Jack-RateLimit-Remaining: 1\nX-Jack-RateLimit-Reset: 60\n\n\n\n=== TEST 14: match bar\n--- request\nGET /hello\n--- more_headers\nproject: apisix\napisix-count: 3\n--- error_code: 200\n--- response_headers\nX-Bar-RateLimit-Limit: 3\nX-Bar-RateLimit-Remaining: 2\nX-Bar-RateLimit-Reset: 60\n\n\n\n=== TEST 15: setup route without header_prefix\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rejected_msg\" : \"rejected\",\n                                \"rules\": [\n                                    {\n                                        \"key\": \"${http_user}\",\n                                        \"count\": \"${http_jack_count}\",\n                                        \"time_window\": 60\n                                    },\n                                    {\n                                        \"key\": \"${http_project}\",\n                                        \"count\": \"${http_apisix_count}\",\n                                        \"time_window\": 60\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: match jack\n--- request\nGET /hello\n--- more_headers\nuser: jack\njack-count: 2\n--- error_code: 200\n--- response_headers\nX-1-RateLimit-Limit: 2\nX-1-RateLimit-Remaining: 1\nX-1-RateLimit-Reset: 60\n\n\n\n=== TEST 17: match bar\n--- request\nGET /hello\n--- more_headers\nproject: apisix\napisix-count: 3\n--- error_code: 200\n--- response_headers\nX-2-RateLimit-Limit: 3\nX-2-RateLimit-Remaining: 2\nX-2-RateLimit-Reset: 60\n\n\n\n=== TEST 18: use variable with default value in rules.key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"rejected_code\": 503,\n                                \"rules\": [\n                                    {\n                                        \"count\": 1,\n                                        \"time_window\": 10,\n                                        \"key\": \"${http_project ?? apisix}\"\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: with project header\n--- request\nGET /hello\n--- more_headers\nproject: kubernetes\n--- error_log eval\nqr/limit key: \\/apisix\\/routes\\/1:[^:]+:kubernetes/\n\n\n\n=== TEST 20: without project header\n--- request\nGET /hello\n--- error_log eval\nqr/limit key: \\/apisix\\/routes\\/1:[^:]+:apisix/\n"
  },
  {
    "path": "t/plugin/limit-count-variable.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: use variable in count and time_window with default value\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": \"${http_count ?? 2}\",\n                                \"time_window\": \"${http_time_window ?? 5}\",\n                                \"rejected_code\": 503,\n                                \"key_type\": \"var\",\n                                \"key\": \"remote_addr\",\n                                \"policy\": \"local\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: request without count/time_window headers\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503]\n\n\n\n=== TEST 3: request with count header\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\ncount: 5\n--- error_code eval\n[200, 200, 200, 200, 200, 503]\n\n\n\n=== TEST 4: request with count and time_window header\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require(\"resty.http\")\n            local core = require(\"apisix.core\")\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local opt = {method = \"GET\", headers = { [\"count\"] = 3, [\"time-window\"] = \"2\" }}\n            local httpc = http.new()\n\n            for i = 1, 3, 1 do\n                local res = httpc:request_uri(uri, opt)\n                if res.status ~= 200 then\n                    ngx.say(\"first two requests should return 200, but got \" .. res.status)\n                    return\n                end\n                if res.headers[\"X-RateLimit-Limit\"] ~= \"3\" then\n                    ngx.say(\"X-RateLimit-Limit should be 3, but got \" .. core.json.encode(res.headers))\n                    return\n                end\n            end\n            local res = httpc:request_uri(uri, opt)\n            if res.status ~= 503 then\n                ngx.say(\"third requests should return 503, but got \" .. res.status)\n                return\n            end\n\n            ngx.sleep(2)\n\n            for i = 1, 3, 1 do\n                local res = httpc:request_uri(uri, opt)\n                if res.status ~= 200 then\n                    ngx.say(\"two requests after sleep 2s should return 200, but got \" .. res.status)\n                    return\n                end\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- timeout: 10\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/limit-count.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-count\")\n            local ok, err = plugin.check_schema({count = 2, time_window = 60, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: set key empty\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-count\")\n            local ok, err = plugin.check_schema({count = 2, time_window = 60, rejected_code = 503})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 3: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 5: up the limit\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 503, 404, 503, 503]\n\n\n\n=== TEST 6: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 3,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 503]\n\n\n\n=== TEST 8: invalid route: missing count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"time_window\": 60,\n                                \"rejected_code\": 503\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 9: invalid route: wrong count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": -100,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"count\\\" validation failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 10: invalid route: wrong count + POST method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes',\n                 ngx.HTTP_POST,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": -100,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"count\\\" validation failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 11: invalid service: missing count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"time_window\": 60,\n                                \"rejected_code\": 503\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 12: invalid service: wrong count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": -100,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"count\\\" validation failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 13: invalid service: wrong count + POST method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services',\n                 ngx.HTTP_POST,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": -100,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"count\\\" validation failed: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 14: set route without id in post body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 61,\n                                \"rejected_code\": 503,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 16: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 18: set route(key: server_addr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"server_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 20: default rejected_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 80,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: when the count is changed, check  the limit is correct\n--- config\n    location /t1 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 1,\n                                \"time_window\": 80,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\",\"GET /hello\",\"GET /t1\", \"GET /hello\",\"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 200, 200, 503]\n\n\n\n=== TEST 22: when the count is changed, check  the limit is correct(from 1 to 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 82,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n    location /t1 {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 1,\n                                \"time_window\": 82,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /t1\", \"GET /hello\", \"GET /hello\", \"GET /t\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 200, 200, 200, 503]\n\n\n\n=== TEST 23: when the count is changed, check  the limit is correct(from 2 to 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 83,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /t\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /t\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 503, 200, 503, 503, 503]\n\n\n\n=== TEST 24: create consumer and bind key-auth plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"consumer_jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: create route and consumer_name is consumer_jack\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"key-auth\": {},\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"consumer_name\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 26: up the limit\n--- more_headers\napikey: auth-jack\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 27: set service(id: 1) and binding limit-count plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"service_id\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 28: set route(id: 1) and bind  service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"service_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 29: the number of requests exceeds the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 30: set service(id: 1), and no limit-count plugin configured\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{}]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 31: set route(id: 1) and bind  service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 91,\n                                \"rejected_code\": 503,\n                                \"key\": \"service_id\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"service_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 32: the number of requests exceeds the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 33: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 34: delete service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 35: use 'remote_addr' as default key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 95,\n                                \"rejected_code\": 503\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 36: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 37: add service and route, upstream is the domain name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 3,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test.com:1980\": 1,\n                            \"foo.com:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/hello\",\n                    \"service_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 38: normal, the result is as expected\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        if domain == \"test.com\" then\n            return {address = \"127.0.0.1\"}\n        end\n\n        if domain == \"foo.com\" then\n            return {address = \"127.0.0.1\"}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 503, 503]\n\n\n\n=== TEST 39: plugin is bound to the route and upstream is the domain name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"methods\": [\"GET\"],\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 3,\n                            \"time_window\": 99,\n                            \"rejected_code\": 503\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test.com:1980\": 1,\n                            \"foo.com:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 40: normal, the result is as expected\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        if domain == \"test.com\" then\n            return {address = \"127.0.0.1\"}\n        end\n\n        if domain == \"foo.com\" then\n            return {address = \"127.0.0.1\"}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 503, 503]\n\n\n\n=== TEST 41: check_schema failed (the `count` attribute is equal to 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-count\")\n            local ok, err = plugin.check_schema({count = 0, time_window = 60, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body eval\nqr/property \\\"count\\\" validation failed: value should match only one schema, but matches none/\n\n\n\n=== TEST 42: check_schema failed (the `time_window` attribute is equal to 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-count\")\n            local ok, err = plugin.check_schema({count = 2, time_window = 0, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body eval\nqr/property \\\"time_window\\\" validation failed: value should match only one schema, but matches none/\n"
  },
  {
    "path": "t/plugin/limit-count2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: invalid route: wrong rejected_msg type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 600,\n                            \"rejected_code\": 503,\n                            \"rejected_msg\": true,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"rejected_msg\\\" validation failed: wrong type: expected string, got boolean\"}\n\n\n\n=== TEST 2: invalid route: wrong rejected_msg length\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 600,\n                            \"rejected_code\": 503,\n                            \"rejected_msg\": \"\",\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: property \\\"rejected_msg\\\" validation failed: string too short, expected at least 1, got 0\"}\n\n\n\n=== TEST 3: set route, with rejected_msg\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 600,\n                            \"rejected_code\": 503,\n                            \"rejected_msg\": \"Requests are too frequent, please try again later.\",\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: rejected_msg, request normal\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: rejected_msg, request frequent\n--- request\nGET /hello\n--- error_code: 503\n--- response_body\n{\"error_msg\":\"Requests are too frequent, please try again later.\"}\n\n\n\n=== TEST 6: update route, use new limit configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"http_a\",\n                            \"key_type\": \"var\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: exceed the burst when key_type is var\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {a = 1}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n\n\n\n=== TEST 8: bypass empty key when key_type is var\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n\n\n\n=== TEST 9: update plugin to set key_type to var_combination\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"$http_a $http_b\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: exceed the burst when key_type is var_combination\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {a = 1}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n\n\n\n=== TEST 11: don`t exceed the burst when key_type is var_combination\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {a = i}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[503,200]\n\n\n\n=== TEST 12: request when key is missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 13: update plugin to set invalid key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key\": \"abcdefgh\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: request when key is invalid\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 15: limit count in group\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"group\": \"services_1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"group\": \"services_1\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello_chunked\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit multiple paths\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello_chunked\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local uri\n                if i % 2 == 1 then\n                    uri = uri1\n                else\n                    uri = uri2\n                end\n\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n\n\n\n=== TEST 17: limit count in group, configuration is from services\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"group\": \"afafafhao\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_id\": \"1\",\n                    \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_id\": \"1\",\n                    \"uri\": \"/hello_chunked\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: hit multiple paths\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello_chunked\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local uri\n                if i % 2 == 1 then\n                    uri = uri1\n                else\n                    uri = uri2\n                end\n\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200,503,503]\n\n\n\n=== TEST 19: configuration from the same group should be the same\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"group\": \"afafafhao\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- error_log\n[error]\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-count err: group conf mismatched\"}\n\n\n\n=== TEST 20: group with constant key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"key_type\": \"constant\",\n                            \"group\": \"afafafhao2\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: hit multiple paths\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local uri2 = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello_chunked\"\n            local ress = {}\n            for i = 1, 4 do\n                local httpc = http.new()\n                local uri\n                if i % 2 == 1 then\n                    uri = uri1\n                else\n                    uri = uri2\n                end\n\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- grep_error_log eval\nqr/limit key: afafafhao2:remote_addr/\n--- grep_error_log_out\nlimit key: afafafhao2:remote_addr\nlimit key: afafafhao2:remote_addr\nlimit key: afafafhao2:remote_addr\nlimit key: afafafhao2:remote_addr\n--- response_body\n[200,200,503,503]\n\n\n\n=== TEST 22: group with disable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60,\n                            \"rejected_code\": 503,\n                            \"group\": \"abcd\",\n                            \"_meta\": {\n                                \"disable\": false\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/limit-count3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route, counter will be shared\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200]\n\n\n\n=== TEST 3: set route with conf not changed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"labels\": {\"l\": \"a\"},\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[503,503]\n\n\n\n=== TEST 5: set route with conf changed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 61\n                        }\n                    },\n                    \"labels\": {\"l\": \"a\"},\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200]\n\n\n\n=== TEST 7: set another route with the same conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello1\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 61\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: avoid sharing the same counter\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello1\"\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[200,200]\n\n\n\n=== TEST 9: add consumer jack1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack1\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2019\",\n                            \"password\": \"123456\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: add consumer jack2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack2\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"jack2020\",\n                            \"password\": \"123456\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: set route with consumers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"basic-auth\": {},\n                        \"consumer-restriction\":{\n                            \"whitelist\":[\n                                \"jack1\",\n                                \"jack2\"\n                            ],\n                            \"rejected_code\": 403,\n                            \"type\":\"consumer_name\"\n                        },\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60,\n                            \"rejected_code\": 429\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit jack1, pass\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMTk6MTIzNDU2\n--- response_body\nhello world\n\n\n\n=== TEST 13: hit jack2, reject, the two consumers share the same counter\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic amFjazIwMjA6MTIzNDU2\n--- error_code: 429\n"
  },
  {
    "path": "t/plugin/limit-count4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route, counter will be shared\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 1,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: test X-RateLimit-Reset second number could be decline\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local old_X_RateLimit_Reset = 61\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.sleep(2)\n                if tonumber(res.headers[\"X-RateLimit-Reset\"]) < old_X_RateLimit_Reset then\n                    old_X_RateLimit_Reset = tonumber(res.headers[\"X-RateLimit-Reset\"])\n                    ngx.say(\"OK\")\n                else\n                   ngx.say(\"WRONG\")\n                end\n            end\n            ngx.say(\"Done\")\n        }\n    }\n--- response_body\nOK\nOK\nDone\n\n\n\n=== TEST 3: set route, counter will be shared\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"limit-count\": {\n                            \"count\": 2,\n                            \"time_window\": 60\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test header X-RateLimit-Remaining exist when limit rejected\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.sleep(1)\n                table.insert(ress, res.headers[\"X-RateLimit-Remaining\"])\n\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- response_body\n[\"1\",\"0\",\"0\"]\n\n\n\n=== TEST 5: test header X-RateLimit-Reset shouldn't be set to 0 after request be rejected\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local ress = {}\n            for i = 1, 3 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.sleep(1)\n                local reset = res.headers[\"X-RateLimit-Reset\"]\n                if tonumber(reset) <= 0 then\n                    ngx.say(\"failed\")\n                end\n\n            end\n            ngx.say(\"success\")\n        }\n    }\n--- response_body\nsuccess\n"
  },
  {
    "path": "t/plugin/limit-count5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n\n    $ENV{LIMIT_COUNT_KEY} = \"remote_addr\";\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: modified limit-count.incoming, cost == 2\n--- config\n    location = /t {\n        content_by_lua_block {\n            local conf = {\n                time_window = 60,\n                count = 10,\n                allow_degradation = false,\n                key_type = \"var\",\n                policy = \"local\",\n                rejected_code = 503,\n                show_limit_quota_header = true,\n                key = \"remote_addr\"\n            }\n            local limit_count_local = require \"apisix.plugins.limit-count.limit-count-local\"\n            local lim = limit_count_local.new(\"plugin-limit-count\", 10, 60)\n            local uri = ngx.var.uri\n            for i = 1, 7 do\n                local delay, err = lim:incoming(uri, true, conf, 2)\n                if not delay then\n                    ngx.say(err)\n                else\n                    local remaining = err\n                    ngx.say(\"remaining: \", remaining)\n                end\n            end\n        }\n    }\n--- request\n    GET /t\n--- response_body\nremaining: 8\nremaining: 6\nremaining: 4\nremaining: 2\nremaining: 0\nrejected\nrejected\n\n\n\n=== TEST 2: set route(id: 1) using environment variable for key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key\": \"$ENV://LIMIT_COUNT_KEY\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: up the limit with environment variable for key\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 4: customize rate limit headers by plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"limit-count\": {\n                                \"count\": 10,\n                                \"time_window\": 60,\n                                \"rejected_code\": 503,\n                                \"key_type\": \"var\",\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            local code, meta_body = t('/apisix/admin/plugin_metadata/limit-count',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"limit_header\":\"APISIX-RATELIMIT-QUOTA\",\n                        \"remaining_header\":\"APISIX-RATELIMIT-REMAINING\",\n                        \"reset_header\":\"APISIX-RATELIMIT-RESET\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: check rate limit headers\n--- request\nGET /hello\n--- response_headers_like\nAPISIX-RATELIMIT-QUOTA: 10\nAPISIX-RATELIMIT-REMAINING: 9\nAPISIX-RATELIMIT-RESET: \\d+\n"
  },
  {
    "path": "t/plugin/limit-req-redis-cluster.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-req\")\n            local ok, err = plugin.check_schema({\n                rate = 1,\n                burst = 0,\n                rejected_code = 503,\n                key = 'remote_addr',\n                policy = 'redis',\n                redis_host = '127.0.0.1'\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: add plugin with redis cluster with ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:7000\",\n                                \"127.0.0.1:7001\",\n                                \"127.0.0.1:7002\"\n                            ],\n                            \"redis_cluster_ssl\": true,\n                            \"redis_cluster_ssl_verify\": false\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: not exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 4: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 503]\n\n\n\n=== TEST 5: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 7: wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": -1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-req err: property \\\"rate\\\" validation failed: expected -1 to be greater than 0\"}\n\n\n\n=== TEST 8: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 10: set route (key: server_addr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"rejected_code\": 503,\n                            \"key\": \"server_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: default rejected_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: consumer binds the limit-req plugin and `key` is `consumer_name`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 3,\n                            \"burst\": 2,\n                            \"rejected_code\": 403,\n                            \"key\": \"consumer_name\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: route add \"key-auth\" plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: not exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 200, 200]\n\n\n\n=== TEST 15: update the limit-req plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 403,\n                            \"key\": \"consumer_name\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 403, 403, 403]\n\n\n\n=== TEST 17: key is consumer_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 2,\n                            \"burst\": 1,\n                            \"key\": \"consumer_name\",\n                            \"policy\": \"redis-cluster\",\n                            \"redis_cluster_name\": \"test\",\n                            \"redis_cluster_nodes\": [\n                                \"127.0.0.1:5000\",\n                                \"127.0.0.1:5002\"\n                            ]\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: get \"consumer_name\" is empty\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 19: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/new_consumer', ngx.HTTP_DELETE)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n            ngx.status =code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: check_schema failed (the `rate` attribute is equal to 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-req\")\n            local ok, err = plugin.check_schema({rate = 0, burst = 0, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/property \\\"rate\\\" validation failed: expected 0 to be greater than 0/\n\n\n\n=== TEST 22: check redis cluster keepalive param\n--- config\n    location /t {\n        content_by_lua_block {\n            local lim_req_redis_cluster = require(\"apisix.plugins.limit-req.limit-req-redis-cluster\")\n            local conf = {\n                rate = 2,\n                burst = 1,\n                key = \"consumer_name\",\n                policy = \"redis-cluster\",\n                redis_cluster_name = \"test\",\n                redis_cluster_nodes = {\n                    \"127.0.0.1:5000\",\n                    \"127.0.0.1:5002\"\n                },\n                redis_keepalive_timeout = 10000,\n                redis_keepalive_pool = 100\n            }\n            local lim = lim_req_redis_cluster.new(\"limit-req\", conf, 2, 1)\n            local redis_conf = lim.red_cli.config\n            if redis_conf.keepalive_timeout ==10000 and redis_conf.keepalive_cons == 100  then\n                ngx.say(\"keepalive set success\")\n                return\n            end\n            ngx.say(\"keepalive set abnormal, keepalive_timeout: \",\n                    redis_conf.keepalive_timeout, \", keepalive_cons: \",redis_conf.keepalive_cons)\n        }\n    }\n--- request\nGET /t\n--- response_body\nkeepalive set success\n"
  },
  {
    "path": "t/plugin/limit-req-redis.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn')\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $extra_init_worker_by_lua = $block->extra_init_worker_by_lua // \"\";\n    $extra_init_worker_by_lua .= <<_EOC_;\n        require(\"lib.test_redis\").flush_all()\n_EOC_\n\n    $block->set_value(\"extra_init_worker_by_lua\", $extra_init_worker_by_lua);\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-req\")\n            local ok, err = plugin.check_schema({\n                rate = 1,\n                burst = 0,\n                rejected_code = 503,\n                key = 'remote_addr',\n                policy = 'redis',\n                redis_host = '127.0.0.1'\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: add plugin with redis\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: not exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 4: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 200, 200, 200, 503]\n\n\n\n=== TEST 5: verify redis connection reused times in debug log\n--- log_level: debug\n--- pipelined_requests eval\n[ \"GET /hello\", \"GET /hello\"]\n--- error_log_like eval\n[qr/redis connection reused times: 0/, qr/redis connection reused times: 1/]\n\n\n\n=== TEST 6: update plugin with username password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_username\": \"alice\",\n                            \"redis_password\": \"somepassword\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 8: update plugin with username, wrong password\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379,\n                            \"redis_username\": \"alice\",\n                            \"redis_password\": \"someerrorpassword\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: catch wrong pass\n--- request\nGET /hello\n--- error_code: 500\n--- error_log\nfailed to limit req: WRONGPASS invalid username-password pair or user is disabled.\n\n\n\n=== TEST 10: invalid route: missing redis_host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-req err: then clause did not match\"}\n\n\n\n=== TEST 11: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 13: set route (key: server_addr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"rejected_code\": 503,\n                            \"key\": \"server_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: default rejected_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"key\": \"remote_addr\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: consumer binds the limit-req plugin and `key` is `consumer_name`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 3,\n                            \"burst\": 2,\n                            \"rejected_code\": 403,\n                            \"key\": \"consumer_name\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: route add \"key-auth\" plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: not exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 200, 200]\n\n\n\n=== TEST 18: update the limit-req plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 403,\n                            \"key\": \"consumer_name\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 403, 403, 403]\n\n\n\n=== TEST 20: key is consumer_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 2,\n                            \"burst\": 1,\n                            \"key\": \"consumer_name\",\n                            \"policy\": \"redis\",\n                            \"redis_host\": \"127.0.0.1\",\n                            \"redis_port\": 6379\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: get \"consumer_name\" is empty\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 22: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/new_consumer', ngx.HTTP_DELETE)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n            ngx.status =code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: check_schema failed (the `rate` attribute is equal to 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-req\")\n            local ok, err = plugin.check_schema({rate = 0, burst = 0, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/property \\\"rate\\\" validation failed: expected 0 to be greater than 0/\n"
  },
  {
    "path": "t/plugin/limit-req-shared-counter.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: consumer jack with limit-req\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/consumers/jack',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"jack-key\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 1,\n                            \"burst\": 1,\n                            \"key\": \"remote_addr\",\n                            \"rejected_code\": 429,\n                            \"nodelay\": true\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: set 2 routes with key-auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello1\",\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\npassed\n\n\n\n=== TEST 3: verify rate limiting shared by both routes\n--- pipelined_requests eval\n[\n    \"GET /hello\",\n    \"GET /hello\",\n    \"GET /hello1\"\n]\n--- more_headers\napikey: jack-key\n--- error_code eval\n[200, 200, 429]\n"
  },
  {
    "path": "t/plugin/limit-req.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-req\")\n            local ok, err = plugin.check_schema({rate = 1, burst = 0, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong value of key\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-conn\")\n            local ok, err = plugin.check_schema({burst = 0, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches none\ndone\n\n\n\n=== TEST 3: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: not exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 5: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 503, 503, 503]\n\n\n\n=== TEST 7: wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": -1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin limit-req err: property \\\"rate\\\" validation failed: expected -1 to be greater than 0\"}\n\n\n\n=== TEST 8: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 10: set route (key: server_addr)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"rejected_code\": 503,\n                            \"key\": \"server_addr\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: default rejected_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 4,\n                            \"burst\": 2,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: consumer binds the limit-req plugin and `key` is `consumer_name`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 3,\n                            \"burst\": 2,\n                            \"rejected_code\": 403,\n                            \"key\": \"consumer_name\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: route add \"key-auth\" plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"key-auth\": {}\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: not exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 200, 200]\n\n\n\n=== TEST 15: update the limit-req plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"new_consumer\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-jack\"\n                        },\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 403,\n                            \"key\": \"consumer_name\"\n                        }\n                    }\n                }]]\n                )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: exceeding the burst\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-jack\n--- error_code eval\n[200, 403, 403, 403]\n\n\n\n=== TEST 17: key is consumer_name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 2,\n                            \"burst\": 1,\n                            \"key\": \"consumer_name\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: get \"consumer_name\" is empty\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 19: delete consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers/new_consumer', ngx.HTTP_DELETE)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: delete route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n            ngx.status =code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: check_schema failed (the `rate` attribute is equal to 0)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.limit-req\")\n            local ok, err = plugin.check_schema({rate = 0, burst = 0, rejected_code = 503, key = 'remote_addr'})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/property \\\"rate\\\" validation failed: expected 0 to be greater than 0/\n"
  },
  {
    "path": "t/plugin/limit-req2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin for delay test\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 4,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"desc\": \"upstream_node\",\n                        \"uri\": \"/hello*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: the second request will timeout because of delay,  error code will be ''\n--- abort\n--- timeout: 500ms\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, '']\n\n\n\n=== TEST 3: add nodelay flag\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/plugins',\n                ngx.HTTP_PATCH,\n                [[{\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 4,\n                            \"rejected_code\": 503,\n                            \"key\": \"remote_addr\",\n                            \"nodelay\": true\n                        }\n                 }]]\n            )\n\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: the second request will not timeout because of nodelay\n--- abort\n--- timeout: 500ms\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200]\n\n\n\n=== TEST 5: key type is var_combination\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"$http_a $http_b\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: exceed the burst\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {a = 1}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- request\nGET /t\n--- response_body\n[200,503]\n\n\n\n=== TEST 7: don't exceed the burst\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {a = i}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- request\nGET /t\n--- response_body\n[200,200]\n\n\n\n=== TEST 8: request when key is missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- request\nGET /t\n--- response_body\n[200,503]\n--- error_log\nThe value of the configured key is empty, use client IP instead\n\n\n\n=== TEST 9: update plugin to set invalid key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"abcdefgh\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: request when key is invalid\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require \"t.toolkit.json\"\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n\n            local ress = {}\n            for i = 1, 2 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                table.insert(ress, res.status)\n            end\n            ngx.say(json.encode(ress))\n        }\n    }\n--- request\nGET /t\n--- response_body\n[200,503]\n--- error_log\nThe value of the configured key is empty, use client IP instead\n"
  },
  {
    "path": "t/plugin/limit-req3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create route with limit-req plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-req\": {\n                            \"rate\": 0.1,\n                            \"burst\": 0.1,\n                            \"rejected_code\": 503,\n                            \"key\": \"$remote_addr\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\",\n                    \"host\": \"www.test.com\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: create ssl(sni: www.test.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 3: use HTTP version 2 to request\n--- exec\ncurl --http2 --parallel -k https://www.test.com:1994/hello https://www.test.com:1994/hello --resolve www.test.com:1994:127.0.0.1\n--- response_body_like\n503 Service Temporarily Unavailable.*.hello world\n"
  },
  {
    "path": "t/plugin/log-rotate.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:                          # plugin list\n  - log-rotate\n\nplugin_attr:\n  log-rotate:\n    interval: 1\n    max_kept: 3\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: log rotate\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.log(ngx.ERR, \"start xxxxxx\")\n            ngx.sleep(2.5)\n            local has_split_access_file = false\n            local has_split_error_file = false\n            local lfs = require(\"lfs\")\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__access.log$\") then\n                    has_split_access_file = true\n                end\n\n                if string.match(file_name, \"__error.log$\") then\n                    local f = assert(io.open(ngx.config.prefix() .. \"/logs/\" .. file_name, \"r\"))\n                    local content = f:read(\"*all\")\n                    f:close()\n                    local index = string.find(content, \"start xxxxxx\")\n                    if index then\n                        has_split_error_file = true\n                    end\n                end\n            end\n\n            if not has_split_access_file or not has_split_error_file then\n               ngx.status = 500\n            else\n               ngx.status = 200\n            end\n        }\n    }\n--- error_code eval\n[200]\n\n\n\n=== TEST 2: in current log\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.1)\n            ngx.log(ngx.WARN, \"start xxxxxx\")\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nstart xxxxxx\n\n\n\n=== TEST 3: fix: ensure only one timer is running\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.5)\n            local t = require(\"lib.test_admin\").test\n            local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                        ngx.HTTP_PUT)\n\n            ngx.status = code\n            ngx.say(org_body)\n\n            ngx.sleep(1)\n\n            local lfs = require(\"lfs\")\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__error.log$\") then\n                    local f = assert(io.open(ngx.config.prefix() .. \"/logs/\" .. file_name, \"r\"))\n                    local content = f:read(\"*all\")\n                    f:close()\n                    local counter = 0\n                    ngx.re.gsub(content, [=[run timer\\[plugin#log-rotate\\]]=], function()\n                        counter = counter + 1\n                        return \"\"\n                    end)\n\n                    if counter ~= 1 then\n                        ngx.say(\"not a single rotator run at the same time: \", file_name)\n                    end\n                end\n            end\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 4: disable log-rotate via hot reload\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = [[\napisix:\n  node_listen: 1984\n  admin_key: null\nplugins:\n  - prometheus\n            ]]\n            require(\"lib.test_admin\").set_config_yaml(data)\n            local t = require(\"lib.test_admin\").test\n            local code, _, org_body = t('/apisix/admin/plugins/reload',\n                                        ngx.HTTP_PUT)\n\n            ngx.status = code\n            ngx.say(org_body)\n\n            ngx.sleep(2.1) -- make sure two files will be rotated out if we don't disable it\n\n            local n_split_error_file = 0\n            local lfs = require(\"lfs\")\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__error.log$\") then\n                    n_split_error_file = n_split_error_file + 1\n                end\n            end\n\n            -- Before hot reload, the log rotate may or may not take effect.\n            -- It depends on the time we start the test\n            ngx.say(n_split_error_file <= 1)\n        }\n    }\n--- response_body\ndone\ntrue\n\n\n\n=== TEST 5: check file changes (disable compression)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(2)\n\n            local default_logs = {}\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__error.log$\") or string.match(file_name, \"__access.log$\") then\n                    local filepath = ngx.config.prefix() .. \"/logs/\" .. file_name\n                    local attr = lfs.attributes(filepath)\n                    if attr then\n                        default_logs[filepath] = { change = attr.change, size = attr.size }\n                    end\n                end\n            end\n\n            ngx.sleep(1)\n\n            local passed = false\n            for filepath, origin_attr in pairs(default_logs) do\n                local check_attr = lfs.attributes(filepath)\n                if check_attr.change == origin_attr.change and check_attr.size == origin_attr.size then\n                    passed = true\n                else\n                    passed = false\n                    break\n                end\n            end\n\n            if passed then\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/log-rotate2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 1\n    max_kept: 3\n    enable_compression: true\n_EOC_\n\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: log rotate, with enable log file compression\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.log(ngx.ERR, \"start xxxxxx\")\n            ngx.sleep(3.5)\n            local has_split_access_file = false\n            local has_split_error_file = false\n            local lfs = require(\"lfs\")\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__access.log.tar.gz$\") then\n                    has_split_access_file = true\n                end\n\n                if string.match(file_name, \"__error.log.tar.gz$\") then\n                    has_split_error_file = true\n                end\n            end\n\n            if not has_split_access_file or not has_split_error_file then\n               ngx.status = 500\n            else\n               ngx.status = 200\n            end\n        }\n    }\n\n\n\n=== TEST 2: in current log, with enable log file compression\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.1)\n            ngx.log(ngx.WARN, \"start xxxxxx\")\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nstart xxxxxx\n\n\n\n=== TEST 3: check file changes (enable compression)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3)\n\n            local default_logs = {}\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__error.log.tar.gz$\") or string.match(file_name, \"__access.log.tar.gz$\") then\n                    local filepath = ngx.config.prefix() .. \"/logs/\" .. file_name\n                    local attr = lfs.attributes(filepath)\n                    if attr then\n                        default_logs[filepath] = { change = attr.change, size = attr.size }\n                    end\n                end\n            end\n\n            ngx.sleep(1)\n\n            local passed = false\n            for filepath, origin_attr in pairs(default_logs) do\n                local check_attr = lfs.attributes(filepath)\n                if check_attr.change == origin_attr.change and check_attr.size == origin_attr.size then\n                    passed = true\n                else\n                    passed = false\n                    break\n                end\n            end\n\n            if passed then\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: test rotate time align\n--- extra_yaml_config\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 3600\n    max_kept: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.1)\n            local log_file = ngx.config.prefix() .. \"logs/error.log\"\n            local file = io.open(log_file, \"r\")\n            local log = file:read(\"*a\")\n\n            local m, err = ngx.re.match(log, [[first init rotate time is: (\\d+)]], \"jom\")\n            if not m then\n                ngx.log(ngx.ERR, \"failed to gmatch: \", err)\n                return\n            end\n\n            ngx.sleep(2)\n\n            local now_time = ngx.time()\n            local interval = 3600\n            local rotate_time = now_time + interval - (now_time % interval)\n            if tonumber(m[1]) == tonumber(rotate_time) then\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: max_kept effective on compression files\n--- extra_yaml_config\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 1\n    max_kept: 1\n    enable_compression: true\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(3.5)\n            local has_split_access_file = false\n            local has_split_error_file = false\n            local lfs = require(\"lfs\")\n            local count = 0\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \".tar.gz$\") then\n                    count = count + 1\n                end\n            end\n            --- only two compression file, access.log.tar.gz and error.log.tar.gz\n            ngx.say(count)\n        }\n    }\n--- response_body\n2\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/log-rotate3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 86400\n    max_size: 9\n    max_kept: 3\n    enable_compression: false\n_EOC_\n\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: log rotate by max_size\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.log(ngx.ERR, \"start xxxxxx\")\n            ngx.sleep(2)\n            local has_split_access_file = false\n            local has_split_error_file = false\n            local lfs = require(\"lfs\")\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__access.log$\") then\n                    has_split_access_file = true\n                end\n\n                if string.match(file_name, \"__error.log$\") then\n                    has_split_error_file = true\n                end\n            end\n\n            if not has_split_access_file and has_split_error_file then\n               ngx.status = 200\n            else\n               ngx.status = 500\n            end\n        }\n    }\n\n\n\n=== TEST 2: in current log\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(0.1)\n            ngx.log(ngx.WARN, \"start xxxxxx\")\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nstart xxxxxx\n\n\n\n=== TEST 3: check file changes\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1)\n\n            local default_logs = {}\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"__error.log$\") or string.match(file_name, \"__access.log$\") then\n                    local filepath = ngx.config.prefix() .. \"/logs/\" .. file_name\n                    local attr = lfs.attributes(filepath)\n                    if attr then\n                        default_logs[filepath] = { change = attr.change, size = attr.size }\n                    end\n                end\n            end\n\n            ngx.sleep(1)\n\n            local passed = false\n            for filepath, origin_attr in pairs(default_logs) do\n                local check_attr = lfs.attributes(filepath)\n                if check_attr.change == origin_attr.change and check_attr.size == origin_attr.size then\n                    passed = true\n                else\n                    passed = false\n                    break\n                end\n            end\n\n            if passed then\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: max_kept effective on differently named compression files\n--- extra_yaml_config\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 1\n    max_kept: 1\n    enable_compression: true\n--- yaml_config\nnginx_config:\n    error_log: logs/err1.log\n    http:\n        access_log: logs/acc1.log\n--- config\n    location /t {\n        error_log logs/err1.log info;\n        access_log logs/acc1.log;\n\n        content_by_lua_block {\n            ngx.sleep(3)\n            local lfs = require(\"lfs\")\n            local count = 0\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \".tar.gz$\") then\n                    count = count + 1\n                end\n            end\n            --- only two compression file\n            ngx.say(count)\n        }\n    }\n--- response_body\n2\n\n\n\n=== TEST 5: check whether new log files were created\n--- extra_yaml_config\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 1\n    max_kept: 0\n    enable_compression: false\n--- yaml_config\nnginx_config:\n    error_log: logs/err2.log\n    http:\n        access_log: logs/acc2.log\n--- config\n    location /t {\n        error_log logs/err2.log info;\n        access_log logs/acc2.log;\n\n        content_by_lua_block {\n            ngx.sleep(3)\n            local lfs = require(\"lfs\")\n            local count = 0\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"err2.log$\") or string.match(file_name, \"acc2.log$\") then\n                    count = count + 1\n                end\n            end\n            ngx.say(count)\n        }\n    }\n--- response_body\n2\n\n\n\n=== TEST 6: do not rotate access log files when access log is disable\n--- extra_yaml_config\nplugins:\n  - log-rotate\nplugin_attr:\n  log-rotate:\n    interval: 1\n    max_kept: 2\n    enable_compression: false\n--- yaml_config\nnginx_config:\n    http:\n        enable_access_log: false\n        access_log: logs/acc3.log\n--- config\n    location /t {\n        access_log off;\n\n        content_by_lua_block {\n            ngx.sleep(3)\n            local lfs = require(\"lfs\")\n            local access_count = 0\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/logs/\") do\n                if string.match(file_name, \"acc3.log$\") then\n                    access_count = access_count + 1\n                end\n            end\n            ngx.say(access_count)\n        }\n    }\n--- response_body\n0\n"
  },
  {
    "path": "t/plugin/loggly.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"stream_conf_enable\", 1);\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen 8126 udp;\n        content_by_lua_block {\n            -- mock udp server is just accepts udp connection and log into error.log\n            require(\"lib.mock_layer4\").loggly()\n        }\n    }\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 10420;\n\n        location /loggly/bulk/tok/tag/bulk {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local data = ngx.req.get_body_data()\n                local headers = ngx.req.get_headers()\n                ngx.log(ngx.ERR, \"loggly body: \", data)\n                ngx.log(ngx.ERR, \"loggly tags: \" .. require(\"toolkit.json\").encode(headers[\"X-LOGGLY-TAG\"]))\n                ngx.say(\"ok\")\n            }\n        }\n\n        location /loggly/503 {\n            content_by_lua_block {\n                ngx.status = 503\n                ngx.say(\"service temporarily unavailable\")\n                ngx.exit(ngx.OK)\n            }\n        }\n\n        location /loggly/410 {\n            content_by_lua_block {\n                ngx.status = 410\n                ngx.say(\"expired link\")\n                ngx.exit(ngx.OK)\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity check metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.loggly\")\n            local configs = {\n                -- full configuration\n                {\n                    customer_token = \"TEST-Token-Must-Be-Passed\",\n                    severity = \"INFO\",\n                    tags = {\"special-route\", \"highpriority-route\"},\n                    max_retry_count = 0,\n                    retry_delay = 1,\n                    buffer_duration = 60,\n                    inactive_timeout = 2,\n                    batch_max_size = 10,\n                },\n                -- minimize schema\n                {\n                    customer_token = \"minimized-cofig\",\n                },\n                -- property \"customer_token\" is required\n                {\n                    severity = \"DEBUG\",\n                },\n                -- unknown severity\n                {\n                    customer_token = \"test\",\n                    severity = \"UNKNOWN\",\n                },\n                -- severity in lower case, should pass\n                {\n                    customer_token = \"test\",\n                    severity = \"crit\",\n                }\n            }\n\n            for i = 1, #configs do\n                local ok, err = plugin.check_schema(configs[i])\n                if not ok then\n                    ngx.say(err)\n                else\n                    ngx.say(\"passed\")\n                end\n            end\n        }\n    }\n--- response_body\npassed\npassed\nproperty \"customer_token\" is required\nproperty \"severity\" validation failed: matches none of the enum values\npassed\n\n\n\n=== TEST 2: set route with loggly enabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"test-token\",\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"name\": \"loggly-enabled-route\",\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: update loggly metadata with host port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/loggly',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8126\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: testing udp packet with mock loggly udp suite\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            -- request 1\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n            -- request 2\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\nopentracing\nopentracing\n--- grep_error_log eval\nqr/message received: .+?(?= \\{)/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[test-token\\@41058 tag=\"apisix\"]\nmessage received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[test-token\\@41058 tag=\"apisix\"]/\n\n\n\n=== TEST 5: checking loggly tags\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"token-1\",\n                                \"batch_max_size\": 1,\n                                \"tags\": [\"abc\", \"def\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: .+?(?= \\{)/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[token-1\\@41058 tag=\"abc\" tag=\"def\"]/\n\n\n\n=== TEST 6: checking loggly log severity\nlog severity is calculated based on PRIVAL\n8 + LOG_SEVERITY value\nCRIT has value 2 so test should return PRIVAL <10>\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"token-1\",\n                                \"batch_max_size\": 1,\n                                \"severity\": \"CRIT\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 0.5\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: .+?(?= \\{)/\n--- grep_error_log_out eval\nqr/message received: <10>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[token-1\\@41058 tag=\"apisix\"]/\n\n\n\n=== TEST 7: collect response full log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <10>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[token-1\\@41058 tag=\"apisix\"] \\{\"apisix_latency\":[\\d.]*,\"client_ip\":\"127\\.0\\.0\\.1\",\"latency\":[\\d.]*,\"request\":\\{\"headers\":\\{\"content-type\":\"application\\/x-www-form-urlencoded\",\"host\":\"127\\.0\\.0\\.1:1984\",\"user-agent\":\"lua-resty-http\\/[\\d.]* \\(Lua\\) ngx_lua\\/[\\d]*\"\\},\"method\":\"GET\",\"querystring\":\\{\\},\"size\":[\\d]+,\"uri\":\"\\/opentracing\",\"url\":\"http:\\/\\/127\\.0\\.0\\.1:1984\\/opentracing\"\\},\"response\":\\{\"headers\":\\{\"connection\":\"close\",\"content-type\":\"text\\/plain\",\"server\":\"APISIX\\/[\\d.]+\",\"transfer-encoding\":\"chunked\"\\},\"size\":[\\d]*,\"status\":200\\},\"route_id\":\"1\",\"server\":\\{\"hostname\":\"[ -~]*\",\"version\":\"[\\d.]+\"\\},\"service_id\":\"\",\"start_time\":[\\d]*,\"upstream\":\"127\\.0\\.0\\.1:1982\",\"upstream_latency\":[\\d]*\\}/\n\n\n\n=== TEST 8: collect response log with include_resp_body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"tok\",\n                                \"batch_max_size\": 1,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            local code, _, body = t(\"/opentracing\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"apisix_latency\":[\\d.]*,\"client_ip\":\"127\\.0\\.0\\.1\",\"latency\":[\\d.]*,\"request\":\\{\"headers\":\\{\"content-type\":\"application\\/x-www-form-urlencoded\",\"host\":\"127\\.0\\.0\\.1:1984\",\"user-agent\":\"lua-resty-http\\/[\\d.]* \\(Lua\\) ngx_lua\\/[\\d]*\"\\},\"method\":\"GET\",\"querystring\":\\{\\},\"size\":[\\d]+,\"uri\":\"\\/opentracing\",\"url\":\"http:\\/\\/127\\.0\\.0\\.1:1984\\/opentracing\"\\},\"response\":\\{\"body\":\"opentracing\\\\n\",\"headers\":\\{\"connection\":\"close\",\"content-type\":\"text\\/plain\",\"server\":\"APISIX\\/[\\d.]+\",\"transfer-encoding\":\"chunked\"\\},\"size\":[\\d]*,\"status\":200\\},\"route_id\":\"1\",\"server\":\\{\"hostname\":\"[ -~]*\",\"version\":\"[\\d.]+\"\\},\"service_id\":\"\",\"start_time\":[\\d]*,\"upstream\":\"127\\.0\\.0\\.1:1982\",\"upstream_latency\":[\\d]*\\}/\n\n\n\n=== TEST 9: collect log with include_resp_body_expr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"tok\",\n                                \"batch_max_size\": 1,\n                                \"include_resp_body\": true,\n                                \"include_resp_body_expr\": [\n                                    [\"arg_bar\", \"==\", \"bar\"]\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            -- this will include resp body\n            local code, _, body = t(\"/opentracing?bar=bar\", \"GET\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n        }\n    }\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"apisix_latency\":[\\d.]*,\"client_ip\":\"127\\.0\\.0\\.1\",\"latency\":[\\d.]*,\"request\":\\{\"headers\":\\{\"content-type\":\"application\\/x-www-form-urlencoded\",\"host\":\"127\\.0\\.0\\.1:1984\",\"user-agent\":\"lua-resty-http\\/[\\d.]* \\(Lua\\) ngx_lua\\/[\\d]*\"\\},\"method\":\"GET\",\"querystring\":\\{\"bar\":\"bar\"\\},\"size\":[\\d]+,\"uri\":\"\\/opentracing\\?bar=bar\",\"url\":\"http:\\/\\/127\\.0\\.0\\.1:1984\\/opentracing\\?bar=bar\"\\},\"response\":\\{\"body\":\"opentracing\\\\n\",\"headers\":\\{\"connection\":\"close\",\"content-type\":\"text\\/plain\",\"server\":\"APISIX\\/[\\d.]+\",\"transfer-encoding\":\"chunked\"\\},\"size\":[\\d]*,\"status\":200\\},\"route_id\":\"1\",\"server\":\\{\"hostname\":\"[ -~]*\",\"version\":\"[\\d.]+\"\\},\"service_id\":\"\",\"start_time\":[\\d]*,\"upstream\":\"127\\.0\\.0\\.1:1982\",\"upstream_latency\":[\\d]*\\}/\n\n\n\n=== TEST 10: collect log with include_resp_body_expr mismatch\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/opentracing?foo=bar\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n        }\n    }\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"apisix_latency\":[\\d.]*,\"client_ip\":\"127\\.0\\.0\\.1\",\"latency\":[\\d.]*,\"request\":\\{\"headers\":\\{\"content-type\":\"application\\/x-www-form-urlencoded\",\"host\":\"127\\.0\\.0\\.1:1984\",\"user-agent\":\"lua-resty-http\\/[\\d.]* \\(Lua\\) ngx_lua\\/[\\d]*\"\\},\"method\":\"GET\",\"querystring\":\\{\"foo\":\"bar\"\\},\"size\":[\\d]+,\"uri\":\"\\/opentracing\\?foo=bar\",\"url\":\"http:\\/\\/127\\.0\\.0\\.1:1984\\/opentracing\\?foo=bar\"\\},\"response\":\\{\"headers\":\\{\"connection\":\"close\",\"content-type\":\"text\\/plain\",\"server\":\"APISIX\\/[\\d.]+\",\"transfer-encoding\":\"chunked\"\\},\"size\":[\\d]*,\"status\":200\\},\"route_id\":\"1\",\"server\":\\{\"hostname\":\"[ -~]*\",\"version\":\"[\\d.]+\"\\},\"service_id\":\"\",\"start_time\":[\\d]*,\"upstream\":\"127\\.0\\.0\\.1:1982\",\"upstream_latency\":[\\d]*\\}/\n\n\n\n=== TEST 11: collect request log with include_req_body\n--- log_level: info\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"tok\",\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            local code, _, body = t(\"/opentracing\", \"POST\", \"body-data\")\n        }\n    }\n--- error_log\n\"request\":{\"body\":\"body-data\"\n\n\n\n=== TEST 12: collect log with include_req_body_expr\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"tok\",\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true,\n                                \"include_req_body_expr\": [\n                                    [\"arg_bar\", \"==\", \"bar\"]\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            -- this will include resp body\n            local code, _, body = t(\"/opentracing?bar=bar\", \"POST\", \"body-data\")\n             if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n        }\n    }\n--- error_log\n\"request\":{\"body\":\"body-data\"\n\n\n\n=== TEST 13: collect log with include_req_body_expr mismatch\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/opentracing?foo=bar\", \"POST\", \"body-data\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n        }\n    }\n--- no_error_log\n\"request\":{\"body\":\"body-data\"\n\n\n\n=== TEST 14: collect log with log_format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/loggly',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8126,\n                        \"log_format\":{\n                            \"host\":\"$host\",\n                            \"client\":\"$remote_addr\"\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n\n            local code, _, body = t(\"/opentracing?foo=bar\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- response_body\npassed\nopentracing\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"client\":\"[\\d.]+\",\"host\":\"[\\d.]+\",\"route_id\":\"1\"\\}/\n\n\n\n=== TEST 15: loggly http protocol\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/loggly',\n                 ngx.HTTP_PUT,\n                 {\n                    host = ngx.var.server_addr .. \":10420/loggly\",\n                    protocol = \"http\",\n                    log_format = {\n                        [\"route_id\"] = \"$route_id\",\n                    }\n                }\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n\n            local code, _, body = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- wait: 2\n--- response_body\npassed\nopentracing\n--- error_log\nloggly body: {\"route_id\":\"1\"}\nloggly tags: \"apisix\"\n\n\n\n=== TEST 16: test setup for collecting syslog with severity based on http response code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"tok\",\n                                \"batch_max_size\": 1,\n                                \"severity_map\": {\n                                    \"503\": \"ERR\",\n                                    \"410\": \"ALERT\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:10420\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/loggly/*\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/plugin_metadata/loggly',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8126,\n                        \"log_format\":{\n                            \"route_id\": \"$route_id\"\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\npassed\n\n\n\n=== TEST 17: syslog PRIVAL 9 for type severity level ALERT\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, _ = t(\"/loggly/410\", \"GET\")\n            ngx.print(body)\n        }\n    }\n--- response_body\nexpired link\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <9>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"route_id\":\"1\"\\}/\n\n\n\n=== TEST 18: syslog PRIVAL 11 for type severity level ERR\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body, _ = t(\"/loggly/503\", \"GET\")\n            ngx.print(body)\n        }\n    }\n--- response_body\nservice temporarily unavailable\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <11>1 [\\d\\-T:.]+Z [\\d.]+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"route_id\":\"1\"\\}/\n\n\n\n=== TEST 19: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/loggly',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"host\":\"127.0.0.1\",\n                        \"port\": 8126,\n                        \"log_format\":{\n                            \"client\":\"$remote_addr\"\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"loggly\": {\n                                \"customer_token\" : \"tok\",\n                                \"log_format\":{\n                                    \"host\":\"$host\",\n                                    \"client\":\"$remote_addr\"\n                                },\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: hit\n--- request\nGET /opentracing?foo=bar\n--- response_body\nopentracing\n--- wait: 0.5\n--- grep_error_log eval\nqr/message received: [ -~]+/\n--- grep_error_log_out eval\nqr/message received: <14>1 [\\d\\-T:.]+Z \\w+ apisix [\\d]+ - \\[tok\\@41058 tag=\"apisix\"] \\{\"client\":\"[\\d.]+\",\"host\":\"\\w+\",\"route_id\":\"1\"\\}/\n"
  },
  {
    "path": "t/plugin/loki-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}},\n                {endpoint_addrs = \"http://127.0.0.1:8199\"},\n                {endpoint_addrs = {}},\n                {},\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}, endpoint_uri = \"/loki/api/v1/push\"},\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}, endpoint_uri = 1234},\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}, tenant_id = 1234},\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}, headers = 1234},\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}, log_labels = \"1234\"},\n                {endpoint_addrs = {\"http://127.0.0.1:8199\"}, log_labels = {job = \"apisix6\"}},\n            }\n            local plugin = require(\"apisix.plugins.loki-logger\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"endpoint_addrs\" validation failed: wrong type: expected array, got string\nproperty \"endpoint_addrs\" validation failed: expect array to have at least 1 items\nproperty \"endpoint_addrs\" is required\ndone\nproperty \"endpoint_uri\" validation failed: wrong type: expected string, got number\nproperty \"tenant_id\" validation failed: wrong type: expected string, got number\nproperty \"headers\" validation failed: wrong type: expected object, got number\nproperty \"log_labels\" validation failed: wrong type: expected object, got string\ndone\n\n\n\n=== TEST 2: setup route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"loki-logger\": {\n                            \"endpoint_addrs\": [\"http://127.0.0.1:3100\"],\n                            \"tenant_id\": \"tenant_1\",\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: hit route\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test#1\n--- response_body\nhello world\n\n\n\n=== TEST 4: check loki log\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require(\"cjson\")\n            local now = ngx.now() * 1000\n            local data, err = require(\"lib.grafana_loki\").fetch_logs_from_loki(\n                tostring(now - 3000) .. \"000000\", -- from\n                tostring(now) .. \"000000\"         -- to\n            )\n\n            assert(err == nil, \"fetch logs error: \" .. (err or \"\"))\n            assert(data.status == \"success\", \"loki response error: \" .. cjson.encode(data))\n            assert(#data.data.result > 0, \"loki log empty: \" .. cjson.encode(data))\n\n            local entry = data.data.result[1]\n            assert(entry.stream.request_headers_test_header == \"only-for-test#1\",\n                  \"expected field request_headers_test_header value: \" .. cjson.encode(entry))\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 5: setup route (with log_labels)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"loki-logger\": {\n                            \"endpoint_addrs\": [\"http://127.0.0.1:3100\"],\n                            \"tenant_id\": \"tenant_1\",\n                            \"log_labels\": {\n                                \"custom_label\": \"custom_label_value\"\n                            },\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test#2\n--- response_body\nhello world\n\n\n\n=== TEST 7: check loki log (with custom_label)\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require(\"cjson\")\n            local now = ngx.now() * 1000\n            local data, err = require(\"lib.grafana_loki\").fetch_logs_from_loki(\n                tostring(now - 3000) .. \"000000\", -- from\n                tostring(now) .. \"000000\",        -- to\n                { query = [[{custom_label=\"custom_label_value\"} | json]] }\n            )\n\n            assert(err == nil, \"fetch logs error: \" .. (err or \"\"))\n            assert(data.status == \"success\", \"loki response error: \" .. cjson.encode(data))\n            assert(#data.data.result > 0, \"loki log empty: \" .. cjson.encode(data))\n\n            local entry = data.data.result[1]\n            assert(entry.stream.request_headers_test_header == \"only-for-test#2\",\n                  \"expected field request_headers_test_header value: \" .. cjson.encode(entry))\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 8: setup route (with tenant_id)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"loki-logger\": {\n                            \"endpoint_addrs\": [\"http://127.0.0.1:3100\"],\n                            \"tenant_id\": \"tenant_2\",\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test#3\n--- response_body\nhello world\n\n\n\n=== TEST 10: check loki log (with tenant_id tenant_1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require(\"cjson\")\n            local now = ngx.now() * 1000\n            local data, err = require(\"lib.grafana_loki\").fetch_logs_from_loki(\n                tostring(now - 10000) .. \"000000\", -- from\n                tostring(now) .. \"000000\"          -- to\n            )\n\n            assert(err == nil, \"fetch logs error: \" .. (err or \"\"))\n            assert(data.status == \"success\", \"loki response error: \" .. cjson.encode(data))\n            assert(#data.data.result > 0, \"loki log empty: \" .. cjson.encode(data))\n\n            local entry = data.data.result[1]\n            assert(entry.stream.request_headers_test_header ~= \"only-for-test#3\",\n                  \"expected field request_headers_test_header value: \" .. cjson.encode(entry))\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 11: check loki log (with tenant_id tenant_2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require(\"cjson\")\n            local now = ngx.now() * 1000\n            local data, err = require(\"lib.grafana_loki\").fetch_logs_from_loki(\n                tostring(now - 3000) .. \"000000\", -- from\n                tostring(now) .. \"000000\",        -- to\n                { headers = {\n                        [\"X-Scope-OrgID\"] = \"tenant_2\"\n                } }\n            )\n\n            assert(err == nil, \"fetch logs error: \" .. (err or \"\"))\n            assert(data.status == \"success\", \"loki response error: \" .. cjson.encode(data))\n            assert(#data.data.result > 0, \"loki log empty: \" .. cjson.encode(data))\n\n            local entry = data.data.result[1]\n            assert(entry.stream.request_headers_test_header == \"only-for-test#3\",\n                  \"expected field request_headers_test_header value: \" .. cjson.encode(entry))\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 12: setup route (with log_labels as variables)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"loki-logger\": {\n                            \"endpoint_addrs\": [\"http://127.0.0.1:3100\"],\n                            \"tenant_id\": \"tenant_1\",\n                            \"log_labels\": {\n                                \"custom_label\": \"$remote_addr\"\n                            },\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 14: check loki log (with custom_label)\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require(\"cjson\")\n            local now = ngx.now() * 1000\n            local data, err = require(\"lib.grafana_loki\").fetch_logs_from_loki(\n                tostring(now - 3000) .. \"000000\", -- from\n                tostring(now) .. \"000000\",        -- to\n                { query = [[{custom_label=\"127.0.0.1\"} | json]] }\n            )\n\n            assert(err == nil, \"fetch logs error: \" .. (err or \"\"))\n            assert(data.status == \"success\", \"loki response error: \" .. cjson.encode(data))\n            assert(#data.data.result > 0, \"loki log empty: \" .. cjson.encode(data))\n        }\n    }\n--- error_code: 200\n\n\n\n=== TEST 15: setup route (test headers)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"loki-logger\": {\n                            \"endpoint_addrs\": [\"http://127.0.0.1:1980\"],\n                            \"endpoint_uri\": \"/log_request\",\n                            \"headers\": {\"Authorization\": \"test1234\"},\n                            \"batch_max_size\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route (test headers)\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\ngo(): authorization: test1234\n"
  },
  {
    "path": "t/plugin/loki-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: should drop entries when max_pending_entries is exceeded\n--- extra_yaml_config\nplugins:\n  - loki-logger\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"loki-logger\"] = {\n                            endpoint_addrs = {\"http://127.0.0.1:1234\"},\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/loki-logger', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/mcp/assets/bridge-list-tools.json",
    "content": "{\n  \"tools\": [\n    {\n      \"name\": \"read_file\",\n      \"description\": \"Read the complete contents of a file from the file system. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Use the 'head' parameter to read only the first N lines of a file, or the 'tail' parameter to read only the last N lines of a file. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          },\n          \"tail\": {\n            \"type\": \"number\",\n            \"description\": \"If provided, returns only the last N lines of the file\"\n          },\n          \"head\": {\n            \"type\": \"number\",\n            \"description\": \"If provided, returns only the first N lines of the file\"\n          }\n        },\n        \"required\": [\n          \"path\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"read_multiple_files\",\n      \"description\": \"Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files. Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"paths\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        \"required\": [\n          \"paths\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"write_file\",\n      \"description\": \"Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          },\n          \"content\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"path\",\n          \"content\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"edit_file\",\n      \"description\": \"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Returns a git-style diff showing the changes made. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          },\n          \"edits\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"oldText\": {\n                  \"type\": \"string\",\n                  \"description\": \"Text to search for - must match exactly\"\n                },\n                \"newText\": {\n                  \"type\": \"string\",\n                  \"description\": \"Text to replace with\"\n                }\n              },\n              \"required\": [\n                \"oldText\",\n                \"newText\"\n              ],\n              \"additionalProperties\": false\n            }\n          },\n          \"dryRun\": {\n            \"type\": \"boolean\",\n            \"default\": false,\n            \"description\": \"Preview changes using git-style diff format\"\n          }\n        },\n        \"required\": [\n          \"path\",\n          \"edits\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"create_directory\",\n      \"description\": \"Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"path\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"list_directory\",\n      \"description\": \"Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"path\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"list_directory_with_sizes\",\n      \"description\": \"Get a detailed listing of all files and directories in a specified path, including sizes. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is useful for understanding directory structure and finding specific files within a directory. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          },\n          \"sortBy\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"name\",\n              \"size\"\n            ],\n            \"default\": \"name\",\n            \"description\": \"Sort entries by name or size\"\n          }\n        },\n        \"required\": [\n          \"path\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"directory_tree\",\n      \"description\": \"Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories. Files have no children array, while directories always have a children array (which may be empty). The output is formatted with 2-space indentation for readability. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"path\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"move_file\",\n      \"description\": \"Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"source\": {\n            \"type\": \"string\"\n          },\n          \"destination\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"source\",\n          \"destination\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"search_files\",\n      \"description\": \"Recursively search for files and directories matching a pattern. Searches through all subdirectories from the starting path. The search is case-insensitive and matches partial names. Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          },\n          \"pattern\": {\n            \"type\": \"string\"\n          },\n          \"excludePatterns\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"default\": []\n          }\n        },\n        \"required\": [\n          \"path\",\n          \"pattern\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"get_file_info\",\n      \"description\": \"Retrieve detailed metadata about a file or directory. Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"path\"\n        ],\n        \"additionalProperties\": false,\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n      }\n    },\n    {\n      \"name\": \"list_allowed_directories\",\n      \"description\": \"Returns the list of directories that this server is allowed to access. Use this to understand which directories are available before trying to access files.\",\n      \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {},\n        \"required\": []\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "t/plugin/mcp-bridge.spec.mts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { readFileSync } from 'node:fs';\nimport { unlink, writeFile } from 'node:fs/promises';\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { resolve } from 'node:path';\n\n\nconst tools = JSON.parse(\n  readFileSync(resolve(`./plugin/mcp/assets/bridge-list-tools.json`), 'utf-8'),\n);\nconst sseEndpoint = new URL('http://localhost:1984/mcp/sse');\n\ndescribe('mcp-bridge', () => {\n  let client: Client;\n\n  beforeEach(async () => {\n    client = new Client({ name: 'apisix-e2e-test', version: '1.0.0' });\n    await expect(\n      client.connect(new SSEClientTransport(sseEndpoint)),\n    ).resolves.not.toThrow();\n  });\n\n  afterEach(() => expect(client.close()).resolves.not.toThrow());\n\n  it('should list tools', () =>\n    expect(client.listTools()).resolves.toMatchObject(tools));\n\n  it('should call tool', async () => {\n    const result = await client.callTool({\n      name: 'list_directory',\n      arguments: { path: '/' },\n    });\n    expect((result.content as { text: string }[])[0].text).toContain('[DIR] ');\n  });\n\n  it('should call both clients at the same time', async () => {\n    // write test file\n    await writeFile('/tmp/test.txt', 'test file');\n\n    // create client2\n    const client2 = new Client({ name: 'apisix-e2e-test', version: '1.0.0' });\n    await expect(\n      client2.connect(new SSEClientTransport(sseEndpoint)),\n    ).resolves.not.toThrow();\n\n    // list tools both clients\n    await expect(client.listTools()).resolves.toMatchObject(tools);\n    await expect(client2.listTools()).resolves.toMatchObject(tools);\n\n    // list directory both clients\n    const result1 = await client.callTool({\n      name: 'list_directory',\n      arguments: { path: '/' },\n    });\n    const result2 = await client2.callTool({\n      name: 'list_directory',\n      arguments: { path: '/tmp' },\n    });\n    expect((result1.content as { text: string }[])[0].text).toContain('[DIR] home');\n    expect((result2.content as { text: string }[])[0].text).toContain('[FILE] test.txt');\n\n    // close client2\n    await expect(client2.close()).resolves.not.toThrow();\n\n    // remove test file\n    await unlink('/tmp/test.txt');\n  });\n});\n"
  },
  {
    "path": "t/plugin/mcp-bridge.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {command = \"npx\"},\n                {},\n                {command = 123},\n                {command = \"npx\", args = { \"-y\", \"test\" }},\n                {command = \"npx\", args = \"test\"},\n            }\n            local plugin = require(\"apisix.plugins.mcp-bridge\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"command\" is required\nproperty \"command\" validation failed: wrong type: expected string, got number\ndone\nproperty \"args\" validation failed: wrong type: expected array, got string\n\n\n\n=== TEST 2: setup route (mcp filesystem)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"mcp-bridge\": {\n                                \"base_uri\": \"/mcp\",\n                                \"command\": \"node\",\n                                \"args\": [\"t/plugin/mcp/servers/src/filesystem/dist/index.js\", \"/\"]\n                            }\n                        },\n                        \"uri\": \"/mcp/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- wait: 2\n\n\n\n=== TEST 3: test mcp client\n--- timeout: 20\n--- exec\ncd t && pnpm test plugin/mcp-bridge.spec.mts 2>&1\n--- no_error_log\nfailed to execute the script with status\n--- response_body eval\nqr/PASS plugin\\/mcp-bridge.spec.mts/\n"
  },
  {
    "path": "t/plugin/mocking.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(return response example:\"hello world\")\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_example\": \"hello world\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route(return response example:\"hello world\")\n--- request\nGET /hello\n--- response_body chomp\nhello world\n\n\n\n=== TEST 3: set route(return response schema: string case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_schema\": {\n                                       \"type\": \"object\",\n                                       \"properties\": {\n                                           \"field1\":{\n                                               \"type\":\"string\",\n                                               \"example\":\"hello\"\n                                           }\n                                       }\n                                   }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route(return response schema: string case)\n--- request\nGET /hello\n--- response_body chomp\n{\"field1\":\"hello\"}\n\n\n\n=== TEST 5: set route(return response schema: integer case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_schema\": {\n                                       \"type\": \"object\",\n                                       \"properties\": {\n                                           \"field1\":{\n                                               \"type\":\"integer\",\n                                               \"example\":4\n                                           }\n                                       }\n                                   }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route(return response schema: integer case)\n--- request\nGET /hello\n--- response_body chomp\n{\"field1\":4}\n\n\n\n=== TEST 7: set route(return response schema: number case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_schema\": {\n                                       \"type\": \"object\",\n                                       \"properties\": {\n                                           \"field1\":{\n                                               \"type\":\"number\",\n                                               \"example\":5.5\n                                           }\n                                       }\n                                   }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route(return response schema: number case)\n--- request\nGET /hello\n--- response_body chomp\n{\"field1\":5.5}\n\n\n\n=== TEST 9: set route(return response schema: boolean case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_schema\": {\n                                       \"type\": \"object\",\n                                       \"properties\": {\n                                           \"field1\":{\n                                               \"type\":\"boolean\",\n                                               \"example\":true\n                                           }\n                                       }\n                                   }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route(return response schema: boolean case)\n--- request\nGET /hello\n--- response_body chomp\n{\"field1\":true}\n\n\n\n=== TEST 11: set route(return response schema: object case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_schema\": {\n                                       \"type\": \"object\",\n                                       \"properties\": {\n                                           \"field1\":{\n                                               \"type\":\"object\"\n                                           }\n                                       }\n                                   }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route(return response schema: object case)\n--- request\nGET /hello\n--- response_body chomp\n{\"field1\":{}}\n\n\n\n=== TEST 13: set route(return response header: application/json)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"application/json\",\n                                   \"response_status\": 200,\n                                   \"response_example\": \"{\\\"field1\\\":{}}\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 14: hit route(return response header: application/json)\n--- request\nGET /hello\n--- response_headers\nContent-Type: application/json\n\n\n\n=== TEST 15: set route(return response example:\"remote_addr:127.0.0.1\")\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_example\": \"remote_addr:$remote_addr\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route(return response example:\"remote_addr:127.0.0.1\")\n--- request\nGET /hello\n--- response_body chomp\nremote_addr:127.0.0.1\n\n\n\n=== TEST 17: set route(return response example:\"empty_var:\")\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"delay\": 1,\n                                   \"content_type\": \"text/plain\",\n                                   \"response_status\": 200,\n                                   \"response_example\": \"empty_var:$foo\"\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route(return response example:\"empty_var:\")\n--- request\nGET /hello\n--- response_body chomp\nempty_var:\n\n\n\n=== TEST 19: set route (return headers)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"response_example\": \"hello world\",\n                                   \"response_headers\": {\n                                        \"X-Apisix\": \"is, cool\",\n                                        \"X-Really\": \"yes\"\n                                    }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 20: hit route\n--- request\nGET /hello\n--- response_headers\nX-Apisix: is, cool\nX-Really: yes\n\n\n\n=== TEST 21: set route (return headers support built-in variables)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                           \"plugins\": {\n                               \"mocking\": {\n                                   \"response_example\": \"hello world\",\n                                   \"response_headers\": {\n                                        \"X-Route-Id\": \"$route_id\"\n                                    }\n                               }\n                           },\n                           \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 22: hit route\n--- request\nGET /hello\n--- response_headers\nX-Route-Id: 1\n"
  },
  {
    "path": "t/plugin/multi-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with basic-auth and key-auth plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        },\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: enable multi auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {}\n                                },\n                                {\n                                    \"key-auth\": {\n                                        \"query\": \"apikey\",\n                                        \"hide_credentials\": true,\n                                        \"header\": \"apikey\"\n                                    }\n                                },\n                                {\n                                    \"jwt-auth\": {\n                                        \"cookie\": \"jwt\",\n                                        \"query\": \"jwt\",\n                                        \"hide_credentials\": true,\n                                        \"header\": \"authorization\"\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: verify, missing authorization\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n\n\n\n=== TEST 4: verify basic-auth\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n--- error_log\nfind consumer foo\n\n\n\n=== TEST 5: verify key-auth\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\n--- response_body\nhello world\n\n\n\n=== TEST 6: verify, invalid basic credentials\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic YmFyOmJhcgo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n\n\n\n=== TEST 7: verify, invalid api key\n--- request\nGET /hello\n--- more_headers\napikey: auth-two\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n\n\n\n=== TEST 8: enable multi auth plugin with invalid plugin conf in first auth_plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {\n                                        \"hide_credentials\": \"false\"\n                                    }\n                                },\n                                {\n                                    \"key-auth\": {}\n                                },\n                                {\n                                    \"jwt-auth\": {}\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin multi-auth err: plugin basic-auth check schema failed: property \\\"hide_credentials\\\" validation failed: wrong type: expected boolean, got string\"}\n\n\n\n=== TEST 9: enable multi auth plugin with invalid plugin conf in second auth_plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"key-auth\": {}\n                                },\n                                {\n                                    \"basic-auth\": \"blah\"\n                                },\n                                {\n                                    \"jwt-auth\": {}\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin multi-auth err: plugin basic-auth check schema failed: wrong type: expected object, got string\"}\n\n\n\n=== TEST 10: enable multi auth plugin with invalid plugin conf in third auth_plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"key-auth\": {}\n                                },\n                                {\n                                    \"basic-auth\": {}\n                                },\n                                {\n                                    \"jwt-auth\": {\n                                        \"header\": 123\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin multi-auth err: plugin jwt-auth check schema failed: property \\\"header\\\" validation failed: wrong type: expected string, got number\"}\n\n\n\n=== TEST 11: enable multi auth plugin with default plugin conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {}\n                                },\n                                {\n                                    \"key-auth\": {}\n                                },\n                                {\n                                    \"jwt-auth\": {}\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: verify, missing authorization\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n\n\n\n=== TEST 13: verify basic-auth\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n--- error_log\nfind consumer foo\n\n\n\n=== TEST 14: verify key-auth\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\n--- response_body\nhello world\n\n\n\n=== TEST 15: verify, invalid basic credentials\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic YmFyOmJhcgo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n\n\n\n=== TEST 16: verify, invalid api key\n--- request\nGET /hello\n--- more_headers\napikey: auth-two\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n\n\n\n=== TEST 17: enable multi auth plugin using admin api, without any auth_plugins configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": { }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/\\{\"error_msg\":\"failed to check the configuration of plugin multi-auth err: property \\\\\"auth_plugins\\\\\" is required\"\\}/\n\n\n\n=== TEST 18: enable multi auth plugin using admin api, with auth_plugins configuration but with one authorization plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {}\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body_like eval\nqr/\\{\"error_msg\":\"failed to check the configuration of plugin multi-auth err: property \\\\\"auth_plugins\\\\\" validation failed: expect array to have at least 2 items\"\\}/\n\n\n\n=== TEST 19: add consumer with username and jwt-auth plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: sign / verify jwt-auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local sign = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsIm5iZiI6MTcyNzI3NDk4M30.N6ebc4U5ms976pwKZ_iQ88w_uJKqUVNtTYZ_nXhRpWo\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n\n\n\n=== TEST 21: verify multi-auth with plugin config will cause the conf_version change\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, err = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"desc\": \"Multiple Authentication\",\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {}\n                                },\n                                {\n                                    \"key-auth\": {\n                                        \"query\": \"apikey\",\n                                        \"hide_credentials\": true,\n                                        \"header\": \"apikey\"\n                                    }\n                                },\n                                {\n                                    \"jwt-auth\": {\n                                        \"cookie\": \"jwt\",\n                                        \"query\": \"jwt\",\n                                        \"hide_credentials\": true,\n                                        \"header\": \"authorization\"\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                  }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n\n            local code, err = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugin_config_id\": 1\n                }]]\n            )\n            if code > 300 then\n                ngx.log(ngx.ERR, err)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local sign = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsIm5iZiI6MTcyNzI3NDk4M30.N6ebc4U5ms976pwKZ_iQ88w_uJKqUVNtTYZ_nXhRpWo\"\n            local code, _, res = t('/hello?jwt=' .. sign,\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello world\n"
  },
  {
    "path": "t/plugin/multi-auth2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nno_shuffle();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add consumer with basic-auth and key-auth plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"foo\",\n                    \"plugins\": {\n                        \"basic-auth\": {\n                            \"username\": \"foo\",\n                            \"password\": \"bar\"\n                        },\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: enable multi auth plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {}\n                                },\n                                {\n                                    \"key-auth\": {}\n                                },\n                                {\n                                    \"jwt-auth\": {}\n                                },\n                                {\n                                    \"hmac-auth\": {}\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: invalid key-auth apikey\n--- request\nGET /hello\n--- more_headers\napikey: 123\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\nbasic-auth failed to authenticate the request, code: 401. error: Missing authorization in request\nkey-auth failed to authenticate the request, code: 401. error: Invalid API key in request\njwt-auth failed to authenticate the request, code: 401. error: Missing JWT token in request\nhmac-auth failed to authenticate the request, code: 401. error: client request can't be validated: missing Authorization header\n\n\n\n=== TEST 4: valid key-auth apikey\n--- request\nGET /hello\n--- more_headers\napikey: auth-one\n--- error_code: 200\n--- response_body\nhello world\n--- no_error_log\nfailed to authenticate the request\n\n\n\n=== TEST 5: invalid basic-auth credentials\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic YmFyOmJhcgo=\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\nbasic-auth failed to authenticate the request, code: 401. error: failed to find user: invalid user\nkey-auth failed to authenticate the request, code: 401. error: Missing API key in request\njwt-auth failed to authenticate the request, code: 401. error: JWT token invalid: invalid jwt string\nhmac-auth failed to authenticate the request, code: 401. error: client request can't be validated: Authorization header does not start with 'Signature'\n\n\n\n=== TEST 6: valid basic-auth creds\n--- request\nGET /hello\n--- more_headers\nAuthorization: Basic Zm9vOmJhcg==\n--- response_body\nhello world\n--- no_error_log\nfailed to authenticate the request\n\n\n\n=== TEST 7: missing hmac auth authorization header\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\nhmac-auth failed to authenticate the request, code: 401. error: client request can't be validated: missing Authorization header\n\n\n\n=== TEST 8: hmac auth missing algorithm\n--- request\nGET /hello\n--- more_headers\nAuthorization: Signature keyId=\"my-access-key\",headers=\"@request-target date\" ,signature=\"asdf\"\nDate: Thu, 24 Sep 2020 06:39:52 GMT\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\nhmac-auth failed to authenticate the request, code: 401. error: client request can't be validated: algorithm missing\n\n\n\n=== TEST 9: test with invalid jwt-auth token\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\njwt-auth failed to authenticate the request, code: 401. error: Invalid user key in JWT token\n\n\n\n=== TEST 10: create public API route (jwt-auth sign)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/jwt/sign\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: add consumer with username and jwt-auth plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: test with expired jwt token\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\njwt-auth failed to authenticate the request, code: 401. error: failed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2Mzg3MDUwMX0.pPNVvh-TQsdDzorRwa-uuiLYiEBODscp9wv0cwD6c68\n\n\n\n=== TEST 13: test with jwt token containing wrong signature\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\njwt-auth failed to authenticate the request, code: 401. error: failed to verify jwt: signature mismatch: fNtFJnNnJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNnJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n\n\n\n=== TEST 14: verify jwt-auth\n--- request\nGET /hello\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs\n--- response_body\nhello world\n--- no_error_log\nfailed to authenticate the request\n\n\n\n=== TEST 15: enable multi auth plugin with non-existent anonymous consumer\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"multi-auth\": {\n                            \"auth_plugins\": [\n                                {\n                                    \"basic-auth\": {\n                                        \"anonymous_consumer\": \"not-found-anonymous\"\n                                    }\n                                },\n                                {\n                                    \"key-auth\": {\n                                        \"anonymous_consumer\": \"not-found-anonymous\"\n                                    }\n                                },\n                                {\n                                    \"jwt-auth\": {\n                                        \"anonymous_consumer\": \"not-found-anonymous\"\n                                    }\n                                },\n                                {\n                                    \"hmac-auth\": {\n                                        \"anonymous_consumer\": \"anonymous\"\n                                    }\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: invalid basic-auth credentials\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Authorization Failed\"}\n--- error_log\nbasic-auth failed to authenticate the request, code: 401. error: failed to get anonymous consumer not-found-anonymous\nkey-auth failed to authenticate the request, code: 401. error: failed to get anonymous consumer not-found-anonymous\njwt-auth failed to authenticate the request, code: 401. error: failed to get anonymous consumer not-found-anonymous\nhmac-auth failed to authenticate the request, code: 401. error: failed to get anonymous consumer anonymous\n"
  },
  {
    "path": "t/plugin/node-status.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - public-api\n    - node-status\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/status\"\n                 }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: sanity\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(0.5)\n        local t = require(\"lib.test_admin\").test\n        local code, body, body_org = t('/apisix/status', ngx.HTTP_GET)\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body_org)\n    }\n}\n--- response_body eval\nqr/\"accepted\":/\n\n\n\n=== TEST 3: test for unsupported method\n--- request\nPATCH /apisix/status\n--- error_code: 404\n\n\n\n=== TEST 4: test for use default uuid as apisix_uid\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(0.5)\n        local t = require(\"lib.test_admin\").test\n        local code, body, body_org = t('/apisix/status', ngx.HTTP_GET)\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        local json_decode = require(\"cjson\").decode\n        local body_json = json_decode(body_org)\n        ngx.say(body_json.id)\n    }\n}\n--- response_body_like eval\nqr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/\n\n\n\n=== TEST 5: test for allow user to specify a meaningful id as apisix_uid\n--- yaml_config\napisix:\n    id: \"user-set-apisix-instance-id-A\"\n#END\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(0.5)\n        local t = require(\"lib.test_admin\").test\n        local code, body, body_org = t('/apisix/status', ngx.HTTP_GET)\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body_org)\n    }\n}\n--- response_body eval\nqr/\"id\":\"user-set-apisix-instance-id-A\"/\n"
  },
  {
    "path": "t/plugin/ocsp-stapling.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nmy $openssl_bin = $ENV{OPENSSL_BIN};\nif (! -x $openssl_bin) {\n    $ENV{OPENSSL_BIN} = '/usr/local/openresty/openssl3/bin/openssl';\n    if (! -x $ENV{OPENSSL_BIN}) {\n        plan(skip_all => \"openssl3 not installed\");\n    }\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    # setup default conf.yaml\n    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;\nplugins:\n    - ocsp-stapling\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: disable ocsp-stapling plugin\n--- extra_yaml_config\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"test.com\",\n            ocsp_stapling = {}\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        ngx.status = code\n        ngx.print(body)\n    }\n}\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: additional properties forbidden, found ocsp_stapling\"}\n\n\n\n=== TEST 2: check schema when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local json = require(\"toolkit.json\")\n\n        for _, conf in ipairs({\n            {},\n            {enabled = true},\n            {skip_verify = true},\n            {cache_ttl = 6000},\n            {enabled = true, skip_verify = true, cache_ttl = 6000},\n        }) do\n            local ok, err = core.schema.check(core.schema.ssl.properties.ocsp_stapling, conf)\n            if not ok then\n                ngx.say(err)\n                return\n            end\n            ngx.say(json.encode(conf))\n        end\n    }\n}\n--- response_body\n{\"cache_ttl\":3600,\"enabled\":false,\"skip_verify\":false}\n{\"cache_ttl\":3600,\"enabled\":true,\"skip_verify\":false}\n{\"cache_ttl\":3600,\"enabled\":false,\"skip_verify\":true}\n{\"cache_ttl\":6000,\"enabled\":false,\"skip_verify\":false}\n{\"cache_ttl\":6000,\"enabled\":true,\"skip_verify\":true}\n\n\n\n=== TEST 3: ssl config without \"ocsp-stapling\" field when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"test.com\",\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 4: hit, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername test.com -status 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n--- error_log\nno 'ocsp_stapling' field found, no need to run ocsp-stapling plugin\n\n\n\n=== TEST 5: hit, no ocsp response send:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername test.com -status 2>&1 | cat\n--- response_body eval\nqr/OCSP response: no response sent/\n--- error_log\nno 'ocsp_stapling' field found, no need to run ocsp-stapling plugin\n\n\n\n=== TEST 6: client hello without status request extension required when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/ocsp/rsa_good.crt\")\n        local ssl_key =  t.read_file(\"t/certs/ocsp/rsa_good.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"ocsp.test.com\",\n            ocsp_stapling = {\n                enabled = true\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 7: hit, handshake ok and no ocsp response send\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n--- error_log\nno status request required, no need to send ocsp response\n\n\n\n=== TEST 8: cert without ocsp supported when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"test.com\",\n            ocsp_stapling = {\n                enabled = true\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 9: hit, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername test.com -status 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n--- error_log\nno ocsp response send: failed to get ocsp url: cert not contains authority_information_access extension\n\n\n\n=== TEST 10: hit, no ocsp response send due to get ocsp responder url failed:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername test.com -status 2>&1 | cat\n--- response_body eval\nqr/OCSP response: no response sent/\n--- error_log\nno ocsp response send: failed to get ocsp url: cert not contains authority_information_access extension\n\n\n\n=== TEST 11: run ocsp responder, will exit when test finished\n--- config\nlocation /t {\n    content_by_lua_block {\n        local shell = require(\"resty.shell\")\n        local cmd = [[ openssl ocsp -index t/certs/ocsp/index.txt -port 11451 -rsigner t/certs/ocsp/signer.crt -rkey t/certs/ocsp/signer.key -CA t/certs/apisix.crt -nrequest 16 2>&1 1>/dev/null & ]]\n        local ok, stdout, stderr, reason, status = shell.run(cmd, nil, 1000, 8096)\n        if not ok then\n            ngx.log(ngx.WARN, \"failed to execute the script with status: \" .. status .. \", reason: \" .. reason .. \", stderr: \" .. stderr)\n            return\n        end\n        ngx.print(stderr)\n    }\n}\n\n\n\n=== TEST 12: cert with ocsp supported when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/ocsp/rsa_good.crt\")\n        local ssl_key =  t.read_file(\"t/certs/ocsp/rsa_good.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"ocsp.test.com\",\n            ocsp_stapling = {\n                enabled = true\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 13: hit, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp.test.com 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/CONNECTED/\n\n\n\n=== TEST 14: hit, get ocsp response and status is good:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp.test.com 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/Cert Status: good/\n\n\n\n=== TEST 15: muilt cert with ocsp supported when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local rsa_cert = t.read_file(\"t/certs/ocsp/rsa_good.crt\")\n        local rsa_key =  t.read_file(\"t/certs/ocsp/rsa_good.key\")\n\n        local ecc_cert = t.read_file(\"t/certs/ocsp/ecc_good.crt\")\n        local ecc_key =  t.read_file(\"t/certs/ocsp/ecc_good.key\")\n\n        local data = {\n            cert = rsa_cert,\n            key = rsa_key,\n            certs = { ecc_cert },\n            keys = { ecc_key },\n            sni = \"ocsp.test.com\",\n            ocsp_stapling = {\n                enabled = true\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"ocsp.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 16: hit ecc cert, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com -status -tls1_2 -cipher ECDHE-ECDSA-AES128-GCM-SHA256 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/CONNECTED/\n\n\n\n=== TEST 17: hit ecc cert, get cert signature type:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com -status -tls1_2 -cipher ECDHE-ECDSA-AES128-GCM-SHA256 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/Peer signature type: ECDSA/\n\n\n\n=== TEST 18: hit ecc cert, get ocsp response and status is good:3\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com -status -tls1_2 -cipher ECDHE-ECDSA-AES128-GCM-SHA256 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/Cert Status: good/\n\n\n\n=== TEST 19: hit rsa cert, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com -status -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/CONNECTED/\n\n\n\n=== TEST 20: hit rsa cert, get cert signature type:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com -status -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/Peer signature type: RSA/\n\n\n\n=== TEST 21: hit rsa cert, get ocsp response and status is good:3\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -connect localhost:1994 -servername ocsp.test.com -status -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/Cert Status: good/\n\n\n\n=== TEST 22: cert with ocsp supported and revoked when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/ocsp/rsa_revoked.crt\")\n        local ssl_key =  t.read_file(\"t/certs/ocsp/rsa_revoked.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"ocsp-revoked.test.com\",\n            ocsp_stapling = {\n                enabled = true\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 23: hit revoked rsa cert, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-revoked.test.com 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n--- error_log\nno ocsp response send: failed to validate ocsp response: certificate status \"revoked\" in the OCSP response\n\n\n\n=== TEST 24: hit revoked rsa cert, no ocsp response send:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-revoked.test.com 2>&1 | cat\n--- response_body eval\nqr/OCSP response: no response sent/\n--- error_log\nno ocsp response send: failed to validate ocsp response: certificate status \"revoked\" in the OCSP response\n\n\n\n=== TEST 25: cert with ocsp supported and revoked when enabled ocsp-stapling plugin, and skip verify\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/ocsp/rsa_revoked.crt\")\n        local ssl_key =  t.read_file(\"t/certs/ocsp/rsa_revoked.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"ocsp-revoked.test.com\",\n            ocsp_stapling = {\n                enabled = true,\n                skip_verify = true,\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 26: hit revoked rsa cert, handshake ok:1\n--- max_size: 16096\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-revoked.test.com 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n\n\n\n=== TEST 27: hit revoked rsa cert, get ocsp response and status is revoked:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-revoked.test.com 2>&1 | cat\n--- max_size: 16096\n--- response_body eval\nqr/Cert Status: revoked/\n\n\n\n=== TEST 28: cert with ocsp supported and unknown status when enabled ocsp-stapling plugin\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/ocsp/rsa_unknown.crt\")\n        local ssl_key =  t.read_file(\"t/certs/ocsp/rsa_unknown.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"ocsp-unknown.test.com\",\n            ocsp_stapling = {\n                enabled = true\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 29: hit unknown rsa cert, handshake ok:1\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-unknown.test.com 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n--- error_log\nno ocsp response send: failed to validate ocsp response: certificate status \"unknown\" in the OCSP response\n\n\n\n=== TEST 30: hit unknown rsa cert, no ocsp response send:2\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-unknown.test.com 2>&1 | cat\n--- response_body eval\nqr/OCSP response: no response sent/\n--- error_log\nno ocsp response send: failed to validate ocsp response: certificate status \"unknown\" in the OCSP response\n\n\n\n=== TEST 31: cert with ocsp supported and unknown status when enabled ocsp-stapling plugin, and skip verify\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/ocsp/rsa_unknown.crt\")\n        local ssl_key =  t.read_file(\"t/certs/ocsp/rsa_unknown.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            sni = \"ocsp-unknown.test.com\",\n            ocsp_stapling = {\n                enabled = true,\n                skip_verify = true,\n            }\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 32: hit unknown rsa cert, handshake ok:1\n--- max_size: 16096\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-unknown.test.com 2>&1 | cat\n--- response_body eval\nqr/CONNECTED/\n\n\n\n=== TEST 33: hit unknown rsa cert, get ocsp response and status is unknown:2\n--- max_size: 16096\n--- exec\necho -n \"Q\" | $OPENSSL_BIN s_client -status -connect localhost:1994 -servername ocsp-unknown.test.com 2>&1 | cat\n--- response_body eval\nqr/Cert Status: unknown/\n"
  },
  {
    "path": "t/plugin/opa.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {host = \"http://127.0.0.1:8181\", policy = \"example/allow\"},\n                {host = \"http://127.0.0.1:8181\"},\n                {host = 3233, policy = \"example/allow\"},\n            }\n            local plugin = require(\"apisix.plugins.opa\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"policy\" is required\nproperty \"host\" validation failed: wrong type: expected string, got number\n\n\n\n=== TEST 2: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"opa\": {\n                                \"host\": \"http://127.0.0.1:8181\",\n                                \"policy\": \"example\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uris\": [\"/hello\", \"/test\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: hit route (with correct request)\n--- request\nGET /hello?test=1234&user=none\n--- more_headers\ntest-header: only-for-test\n--- response_body\nhello world\n\n\n\n=== TEST 4: hit route (with wrong header request)\n--- request\nGET /hello?test=1234&user=none\n--- more_headers\ntest-header: not-for-test\n--- error_code: 403\n\n\n\n=== TEST 5: hit route (with wrong query request)\n--- request\nGET /hello?test=abcd&user=none\n--- more_headers\ntest-header: only-for-test\n--- error_code: 403\n\n\n\n=== TEST 6: hit route (with wrong method request)\n--- request\nPOST /hello?test=1234&user=none\n--- more_headers\ntest-header: only-for-test\n--- error_code: 403\n\n\n\n=== TEST 7: hit route (with wrong path request)\n--- request\nGET /test?test=1234&user=none\n--- more_headers\ntest-header: only-for-test\n--- error_code: 403\n\n\n\n=== TEST 8: hit route (response status code and header)\n--- request\nGET /test?test=abcd&user=alice\n--- more_headers\ntest-header: only-for-test\n--- error_code: 302\n--- response_headers\nLocation: http://example.com/auth\n\n\n\n=== TEST 9: hit route (response multiple header reason)\n--- request\nGET /test?test=abcd&user=bob\n--- more_headers\ntest-header: only-for-test\n--- error_code: 403\n--- response_headers\ntest: abcd\nabcd: test\n\n\n\n=== TEST 10: hit route (response string reason)\n--- request\nGET /test?test=abcd&user=carla\n--- more_headers\ntest-header: only-for-test\n--- error_code: 403\n--- response\nGive you a string reason\n\n\n\n=== TEST 11: hit route (response json reason)\n--- request\nGET /test?test=abcd&user=dylon\n--- more_headers\ntest-header: only-for-test\n--- error_code: 403\n--- response\n{\"code\":40001,\"desc\":\"Give you a object reason\"}\n\n\n\n=== TEST 12: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"opa\": {\n                                \"host\": \"http://127.0.0.1:8181\",\n                                \"policy\": \"example\",\n                                \"send_headers_upstream\": [\"user\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uris\": [\"/echo\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route\n--- request\nGET /echo?test=1234&user=none\n--- response_headers\nuser: none\n"
  },
  {
    "path": "t/plugin/opa2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup all-in-one test\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/upstreams/u1\",\n                    data = [[{\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/consumers\",\n                    data = [[{\n                        \"username\": \"test\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"_meta\": {\n                                    \"disable\": false\n                                },\n                                \"key\": \"test-key\"\n                            }\n                        }\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/services/s1\",\n                    data = [[{\n                        \"name\": \"s1\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"_meta\": {\n                                    \"disable\": false\n                                }\n                            }\n                        }\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"opa\": {\n                                \"host\": \"http://127.0.0.1:8181\",\n                                \"policy\": \"echo\",\n                                \"with_route\": true,\n                                \"with_consumer\": true,\n                                \"with_service\": true\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"service_id\": \"s1\",\n                        \"uri\": \"/hello\"\n                    }]],\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(code..body)\n            end\n        }\n    }\n--- response_body eval\n\"201passed\\n\" x 4\n\n\n\n=== TEST 2: hit route (test route data)\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test\napikey: test-key\n--- error_code: 403\n--- response_body eval\nqr/\\\"route\\\":/ and qr/\\\"id\\\":\\\"r1\\\"/ and qr/\\\"plugins\\\":\\{\\\"opa\\\"/ and\nqr/\\\"with_route\\\":true/\n\n\n\n=== TEST 3: hit route (test consumer data)\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test\napikey: test-key\n--- error_code: 403\n--- response_body eval\nqr/\\\"consumer\\\":/ and qr/\\\"username\\\":\\\"test\\\"/ and qr/\\\"key\\\":\\\"test-key\\\"/\n\n\n\n=== TEST 4: hit route (test service data)\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test\napikey: test-key\n--- error_code: 403\n--- response_body eval\nqr/\\\"service\\\":/ and qr/\\\"id\\\":\\\"s1\\\"/ and qr/\\\"query\\\":\\\"apikey\\\"/ and\nqr/\\\"header\\\":\\\"apikey\\\"/\n\n\n\n=== TEST 5: setup route without service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opa\": {\n                            \"host\": \"http://127.0.0.1:8181\",\n                            \"policy\": \"echo\",\n                            \"with_route\": true,\n                            \"with_consumer\": true,\n                            \"with_service\": true\n                        }\n                    },\n                    \"upstream_id\": \"u1\",\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route (test without service and consumer)\n--- request\nGET /hello\n--- more_headers\ntest-header: only-for-test\napikey: test-key\n--- error_code: 403\n--- response_body_unlike eval\nqr/\\\"service\\\"/ and qr/\\\"consumer\\\"/\n\n\n\n=== TEST 7: setup route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opa\": {\n                            \"host\": \"http://127.0.0.1:8181\",\n                            \"policy\": \"example\"\n                        }\n                    },\n                    \"upstream_id\": \"u1\",\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route (with JSON empty array)\n--- request\nGET /hello?user=elisa\n--- error_code: 403\n--- response_body chomp\n{\"info\":[]}\n\n\n\n=== TEST 9: create route: `with_route = true` and opa validation passes when route name == \"valid\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"valid\",\n                    \"plugins\": {\n                        \"opa\": {\n                            \"host\": \"http://127.0.0.1:8181\",\n                            \"policy\": \"with_route\",\n                            \"with_route\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 11: create route: `with_route = true` and opa validation fails when route name != \"valid\"\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"not_valid\",\n                    \"plugins\": {\n                        \"opa\": {\n                            \"host\": \"http://127.0.0.1:8181\",\n                            \"policy\": \"with_route\",\n                            \"with_route\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route\n--- request\nGET /hello\n--- error_code: 403\n"
  },
  {
    "path": "t/plugin/openfunction.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openfunction\")\n            local ok, err = plugin.check_schema({function_uri = \"http://127.0.0.1:30585/default/test-body\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: missing `function_uri`\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openfunction\")\n            local ok, err = plugin.check_schema({timeout = 60000})\n            if not ok then\n                ngx.say(err)\n            end\n        }\n    }\n--- response_body\nproperty \"function_uri\" is required\n\n\n\n=== TEST 3: wrong type for `function_uri`\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openfunction\")\n            local ok, err = plugin.check_schema({function_uri = 30858})\n            if not ok then\n                ngx.say(err)\n            end\n        }\n    }\n--- response_body\nproperty \"function_uri\" validation failed: wrong type: expected string, got number\n\n\n\n=== TEST 4: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openfunction\": {\n                                \"function_uri\": \"http://127.0.0.1:30584/function-sample\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route (with GET request)\n--- request\nGET /hello\n--- response_body\nHello, function-sample!\n\n\n\n=== TEST 6: reset route with test-body function\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openfunction\": {\n                                \"function_uri\": \"http://127.0.0.1:30585/default/test-body\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route with POST method\n--- request\nPOST /hello\ntest\n--- response_body\nHello, test!\n\n\n\n=== TEST 8: reset route with test-header function with service_token\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openfunction\": {\n                                \"function_uri\": \"http://127.0.0.1:30583/\",\n                                \"authorization\": {\n                                    \"service_token\": \"test:test\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route with POST request with service_token\n--- request\nPOST /hello\n--- response_body chomp\n[Basic dGVzdDp0ZXN0]\n\n\n\n=== TEST 10: reset route with test-header function without service_token\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openfunction\": {\n                                \"function_uri\": \"http://127.0.0.1:30583/\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route with user-specific Authorization header\n--- request\nPOST /hello\n--- more_headers\nauthorization: user-token-xxx\n--- response_body chomp\n[user-token-xxx]\n\n\n\n=== TEST 12: reset route to non-existent function_uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openfunction\": {\n                                \"function_uri\": \"http://127.0.0.1:30584/default/non-existent\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route (with non-existent function_uri)\n--- request\nPOST /hello\ntest\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 404\n--- response_body_like eval\nqr/not found/\n\n\n\n=== TEST 14: reset route with test-uri function and path forwarding\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openfunction\": {\n                                \"function_uri\": \"http://127.0.0.1:30584\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route with GET method\n--- request\nGET /hello/openfunction\n--- response_body\nHello, openfunction!\n"
  },
  {
    "path": "t/plugin/openid-connect/configuration.json",
    "content": "{\n    \"issuer\": \"https://samples.auth0.com/\",\n    \"authorization_endpoint\": \"https://samples.auth0.com/authorize\",\n    \"token_endpoint\": \"https://samples.auth0.com/oauth/token\",\n    \"device_authorization_endpoint\": \"https://samples.auth0.com/oauth/device/code\",\n    \"userinfo_endpoint\": \"https://samples.auth0.com/userinfo\",\n    \"mfa_challenge_endpoint\": \"https://samples.auth0.com/mfa/challenge\",\n    \"jwks_uri\": \"https://samples.auth0.com/.well-known/jwks.json\",\n    \"registration_endpoint\": \"https://samples.auth0.com/oidc/register\",\n    \"revocation_endpoint\": \"https://samples.auth0.com/oauth/revoke\",\n    \"scopes_supported\": [\n        \"openid\",\n        \"profile\",\n        \"offline_access\",\n        \"name\",\n        \"given_name\",\n        \"family_name\",\n        \"nickname\",\n        \"email\",\n        \"email_verified\",\n        \"picture\",\n        \"created_at\",\n        \"identities\",\n        \"phone\",\n        \"address\"\n    ],\n    \"response_types_supported\": [\n        \"code\",\n        \"token\",\n        \"id_token\",\n        \"code token\",\n        \"code id_token\",\n        \"token id_token\",\n        \"code token id_token\"\n    ],\n    \"code_challenge_methods_supported\": [\n        \"S256\",\n        \"plain\"\n    ],\n    \"response_modes_supported\": [\n        \"query\",\n        \"fragment\",\n        \"form_post\"\n    ],\n    \"subject_types_supported\": [\n        \"public\"\n    ],\n    \"id_token_signing_alg_values_supported\": [\n        \"HS256\",\n        \"RS256\"\n    ],\n    \"token_endpoint_auth_methods_supported\": [\n        \"client_secret_basic\",\n        \"client_secret_post\"\n    ],\n    \"claims_supported\": [\n        \"aud\",\n        \"auth_time\",\n        \"created_at\",\n        \"email\",\n        \"email_verified\",\n        \"exp\",\n        \"family_name\",\n        \"given_name\",\n        \"iat\",\n        \"identities\",\n        \"iss\",\n        \"name\",\n        \"nickname\",\n        \"phone_number\",\n        \"picture\",\n        \"sub\"\n    ],\n    \"request_uri_parameter_supported\": false\n}\n"
  },
  {
    "path": "t/plugin/openid-connect-redis.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: check schema with valid redis session configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"c\",\n                session = {\n                    secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                    storage = \"redis\",\n                    redis = {\n                        host = \"127.0.0.1\",\n                        port = 6379,\n                        prefix = \"mysessions\",\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: check schema with invalid redis session configuration (port string)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"c\",\n                session = {\n                    secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                    storage = \"redis\",\n                    redis = {\n                        port = \"invalid\",\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body_like\nproperty \"port\" validation failed: wrong type: expected integer, got string\n\n\n\n=== TEST 3: verify session sharing across routes with Redis (Simulate Refresh Scenario)\n--- http_config\n    server {\n        listen 11980;\n        server_name localhost;\n\n        location / {\n            content_by_lua_block {\n                ngx.say(\"lesgooo!!\")\n            }\n        }\n    }\n    server {\n        listen 16969;\n        server_name localhost;\n\n        location /.well-known/openid-configuration {\n            content_by_lua_block {\n                ngx.header.content_type = \"application/json\"\n                ngx.say([[\n                {\n                    \"issuer\": \"http://127.0.0.1:16969\",\n                    \"authorization_endpoint\": \"http://127.0.0.1:16969/authorize\",\n                    \"token_endpoint\": \"http://127.0.0.1:16969/token\",\n                    \"userinfo_endpoint\": \"http://127.0.0.1:16969/userinfo\",\n                    \"jwks_uri\": \"http://127.0.0.1:16969/jwks\"\n                }\n                ]])\n            }\n        }\n\n        location /token {\n            content_by_lua_block {\n                local jwt = require(\"resty.jwt\")\n                local validators = require(\"resty.jwt-validators\")\n                local cjson = require(\"cjson\")\n\n                ngx.header.content_type = \"application/json\"\n                ngx.req.read_body()\n                local args = ngx.req.get_post_args()\n\n                if args.grant_type == \"authorization_code\" then\n                    local claim_spec = {\n                       sub = \"user_123\",\n                       iss = \"http://127.0.0.1:16969\",\n                       aud = \"test_client\",\n                       exp = ngx.time() + 60,\n                       iat = ngx.time(),\n                       name = \"Test User\"\n                    }\n\n                    local jwt_token = jwt:sign(\n                       \"test_secret\",\n                       {\n                           header = {typ = \"JWT\", alg = \"HS256\"},\n                           payload = claim_spec\n                       }\n                    )\n\n                    ngx.say(cjson.encode({\n                       access_token = \"access_token_1\",\n                       expires_in = 1,\n                       refresh_token = \"refresh_token_1\",\n                       id_token = jwt_token,\n                       token_type = \"Bearer\"\n                    }))\n                elseif args.grant_type == \"refresh_token\" then\n                    -- Verify that the refresh token matches what we issued\n                    if args.refresh_token == \"refresh_token_1\" then\n                        local claim_spec = {\n                           sub = \"user_123\",\n                           iss = \"http://127.0.0.1:16969\",\n                           aud = \"test_client\",\n                           exp = ngx.time() + 3600,\n                           iat = ngx.time(),\n                           name = \"Test User\"\n                        }\n\n                        local jwt_token = jwt:sign(\n                           \"test_secret\",\n                           {\n                               header = {typ = \"JWT\", alg = \"HS256\"},\n                               payload = claim_spec\n                           }\n                        )\n\n                        ngx.say(cjson.encode({\n                           access_token = \"access_token_2\",\n                           expires_in = 3600,\n                           refresh_token = \"refresh_token_2\",\n                           id_token = jwt_token,\n                           token_type = \"Bearer\"\n                        }))\n                    else\n                        ngx.status = 400\n                        ngx.say('{\"error\":\"invalid_grant\"}')\n                    end\n                else\n                    ngx.status = 400\n                    ngx.say('{\"error\":\"unsupported_grant_type\"}')\n                end\n            }\n        }\n\n        location /userinfo {\n            content_by_lua_block {\n                ngx.header.content_type = \"application/json\"\n                ngx.say([[{\"sub\": \"user_123\", \"name\": \"Test User\"}]])\n            }\n        }\n\n        location /jwks {\n            content_by_lua_block {\n                ngx.header.content_type = \"application/json\"\n                ngx.say([[{\"keys\": []}]])\n            }\n        }\n    }\n\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require(\"resty.http\")\n\n            -- Create Route 1\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"plugins\": {\n                        \"openid-connect\": {\n                            \"client_id\": \"test_client\",\n                            \"client_secret\": \"test_secret\",\n                            \"discovery\": \"http://127.0.0.1:16969/.well-known/openid-configuration\",\n                            \"redirect_uri\": \"http://127.0.0.1/api/route1/callback\",\n                            \"session\": {\n                                \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                                \"storage\": \"redis\",\n                                \"redis\": {\n                                    \"host\": \"127.0.0.1\",\n                                    \"port\": 6379,\n                                    \"prefix\": \"test_shared_sessions\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api/route1*\"\n                }]=]\n            )\n\n            if code >= 300 then\n                ngx.say(\"setup route 1 failed\")\n                return\n            end\n\n            -- Create Route 2\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [=[{\n                    \"plugins\": {\n                        \"openid-connect\": {\n                            \"client_id\": \"test_client\",\n                            \"client_secret\": \"test_secret\",\n                            \"discovery\": \"http://127.0.0.1:16969/.well-known/openid-configuration\",\n                            \"redirect_uri\": \"http://127.0.0.1/api/route2/callback\",\n                            \"session\": {\n                                \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                                \"storage\": \"redis\",\n                                \"redis\": {\n                                    \"host\": \"127.0.0.1\",\n                                    \"port\": 6379,\n                                    \"prefix\": \"test_shared_sessions\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/api/route2*\"\n                }]=]\n            )\n\n            if code >= 300 then\n                ngx.say(\"setup route 2 failed\")\n                return\n            end\n\n            local httpc = http.new()\n\n            -- extract cookie value by name from Set-Cookie header\n            local function get_cookie(headers, name)\n                local cookies = headers[\"Set-Cookie\"]\n                if not cookies then return nil end\n                if type(cookies) == \"string\" then cookies = { cookies } end\n                for _, c in ipairs(cookies) do\n                    local val = string.match(c, name .. \"=([^;]+)\")\n                    if val then return name .. \"=\" .. val end\n                end\n                return nil\n            end\n\n            -- access without login state\n            local uri_start = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/route1/start\"\n            local res, err = httpc:request_uri(uri_start, { method = \"GET\" })\n\n            if not res or res.status ~= 302 then\n                ngx.say(\"failed to start flow: \", res and res.status or err)\n                return\n            end\n\n            local initial_cookie = get_cookie(res.headers, \"session\")\n            if not initial_cookie then\n                ngx.say(\"failed to get initial session cookie\")\n                return\n            end\n\n            -- extract state from the Location URL example: http://.../authorize?client_id=...&state=...&nonce=...\n            local loc = res.headers[\"Location\"]\n            local state = string.match(loc, \"state=([^&]+)\")\n            if not state then\n                ngx.say(\"failed to extract state from location header\")\n                return\n            end\n\n            -- act as the IdP redirecting back with the code and the SAME state.\n            local uri_cb = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/route1/callback?code=mock_code&state=\" .. state\n            res, err = httpc:request_uri(uri_cb, {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = initial_cookie\n                }\n            })\n\n            -- We expect a successful login (likely redirect to original URL or 200)\n            if not res then\n                ngx.say(\"callback request failed: \", err)\n                return\n            end\n\n            -- After callback, we get the FINAL authenticated session cookie.\n            local auth_cookie = get_cookie(res.headers, \"session\")\n            if not auth_cookie then\n                ngx.say(\"failed to get authenticated session cookie after callback. status: \", res.status)\n                return\n            end\n            ngx.log(ngx.INFO, \"auth_cookie: \", auth_cookie)\n\n            -- wait for token expiry as our mock idp issues tokens with 'expires_in: 1'\n            ngx.sleep(2)\n\n            -- access route 2 with the expired (but valid refresh) session\n            local uri_r2 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/api/route2/resource\"\n            local res, err = httpc:request_uri(uri_r2, {\n                method = \"GET\",\n                headers = {\n                    [\"Cookie\"] = auth_cookie\n                }\n            })\n\n            if not res then\n                ngx.say(\"request to route 2 failed: \", err)\n                return\n            end\n\n            if res.status == 200 then\n                ngx.say(\"refresh successful - request passed to upstream\")\n            elseif res.status == 302 then\n                ngx.say(\"refresh failed - redirected to login\")\n            else\n                ngx.say(\"unexpected status: \", res.status, \" body: \", res.body)\n            end\n\n        }\n    }\n--- request\nGET /t\n--- response_body\nrefresh successful - request passed to upstream\n--- no_error_log\n[error]\n\n\n\n=== TEST 4: check schema with missing redis configuration when storage is redis\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"c\",\n                session = {\n                    secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                    storage = \"redis\",\n                    -- redis object missing\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"session\" validation failed: then clause did not match\n"
  },
  {
    "path": "t/plugin/openid-connect.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Sanity check with minimal valid configuration.\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"c\",\n                session = {secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"}\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: Missing `client_id`.\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_secret = \"b\",\n                discovery = \"c\",\n                session = {secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"}\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"client_id\" is required\ndone\n\n\n\n=== TEST 3: Wrong type for `client_id`.\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = 123,\n                client_secret = \"b\",\n                discovery = \"c\",\n                session = {secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"}\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"client_id\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 4: Set up new route with plugin matching URI `/hello`.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"client_rsa_private_key\": \"89ae4c8edadf1cd1c9f034335f136f87ad84b625c8f1\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify encrypted field\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n\n            -- get plugin conf from etcd, client_rsa_private_key is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"openid-connect\"].client_rsa_private_key)\n\n        }\n    }\n--- response_body\nqO8TJbXcxCUnkkaTs3PxWDk5a54lv7FmngKQaxuXV4cL+7Kp1R4D8NS4w88so4e+\n\n\n\n=== TEST 6: Access route w/o bearer token. Should redirect to authentication endpoint of ID provider.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://samples.auth0.com/authorize') ~= -1 and\n                string.find(location, 'scope=apisix') ~= -1 and\n                string.find(location, 'client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH') ~= -1 and\n                string.find(location, 'response_type=code') ~= -1 and\n                string.find(location, 'redirect_uri=https://iresty.com') ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n\n\n\n=== TEST 7: Modify route to match catch-all URI `/*` and point plugin to local Keycloak instance.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"set_refresh_token_header\": true,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: Access route w/o bearer token and go through the full OIDC Relying Party authentication process.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/uri\"\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])\n            -- Make the final call back to the original URI.\n            local redirect_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. res.headers['Location']\n            res, err = httpc:request_uri(redirect_uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Cookie\"] = cookie_str\n                    }\n                })\n\n            if not res then\n                -- No response, must be an error.\n                ngx.status = 500\n                ngx.say(err)\n                return\n            elseif res.status ~= 200 then\n                -- Not a valid response.\n                -- Use 500 to indicate error.\n                ngx.status = 500\n                ngx.say(\"Invoking the original URI didn't return the expected result.\")\n                return\n            end\n\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- response_body_like\nuri: /uri\ncookie: .*\nhost: 127.0.0.1:1984\nuser-agent: .*\nx-access-token: ey.*\nx-id-token: ey.*\nx-real-ip: 127.0.0.1\nx-refresh-token: ey.*\nx-userinfo: ey.*\n\n\n\n=== TEST 9: Re-configure plugin with respect to headers that get sent to upstream.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": true,\n                                \"set_id_token_header\": false,\n                                \"set_userinfo_header\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: Access route w/o bearer token and go through the full OIDC Relying Party authentication process.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/uri\"\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])\n            -- Make the final call back to the original URI.\n            local redirect_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. res.headers['Location']\n            res, err = httpc:request_uri(redirect_uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Cookie\"] = cookie_str\n                    }\n                })\n\n            if not res then\n                -- No response, must be an error.\n                ngx.status = 500\n                ngx.say(err)\n                return\n            elseif res.status ~= 200 then\n                -- Not a valid response.\n                -- Use 500 to indicate error.\n                ngx.status = 500\n                ngx.say(\"Invoking the original URI didn't return the expected result.\")\n                return\n            end\n\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- response_body_like\nuri: /uri\nauthorization: Bearer ey.*\ncookie: .*\nhost: 127.0.0.1:1984\nuser-agent: .*\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 11: Update plugin with `bearer_only=true`.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: Access route w/o bearer token. Should return 401 (Unauthorized).\n--- timeout: 10s\n--- request\nGET /hello\n--- error_code: 401\n--- response_headers_like\nWWW-Authenticate: Bearer realm=\"apisix\"\n--- error_log\nOIDC introspection failed: No bearer token found in request.\n\n\n\n=== TEST 13: Access route with invalid Authorization header value. Should return 400 (Bad Request).\n--- timeout: 10s\n--- request\nGET /hello\n--- more_headers\nAuthorization: foo\n--- error_code: 400\n--- error_log\nOIDC introspection failed: Invalid Authorization header format.\n\n\n\n=== TEST 14: Update plugin with ID provider public key, so tokens can be validated locally.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\\n]] ..\n                                    [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\\n]] ..\n                                    [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\\n]] ..\n                                    [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\\n]] ..\n                                    [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\\n]] ..\n                                    [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\\n]] ..\n                                    [[zQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"Mysoft corp\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: Access route with valid token.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = [[Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A]]\n                    }\n                })\n            ngx.status = res.status\n            if res.status == 200 then\n                ngx.say(true)\n            end\n        }\n    }\n--- response_body\ntrue\n\n\n\n=== TEST 16: Update route URI to '/uri' where upstream endpoint returns request headers in response body.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\\n]] ..\n                                    [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\\n]] ..\n                                    [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\\n]] ..\n                                    [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\\n]] ..\n                                    [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\\n]] ..\n                                    [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\\n]] ..\n                                    [[zQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"Mysoft corp\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/uri\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: Access route with valid token in `Authorization` header. Upstream should additionally get the token in the `X-Access-Token` header.\n--- request\nGET /uri HTTP/1.1\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A\n--- response_body_like\nuri: /uri\nauthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A\nhost: localhost\nx-access-token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A\nx-real-ip: 127.0.0.1\nx-userinfo: ey.*\n--- error_code: 200\n\n\n\n=== TEST 18: Update plugin to only use `Authorization` header.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\\n]] ..\n                                    [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\\n]] ..\n                                    [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\\n]] ..\n                                    [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\\n]] ..\n                                    [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\\n]] ..\n                                    [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\\n]] ..\n                                    [[zQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": true,\n                                \"set_id_token_header\": false,\n                                \"set_userinfo_header\": false,\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"Mysoft corp\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/uri\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: Access route with valid token in `Authorization` header. Upstream should not get the additional `X-Access-Token` header.\n--- request\nGET /uri HTTP/1.1\n--- more_headers\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A\n--- response_body\nuri: /uri\nauthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCBjb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.Vq_sBN7nH67vMDbiJE01EP4hvJYE_5ju6izjkOX8pF5OS4g2RWKWpL6h6-b0tTkCzG4JD5BEl13LWW-Gxxw0i9vEK0FLg_kC_kZLYB8WuQ6B9B9YwzmZ3OLbgnYzt_VD7D-7psEbwapJl5hbFsIjDgOAEx-UCmjUcl2frZxZavG2LUiEGs9Ri7KqOZmTLgNDMWfeWh1t1LyD0_b-eTInbasVtKQxMlb5kR0Ln_Qg5092L-irJ7dqaZma7HItCnzXJROdqJEsMIBAYRwDGa_w5kIACeMOdU85QKtMHzOenYFkm6zh_s59ndziTctKMz196Y8AL08xuTi6d1gEWpM92A\nhost: localhost\nx-real-ip: 127.0.0.1\n--- error_code: 200\n\n\n\n=== TEST 20: Switch route URI back to `/hello`.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw\\n]] ..\n                                    [[94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD\\n]] ..\n                                    [[z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5\\n]] ..\n                                    [[sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO\\n]] ..\n                                    [[oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh\\n]] ..\n                                    [[G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD\\n]] ..\n                                    [[zQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"Mysoft corp\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: Access route with invalid token. Should return 401.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9\" ..\n                        \".eyJkYXRhMSI6IkRhdGEgMSIsImlhdCI6MTU4NTEyMjUwMiwiZXhwIjoxOTAwNjk\" ..\n                        \"4NTAyLCJhdWQiOiJodHRwOi8vbXlzb2Z0Y29ycC5pbiIsImlzcyI6Ik15c29mdCB\" ..\n                        \"jb3JwIiwic3ViIjoic29tZUB1c2VyLmNvbSJ9.u1ISx7JbuK_GFRIUqIMP175FqX\" ..\n                        \"RyF9V7y86480Q4N3jNxs3ePbc51TFtIHDrKttstU4Tub28PYVSlr-HXfjo7\",\n                    }\n                })\n            ngx.status = res.status\n            if res.status == 200 then\n                ngx.say(true)\n            end\n        }\n    }\n--- error_code: 401\n--- error_log\njwt signature verification failed\n\n\n\n=== TEST 22: Update route with Keycloak introspection endpoint and public key removed. Should now invoke introspection endpoint to validate tokens.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"realm\": \"University\",\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: Obtain valid token and access route with it.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n--- grep_error_log eval\nqr/token validate successfully by \\w+/\n--- grep_error_log_out\ntoken validate successfully by introspection\n\n\n\n=== TEST 24: Access route with an invalid token.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Access route using a fake access token.\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                }\n             })\n\n            if res.status == 200 then\n                ngx.say(true)\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\nfalse\n--- error_log\nOIDC introspection failed: invalid token\n\n\n\n=== TEST 25: Check defaults.\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"t.toolkit.json\")\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local s = {\n                client_id = \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                client_secret = \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                discovery = \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                session = {\n                    secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                },\n            }\n            local ok, err = plugin.check_schema(s)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(json.encode(s))\n        }\n    }\n--- response_body\n{\"accept_none_alg\":false,\"accept_unsupported_alg\":true,\"access_token_expires_leeway\":0,\"access_token_in_authorization_header\":false,\"bearer_only\":false,\"client_id\":\"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\"client_jwt_assertion_expires_in\":60,\"client_secret\":\"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\"discovery\":\"http://127.0.0.1:1980/.well-known/openid-configuration\",\"force_reauthorize\":false,\"iat_slack\":120,\"introspection_endpoint_auth_method\":\"client_secret_basic\",\"introspection_interval\":0,\"jwk_expires_in\":86400,\"jwt_verification_cache_ignore\":false,\"logout_path\":\"/logout\",\"realm\":\"apisix\",\"renew_access_token_on_expiry\":true,\"revoke_tokens_on_logout\":false,\"scope\":\"openid\",\"session\":{\"secret\":\"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\"storage\":\"cookie\"},\"set_access_token_header\":true,\"set_id_token_header\":true,\"set_refresh_token_header\":false,\"set_userinfo_header\":true,\"ssl_verify\":true,\"timeout\":3,\"token_endpoint_auth_method\":\"client_secret_basic\",\"unauth_action\":\"auth\",\"use_jwks\":false,\"use_nonce\":false,\"use_pkce\":false}\n\n\n\n=== TEST 26: Update plugin with ID provider jwks endpoint for token verification.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"use_jwks\": true,\n                                \"realm\": \"University\",\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 27: Obtain valid token and access route with it.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n--- grep_error_log eval\nqr/token validate successfully by \\w+/\n--- grep_error_log_out\ntoken validate successfully by jwks\n\n\n\n=== TEST 28: Access route with an invalid token.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Access route using a fake access token.\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                }\n             })\n\n            if res.status == 200 then\n                ngx.say(true)\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\nfalse\n--- error_log\nOIDC introspection failed: invalid jwt: invalid jwt string\n\n\n\n=== TEST 29: Modify route to match catch-all URI `/*` and add post_logout_redirect_uri option.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"post_logout_redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/hello\",\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 30: Access route w/o bearer token and request logout to redirect to post_logout_redirect_uri.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/uri\"\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])\n\n            -- Request the logout uri with the log-in cookie\n            local logout_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/logout\"\n            res, err = httpc:request_uri(logout_uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Cookie\"] = cookie_str\n                    }\n            })\n            if not res then\n                -- No response, must be an error\n                -- Use 500 to indicate error\n                ngx.status = 500\n                ngx.say(err)\n                return\n            elseif res.status ~= 302 then\n                ngx.status = 500\n                ngx.say(\"Request the logout URI didn't return the expected status.\")\n                return\n            end\n\n            -- Request the location, it's a URL of keycloak and contains the post_logout_redirect_uri\n            -- Like:\n            -- http://127.0.0.1:8080/realms/University/protocol/openid-connect/logout?post_logout_redirect=http://127.0.0.1:1984/hello\n            local location = res.headers[\"Location\"]\n            res, err = httpc:request_uri(location, {\n               method = \"GET\"\n            })\n            if not res then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            elseif res.status ~= 302 then\n                ngx.status = 500\n                ngx.say(\"Request the keycloak didn't return the expected status.\")\n                return\n            end\n\n            ngx.status = 200\n            ngx.say(res.headers[\"Location\"])\n        }\n    }\n--- response_body_like\nhttp://127.0.0.1:.*/hello\n\n\n\n=== TEST 31: Switch route URI back to `/hello` and enable pkce.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"use_pkce\": true,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 32: Access route w/o bearer token. Should redirect to authentication endpoint of ID provider with code_challenge parameters.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://samples.auth0.com/authorize') ~= -1 and\n                string.find(location, 'scope=apisix') ~= -1 and\n                string.find(location, 'client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH') ~= -1 and\n                string.find(location, 'response_type=code') ~= -1 and\n                string.find(location, 'redirect_uri=https://iresty.com') ~= -1 and\n                string.match(location, '.*code_challenge=.*') and\n                string.match(location, '.*code_challenge_method=S256.*') then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n\n\n\n=== TEST 33: set use_jwks and set_userinfo_header to validate \"x-userinfo\" in request header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"bearer_only\": true,\n                                \"access_token_in_authorization_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"use_jwks\": true,\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 34: Access route to validate \"x-userinfo\" in request header\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/uri\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if not res then\n                    -- No response, must be an error.\n                    ngx.status = 500\n                    ngx.say(err)\n                    return\n                elseif res.status ~= 200 then\n                    -- Not a valid response.\n                    -- Use 500 to indicate error.\n                    ngx.status = 500\n                    ngx.say(\"Invoking the original URI didn't return the expected result.\")\n                    return\n                end\n\n                ngx.status = res.status\n                ngx.say(res.body)\n\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body_like\nx-userinfo: ey.*\n\n\n\n=== TEST 35: Set up new route with plugin matching URI `/*`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"post_logout_redirect_uri\": \"https://iresty.com\",\n                                \"scope\": \"openid profile\",\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 36: Check whether auth0 can redirect normally using post_logout_redirect_uri configuration\n--- custom_trusted_cert: /etc/ssl/certs/ca-certificates.crt\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/logout\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://iresty.com') ~= -1 and\n                string.find(location, 'post_logout_redirect_uri=https://iresty.com') ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n\n\n\n=== TEST 37: Set up new route with plugin matching URI `/*`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"942299072001-vhduu1uljmdhhbbp7g22m3qsmo246a75.apps.googleusercontent.com\",\n                                \"client_secret\": \"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\",\n                                \"discovery\": \"https://accounts.google.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"post_logout_redirect_uri\": \"https://iresty.com\",\n                                \"scope\": \"openid profile\",\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 38: Check whether google can redirect normally using post_logout_redirect_uri configuration\n--- custom_trusted_cert: /etc/ssl/certs/ca-certificates.crt\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/logout\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://iresty.com') ~= -1 and\n                string.find(location, 'post_logout_redirect_uri=https://iresty.com') ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n\n\n\n=== TEST 39: Update plugin config to use_jwk and bear_only false\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                },\n                                \"use_jwks\": true,\n                                \"realm\": \"University\",\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 40: Test that jwt with bearer_only false still allows a valid Authorization header\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n--- grep_error_log eval\nqr/token validate successfully by \\w+/\n--- grep_error_log_out\ntoken validate successfully by jwks\n\n\n\n=== TEST 41: Missing `session.secret`.\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"c\",\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"session.secret\" is required when \"bearer_only\" is false\ndone\n"
  },
  {
    "path": "t/plugin/openid-connect2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {\n                    name = \"sanity (bearer_only = true)\",\n                    data = {client_id = \"a\", client_secret = \"b\", discovery = \"c\", bearer_only = true},\n                    cb = function(ok, err, case)\n                        assert(ok and not case.session, \"not expect session was generated\")\n                    end,\n                },\n                {\n                    name = \"sanity (bearer_only = false, session.secret is not set)\",\n                    data = {client_id = \"a\", client_secret = \"b\", discovery = \"c\", bearer_only = false},\n                    cb = function(ok, err, case)\n                        assert(\n                            not ok and err == \"property \\\"session.secret\\\" is required when \\\"bearer_only\\\" is false\",\n                            \"session secret should be required\"\n                        )\n                    end,\n                },\n                {\n                    name = \"sanity (bearer_only = false, session.secret is set)\",\n                    data = {client_id = \"a\", client_secret = \"b\", discovery = \"c\", bearer_only = false, session = {secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"}},\n                    cb = function(ok, err, case)\n                        assert(\n                            ok and case.session and case.session.secret and case.session.secret == \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                            \"session secret should be set\"\n                        )\n                    end,\n                },\n                {\n                    name = \"sanity (bearer_only = false, user-set secret, less than 16 characters)\",\n                    data = {client_id = \"a\", client_secret = \"b\", discovery = \"c\", bearer_only = false, session = {secret = \"test\"}},\n                    cb = function(ok, err, case)\n                        assert(not ok and err == \"property \\\"session\\\" validation failed: property \\\"secret\\\" validation failed: string too short, expected at least 16, got 4\", \"too short key passes validation\")\n                    end,\n                },\n                {\n                    name = \"sanity (bearer_only = false, user-set secret, more than 16 characters)\",\n                    data = {client_id = \"a\", client_secret = \"b\", discovery = \"c\", bearer_only = false, session = {secret = \"test_secret_more_than_16\"}},\n                    cb = function(ok, err, case)\n                        assert(ok and case.session and case.session.secret and case.session.secret == \"test_secret_more_than_16\", \"user-set secret is incorrect\")\n                    end,\n                },\n            }\n\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case.data)\n                case.cb(ok, err, case.data)\n            end\n        }\n    }\n\n\n\n=== TEST 2: data encryption for client_secret\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"openid-connect\"].client_secret)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"openid-connect\"].client_secret)\n        }\n    }\n--- response_body\n60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\nxMlerg8pE2lPSDlQdPi+MsAwBnzqpyLRar3lUhP2Tdc2oXnWmit92p8cannhDYkBPc6P/Hlx0wSA0T2wle9QyHaW2oqw3bXDQSWWk8Vqq0o=\n\n\n\n=== TEST 3: Set up route with plugin matching URI `/hello` with unauth_action = \"auth\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"unauth_action\": \"auth\",\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: Access route w/o bearer token. Should redirect to authentication endpoint of ID provider.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://samples.auth0.com/authorize') ~= -1 and\n                string.find(location, 'scope=apisix') ~= -1 and\n                string.find(location, 'client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH') ~= -1 and\n                string.find(location, 'response_type=code') ~= -1 and\n                string.find(location, 'redirect_uri=https://iresty.com') ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n\n\n\n=== TEST 5: Set up route with plugin matching URI `/hello` with unauth_action = \"deny\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"unauth_action\": \"deny\",\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: Access route w/o bearer token. Should return unauthorized.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            ngx.say(true)\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 401\n\n\n\n=== TEST 7: Set up route with plugin matching URI `/hello` with unauth_action = \"pass\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"unauth_action\": \"pass\",\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: Access route w/o bearer token. Should return ok.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if res.status == 200 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n\n\n\n=== TEST 9: Set up route with plugin matching URI `/hello` with redirect_uri use default value.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"http://127.0.0.1:1980/.well-known/openid-configuration\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"unauth_action\": \"auth\",\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: The value of redirect_uri should be appended to `.apisix/redirect` in the original request.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local redirect_uri = uri .. \"/.apisix/redirect\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://samples.auth0.com/authorize') ~= -1 and\n                string.find(location, 'scope=apisix') ~= -1 and\n                string.find(location, 'client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH') ~= -1 and\n                string.find(location, 'response_type=code') ~= -1 and\n                string.find(location, 'redirect_uri=' .. redirect_uri) ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n\n\n\n=== TEST 11: Set up route with plugin matching  URI `/*` and point plugin to local Keycloak instance and set claim validator.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"set_refresh_token_header\": true,\n                                \"claim_schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"access_token\": { \"type\" : \"string\"},\n                                        \"id_token\": { \"type\" : \"object\"},\n                                        \"user\": { \"type\" : \"object\"}\n                                    },\n                                    \"required\" : [\"access_token\",\"id_token\",\"user\"]\n                                },\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: Access route w/o bearer token and go through the full OIDC Relying Party authentication process and validate claim successfully.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/uri\"\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])\n            -- Make the final call back to the original URI.\n            local redirect_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. res.headers['Location']\n            res, err = httpc:request_uri(redirect_uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Cookie\"] = cookie_str\n                    }\n                })\n\n            if not res then\n                -- No response, must be an error.\n                ngx.status = 500\n                ngx.say(err)\n                return\n            elseif res.status ~= 200 then\n                -- Not a valid response.\n                -- Use 500 to indicate error.\n                ngx.status = 500\n                ngx.say(\"Invoking the original URI didn't return the expected result.\")\n                return\n            end\n\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- response_body_like\nuri: /uri\ncookie: .*\nhost: 127.0.0.1:1984\nuser-agent: .*\nx-access-token: ey.*\nx-id-token: ey.*\nx-real-ip: 127.0.0.1\nx-refresh-token: ey.*\nx-userinfo: ey.*\n\n\n\n=== TEST 13: Set up route with plugin matching  URI `/*` and point plugin to local Keycloak instance and set claim validator with more strict schema.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"set_refresh_token_header\": true,\n                                \"claim_schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"access_token\": { \"type\" : \"string\"},\n                                        \"id_token\": { \"type\" : \"object\"},\n                                        \"user\": { \"type\" : \"object\"},\n                                        \"user1\": { \"type\" : \"object\"}\n                                    },\n                                    \"required\" : [\"access_token\",\"id_token\",\"user\",\"user1\"]\n                                },\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: Access route w/o bearer token and go through the full OIDC Relying Party authentication process and fail to validate claim.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/uri\"\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])\n            -- Make the final call back to the original URI.\n            local redirect_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. res.headers['Location']\n            res, err = httpc:request_uri(redirect_uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Cookie\"] = cookie_str\n                    }\n                })\n\n            if not res then\n                -- No response, must be an error.\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- error_code: 401\n--- error_log\nproperty \"user1\" is required\n\n\n\n=== TEST 15: Set up route with plugin matching  URI `/*` and point plugin to local Keycloak instance and set invalid claim schema.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"set_refresh_token_header\": true,\n                                \"claim_schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"access_token\": { \"type\" : \"string\"},\n                                        \"id_token\": { \"type\" : \"object\"},\n                                        \"user\": { \"type\" : \"invalid_type\"}\n                                    },\n                                    \"required\" : [\"access_token\",\"id_token\",\"user\"]\n                                },\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body_like\n{\"error_msg\":\"failed to check the configuration of plugin openid-connect err: check claim_schema failed: .*: invalid JSON type: invalid_type\"}\n"
  },
  {
    "path": "t/plugin/openid-connect3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Set up new route access the auth server via http proxy\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"proxy_opts\": {\n                                    \"http_proxy\": \"http://127.0.0.1:8080\",\n                                    \"http_proxy_authorization\": \"Basic dXNlcm5hbWU6cGFzc3dvcmQK\"\n                                },\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Access route w/o bearer token. Should redirect to authentication endpoint of ID provider.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://samples.auth0.com/authorize') ~= -1 and\n                string.find(location, 'scope=apisix') ~= -1 and\n                string.find(location, 'client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH') ~= -1 and\n                string.find(location, 'response_type=code') ~= -1 and\n                string.find(location, 'redirect_uri=https://iresty.com') ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n--- error_log\nuse http proxy\n"
  },
  {
    "path": "t/plugin/openid-connect4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Set up new route access the auth server with header test\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"authorization_params\":{\n                                    \"test\":\"abc\"\n                                },\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"scope\": \"apisix\",\n                                \"proxy_opts\": {\n                                    \"http_proxy\": \"http://127.0.0.1:8080\",\n                                    \"http_proxy_authorization\": \"Basic dXNlcm5hbWU6cGFzc3dvcmQK\"\n                                },\n                                \"use_pkce\": false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Check the uri of the authorization endpoint for passed headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            ngx.status = res.status\n            local location = res.headers['Location']\n            if location and string.find(location, 'https://samples.auth0.com/authorize') ~= -1 and\n                string.find(location, 'test=abc') ~= -1 then\n                ngx.say(true)\n            end\n        }\n    }\n--- timeout: 10s\n--- response_body\ntrue\n--- error_code: 302\n--- error_log\nuse http proxy\n\n\n\n=== TEST 3: Set an unsupported scope in the required scopes field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            require(\"apisix.plugins.openid-connect\")\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"realm\": \"University\",\n                                \"required_scopes\": [\"unsupported\"],\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: Access route\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- error_log\nrequired scopes not present\n\n\n\n=== TEST 5: Set a supported scope in the required scopes field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            require(\"apisix.plugins.openid-connect\")\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"realm\": \"University\",\n                                \"required_scopes\": [\"profile\"],\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: Access route\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n"
  },
  {
    "path": "t/plugin/openid-connect5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Call to route with locking session storage should not block subsequent requests with same session\n--- config\n    set $session_storage redis;\n    set $session_redis_prefix                   sessions;\n    set $session_redis_database                 0;\n    set $session_redis_connect_timeout          1000; # (in milliseconds)\n    set $session_redis_send_timeout             1000; # (in milliseconds)\n    set $session_redis_read_timeout             1000; # (in milliseconds)\n    set $session_redis_host                     127.0.0.1;\n    set $session_redis_port                     6379;\n    set $session_redis_ssl                      off;\n    set $session_redis_ssl_verify               off;\n    set $session_redis_uselocking               on;\n    set $session_redis_spinlockwait             150;  # (in milliseconds)\n    set $session_redis_maxlockwait              30;   # (in seconds)\n\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"bearer_only\" : false,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                },\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"set_refresh_token_header\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookie_str = concatenate_cookies(res.headers['Set-Cookie'])\n            local redirect_uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. res.headers['Location']\n\n            -- Make the final call to protected route\n            local function firstRequest()\n               local httpc = http.new()\n               httpc:request_uri(redirect_uri, {\n                        method = \"GET\",\n                        headers = {\n                            [\"Cookie\"] = cookie_str\n                        }\n                    })\n            end\n\n            ngx.thread.spawn(firstRequest)\n\n            -- Make second call to protected route which should not timeout due to blocked session\n            local httpc = http.new()\n            httpc:set_timeout(2000)\n\n            res, err = httpc:request_uri(redirect_uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Cookie\"] = cookie_str\n                    }\n            })\n\n            if err then\n                ngx.say(\"request error: \", err)\n                return\n            end\n\n            ngx.say(res.body)\n        }\n    }\n--- response_body_like\nhello world\n\n\n\n=== TEST 2: Call to route with locking session storage, no authentication and unauth_action 'deny' should not block subsequent requests on same session\n--- config\n    set $session_storage redis;\n    set $session_redis_uselocking               on;\n\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"unauth_action\": \"deny\",\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            -- Make the final call to protected route WITHOUT cookie\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n\n            -- set a random cookie\n            local cookie_str = \"foobaar\"\n\n            -- Make the call to protected route with cookie\n            local function firstRequest()\n               local httpc = http.new()\n\n               local res, err = httpc:request_uri(uri, {\n                        method = \"GET\",\n                        headers = {\n                            [\"Cookie\"] = cookie_str\n                        }\n                    })\n\n                if not res then\n                    ngx.log(ngx.ERR, \"request failed with err: \", err)\n                    return\n                end\n                return res\n            end\n\n            local thread = ngx.thread.spawn(firstRequest)\n            ok, res = ngx.thread.wait(thread)\n\n            if not ok then\n                ngx.log(ngx.ERR, \"First request did not complete: \", res)\n                return\n            end\n\n            if res.status ~= 401 then\n                ngx.log(ngx.ERR, \"Expected status 401 received: \", res.status)\n                return\n            end\n\n            -- Make second call to protected route and same cookie which should not timeout due to a blocked session\n            local httpc = http.new()\n            httpc:set_timeout(2000)\n\n            res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                     headers = {\n                        [\"Cookie\"] = cookie_str\n                     }\n            })\n            ngx.status = res.status\n        }\n    }\n--- error_code: 401\n"
  },
  {
    "path": "t/plugin/openid-connect6.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n# no_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Check configuration of cookie\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {\n                    client_id = \"course_management\",\n                    client_secret = \"tbsmDOpsHwdgIqYl2NltGRTKzjIzvEmT\",\n                    discovery = \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                    session = {\n                        secret = \"6S8IO+Pydgb33LIor8T9ClER0T/sglFAjClFeAF3RsY=\",\n                        cookie = {\n                            lifetime = 86400\n                        }\n                    }\n                },\n            }\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: Set up new route access the auth server\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"realm\": \"University\",\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"redirect_uri\": \"http://127.0.0.1:]] .. ngx.var.server_port .. [[/authenticated\",\n                                \"ssl_verify\": false,\n                                \"bearer_only\" : false,\n                                \"timeout\": 10,\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"required_scopes\": [\"profile\"],\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"set_access_token_header\": true,\n                                \"access_token_in_authorization_header\": false,\n                                \"set_id_token_header\": true,\n                                \"set_userinfo_header\": true,\n                                \"set_refresh_token_header\": true,\n                                \"session\": {\n                                    \"secret\": \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\",\n                                    \"cookie\": {\n                                        \"lifetime\": 86400\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: Call to route to get session\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local login_keycloak = require(\"lib.keycloak\").login_keycloak\n            local concatenate_cookies = require(\"lib.keycloak\").concatenate_cookies\n\n            local current_time = os.time()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            local res, err = login_keycloak(uri, \"teacher@gmail.com\", \"123456\")\n            if err then\n                ngx.status = 500\n                ngx.say(err)\n                return\n            end\n\n            local cookies = res.headers['Set-Cookie']\n            -- lua-resty-session v4 changed cookie format/handling.\n            -- We verify that a cookie is returned, indicating a session was created.\n            if cookies then\n                ngx.say(\"passed\")\n            else\n                ngx.say(\"failed: no Set-Cookie header found\")\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: Update route with Keycloak introspection endpoint and introspection addon headers.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"realm\": \"University\",\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token/introspect\",\n                                \"introspection_addon_headers\": [\"X-Addon-Header-A\", \"X-Addon-Header-B\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: Obtain valid token and access route with it, introspection work as expected when configured extras headers.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"],\n                        [\"X-Addon-Header-A\"] = \"Value-A\",\n                        [\"X-Addon-Header-B\"] = \"Value-b\"\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n--- error_log\ntoken validate successfully by introspection\n\n\n\n=== TEST 6: Access route with an invalid token, should fail.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Access route using a fake access token.\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                    [\"X-Addon-Header-A\"] = \"Value-A\",\n                    [\"X-Addon-Header-B\"] = \"Value-b\"\n                }\n             })\n\n            if res.status == 200 then\n                ngx.say(true)\n            else\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\nfalse\n--- error_log\nOIDC introspection failed: invalid token\n\n\n\n=== TEST 7: Update route with fake Keycloak introspection endpoint and introspection addon headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"course_management\",\n                                \"client_secret\": \"d1ec69e9-55d2-4109-a3ea-befa071579d5\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"realm\": \"University\",\n                                \"introspection_endpoint_auth_method\": \"client_secret_post\",\n                                \"introspection_endpoint\": \"http://127.0.0.1:1980/log_request\",\n                                \"introspection_addon_headers\": [\"X-Addon-Header-A\", \"X-Addon-Header-B\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: Check http headers from fake introspection endpoint.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. \"fake access token\",\n                        [\"X-Addon-Header-A\"] = \"Value-A\",\n                        [\"X-Addon-Header-B\"] = \"Value-b\"\n                    }\n                })\n            ngx.status = res.status\n        }\n    }\n--- error_code: 401\n--- error_log\nOIDC introspection failed: JSON decoding failed\n--- grep_error_log eval\nqr/x-addon-header-.{10}/\n--- grep_error_log_out\nx-addon-header-a: Value-A\nx-addon-header-b: Value-b\n"
  },
  {
    "path": "t/plugin/openid-connect7.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n# no_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Create route (jwt local, audience required)\nIt reuses Keycloak's TLS private key to export the public key.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"apisix\",\n                                \"client_secret\": \"secret\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/basic/.well-known/openid-configuration\",\n                                \"bearer_only\": true,\n                                \"claim_validator\": {\n                                    \"audience\": {\n                                        \"required\": true\n                                    }\n                                },\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvxeMCu3jE1QChgzCwlxP\\n]] ..\n                                    [[mOkRHQORlOvwGpCX9zRCkMAq7a6jvlQTyM+OOfnnX9xBF4YxRRj3VOqdBJBdEjC2\\n]] ..\n                                    [[jLFQUECdqnD+hZaCGIsk91grP4G7XaFqud7nAH1rniMh1rKLy3NFYTl5tK4U2IPP\\n]] ..\n                                    [[JzIye8ur2JHyzE+qpcAEp/U6M4I2rdPX1gE2ze8gYuIr1VbCg6Nkt45DslZ2GDI8\\n]] ..\n                                    [[2TtwkpMlEjJfmbEnrLHkigPXNs6IHyiFPN95462gPG5TBX3YpxDCP/cnHhMeeyFI\\n]] ..\n                                    [[56WNYlhy0iLYmRfiyhKXi76fYKa/PIIUfOSErrKgKsHJp7HQKo48O4Gz5tQyL1IF\\n]] ..\n                                    [[QQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: Access route with a valid token (with audience)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:8080/realms/basic/protocol/openid-connect/token\", {\n                method = \"POST\",\n                body = \"client_id=apisix&client_secret=secret&grant_type=password&username=jack&password=jack\",\n                headers = { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            local access_token = require(\"toolkit.json\").decode(res.body).access_token\n            local res, err = httpc:request_uri(\"http://127.0.0.1:1980/hello\", {\n                method = \"GET\",\n                headers = { Authorization = \"Bearer \" .. access_token }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            ngx.status = res.status\n        }\n    }\n\n\n\n=== TEST 3: Update route (jwt local, audience required, custom claim)\nUse a custom non-existent claim to simulate the case where the standard field \"aud\" is not included.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"apisix\",\n                                \"client_secret\": \"secret\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/basic/.well-known/openid-configuration\",\n                                \"bearer_only\": true,\n                                \"claim_validator\": {\n                                    \"audience\": {\n                                        \"claim\": \"custom_claim\",\n                                        \"required\": true\n                                    }\n                                },\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvxeMCu3jE1QChgzCwlxP\\n]] ..\n                                    [[mOkRHQORlOvwGpCX9zRCkMAq7a6jvlQTyM+OOfnnX9xBF4YxRRj3VOqdBJBdEjC2\\n]] ..\n                                    [[jLFQUECdqnD+hZaCGIsk91grP4G7XaFqud7nAH1rniMh1rKLy3NFYTl5tK4U2IPP\\n]] ..\n                                    [[JzIye8ur2JHyzE+qpcAEp/U6M4I2rdPX1gE2ze8gYuIr1VbCg6Nkt45DslZ2GDI8\\n]] ..\n                                    [[2TtwkpMlEjJfmbEnrLHkigPXNs6IHyiFPN95462gPG5TBX3YpxDCP/cnHhMeeyFI\\n]] ..\n                                    [[56WNYlhy0iLYmRfiyhKXi76fYKa/PIIUfOSErrKgKsHJp7HQKo48O4Gz5tQyL1IF\\n]] ..\n                                    [[QQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: Access route with an invalid token (without audience)\nUse a custom non-existent claim to simulate the case where the standard field \"aud\" is not included.\nNote the assertion in the error log, where it is shown that the custom claim field name did take effect.\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:8080/realms/basic/protocol/openid-connect/token\", {\n                method = \"POST\",\n                body = \"client_id=apisix&client_secret=secret&grant_type=password&username=jack&password=jack\",\n                headers = { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            local access_token = require(\"toolkit.json\").decode(res.body).access_token\n            res, err = httpc:request_uri(\"http://127.0.0.1:\"..ngx.var.server_port..\"/hello\", {\n                method = \"GET\",\n                headers = { Authorization = \"Bearer \" .. access_token }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- error_code: 403\n--- response_body\n{\"error\":\"required audience claim not present\"}\n--- error_log\nOIDC introspection failed: required audience (custom_claim) not present\n\n\n\n=== TEST 5: Update route (jwt local, audience required, custom claim)\nUse \"iss\" to fake \"aud\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"apisix\",\n                                \"client_secret\": \"secret\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/basic/.well-known/openid-configuration\",\n                                \"bearer_only\": true,\n                                \"claim_validator\": {\n                                    \"audience\": {\n                                        \"claim\": \"iss\",\n                                        \"required\": true\n                                    }\n                                },\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvxeMCu3jE1QChgzCwlxP\\n]] ..\n                                    [[mOkRHQORlOvwGpCX9zRCkMAq7a6jvlQTyM+OOfnnX9xBF4YxRRj3VOqdBJBdEjC2\\n]] ..\n                                    [[jLFQUECdqnD+hZaCGIsk91grP4G7XaFqud7nAH1rniMh1rKLy3NFYTl5tK4U2IPP\\n]] ..\n                                    [[JzIye8ur2JHyzE+qpcAEp/U6M4I2rdPX1gE2ze8gYuIr1VbCg6Nkt45DslZ2GDI8\\n]] ..\n                                    [[2TtwkpMlEjJfmbEnrLHkigPXNs6IHyiFPN95462gPG5TBX3YpxDCP/cnHhMeeyFI\\n]] ..\n                                    [[56WNYlhy0iLYmRfiyhKXi76fYKa/PIIUfOSErrKgKsHJp7HQKo48O4Gz5tQyL1IF\\n]] ..\n                                    [[QQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: Access route with an valid token (with custom audience claim)\nUse \"iss\" to fake \"aud\".\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:8080/realms/basic/protocol/openid-connect/token\", {\n                method = \"POST\",\n                body = \"client_id=apisix&client_secret=secret&grant_type=password&username=jack&password=jack\",\n                headers = { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            local access_token = require(\"toolkit.json\").decode(res.body).access_token\n            res, err = httpc:request_uri(\"http://127.0.0.1:\"..ngx.var.server_port..\"/hello\", {\n                method = \"GET\",\n                headers = { Authorization = \"Bearer \" .. access_token }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n\n\n\n=== TEST 7: Update route (jwt local, audience required, match client_id)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"apisix\",\n                                \"client_secret\": \"secret\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/basic/.well-known/openid-configuration\",\n                                \"bearer_only\": true,\n                                \"claim_validator\": {\n                                    \"audience\": {\n                                        \"required\": true,\n                                        \"match_with_client_id\": true\n                                    }\n                                },\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvxeMCu3jE1QChgzCwlxP\\n]] ..\n                                    [[mOkRHQORlOvwGpCX9zRCkMAq7a6jvlQTyM+OOfnnX9xBF4YxRRj3VOqdBJBdEjC2\\n]] ..\n                                    [[jLFQUECdqnD+hZaCGIsk91grP4G7XaFqud7nAH1rniMh1rKLy3NFYTl5tK4U2IPP\\n]] ..\n                                    [[JzIye8ur2JHyzE+qpcAEp/U6M4I2rdPX1gE2ze8gYuIr1VbCg6Nkt45DslZ2GDI8\\n]] ..\n                                    [[2TtwkpMlEjJfmbEnrLHkigPXNs6IHyiFPN95462gPG5TBX3YpxDCP/cnHhMeeyFI\\n]] ..\n                                    [[56WNYlhy0iLYmRfiyhKXi76fYKa/PIIUfOSErrKgKsHJp7HQKo48O4Gz5tQyL1IF\\n]] ..\n                                    [[QQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: Access route with an valid token (with client id as audience)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:8080/realms/basic/protocol/openid-connect/token\", {\n                method = \"POST\",\n                body = \"client_id=apisix&client_secret=secret&grant_type=password&username=jack&password=jack\",\n                headers = { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            local access_token = require(\"toolkit.json\").decode(res.body).access_token\n            res, err = httpc:request_uri(\"http://127.0.0.1:\"..ngx.var.server_port..\"/hello\", {\n                method = \"GET\",\n                headers = { Authorization = \"Bearer \" .. access_token }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n\n\n\n=== TEST 9: Update route (jwt local, audience required, match client_id)\nUse the apisix-no-aud client. According to Keycloak's default implementation, when unconfigured,\nonly the account is listed as an audience, not the client id.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"apisix-no-aud\",\n                                \"client_secret\": \"secret\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/basic/.well-known/openid-configuration\",\n                                \"bearer_only\": true,\n                                \"claim_validator\": {\n                                    \"audience\": {\n                                        \"required\": true,\n                                        \"match_with_client_id\": true\n                                    }\n                                },\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvxeMCu3jE1QChgzCwlxP\\n]] ..\n                                    [[mOkRHQORlOvwGpCX9zRCkMAq7a6jvlQTyM+OOfnnX9xBF4YxRRj3VOqdBJBdEjC2\\n]] ..\n                                    [[jLFQUECdqnD+hZaCGIsk91grP4G7XaFqud7nAH1rniMh1rKLy3NFYTl5tK4U2IPP\\n]] ..\n                                    [[JzIye8ur2JHyzE+qpcAEp/U6M4I2rdPX1gE2ze8gYuIr1VbCg6Nkt45DslZ2GDI8\\n]] ..\n                                    [[2TtwkpMlEjJfmbEnrLHkigPXNs6IHyiFPN95462gPG5TBX3YpxDCP/cnHhMeeyFI\\n]] ..\n                                    [[56WNYlhy0iLYmRfiyhKXi76fYKa/PIIUfOSErrKgKsHJp7HQKo48O4Gz5tQyL1IF\\n]] ..\n                                    [[QQIDAQAB\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: Access route with an invalid token (without client id as audience)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:8080/realms/basic/protocol/openid-connect/token\", {\n                method = \"POST\",\n                body = \"client_id=apisix-no-aud&client_secret=secret&grant_type=password&username=jack&password=jack\",\n                headers = { [\"Content-Type\"] = \"application/x-www-form-urlencoded\" }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            local access_token = require(\"toolkit.json\").decode(res.body).access_token\n            res, err = httpc:request_uri(\"http://127.0.0.1:\"..ngx.var.server_port..\"/hello\", {\n                method = \"GET\",\n                headers = { Authorization = \"Bearer \" .. access_token }\n            })\n            if not res then\n                ngx.say(\"FAILED: \", err)\n                return\n            end\n            ngx.status = res.status\n            ngx.say(res.body)\n        }\n    }\n--- error_code: 403\n--- response_body\n{\"error\":\"mismatched audience\"}\n--- error_log\nOIDC introspection failed: audience does not match the client id\n"
  },
  {
    "path": "t/plugin/openid-connect8.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n# no_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Set up new route with wrong valid_issuers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"dummy\",\n                                \"client_secret\": \"dummy\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"ssl_verify\": true,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"use_jwks\": true,\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": 123\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body eval\nqr/\\{\"error_msg\":\"failed to check the configuration of plugin openid-connect err: property \\\\\"claim_validator\\\\\" validation failed.*\"\\}/\n\n\n\n=== TEST 2: Set up new route with valid valid_issuers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"dummy\",\n                                \"client_secret\": \"dummy\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"ssl_verify\": true,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"use_jwks\": true,\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"https://securetoken.google.com/test-firebase-project\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: Update plugin with ID provider jwks endpoint for token verification with invalid issuer.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"not required\",\n                                \"client_secret\": \"not required\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"use_jwks\": true,\n                                \"realm\": \"University\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                        \"valid_issuers\": [\"https://securetoken.google.com/test-firebase-project\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: verification fails because issuer not in valid_issuer\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\nfalse\n--- error_log\nOIDC introspection failed: jwt signature verification failed: Claim 'iss' ('http://127.0.0.1:8080/realms/University') returned failure\n\n\n\n=== TEST 5: Update plugin with ID provider jwks endpoint for token verification with valid issuer.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"dummy\",\n                                \"client_secret\": \"dummy\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"use_jwks\": true,\n                                \"realm\": \"University\",\n                                \"claim_validator\": {\n                                    \"issuer\": {\n                                         \"valid_issuers\": [\"http://127.0.0.1:8080/realms/University\"]\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: Obtain valid token and access route with it.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n--- grep_error_log eval\nqr/token validate successfully by \\w+/\n--- grep_error_log_out\ntoken validate successfully by jwks\n\n\n\n=== TEST 7: Update plugin with ID provider jwks endpoint for token verification with valid issuer in discovery endpoint.\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"dummy\",\n                                \"client_secret\": \"dummy\",\n                                \"discovery\": \"http://127.0.0.1:8080/realms/University/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"http://localhost:3000\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"use_jwks\": true,\n                                \"realm\": \"University\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: Obtain valid token and access route with it. Use valid_issuer from discovery endpoint.\n--- config\n    location /t {\n        content_by_lua_block {\n            -- Obtain valid access token from Keycloak using known username and password.\n            local json_decode = require(\"toolkit.json\").decode\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:8080/realms/University/protocol/openid-connect/token\"\n            local res, err = httpc:request_uri(uri, {\n                    method = \"POST\",\n                    body = \"grant_type=password&client_id=course_management&client_secret=d1ec69e9-55d2-4109-a3ea-befa071579d5&username=teacher@gmail.com&password=123456\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n                    }\n                })\n\n            -- Check response from keycloak and fail quickly if there's no response.\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            -- Check if response code was ok.\n            if res.status == 200 then\n                -- Get access token from JSON response body.\n                local body = json_decode(res.body)\n                local accessToken = body[\"access_token\"]\n\n                -- Access route using access token. Should work.\n                uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n                local res, err = httpc:request_uri(uri, {\n                    method = \"GET\",\n                    headers = {\n                        [\"Authorization\"] = \"Bearer \" .. body[\"access_token\"]\n                    }\n                 })\n\n                if res.status == 200 then\n                    -- Route accessed successfully.\n                    ngx.say(true)\n                else\n                    -- Couldn't access route.\n                    ngx.say(false)\n                end\n            else\n                -- Response from Keycloak not ok.\n                ngx.say(false)\n            end\n        }\n    }\n--- response_body\ntrue\n--- grep_error_log eval\nqr/token validate successfully by \\w+/\n--- grep_error_log_out\ntoken validate successfully by jwks\n--- error_log\nvalid_issuers not provided explicitly, using issuer from discovery doc: http://127.0.0.1:8080/realms/University\n"
  },
  {
    "path": "t/plugin/openid-connect9.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nBEGIN {\n    $ENV{CLIENT_SECRET_ENV} = \"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\";\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: configure oidc plugin with small public key using environment variable\n    --- config\n        location /t {\n            content_by_lua_block {\n                local t = require(\"lib.test_admin\").test\n                    local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{ \"plugins\": {\n                    \"openid-connect\": {\n                        \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                            \"client_secret\": \"$ENV://CLIENT_SECRET_ENV\",\n                            \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                            \"redirect_uri\": \"https://iresty.com\",\n                            \"ssl_verify\": false,\n                            \"timeout\": 10,\n                            \"bearer_only\": true,\n                            \"scope\": \"apisix\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\\n]] ..\n                                    [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                            \"token_signing_alg_values_expected\": \"RS256\"\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                    end\n                        ngx.say(body)\n        }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo client_secret=60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa\n--- response_body\nSuccess! Data written to: kv/apisix/foo\n\n\n\n=== TEST 3: configure oidc plugin with small public key using vault\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{ \"plugins\": {\n                            \"openid-connect\": {\n                                \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                                \"client_secret\": \"$secret://vault/test1/foo/client_secret\",\n                                \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                                \"redirect_uri\": \"https://iresty.com\",\n                                \"ssl_verify\": false,\n                                \"timeout\": 10,\n                                \"bearer_only\": true,\n                                \"scope\": \"apisix\",\n                                \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                                    [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\\n]] ..\n                                    [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\\n]] ..\n                                    [[-----END PUBLIC KEY-----\",\n                                \"token_signing_alg_values_expected\": \"RS256\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n}\n}\n--- response_body\npassed\n\n\n\n=== TEST 4: configure oidc plugin with small public key using vault and request with token should success\n--- config\n    location /hello {\n        content_by_lua_block {\n            ngx.say(\"success\")\n        }\n    }\n\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"openid-connect\": {\n                            \"client_id\": \"kbyuFDidLLm280LIwVFiazOqjO3ty8KH\",\n                            \"client_secret\": \"$secret://vault/test1/foo/client_secret\",\n                            \"discovery\": \"https://samples.auth0.com/.well-known/openid-configuration\",\n                            \"redirect_uri\": \"https://iresty.com\",\n                            \"ssl_verify\": false,\n                            \"timeout\": 10,\n                            \"bearer_only\": true,\n                            \"scope\": \"apisix\",\n                            \"public_key\": \"-----BEGIN PUBLIC KEY-----\\n]] ..\n                            [[MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANW16kX5SMrMa2t7F2R1w6Bk/qpjS4QQ\\n]] ..\n                            [[hnrbED3Dpsl9JXAx90MYsIWp51hBxJSE/EPVK8WF/sjHK1xQbEuDfEECAwEAAQ==\\n]] ..\n                            [[-----END PUBLIC KEY-----\",\n                            \"token_signing_alg_values_expected\": \"RS256\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /hello HTTP/1.1\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3NhbXBsZXMuYXV0aDAuY29tLyIsInN1YiI6InRlc3Qtc3ViamVjdCIsImF1ZCI6ImtieXVG RGlkTExtMjgwTEl3VkZpYXpPcWpPM3R5OEtIIiwic2NvcGUiOiJhcGlzaXgiLCJpYXQiOjEwMDAwMDAwLCJleHAiOjI1MDAwMDAwMDB9.bfcZsd4ABgo0GoLT8EwfnKgf AWbnJZbZ3kOtqyeSkXYqGlSmgMNW3q5Kx1SGjMNhEKVG_KrFfsPrQmcTljSPZA\n--- response_body\nsuccess\n"
  },
  {
    "path": "t/plugin/opentelemetry.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - opentelemetry\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n    $block;\n});\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"collector\": {\n                        \"address\": \"127.0.0.1:4318\",\n                        \"request_timeout\": 3,\n                        \"request_headers\": {\n                            \"foo\": \"bar\"\n                        }\n                    },\n                    \"trace_id_source\": \"x-request-id\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 2: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 3: trigger opentelemetry\n--- request\nGET /opentracing\n--- wait: 2\n--- response_body\nopentracing\n\n\n\n=== TEST 4: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*opentelemetry-lua.*/\n\n\n\n=== TEST 5: use trace_id_ratio sampler, fraction = 1.0\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"trace_id_ratio\",\n                                \"options\": {\n                                    \"fraction\": 1.0\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 6: trigger opentelemetry\n--- request\nGET /opentracing\n--- wait: 2\n--- response_body\nopentracing\n\n\n\n=== TEST 7: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*opentelemetry-lua.*/\n\n\n\n=== TEST 8: use parent_base sampler, root sampler = trace_id_ratio with default fraction = 0\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"parent_base\",\n                                \"options\": {\n                                    \"root\": {\n                                        \"name\": \"trace_id_ratio\"\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 9: trigger opentelemetry, trace_flag = 1\n--- request\nGET /opentracing\n--- more_headers\ntraceparent: 00-00000000000000000000000000000001-0000000000000001-01\n--- wait: 2\n--- response_body\nopentracing\n\n\n\n=== TEST 10: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*\"traceId\":\"00000000000000000000000000000001\",.*/\n\n\n\n=== TEST 11: use parent_base sampler, root sampler = trace_id_ratio with fraction = 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"parent_base\",\n                                \"options\": {\n                                    \"root\": {\n                                        \"name\": \"trace_id_ratio\",\n                                        \"options\": {\n                                            \"fraction\": 1.0\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 12: trigger opentelemetry, trace_flag = 1\n--- request\nGET /opentracing\n--- more_headers\ntraceparent: 00-00000000000000000000000000000001-0000000000000001-01\n--- wait: 2\n--- response_body\nopentracing\n\n\n\n=== TEST 13: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*\"traceId\":\"00000000000000000000000000000001\",.*/\n\n\n\n=== TEST 14: set additional_attributes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"service_name\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"route_name\",\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            },\n                            \"additional_attributes\": [\n                                \"http_user_agent\",\n                                \"arg_foo\",\n                                \"cookie_token\",\n                                \"remote_addr\"\n                            ]\n                        }\n                    },\n                    \"uri\": \"/opentracing\",\n                    \"service_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 15: trigger opentelemetry\n--- request\nGET /opentracing?foo=bar&a=b\n--- more_headers\nX-Request-Id: 01010101010101010101010101010101\nUser-Agent: test_nginx\nCookie: token=auth_token;\n--- wait: 2\n--- response_body\nopentracing\n\n\n\n=== TEST 16: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*\\/opentracing\\?foo=bar.*/\n\n\n\n=== TEST 17: create route for /specific_status\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"route_name\",\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/specific_status\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 18: test response empty body\n--- request\nHEAD /specific_status\n--- response_body\n--- wait: 2\n\n\n\n=== TEST 19: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*\\/specific_status.*/\n"
  },
  {
    "path": "t/plugin/opentelemetry2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - example-plugin\n    - key-auth\n    - opentelemetry\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"trace_id_source\": \"x-request-id\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 2: trace request rejected by auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"example-plugin\": {\"i\": 1},\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: trigger opentelemetry\n--- request\nGET /hello\n--- error_code: 401\n--- wait: 2\n\n\n\n=== TEST 4: check log\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/.*\\/hello.*/\n"
  },
  {
    "path": "t/plugin/opentelemetry3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - http-logger\n    - opentelemetry\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $upstream_server_config = $block->upstream_server_config // <<_EOC_;\n    set \\$opentelemetry_context_traceparent \"\";\n    set \\$opentelemetry_trace_id \"\";\n    set \\$opentelemetry_span_id \"\";\n    access_log logs/error.log opentelemetry_log;\n_EOC_\n\n    $block->set_value(\"upstream_server_config\", $upstream_server_config);\n\n    my $http_config = $block->http_config // <<_EOC_;\n    log_format opentelemetry_log '{\"time\": \"\\$time_iso8601\",\"opentelemetry_context_traceparent\": \"\\$opentelemetry_context_traceparent\",\"opentelemetry_trace_id\": \"\\$opentelemetry_trace_id\",\"opentelemetry_span_id\": \"\\$opentelemetry_span_id\",\"remote_addr\": \"\\$remote_addr\",\"uri\": \"\\$uri\"}';\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->extra_init_by_lua) {\n        my $extra_init_by_lua = <<_EOC_;\n-- mock exporter http client\nlocal client = require(\"opentelemetry.trace.exporter.http_client\")\nclient.do_request = function()\n    ngx.log(ngx.INFO, \"opentelemetry export span\")\n    return \"ok\"\nend\n_EOC_\n\n        $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n    }\n\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/http-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"opentelemetry_context_traceparent\": \"$opentelemetry_context_traceparent\",\n                        \"opentelemetry_trace_id\": \"$opentelemetry_trace_id\",\n                        \"opentelemetry_span_id\": \"$opentelemetry_span_id\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return body\n            end\n\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"set_ngx_var\": true\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return body\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1980/log\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"concat_method\": \"new_line\"\n                        },\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >=300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: trigger opentelemetry with open set variables\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 1\n--- grep_error_log eval\nqr/opentelemetry export span/\n--- grep_error_log_out\nopentelemetry export span\n--- error_log eval\nqr/request log: \\{.*\"opentelemetry_context_traceparent\":\"00-\\w{32}-\\w{16}-01\".*\\}/\n\n\n\n=== TEST 3: trigger opentelemetry with disable set variables\n--- extra_yaml_config\nplugins:\n    - http-logger\n    - opentelemetry\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"set_ngx_var\": false\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return body\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 4: trigger opentelemetry with open set variables\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 1\n--- error_log eval\nqr/request log: \\{.*\"opentelemetry_context_traceparent\":\"\".*\\}/\n"
  },
  {
    "path": "t/plugin/opentelemetry4-bugfix-pb-state.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - opentelemetry\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    $block;\n});\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"trace_id_source\": \"x-request-id\",\n                    \"collector\": {\n                        \"address\": \"127.0.0.1:4318\",\n                        \"request_timeout\": 3,\n                        \"request_headers\": {\n                            \"foo\": \"bar\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set additional_attributes with match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"route_name\",\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            },\n                            \"additional_header_prefix_attributes\": [\n                                \"x-my-header-*\"\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: opentelemetry expands headers\n--- extra_init_by_lua\n    local otlp = require(\"opentelemetry.trace.exporter.otlp\")\n    local orig_export_spans = otlp.export_spans\n    otlp.export_spans = function(self, spans)\n        if (#spans ~= 1) then\n            ngx.log(ngx.ERR, \"unexpected spans length: \", #spans)\n            return\n        end\n\n        local attributes_names = {}\n        local attributes = {}\n        local span = spans[1]\n        for _, attribute in ipairs(span.attributes) do\n            table.insert(attributes_names, attribute.key)\n            attributes[attribute.key] = attribute.value.string_value or \"\"\n            ::skip::\n        end\n        table.sort(attributes_names)\n        for _, attribute in ipairs(attributes_names) do\n            ngx.log(ngx.INFO, \"attribute \" .. attribute .. \": \\\"\" .. attributes[attribute] .. \"\\\"\")\n        end\n\n        ngx.log(ngx.INFO, \"opentelemetry export span\")\n        return orig_export_spans(self, spans)\n    end\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/protos/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"content\" : \"syntax = \\\"proto3\\\";\n                      package helloworld;\n                      service Greeter {\n                          rpc SayHello (HelloRequest) returns (HelloReply) {}\n                      }\n                      message HelloRequest {\n                          string name = 1;\n                      }\n                      message HelloReply {\n                          string message = 1;\n                         }\"\n                   }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri1 = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local headers = {\n                [\"x-my-header-name\"] = \"william\",\n                [\"x-my-header-nick\"] = \"bill\",\n            }\n            local res, err = httpc:request_uri(uri1, {method = \"GET\", headers = headers})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.status = res.status\n        }\n    }\n--- request\nGET /t\n--- wait: 1\n--- error_code: 200\n--- no_error_log\ntype 'opentelemetry.proto.trace.v1.TracesData' does not exists\n--- grep_error_log eval\nqr/attribute (apisix|x-my).+?:.[^,]*/\n--- grep_error_log_out\nattribute apisix.route_id: \"1\"\nattribute apisix.route_name: \"route_name\"\nattribute x-my-header-name: \"william\"\nattribute x-my-header-nick: \"bill\"\n"
  },
  {
    "path": "t/plugin/opentelemetry5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - opentelemetry\n    - proxy-rewrite\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n    if (!defined $block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n    $block;\n});\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"trace_id_source\": \"x-request-id\",\n                    \"resource\": {\n                        \"service.name\": \"APISIX\"\n                    },\n                    \"collector\": {\n                        \"address\": \"127.0.0.1:4318\",\n                        \"request_timeout\": 3,\n                        \"request_headers\": {\n                            \"foo\": \"bar\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"route-name\",\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        },\n                        \"proxy-rewrite\": {\"uri\": \"/opentracing\"}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/articles/*/comments\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: trigger opentelemetry\n--- request\nGET /articles/12345/comments?foo=bar\n--- more_headers\nUser-Agent: test-client\n--- wait: 2\n--- response_body\nopentracing\n\n\n\n=== TEST 4: (resource) check service.name\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"service.name\",\"value\":\\{\"stringValue\":\"APISIX\"\\}\\}/\n\n\n\n=== TEST 5: (span) check name\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\"name\":\"GET \\/articles\\/\\*\\/comments\"/\n\n\n\n=== TEST 6: (span) check http.status_code\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"http.status_code\",\"value\":\\{\"intValue\":\"200\"\\}\\}/\n\n\n\n=== TEST 7: (span) check http.method\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"http.method\",\"value\":\\{\"stringValue\":\"GET\"\\}\\}/\n\n\n\n=== TEST 8: (span) check http.host\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"net.host.name\",\"value\":\\{\"stringValue\":\"localhost\"\\}\\}/\n\n\n\n=== TEST 9: (span) check http.user_agent\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"http.user_agent\",\"value\":\\{\"stringValue\":\"test-client\"\\}\\}/\n\n\n\n=== TEST 10: (span) check http.target\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"http.target\",\"value\":\\{\"stringValue\":\"\\/articles\\/12345\\/comments\\?foo=bar\"\\}\\}/\n\n\n\n=== TEST 11: (span) check http.route\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"http.route\",\"value\":\\{\"stringValue\":\"\\/articles\\/\\*\\/comments\"\\}\\}/\n\n\n\n=== TEST 12: (span) check apisix.route_id\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"apisix.route_id\",\"value\":\\{\"stringValue\":\"1\"\\}\\}/\n\n\n\n=== TEST 13: (span) check apisix.route_name\n--- exec\ntail -n 1 ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr/\\{\"key\":\"apisix.route_name\",\"value\":\\{\"stringValue\":\"route-name\"\\}\\}/\n"
  },
  {
    "path": "t/plugin/opentelemetry6.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    sub set_env_from_file {\n        my ($env_name, $file_path) = @_;\n\n        open my $fh, '<', $file_path or die $!;\n        my $content = do { local $/; <$fh> };\n        close $fh;\n\n        $ENV{$env_name} = $content;\n    }\n    # set env\n    set_env_from_file('TEST_CERT', 't/certs/apisix.crt');\n    set_env_from_file('TEST_KEY', 't/certs/apisix.key');\n    set_env_from_file('TEST2_CERT', 't/certs/test2.crt');\n    set_env_from_file('TEST2_KEY', 't/certs/test2.key');\n}\nuse t::APISIX 'no_plan';\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\napisix:\n    tracing: true\nplugins:\n    - opentelemetry\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->response_body) {\n        $block->set_value(\"response_body\", \"passed\\n\");\n    }\n    $block;\n});\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: empty file\n--- exec\necho '' > ci/pod/otelcol-contrib/data-otlp.json\n--- response_body eval\nqr//\n\n\n\n=== TEST 2: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"collector\": {\n                        \"address\": \"127.0.0.1:4318\",\n                        \"request_timeout\": 3,\n                        \"request_headers\": {\n                            \"foo\": \"bar\"\n                        }\n                    },\n                    \"trace_id_source\": \"x-request-id\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n\n\n=== TEST 3: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"test1.com:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 4: set ssl with two certs and keys in env\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                snis = {\"test.com\"},\n                key =  \"$env://TEST_KEY\",\n                cert = \"$env://TEST_CERT\",\n                keys = {\"$env://TEST2_KEY\"},\n                certs = {\"$env://TEST2_CERT\"}\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data),\n                [[{\n                    \"value\": {\n                        \"snis\": [\"test.com\"],\n                        \"key\": \"$env://TEST_KEY\",\n                        \"cert\": \"$env://TEST_CERT\",\n                        \"keys\": [\"$env://TEST2_KEY\"],\n                        \"certs\": [\"$env://TEST2_CERT\"]\n                    },\n                    \"key\": \"/apisix/ssls/1\"\n                }]]\n              )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: trigger SSL match with SNI\n--- init_by_lua_block\n    require \"resty.core\"\n    apisix = require(\"apisix\")\n    core = require(\"apisix.core\")\n    apisix.http_init()\n\n    local utils = require(\"apisix.core.utils\")\n    utils.dns_parse = function (domain)  -- mock: DNS parser\n        if domain == \"test1.com\" then\n            return {address = \"127.0.0.2\"}\n        end\n\n        error(\"unknown domain: \" .. domain)\n    end\n--- exec\ncurl -k --resolve \"test.com:1994:127.0.0.1\" https://test.com:1994/opentracing\n--- wait: 5\n--- response_body\nopentracing\n\n\n\n=== TEST 6: verify span tree structure\n--- config\n    location /t {\n        content_by_lua_block {\n            local otel = require(\"lib.test_otel\")\n\n            local ok, err = otel.verify_tree(\n                \"ci/pod/otelcol-contrib/data-otlp.json\",\n                {\n                    name = \"GET /opentracing\",\n                    kind = 2,\n                    attributes = {\n                        [\"apisix.route_id\"] = \"1\",\n                        [\"http.method\"] = \"GET\",\n                        [\"http.status_code\"] = \"200\",\n                    },\n                    children = {\n                        {\n                            name = \"ssl_client_hello_phase\",\n                            kind = 2,\n                            children = {\n                                { name = \"sni_radixtree_match\", kind = 1 },\n                            }\n                        },\n                        {\n                            name = \"apisix.phase.access\",\n                            kind = 2,\n                            children = {\n                                { name = \"sni_radixtree_match\", kind = 1 },\n                                { name = \"http_router_match\", kind = 1 },\n                            }\n                        },\n                        { name = \"resolve_dns\", kind = 1 },\n                        { name = \"apisix.phase.header_filter\", kind = 2 },\n                        { name = \"apisix.phase.body_filter\", kind = 2 },\n                        { name = \"apisix.phase.log.plugins.opentelemetry\", kind = 1 },\n                    }\n                }\n            )\n\n            if not ok then\n                ngx.say(\"FAIL:\\n\" .. err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n"
  },
  {
    "path": "t/plugin/openwhisk.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity check with minimal valid configuration.\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openwhisk\")\n            local ok, err = plugin.check_schema({api_host = \"http://127.0.0.1:3233\", service_token = \"test:test\", namespace = \"test\", action = \"test\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: missing `api_host`\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openwhisk\")\n            local ok, err = plugin.check_schema({service_token = \"test:test\", namespace = \"test\", action = \"test\"})\n            if not ok then\n                ngx.say(err)\n            end\n        }\n    }\n--- response_body\nproperty \"api_host\" is required\n\n\n\n=== TEST 3: wrong type for `api_host`\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openwhisk\")\n            local ok, err = plugin.check_schema({api_host = 3233, service_token = \"test:test\", namespace = \"test\", action = \"test\"})\n            if not ok then\n                ngx.say(err)\n            end\n        }\n    }\n--- response_body\nproperty \"api_host\" validation failed: wrong type: expected string, got number\n\n\n\n=== TEST 4: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"test-params\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: verify encrypted field\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n\n            -- get plugin conf from etcd, service_token is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"openwhisk\"].service_token)\n\n        }\n    }\n--- response_body\npe14btxogtzJ4qPM/W2qj0AQeUK/O5oegLkKJLkkSEsKUIjP+bgyO+qsTXuLrY/h/esLKrRulD2TOtf+Zt/Us+hxZ/svsMwXZqZ9T9/2wWyi8SKALLfTUZDiV69mxCwD2zNBze1jslMlPtdA9JFIOQ==\n\n\n\n=== TEST 6: hit route (with GET request)\n--- request\nGET /hello\n--- response_body chomp\n{\"hello\":\"test\"}\n\n\n\n=== TEST 7: hit route (with POST method and non-json format request body)\n--- request\nPOST /hello\ntest=test\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 400\n--- response_body_like eval\nqr/\"error\":\"The request content was malformed/\n\n\n\n=== TEST 8: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"test-params\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route (with POST and correct request body)\n--- request\nPOST /hello\n{\"name\": \"world\"}\n--- more_headers\nContent-Type: application/json\n--- response_body chomp\n{\"hello\":\"world\"}\n\n\n\n=== TEST 10: reset route to non-existent action\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"non-existent\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route (with non-existent action)\n--- request\nPOST /hello\n{\"name\": \"world\"}\n--- more_headers\nContent-Type: application/json\n--- error_code: 404\n--- response_body_like eval\nqr/\"error\":\"The requested resource does not exist.\"/\n\n\n\n=== TEST 12: reset route to wrong api_host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:1979\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"non-existent\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route (with wrong api_host)\n--- request\nPOST /hello\n{\"name\": \"world\"}\n--- more_headers\nContent-Type: application/json\n--- error_code: 503\n--- error_log\nfailed to process openwhisk action, err:\n\n\n\n=== TEST 14: reset route to packaged action\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"package\": \"pkg\",\n                                \"action\": \"testpkg\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route (with packaged action)\n--- request\nGET /hello\n--- response_body chomp\n{\"hello\":\"world\"}\n\n\n\n=== TEST 16: reset route to status code action\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"test-statuscode\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: hit route (with packaged action)\n--- request\nGET /hello\n--- error_code: 407\n\n\n\n=== TEST 18: reset route to headers action\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"test-headers\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: hit route (with headers action)\n--- request\nGET /hello\n--- response_headers\ntest: header\n\n\n\n=== TEST 20: reset route to body action\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"openwhisk\": {\n                                \"api_host\": \"http://127.0.0.1:3233\",\n                                \"service_token\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\",\n                                \"namespace\": \"guest\",\n                                \"action\": \"test-body\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {},\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: hit route (with body action)\n--- request\nGET /hello\n--- response_body\n{\"test\":\"body\"}\n"
  },
  {
    "path": "t/plugin/plugin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"no_error_log\", \"[error]\");\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nno_long_string();\nno_root_location();\nlog_level(\"info\");\nrun_tests;\n\n__DATA__\n\n=== TEST 1: ensure all plugins have exposed their name\n--- config\n    location /t {\n        content_by_lua_block {\n            local lfs = require(\"lfs\")\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/../../apisix/plugins/\") do\n                if string.match(file_name, \".lua$\") then\n                    local expected = file_name:sub(1, #file_name - 4)\n                    local plugin = require(\"apisix.plugins.\" .. expected)\n                    if plugin.name ~= expected then\n                        ngx.say(\"expected \", expected, \" got \", plugin.name)\n                        return\n                    end\n                end\n            end\n            ngx.say('ok')\n        }\n    }\n--- response_body\nok\n\n\n\n=== TEST 2: define route for /*\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"key\": \"user-key\",\n                            \"secret\": \"my-secret-key\"\n                        }\n                    }\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: sign and verify\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, _, res = t('/hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTg3OTMxODU0MX0.fNtFJnNmJgzbiYmGB0Yjvm-l6A6M4jRV1l4mnVFSYjs',\n                ngx.HTTP_GET\n            )\n\n            ngx.status = code\n            ngx.print(res)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 4: delete /* and define route for /apisix/plugin/blah\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/routes/1', \"DELETE\")\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/apisix/plugin/blah\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit\n--- request\nGET /apisix/plugin/blah\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing JWT token in request\"}\n\n\n\n=== TEST 6: ensure all plugins have unique priority\n--- config\n    location /t {\n        content_by_lua_block {\n            local lfs = require(\"lfs\")\n            local pri_name = {}\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/../../apisix/plugins/\") do\n                if string.match(file_name, \".lua$\") then\n                    local name = file_name:sub(1, #file_name - 4)\n                    local plugin = require(\"apisix.plugins.\" .. name)\n                    if pri_name[plugin.priority] then\n                        ngx.say(name, \" has same priority with \", pri_name[plugin.priority])\n                        return\n                    end\n                    pri_name[plugin.priority] = plugin.name\n                end\n            end\n            ngx.say('ok')\n        }\n    }\n--- response_body\nok\n\n\n\n=== TEST 7: plugin with custom error message\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"jwt-auth\": {\n                            \"_meta\": {\n                                \"error_response\": {\n                                    \"message\":\"Missing credential in request\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: verify, missing token\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing credential in request\"}\n\n\n\n=== TEST 9: validate custom error message configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for _, case in ipairs({\n                {input = true},\n                {input = {\n                    error_response = true\n                }},\n                {input = {\n                    error_response = \"OK\"\n                }},\n            }) do\n                local code, body = t('/apisix/admin/plugin_configs/1',\n                    ngx.HTTP_PUT,\n                    {\n                        plugins = {\n                            [\"jwt-auth\"] = {\n                                _meta = case.input\n                            }\n                        }\n                    }\n                )\n                if code >= 300 then\n                    ngx.print(body)\n                else\n                    ngx.say(body)\n                end\n            end\n        }\n    }\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin jwt-auth err: property \\\"_meta\\\" validation failed: wrong type: expected object, got boolean\"}\n{\"error_msg\":\"failed to check the configuration of plugin jwt-auth err: property \\\"_meta\\\" validation failed: property \\\"error_response\\\" validation failed: value should match only one schema, but matches none\"}\npassed\n\n\n\n=== TEST 10: invalid _meta filter vars schema with wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                {\n                    plugins = {\n                        [\"jwt-auth\"] = {\n                            _meta = {\n                                filter = \"arg_k == v\"\n                            }\n                        }\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.print(body)\n            else\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin jwt-auth err: property \\\"_meta\\\" validation failed: property \\\"filter\\\" validation failed: wrong type: expected array, got string\"}\n\n\n\n=== TEST 11: invalid _meta filter schema with wrong expr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            for _, filter in ipairs({\n                {\"arg_name\", \"==\", \"json\"},\n                {\n                    {\"arg_name\", \"*=\", \"json\"}\n                }\n            }) do\n                local code, body = t('/apisix/admin/plugin_configs/1',\n                    ngx.HTTP_PUT,\n                    {\n                        plugins = {\n                            [\"jwt-auth\"] = {\n                                _meta = {\n                                    filter = filter\n                                }\n                            }\n                        }\n                    }\n                )\n                if code >= 300 then\n                    ngx.print(body)\n                else\n                    ngx.say(body)\n                end\n            end\n        }\n    }\n--- response_body\n{\"error_msg\":\"failed to validate the 'vars' expression: rule should be wrapped inside brackets\"}\n{\"error_msg\":\"failed to validate the 'vars' expression: invalid operator '*='\"}\n\n\n\n=== TEST 12: proxy-rewrite plugin run with _meta filter vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                {\n                    plugins = {\n                        [\"proxy-rewrite\"] = {\n                            _meta = {\n                                filter = {\n                                    {\"arg_version\", \"==\", \"v2\"}\n                                }\n                            },\n                            uri = \"/echo\",\n                            headers = {\n                                [\"X-Api-Version\"] = \"v2\"\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\"\n                }\n            )\n            if code >= 300 then\n                ngx.print(body)\n            else\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route: run proxy-rewrite plugin\n--- request\nGET /hello?version=v2\n--- response_headers\nx-api-version: v2\n\n\n\n=== TEST 14: hit route: not run proxy-rewrite plugin\n--- request\nGET /hello?version=v1\n--- response_body\nhello world\n\n\n\n=== TEST 15: different route，same plugin, different filter (for expr_lrucache)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                {\n                    plugins = {\n                        [\"proxy-rewrite\"] = {\n                            _meta = {\n                                filter = {\n                                    {\"arg_version\", \"==\", \"v3\"}\n                                }\n                            },\n                            uri = \"/echo\",\n                            headers = {\n                                [\"X-Api-Version\"] = \"v3\"\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello1\"\n                }\n            )\n            if code >= 300 then\n                ngx.print(body)\n            else\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route: run proxy-rewrite plugin\n--- request\nGET /hello1?version=v3\n--- response_headers\nx-api-version: v3\n\n\n\n=== TEST 17: same plugin, same id between routes and global_rules, different filter (for expr_lrucache)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/2',\n                ngx.HTTP_PUT,\n                {\n                    plugins = {\n                        [\"proxy-rewrite\"] = {\n                            _meta = {\n                                filter = {\n                                    {\"arg_version\", \"==\", \"v4\"}\n                                }\n                            },\n                            uri = \"/echo\",\n                            headers = {\n                                [\"X-Api-Version\"] = \"v4\"\n                            }\n                        }\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.print(body)\n            else\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route: run global proxy-rewrite plugin\n--- request\nGET /hello1?version=v4\n--- response_headers\nx-api-version: v4\n\n\n\n=== TEST 19: use _meta.filter in response-rewrite plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"response-rewrite\": {\n                                \"_meta\": {\n                                    \"filter\": [\n                                        [\"upstream_status\", \"~=\", 200]\n                                    ]\n                                },\n                                \"headers\": {\n                                    \"set\": {\n                                        \"test-header\": \"error\"\n                                    }\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: upstream_status = 502, enable response-rewrite plugin\n--- request\nGET /specific_status\n--- more_headers\nx-test-upstream-status: 502\n--- response_headers\ntest-header: error\n--- error_code: 502\n\n\n\n=== TEST 21: upstream_status = 200, disable response-rewrite plugin\n--- request\nGET /hello\n--- response_headers\n!test-header\n\n\n\n=== TEST 22: use _meta.filter in response-rewrite plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"headers\": {\n                                    \"foo-age\": \"$arg_age\"\n                                }\n                            },\n                            \"response-rewrite\": {\n                                \"_meta\": {\n                                    \"filter\": [\n                                        [\"http_foo_age\", \"==\", \"18\"]\n                                    ]\n                                },\n                               \"status_code\": 403\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: proxy-rewrite plugin will set $http_foo_age, response-rewrite plugin return 403\n--- request\nGET /hello?age=18\n--- error_code: 403\n\n\n\n=== TEST 24: response-rewrite plugin disable, return 200\n--- request\nGET /hello\n\n\n\n=== TEST 25: use response var in meta.filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"_meta\": {\n                                    \"filter\": [\n                                        [\"upstream_status\", \"==\", \"200\"]\n                                    ]\n                                },\n                                \"uri\": \"/echo\",\n                                \"headers\": {\n                                    \"x-version\": \"v1\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 26: hit route: disable proxy-rewrite plugin\n--- request\nGET /hello\n--- response_headers\n!x-version\n\n\n\n=== TEST 27: use APISIX's built-in variables in  meta.filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                {\n                    plugins = {\n                        [\"proxy-rewrite\"] = {\n                            _meta = {\n                                filter = {\n                                    {\"post_arg_key\", \"==\", \"abc\"}\n                                }\n                            },\n                            uri = \"/echo\",\n                            headers = {\n                                [\"X-Api-Version\"] = \"ga\"\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        }\n                    },\n                    uri = \"/hello\"\n                }\n            )\n            if code >= 300 then\n                ngx.print(body)\n            else\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 28: hit route: proxy-rewrite enable with post_arg_xx in meta.filter\n--- request\nPOST /hello\nkey=abc\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- response_headers\nx-api-version: ga\n"
  },
  {
    "path": "t/plugin/prometheus-ai-proxy.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n    my $user_yaml_config = <<_EOC_;\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\nplugins:\n  - ai-proxy-multi\n  - prometheus\n  - public-api\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $user_yaml_config);\n    my $http_config = $block->http_config // <<_EOC_;\n        server {\n            listen 6724;\n\n            default_type 'application/json';\n\n            location /v1/chat/completions {\n                content_by_lua_block {\n                    ngx.exec(\"\\@chat\")\n                }\n            }\n\n\n            location /delay/v1/chat/completions {\n                content_by_lua_block {\n                    ngx.sleep(2)\n                    ngx.exec(\"\\@chat\")\n                }\n            }\n\n            location \\@chat {\n                content_by_lua_block {\n                    ngx.status = 200\n                    ngx.say([[\n{\n  \"choices\": [\n    {\n      \"message\": {\n        \"content\": \"1 + 1 = 2.\",\n        \"role\": \"assistant\"\n      }\n    }\n  ],\n  \"usage\": {\n    \"completion_tokens\": 5,\n    \"prompt_tokens\": 8,\n    \"total_tokens\": 13\n  }\n}\n                    ]])\n                }\n            }\n        }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create a route with prometheus and ai-proxy-multi plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"ai-proxy-multi\": {\n                                \"instances\": [\n                                    {\n                                        \"name\": \"openai-gpt4\",\n                                        \"provider\": \"openai\",\n                                        \"weight\": 1,\n                                        \"auth\": {\n                                            \"header\": {\n                                                \"Authorization\": \"Bearer token\"\n                                            }\n                                        },\n                                        \"options\": {\n                                            \"model\": \"gpt-4\"\n                                        },\n                                        \"override\": {\n                                            \"endpoint\": \"http://localhost:6724\"\n                                        }\n                                    }\n                                ]\n                            }\n                        },\n                        \"uri\": \"/chat\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/metrics\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/prometheus/metrics\"\n                    }]]\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local _, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\" x 2\n\n\n\n=== TEST 2: send a chat request\n--- request\nPOST /chat\n{\"messages\":[{\"role\":\"user\",\"content\":\"What is 1+1?\"}], \"model\": \"gpt-3\"}\n--- error_code: 200\n\n\n\n=== TEST 3: assert llm_lantency_bucket metric\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_llm_latency_bucket\\{.*route_id=\"1\",.*,node=\"openai-gpt4\".*.*request_type=\"ai_chat\",request_llm_model=\"gpt-3\",llm_model=\"gpt-4\",le=\"\\d+\"\\} 1/\n\n\n\n=== TEST 4: assert llm_lantency_count metric\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_llm_latency_count\\{.*route_id=\"1\",.*,node=\"openai-gpt4\".*.*request_type=\"ai_chat\",request_llm_model=\"gpt-3\",llm_model=\"gpt-4\"\\} 1/\n\n\n\n=== TEST 5: assert llm_lantency_sum metric\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_llm_latency_count\\{.*route_id=\"1\",.*,node=\"openai-gpt4\".*.*request_type=\"ai_chat\",request_llm_model=\"gpt-3\",llm_model=\"gpt-4\"\\} \\d+/\n\n\n\n=== TEST 6: assert llm_prompt_tokens metric\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_llm_prompt_tokens\\{.*route_id=\"1\",.*,node=\"openai-gpt4\".*.*request_type=\"ai_chat\",request_llm_model=\"gpt-3\",llm_model=\"gpt-4\"\\} 8/\n\n\n\n=== TEST 7: assert llm_completion_tokens metric\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_llm_completion_tokens\\{.*route_id=\"1\",.*,node=\"openai-gpt4\".*.*request_type=\"ai_chat\",request_llm_model=\"gpt-3\",llm_model=\"gpt-4\"\\} 5/\n\n\n\n=== TEST 8: assert llm_active_connections metric\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_llm_active_connections\\{.*route_id=\"1\",.*,node=\"openai-gpt4\".*.*request_type=\"ai_chat\",request_llm_model=\"gpt-3\",llm_model=\"gpt-4\"\\} 0/\n\n\n\n=== TEST 9: change ai-proxy-multi to use a slower ai endpoint\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"ai-proxy-multi\": {\n                                \"instances\": [\n                                    {\n                                        \"name\": \"openai-gpt4\",\n                                        \"provider\": \"openai\",\n                                        \"weight\": 1,\n                                        \"auth\": {\n                                            \"header\": {\n                                                \"Authorization\": \"Bearer token\"\n                                            }\n                                        },\n                                        \"options\": {\n                                            \"model\": \"gpt-4\"\n                                        },\n                                        \"override\": {\n                                            \"endpoint\": \"http://localhost:6724/delay/v1/chat/completions\"\n                                        }\n                                    }\n                                ]\n                            }\n                        },\n                        \"uri\": \"/chat\"\n                    }]],\n                },\n            }\n            local t = require(\"lib.test_admin\").test\n            for _, data in ipairs(data) do\n                local _, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\"\n\n\n\n=== TEST 10: assert llm_active_connections metric when the ai endpoint is slow\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local res_list = {}\n            for i = 1, 3 do\n                local url = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/chat\"\n                local function send_chat_request(idx)\n                    local http = require \"resty.http\"\n                    local httpc = http.new()\n                    local res = httpc:request_uri(\n                        url,\n                        {\n                            method = \"POST\",\n                            body = [[ {\"messages\":[{\"role\":\"user\",\"content\":\"What is 1+1?\"}]} ]],\n                        })\n                    res_list[idx] = res\n                end\n                ngx.timer.at(0, send_chat_request, i)\n            end\n            ngx.sleep(1)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local metric_resp = httpc:request_uri(\"http://127.0.0.1:\" .. ngx.var.server_port .. \"/apisix/prometheus/metrics\")\n            if not string.find(metric_resp.body, [[apisix_llm_active_connections{.*} 3]]) then\n                ngx.say(metric_resp.body)\n                ngx.say(\"llm_active_connections should be 3\")\n                return\n            end\n            ngx.sleep(1)\n            for _, res in ipairs(res_list) do\n                if res.status ~= 200 then\n                    ngx.say(\"failed to send chat request\")\n                    return\n                end\n            end\n            metric_resp = httpc:request_uri(\"http://127.0.0.1:\" .. ngx.var.server_port .. \"/apisix/prometheus/metrics\")\n            if not string.find(metric_resp.body, [[apisix_llm_active_connections{.*} 0]]) then\n                ngx.say(metric_resp.body)\n                ngx.say(\"llm_active_connections should be 0 after all requests are done\")\n                return\n            end\n            ngx.say(\"success\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nsuccess\n"
  },
  {
    "path": "t/plugin/prometheus-metric-expire.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route with prometheus ttl\n--- yaml_config\nplugin_attr:\n    prometheus:\n        default_buckets:\n            - 15\n            - 55\n            - 105\n            - 205\n            - 505\n        metrics:\n            http_status:\n                expire: 1\n            http_latency:\n                expire: 1\n            bandwidth:\n                expire: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/routes/metrics',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"public-api\": {}\n                    },\n                    \"uri\": \"/apisix/prometheus/metrics\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            local code, body = t('/hello1',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                nil\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(2)\n            local code, pass, body = t('/apisix/prometheus/metrics',\n                ngx.HTTP_GET,\n                \"\",\n                nil,\n                nil\n            )\n\n            local metrics_to_check = {\"apisix_bandwidth\", \"http_latency\", \"http_status\",}\n\n            -- verify that above mentioned metrics are not in the metrics response\n            for _, v in pairs(metrics_to_check) do\n                local match, err = ngx.re.match(body, \"\\\\b\" .. v .. \"\\\\b\", \"m\")\n                if match then\n                    ngx.status = 500\n                    ngx.say(\"error found \" .. v .. \" in metrics\")\n                    return\n                end\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/prometheus.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'EOF');\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\nEOF\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.prometheus\")\n            local ok, err = plugin.check_schema({})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: setup public API route and test route\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"prometheus\": {}\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/metrics\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/prometheus/metrics\"\n                    }]]\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(code..body)\n            end\n        }\n    }\n--- response_body eval\n\"201passed\\n\" x 2\n\n\n\n=== TEST 3: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like\napisix_etcd_reachable 1\n\n\n\n=== TEST 4: request from client (all hit)\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 5: request from client (part hit)\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /hello\", \"GET /hello2\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[404, 200, 404, 200, 200]\n\n\n\n=== TEST 6: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 7: test for unsupported method\n--- request\nPATCH /apisix/prometheus/metrics\n--- error_code: 404\n\n\n\n=== TEST 8: set route without id in post body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: pipeline of client request\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /not_found\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 404, 200, 200]\n\n\n\n=== TEST 10: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 11: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_latency_count\\{type=\"request\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 12: create service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: use service 1 in route 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"service_id\": 1,\n                    \"uri\": \"/hello1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: pipeline of client request\n--- pipelined_requests eval\n[\"GET /hello1\", \"GET /not_found\", \"GET /hello1\", \"GET /hello1\"]\n--- error_code eval\n[200, 404, 200, 200]\n\n\n\n=== TEST 15: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"2\",service=\"1\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 16: delete route 2\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: set it in route with plugin `fault-injection`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"prometheus\": {},\n                        \"fault-injection\": {\n                            \"abort\": {\n                               \"http_status\": 200,\n                               \"body\": \"Fault Injection!\"\n                            }\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: pipeline of client request\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /not_found\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 404, 200, 200]\n\n\n\n=== TEST 19: set it in global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/routes/3',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello3\"\n                }]]\n            )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\npassed\n\n\n\n=== TEST 20: request from client\n--- pipelined_requests eval\n[\"GET /hello3\", \"GET /hello3\"]\n--- error_code eval\n[404, 404]\n\n\n\n=== TEST 21: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"404\",route=\"3\",matched_uri=\"\\/hello3\",matched_host=\"\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} 2/\n\n\n\n=== TEST 22: fetch the prometheus metric data with apisix latency\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/.*apisix_http_latency_bucket\\{type=\"apisix\".*/\n\n\n\n=== TEST 23: add service 3 to distinguish other services\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 24: add a route 4 to redirect /mysleep?seconds=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/4',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_id\": 3,\n                    \"uri\": \"/mysleep\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: request from client to /mysleep?seconds=1 ( all hit)\n--- pipelined_requests eval\n[\"GET /mysleep?seconds=1\", \"GET /mysleep?seconds=1\", \"GET /mysleep?seconds=1\"]\n--- error_code eval\n[200, 200, 200]\n\n\n\n=== TEST 26: fetch the prometheus metric data with apisix latency (latency < 1s)\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_latency_bucket\\{type=\"apisix\".*service=\\\"3\\\".*le=\\\"500.*/\n\n\n\n=== TEST 27: delete route 4\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/4',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 28: delete service 3\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/3',\n                ngx.HTTP_DELETE\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 29: fetch the prometheus metric data with `modify_indexes consumers`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"consumers\"\\} \\d+/\n\n\n\n=== TEST 30: fetch the prometheus metric data with `modify_indexes global_rules`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"global_rules\"\\} \\d+/\n\n\n\n=== TEST 31: fetch the prometheus metric data with `modify_indexes max_modify_index`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"max_modify_index\"\\} \\d+/\n\n\n\n=== TEST 32: fetch the prometheus metric data with `modify_indexes protos`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"protos\"\\} \\d+/\n\n\n\n=== TEST 33: fetch the prometheus metric data with `modify_indexes routes`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"routes\"\\} \\d+/\n\n\n\n=== TEST 34: fetch the prometheus metric data with `modify_indexes services`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"services\"\\} \\d+/\n\n\n\n=== TEST 35: fetch the prometheus metric data with `modify_indexes ssls`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"ssls\"\\} \\d+/\n\n\n\n=== TEST 36: fetch the prometheus metric data with `modify_indexes stream_routes`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"stream_routes\"\\} \\d+/\n\n\n\n=== TEST 37: fetch the prometheus metric data with `modify_indexes upstreams`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"upstreams\"\\} \\d+/\n\n\n\n=== TEST 38: fetch the prometheus metric data with `modify_indexes prev_index`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"prev_index\"\\} \\d+/\n\n\n\n=== TEST 39: fetch the prometheus metric data with `modify_indexes x_etcd_index`\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_etcd_modify_indexes\\{key=\"x_etcd_index\"\\} \\d+/\n\n\n\n=== TEST 40: fetch the prometheus metric data -- hostname\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_node_info\\{hostname=\".*\"\\} 1/\n\n\n\n=== TEST 41: don't try to provide etcd metrics when you don't use it\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    uri: /apisix/prometheus/metrics\n    plugins:\n        public-api: {}\n  -\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like eval\nqr/apisix_/\n--- response_body_unlike eval\nqr/etcd/\n"
  },
  {
    "path": "t/plugin/prometheus2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'EOF');\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\nEOF\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/metrics\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/prometheus/metrics\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/metrics-custom-uri\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/a\"\n                    }]]\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(code..body)\n            end\n        }\n    }\n--- response_body eval\n\"201passed\\n\" x 2\n\n\n\n=== TEST 2: set route with key-auth enabled for consumer metrics\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {},\n                        \"key-auth\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: pipeline of client request without api-key\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[401, 401, 401, 401]\n\n\n\n=== TEST 4: fetch the prometheus metric data: consumer is empty\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"1\",service=\"\",consumer=\"\",node=\"\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 5: set consumer for metrics data collection\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: pipeline of client request with successfully authorized\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- more_headers\napikey: auth-one\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 7: fetch the prometheus metric data: consumer is jack\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"200\",route=\"1\",matched_uri=\"\\/hello\",matched_host=\"\",service=\"\",consumer=\"jack\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 8: set route(id: 9)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/9',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {}\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"hosts\": [\"foo.com\", \"bar.com\"],\n                        \"uris\": [\"/foo*\", \"/bar*\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: set it in global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: 404 Route Not Found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 11: fetch the prometheus metric data: 404 Route Not Found\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"404\",route=\"\",matched_uri=\"\",matched_host=\"\",service=\"\",consumer=\"\",node=\"\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 12: hit routes(uri = \"/foo*\", host = \"foo.com\")\n--- request\nGET /foo1\n--- more_headers\nHost: foo.com\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n\n\n\n=== TEST 13: fetch the prometheus metric data: hit routes(uri = \"/foo*\", host = \"foo.com\")\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"404\",route=\"9\",matched_uri=\"\\/foo\\*\",matched_host=\"foo.com\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 14: hit routes(uri = \"/bar*\", host = \"bar.com\")\n--- request\nGET /bar1\n--- more_headers\nHost: bar.com\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n\n\n\n=== TEST 15: fetch the prometheus metric data: hit routes(uri = \"/bar*\", host = \"bar.com\")\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"404\",route=\"9\",matched_uri=\"\\/bar\\*\",matched_host=\"bar.com\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 16: customize export uri, not found\n--- yaml_config\nplugin_attr:\n    prometheus:\n        export_uri: /a\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 404\n\n\n\n=== TEST 17: customize export uri, found\n--- yaml_config\nplugin_attr:\n    prometheus:\n        export_uri: /a\n--- request\nGET /a\n--- error_code: 200\n\n\n\n=== TEST 18: customize export uri, missing plugin, use default\n--- yaml_config\nplugin_attr:\n    x:\n        y: z\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n\n\n\n=== TEST 19: customize export uri, missing attr, use default\n--- yaml_config\nplugin_attr:\n    prometheus:\n        y: z\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n\n\n\n=== TEST 20: set sys plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/9',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"syslog\": {\n                                \"host\": \"127.0.0.1\",\n                                \"include_req_body\": false,\n                                \"max_retry_count\": 1,\n                                \"tls\": false,\n                                \"retry_delay\": 1,\n                                \"batch_max_size\": 1000,\n                                \"buffer_duration\": 60,\n                                \"port\": 1000,\n                                \"name\": \"sys-logger\",\n                                \"flush_limit\": 4096,\n                                \"sock_type\": \"tcp\",\n                                \"timeout\": 3,\n                                \"drop_limit\": 1048576,\n                                \"pool_size\": 5\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: hit batch-process-metrics\n--- request\nGET /batch-process-metrics\n--- error_code: 404\n\n\n\n=== TEST 22: check sys logger metrics\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"sys-logger\",route_id=\"9\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 23: set zipkin plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/9',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9447\",\n                                \"service_name\": \"APISIX\",\n                                \"sample_ratio\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 24: hit batch-process-metrics\n--- request\nGET /batch-process-metrics\n--- error_code: 404\n\n\n\n=== TEST 25: check zipkin log metrics\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"zipkin_report\",route_id=\"9\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 26: set http plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/9',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"http-logger\": {\n                                \"inactive_timeout\": 5,\n                                \"include_req_body\": false,\n                                \"timeout\": 3,\n                                \"name\": \"http-logger\",\n                                \"retry_delay\": 1,\n                                \"buffer_duration\": 60,\n                                \"uri\": \"http://127.0.0.1:19080/report\",\n                                \"concat_method\": \"json\",\n                                \"batch_max_size\": 1000,\n                                \"max_retry_count\": 0\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 27: hit batch-process-metrics\n--- request\nGET /batch-process-metrics\n--- error_code: 404\n\n\n\n=== TEST 28: check http log metrics\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"http-logger\",route_id=\"9\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 29: set tcp-logger plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/10',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"include_req_body\": false,\n                                \"timeout\": 1000,\n                                \"name\": \"tcp-logger\",\n                                \"retry_delay\": 1,\n                                \"buffer_duration\": 60,\n                                \"port\": 1000,\n                                \"batch_max_size\": 1000,\n                                \"inactive_timeout\": 60,\n                                \"tls\": false,\n                                \"max_retry_count\": 0\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics-10\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 30:  trigger metrics batch-process-metrics\n--- request\nGET /batch-process-metrics-10\n--- error_code: 404\n\n\n\n=== TEST 31: check tcp log metrics\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"tcp-logger\",route_id=\"10\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 32: set udp-logger plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/10',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1000,\n                                \"include_req_body\": false,\n                                \"timeout\": 3,\n                                \"batch_max_size\": 1000,\n                                \"name\": \"udp-logger\",\n                                \"inactive_timeout\": 5,\n                                \"buffer_duration\": 60\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics-10\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 33:  trigger metrics batch-process-metrics\n--- request\nGET /batch-process-metrics-10\n--- error_code: 404\n\n\n\n=== TEST 34: check udp log metrics\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"udp-logger\",route_id=\"10\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 35: set sls-logger plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/10',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"sls-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"batch_max_size\": 1000,\n                                \"name\": \"sls-logger\",\n                                \"inactive_timeout\": 5,\n                                \"logstore\": \"your_logstore\",\n                                \"buffer_duration\": 60,\n                                \"port\": 10009,\n                                \"max_retry_count\": 0,\n                                \"retry_delay\": 1,\n                                \"access_key_id\": \"your_access_id\",\n                                \"access_key_secret\": \"your_key_secret\",\n                                \"timeout\": 5000,\n                                \"project\": \"your_project\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics-10\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 36:  trigger metrics batch-process-metrics\n--- request\nGET /batch-process-metrics-10\n--- error_code: 404\n\n\n\n=== TEST 37: check sls-logger metrics\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"sls-logger\",route_id=\"10\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 38: create service and route both with name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"service_name\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"route_name\",\n                    \"service_id\": 1,\n                    \"plugins\": {\n                        \"prometheus\": {\n                            \"prefer_name\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\npassed\n\n\n\n=== TEST 39: pipeline of client request\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 40: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"route_name\",service=\"service_name\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 41: set route name but remove service name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 42: pipeline of client request\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 43: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"route_name\",service=\"1\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 44: set service name but remove route name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"name\": \"service_name\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"service_id\": 1,\n                    \"plugins\": {\n                        \"prometheus\": {\n                            \"prefer_name\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\npassed\n\n\n\n=== TEST 45: pipeline of client request\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 46: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"1\",service=\"service_name\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 47: remove both name, but still set prefer_name to true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"service_id\": 1,\n                    \"plugins\": {\n                        \"prometheus\": {\n                            \"prefer_name\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 48: pipeline of client request\n--- request\nGET /hello\n--- error_code: 200\n\n\n\n=== TEST 49: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"1\",service=\"service_name\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} \\d+/\n\n\n\n=== TEST 50: fetch the prometheus shared dict data\n--- http_config\nlua_shared_dict test-shared-dict 10m;\n--- request\nGET /apisix/prometheus/metrics\n--- response_body_like\n.*apisix_shared_dict_capacity_bytes\\{name=\"test-shared-dict\"\\} 10485760(?:.|\\n)*\napisix_shared_dict_free_space_bytes\\{name=\"test-shared-dict\"\\} \\d+.*\n"
  },
  {
    "path": "t/plugin/prometheus3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'EOF');\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\nEOF\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: setup public API route and test route\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/plugin_configs/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"prometheus\":{}\n                        }\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugin_config_id\": 1,\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/metrics\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/prometheus/metrics\"\n                    }]]\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(code..body)\n            end\n        }\n    }\n--- response_body eval\n\"201passed\\n\" x 3\n\n\n\n=== TEST 2: hit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /apisix/prometheus/metrics\"]\n--- error_code eval\n[200, 200]\n\n\n\n=== TEST 3: apisix_batch_process_entries, mess with global rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"prometheus\": {}\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics-aa\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_metadata/error-log-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"tcp\": {\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 1999\n                    },\n                    \"max_retry_count\": 1000,\n                    \"level\": \"NOTICE\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/global_rules/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"http-logger\": {\n                                \"uri\": \"http://127.0.0.1:1979\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: check metrics\n--- yaml_config\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\nplugins:\n  - public-api\n  - error-log-logger\n  - prometheus\n  - http-logger\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/batch-process-metrics-aa\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.sleep(2)\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/apisix/prometheus/metrics\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"http logger\",route_id=\"1\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 5: set prometheus plugin at both global rule and route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: test prometheus plugin at both global rule and route\n--- request\nGET /opentracing\n--- response_body\nopentracing\n\n\n\n=== TEST 7: fetch prometheus plugin at both global rule and route data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"200\",route=\"1\",matched_uri=\"\\/opentracing\",matched_host=\"\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\"\\} 1/\n"
  },
  {
    "path": "t/plugin/prometheus4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", <<'EOF');\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\nEOF\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local t = require(\"lib.test_admin\").test\n            local code = t('/apisix/admin/routes/metrics',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"public-api\": {}\n                    },\n                    \"uri\": \"/apisix/prometheus/metrics\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n        }\n    }\n\n\n\n=== TEST 2: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/10',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: client request\n--- yaml_config\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\n        metrics:\n            bandwidth:\n                extra_labels:\n                    - upstream_addr: $upstream_addr\n                    - upstream_status: $upstream_status\n--- request\nGET /hello\n\n\n\n=== TEST 4: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_bandwidth\\{type=\"egress\",route=\"10\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",upstream_addr=\"127.0.0.1:1980\",upstream_status=\"200\"\\} \\d+/\n\n\n\n=== TEST 5: client request, label with nonexist ngx variable\n--- yaml_config\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\n        metrics:\n            http_status:\n                extra_labels:\n                    - dummy: $dummy\n--- request\nGET /hello\n\n\n\n=== TEST 6: fetch the prometheus metric data, with nonexist ngx variable\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_status\\{code=\"200\",route=\"10\",matched_uri=\"\\/hello\",matched_host=\"\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",dummy=\"\"\\} \\d+/\n\n\n\n=== TEST 7: set route\n--- yaml_config\nplugin_attr:\n    prometheus:\n        refresh_interval: 0.1\n        default_buckets:\n            - 15\n            - 55\n            - 105\n            - 205\n            - 505\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"prometheus\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /t\", \"GET /hello1\"]\n--- response_body eval\n[\"passed\\n\", \"hello1 world\\n\"]\n\n\n\n=== TEST 8: fetch metrics\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_http_latency_bucket\\{type=\"upstream\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",le=\"15\"\\} \\d+\napisix_http_latency_bucket\\{type=\"upstream\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",le=\"55\"\\} \\d+\napisix_http_latency_bucket\\{type=\"upstream\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",le=\"105\"\\} \\d+\napisix_http_latency_bucket\\{type=\"upstream\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",le=\"205\"\\} \\d+\napisix_http_latency_bucket\\{type=\"upstream\",route=\"1\",service=\"\",consumer=\"\",node=\"127.0.0.1\",request_type=\"traditional_http\",request_llm_model=\"\",llm_model=\"\",le=\"505\"\\} \\d+/\n\n\n\n=== TEST 9: set sys plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/9',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"prometheus\": {},\n                            \"syslog\": {\n                                \"host\": \"127.0.0.1\",\n                                \"include_req_body\": false,\n                                \"max_retry_times\": 1,\n                                \"tls\": false,\n                                \"retry_interval\": 1,\n                                \"batch_max_size\": 1000,\n                                \"buffer_duration\": 60,\n                                \"port\": 1000,\n                                \"name\": \"sys-logger\",\n                                \"flush_limit\": 4096,\n                                \"sock_type\": \"tcp\",\n                                \"timeout\": 3,\n                                \"drop_limit\": 1048576,\n                                \"pool_size\": 5\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/batch-process-metrics\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 10: remove prometheus -> reload -> send batch request -> add prometheus for next tests\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n  - example-plugin\nplugin_attr:\n  prometheus:\n    refresh_interval: 0.1\n  example-plugin:\n    val: 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, org_body = t('/v1/plugins/reload', ngx.HTTP_PUT)\n            local code, body = t('/batch-process-metrics', ngx.HTTP_GET)\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 404\n--- response_body_like eval\nqr/404 Not Found/\n\n\n\n=== TEST 11: fetch prometheus metrics -> batch_process_entries metrics should not be present\n--- yaml_config\ndeployment:\n  role: traditional\n  role_traditional:\n    config_provider: etcd\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\nplugins:\n  - prometheus\n  - public-api\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_unlike eval\nqr/apisix_batch_process_entries\\{name=\"sys-logger\",route_id=\"9\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 12: hit batch-process-metrics with prometheus enabled from TEST 11\n--- request\nGET /batch-process-metrics\n--- error_code: 404\n\n\n\n=== TEST 13: batch_process_entries metrics should be present now\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_batch_process_entries\\{name=\"sys-logger\",route_id=\"9\",server_addr=\"127.0.0.1\"\\} \\d+/\n\n\n\n=== TEST 14: node_info metric contains the current apisix version\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n--- response_body_like eval\nqr/apisix_node_info\\{hostname=\"[^\"]+\",version=\"\\d+\\.\\d+\\.\\d+\"\\} \\d+/\n\n\n\n=== TEST 15: verify no errors when prometheus is enabled in both http and stream subsystems\n--- yaml_config\ndeployment:\n  admin:\n    admin_key: null\napisix:\n  node_listen: 1984\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - addr: 9100\n    udp:\n      - 9200\nplugins:\n  - prometheus\n  - public-api\nstream_plugins:\n  - prometheus\nplugin_attr:\n  prometheus:\n    refresh_interval: 0.1\n--- request\nGET /apisix/prometheus/metrics\n--- error_code: 200\n"
  },
  {
    "path": "t/plugin/proxy-cache/disk.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    # for proxy cache\n    proxy_cache_path /tmp/disk_cache_one levels=1:2 keys_zone=disk_cache_one:50m inactive=1d max_size=1G;\n    proxy_cache_path /tmp/disk_cache_two levels=1:2 keys_zone=disk_cache_two:50m inactive=1d max_size=1G;\n\n    # for proxy cache\n    map \\$upstream_cache_zone \\$upstream_cache_zone_info {\n        disk_cache_one /tmp/disk_cache_one,1:2;\n        disk_cache_two /tmp/disk_cache_two,1:2;\n    }\n\n    server {\n        listen 1986;\n        server_tokens off;\n\n        location / {\n            expires 60s;\n            return 200 \"hello world!\";\n        }\n\n        location /hello-not-found {\n            return 404;\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity check (missing cache_zone field, the default value is disk_cache_one)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: sanity check (invalid type for cache_method)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": \"GET\",\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache/\n\n\n\n=== TEST 3: sanity check (invalid type for cache_key)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_key\": \"${uri}-cache-key\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1985\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache/\n\n\n\n=== TEST 4: sanity check (invalid type for cache_bypass)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": \"$arg_bypass\",\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1985\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache/\n\n\n\n=== TEST 5: sanity check (invalid type for no_cache)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": \"$arg_no_cache\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1985\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache/\n\n\n\n=== TEST 6: sanity check (illegal character for cache_key)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_key\": [\"$uri-\", \"-cache-id\"],\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1985\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache/\n\n\n\n=== TEST 7: sanity check (normal case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route (cache miss)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 9: hit route (cache hit)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n--- raw_response_headers_unlike\nExpires:\n\n\n\n=== TEST 10: hit route (cache bypass)\n--- request\nGET /hello?bypass=1\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: BYPASS\n\n\n\n=== TEST 11: purge cache\n--- request\nPURGE /hello\n--- error_code: 200\n\n\n\n=== TEST 12: hit route (nocache)\n--- request\nGET /hello?no_cache=1\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 13: hit route (there's no cache indeed)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n--- raw_response_headers_unlike\nExpires:\n\n\n\n=== TEST 14: hit route (will be cached)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n\n\n\n=== TEST 15: hit route (not found)\n--- request\nGET /hello-not-found\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 16: hit route (404 there's no cache indeed)\n--- request\nGET /hello-not-found\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 17: hit route (will be cached)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n\n\n\n=== TEST 18: hit route (HEAD method mismatch cache_method)\n--- request\nHEAD /hello\n--- error_code: 200\n--- response_headers\nApisix-Cache-Status: BYPASS\n\n\n\n=== TEST 19:  hide cache headers = false\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": false,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 20: hit route (catch the cache headers)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n--- response_headers_like\nCache-Control:\n\n\n\n=== TEST 21: purge cache\n--- request\nPURGE /hello\n--- error_code: 200\n\n\n\n=== TEST 22: purge cache (not found)\n--- request\nPURGE /hello-world\n--- error_code: 404\n\n\n\n=== TEST 23:  invalid cache zone\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"invalid_disk_cache\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": false,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/cache_zone invalid_disk_cache not found/\n\n\n\n=== TEST 24: sanity check (invalid variable for cache_key)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_key\": [\"$uri\", \"$request_method\"],\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1985\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache err: cache_key variable \\$request_method unsupported/\n\n\n\n=== TEST 25: don't override cache relative headers\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: hit route\n--- request\nGET /echo\n--- more_headers\nApisix-Cache-Status: Foo\nCache-Control: bar\nExpires: any\n--- response_headers\nApisix-Cache-Status: Foo\nCache-Control: bar\nExpires: any\n\n\n\n=== TEST 27: sanity check (invalid method for cache_method)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\", \"PUT\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache err/\n\n\n\n=== TEST 28: nil vars for cache_key\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_key\": [\"$arg_foo\", \"$arg_bar\", \"$arg_baz\"],\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: hit route with nil vars in cache_key\n--- request\nGET /hello?bar=a\n--- response_body chop\nhello world!\n"
  },
  {
    "path": "t/plugin/proxy-cache/memory.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{TEST_NGINX_FORCE_RESTART_ON_TEST} = 0;\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\n\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    # for proxy cache\n    proxy_cache_path /tmp/disk_cache_one levels=1:2 keys_zone=disk_cache_one:50m inactive=1d max_size=1G;\n    proxy_cache_path /tmp/disk_cache_two levels=1:2 keys_zone=disk_cache_two:50m inactive=1d max_size=1G;\n    lua_shared_dict memory_cache 50m;\n\n    # for proxy cache\n    map \\$upstream_cache_zone \\$upstream_cache_zone_info {\n        disk_cache_one /tmp/disk_cache_one,1:2;\n        disk_cache_two /tmp/disk_cache_two,1:2;\n    }\n\n    server {\n        listen 1986;\n        server_tokens off;\n\n        location / {\n            expires 60s;\n\n            if (\\$arg_expires) {\n                expires \\$arg_expires;\n            }\n\n            if (\\$arg_cc) {\n                expires off;\n                add_header Cache-Control \\$arg_cc;\n            }\n\n            return 200 \"hello world!\";\n        }\n\n        location /hello-not-found {\n            return 404;\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity check (invalid cache strategy)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"network\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"disk_cache_one\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache err: property \\\\\"cache_strategy\\\\\" validation failed: matches none of the enum values/\n\n\n\n=== TEST 2: sanity check (invalid cache_zone when specifying cache_strategy as memory)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"memory\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"invalid_cache_zone\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": true,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-cache err: cache_zone invalid_cache_zone not found\"/\n\n\n\n=== TEST 3: sanity check (normal case for memory strategy)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"memory\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"memory_cache\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"hide_cache_headers\": false,\n                               \"cache_ttl\": 300,\n                               \"cache_http_status\": [200],\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route (cache miss)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 5: hit route (cache hit)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n\n\n\n=== TEST 6: hit route (cache bypass)\n--- request\nGET /hello?bypass=1\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: BYPASS\n\n\n\n=== TEST 7: purge cache\n--- request\nPURGE /hello\n--- error_code: 200\n\n\n\n=== TEST 8: hit route (nocache)\n--- request\nGET /hello?no_cache=1\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 9: hit route (there's no cache indeed)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n--- raw_response_headers_unlike\nExpires:\n\n\n\n=== TEST 10: hit route (will be cached)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n\n\n\n=== TEST 11: hit route (not found)\n--- request\nGET /hello-not-found\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 12: hit route (404 there's no cache indeed)\n--- request\nGET /hello-not-found\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 13: hit route (HEAD method)\n--- request\nHEAD /hello-world\n--- error_code: 200\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 14: hit route (HEAD method there's no cache)\n--- request\nHEAD /hello-world\n--- error_code: 200\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 15: purge cache\n--- request\nPURGE /hello\n--- error_code: 200\n\n\n\n=== TEST 16: purge cache (not found)\n--- request\nPURGE /hello-world\n--- error_code: 404\n\n\n\n=== TEST 17:  hide cache headers = false\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"memory\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"memory_cache\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_ttl\": 300,\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": false,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 18: hit route (catch the cache headers)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n--- response_headers_like\nCache-Control:\n\n\n\n=== TEST 19: don't override cache relative headers\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: hit route\n--- request\nGET /echo\n--- more_headers\nApisix-Cache-Status: Foo\nCache-Control: bar\nExpires: any\n--- response_headers\nApisix-Cache-Status: Foo\nCache-Control: bar\nExpires: any\n\n\n\n=== TEST 21:  set cache_ttl to 1\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"memory\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"memory_cache\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_method\": [\"GET\"],\n                               \"cache_ttl\": 2,\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": false,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 22: hit route (MISS)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 23: hit route (HIT)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: HIT\n--- wait: 2\n\n\n\n=== TEST 24: hit route (MISS)\n--- request\nGET /hello\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: EXPIRED\n\n\n\n=== TEST 25:  enable cache_control option\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"memory\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_zone\": \"memory_cache\",\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_control\": true,\n                               \"cache_method\": [\"GET\"],\n                               \"cache_ttl\": 10,\n                               \"cache_http_status\": [200],\n                               \"hide_cache_headers\": false,\n                               \"no_cache\": [\"$arg_no_cache\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello*\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 26: hit route (MISS)\n--- request\nGET /hello\n--- more_headers\nCache-Control: max-age=60\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n--- wait: 1\n\n\n\n=== TEST 27: hit route (request header cache-control with max-age)\n--- request\nGET /hello\n--- more_headers\nCache-Control: max-age=1\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: STALE\n\n\n\n=== TEST 28: hit route  (request header cache-control with min-fresh)\n--- request\nGET /hello\n--- more_headers\nCache-Control: min-fresh=300\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: STALE\n--- wait: 1\n\n\n\n=== TEST 29: purge cache\n--- request\nPURGE /hello\n--- error_code: 200\n\n\n\n=== TEST 30: hit route  (request header cache-control with no-store)\n--- request\nGET /hello\n--- more_headers\nCache-Control: no-store\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: BYPASS\n\n\n\n=== TEST 31: hit route  (request header cache-control with no-cache)\n--- request\nGET /hello\n--- more_headers\nCache-Control: no-cache\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: BYPASS\n\n\n\n=== TEST 32: hit route  (response header cache-control with private)\n--- request\nGET /hello?cc=private\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 33: hit route  (response header cache-control with no-store)\n--- request\nGET /hello?cc=no-store\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 34: hit route  (response header cache-control with no-cache)\n--- request\nGET /hello?cc=no-cache\n--- response_body chop\nhello world!\n--- response_headers\nApisix-Cache-Status: MISS\n\n\n\n=== TEST 35: hit route  (request header cache-control with only-if-cached)\n--- request\nGET /hello\n--- more_headers\nCache-Control: only-if-cached\n--- error_code: 504\n\n\n\n=== TEST 36: configure plugin without memory_cache zone for cache_strategy = memory\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-cache\": {\n                               \"cache_strategy\": \"memory\",\n                               \"cache_key\":[\"$host\",\"$uri\"],\n                               \"cache_bypass\": [\"$arg_bypass\"],\n                               \"cache_control\": true,\n                               \"cache_method\": [\"GET\"],\n                               \"cache_ttl\": 10,\n                               \"cache_http_status\": [200]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1986\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- request\nGET /t\n--- response_body_like\n.*err: invalid or empty cache_zone for cache_strategy: memory.*\n--- error_code: 400\n"
  },
  {
    "path": "t/plugin/proxy-control.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: proxy_request_buffering off\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"proxy-control\": {\n                            \"request_buffering\": false\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: hit, only the upstream server will buffer the request\n--- request eval\n\"POST /hello\n\" . \"12345\" x 10240\n--- grep_error_log eval\nqr/a client request body is buffered to a temporary file/\n--- grep_error_log_out\na client request body is buffered to a temporary file\n\n\n\n=== TEST 3: proxy_request_buffering on\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"proxy-control\": {\n                            \"request_buffering\": true\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- request eval\n\"POST /hello\n\" . \"12345\" x 10240\n--- grep_error_log eval\nqr/a client request body is buffered to a temporary file/\n--- grep_error_log_out\na client request body is buffered to a temporary file\na client request body is buffered to a temporary file\n"
  },
  {
    "path": "t/plugin/proxy-mirror.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\nworker_connections(1024);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 1986;\n        server_tokens off;\n\n        location / {\n            content_by_lua_block {\n                local core = require(\"apisix.core\")\n                core.log.info(\"upstream_http_version: \", ngx.req.http_version())\n\n                local headers_tab = ngx.req.get_headers()\n                local headers_key = {}\n                for k in pairs(headers_tab) do\n                    core.table.insert(headers_key, k)\n                end\n                core.table.sort(headers_key)\n\n                for _, v in pairs(headers_key) do\n                    core.log.info(v, \": \", headers_tab[v])\n                end\n\n                core.log.info(\"uri: \", ngx.var.request_uri)\n                ngx.say(\"hello world\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity check (invalid schema)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"ftp://127.0.0.1:1999\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-mirror/\n\n\n\n=== TEST 2: sanity check (invalid port format)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1::1999\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-mirror/\n\n\n\n=== TEST 3: sanity check (without schema)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"127.0.0.1:1999\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-mirror/\n\n\n\n=== TEST 4: sanity check (without port)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 5: sanity check (include uri)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1999/invalid_uri\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 400\n--- response_body eval\nqr/failed to check the configuration of plugin proxy-mirror/\n\n\n\n=== TEST 6: sanity check (normal case)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route\n--- request\nGET /hello\n--- error_code: 200\n--- response_body\nhello world\n--- error_log\nuri: /hello\n\n\n\n=== TEST 8: sanity check (normal case), and uri is \"/uri\"\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/uri\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n        }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 9: the request header does not change\n--- request\nGET /uri\n--- error_code: 200\n--- more_headers\nhost: 127.0.0.2\napi-key: hello\napi-key2: world\nname: jake\n--- response_body\nuri: /uri\napi-key: hello\napi-key2: world\nhost: 127.0.0.2\nname: jake\nx-real-ip: 127.0.0.1\n--- error_log\napi-key: hello\napi-key2: world\nhost: 127.0.0.2\nname: jake\n\n\n\n=== TEST 10: sanity check (normal case), used to test http version\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n        }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 11: after the mirroring request, the upstream http version is 1.1\n--- request\nGET /hello\n--- error_code: 200\n--- more_headers\nhost: 127.0.0.2\napi-key: hello\n--- response_body\nhello world\n--- error_log\nupstream_http_version: 1.1\napi-key: hello\nhost: 127.0.0.2\n\n\n\n=== TEST 12: delete route\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n        }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 13: sanity check (invalid sample_ratio)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\",\n                               \"sample_ratio\": 10\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.print(body)\n           }\n       }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin proxy-mirror err: property \\\"sample_ratio\\\" validation failed: expected 10 to be at most 1\"}\n\n\n\n=== TEST 14: set mirror requests sample_ratio to 1\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\",\n                               \"sample_ratio\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route with sample_ratio 1\n--- request\nGET /hello?sample_ratio=1\n--- error_code: 200\n--- response_body\nhello world\n--- error_log_like eval\nqr/uri: \\/hello\\?sample_ratio=1/\n\n\n\n=== TEST 16: set mirror requests sample_ratio to 0.5\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\",\n                               \"sample_ratio\": 0.5\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 17: send batch requests and get mirror stat count\n--- config\n       location /t {\n           content_by_lua_block {\n                local t = require(\"lib.test_admin\").test\n\n                -- send batch requests\n                local tb = {}\n                for i = 1, 200 do\n                    local th = assert(ngx.thread.spawn(function(i)\n                        t('/hello?sample_ratio=0.5', ngx.HTTP_GET)\n                    end, i))\n                    table.insert(tb, th)\n                end\n                for i, th in ipairs(tb) do\n                    ngx.thread.wait(th)\n                end\n           }\n       }\n--- error_log_like eval\nqr/(uri: \\/hello\\?sample_ratio=0\\.5){75,125}/\n\n\n\n=== TEST 18: custom path\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\",\n                               \"path\": \"/a\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 19: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nuri: /a,\n\n\n\n=== TEST 20: hit route with args\n--- request\nGET /hello?a=1\n--- response_body\nhello world\n--- error_log\nuri: /a?a=1\n\n\n\n=== TEST 21: sanity check (path)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               for _, p in ipairs({\n                    \"a\",\n                    \"/a?a=c\",\n               }) do\n                    local code, body = t('/apisix/admin/routes/1',\n                        ngx.HTTP_PUT,\n                        [[{\n                            \"plugins\": {\n                                \"proxy-mirror\": {\n                                    \"host\": \"http://127.0.0.1:1999\",\n                                    \"path\": \"]] .. p .. [[\"\n                                }\n                            },\n                            \"upstream\": {\n                                \"nodes\": {\n                                    \"127.0.0.1:1980\": 1\n                                },\n                                \"type\": \"roundrobin\"\n                            },\n                            \"uri\": \"/hello\"\n                        }]]\n                        )\n                    ngx.log(ngx.WARN, body)\n                end\n            }\n       }\n--- grep_error_log eval\nqr/property \\\\\"path\\\\\" validation failed: failed to match pattern/\n--- grep_error_log_out\nproperty \\\"path\\\" validation failed: failed to match pattern\nproperty \\\"path\\\" validation failed: failed to match pattern\n\n\n\n=== TEST 22: sanity check (host)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               for _, p in ipairs({\n                    \"http://a\",\n                    \"http://ab.com\",\n                    \"http://[::1]\",\n                    \"http://[::1]:202\",\n               }) do\n                    local code, body = t('/apisix/admin/routes/1',\n                        ngx.HTTP_PUT,\n                        [[{\n                            \"plugins\": {\n                                \"proxy-mirror\": {\n                                    \"host\": \"]] .. p .. [[\"\n                                }\n                            },\n                            \"upstream\": {\n                                \"nodes\": {\n                                    \"127.0.0.1:1980\": 1\n                                },\n                                \"type\": \"roundrobin\"\n                            },\n                            \"uri\": \"/hello\"\n                        }]]\n                        )\n                    ngx.log(ngx.WARN, body)\n                end\n           }\n       }\n--- grep_error_log eval\nqr/(passed|property \\\\\"host\\\\\" validation failed: failed to match pattern)/\n--- grep_error_log_out\npassed\npassed\npassed\npassed\n\n\n\n=== TEST 23: set mirror requests host to domain\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://test.com:1980\",\n                               \"path\": \"/hello\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 24: hit route resolver domain\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log_like eval\nqr/http:\\/\\/test\\.com is resolved to: http:\\/\\/((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}/\n\n\n\n=== TEST 25: set as a domain name that cannot be found.\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://not-find-domian.notfind\",\n                               \"path\": \"/get\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 26: hit route resolver error domain\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\ndns resolver resolves domain: not-find-domian.notfind error:\n\n\n\n=== TEST 27: custom path with prefix path_concat_mode\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"http://127.0.0.1:1986\",\n                               \"path\": \"/a\",\n                               \"path_concat_mode\": \"prefix\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 28: hit route with prefix path_concat_mode\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log\nuri: /a/hello,\n\n\n\n=== TEST 29: hit route with args and prefix path_concat_mode\n--- request\nGET /hello?a=1\n--- response_body\nhello world\n--- error_log\nuri: /a/hello?a=1\n\n\n\n=== TEST 30: (grpc) sanity check (normal case grpc)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"grpc://127.0.0.1:1986\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"scheme\": \"grpc\",\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 31: (grpcs) sanity check (normal case for grpcs)\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-mirror\": {\n                               \"host\": \"grpcs://127.0.0.1:1986\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"scheme\": \"grpc\",\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- error_code: 200\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/proxy-mirror2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\nworker_connections(1024);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 1986;\n        server_tokens off;\n\n        location / {\n            content_by_lua_block {\n                local core = require(\"apisix.core\")\n                core.log.info(\"upstream_http_version: \", ngx.req.http_version())\n\n                local headers_tab = ngx.req.get_headers()\n                local headers_key = {}\n                for k in pairs(headers_tab) do\n                    core.table.insert(headers_key, k)\n                end\n                core.table.sort(headers_key)\n\n                for _, v in pairs(headers_key) do\n                    core.log.info(v, \": \", headers_tab[v])\n                end\n\n                core.log.info(\"uri: \", ngx.var.request_uri)\n                ngx.say(\"hello world\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: use proxy-rewrite to change uri before mirror\n--- config\n       location /t {\n           content_by_lua_block {\n               local t = require(\"lib.test_admin\").test\n               local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\n                                \"_meta\": {\n                                    \"priority\": 1010\n                                },\n                                \"uri\": \"/hello\"\n                            },\n                            \"proxy-mirror\": {\n                                \"_meta\": {\n                                    \"priority\": 1008\n                                },\n                               \"host\": \"http://127.0.0.1:1986\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/nope\"\n                   }]]\n                   )\n\n               if code >= 300 then\n                   ngx.status = code\n               end\n               ngx.say(body)\n           }\n       }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route (with proxy-rewrite)\n--- request\nGET /nope\n--- response_body\nhello world\n--- error_log\nuri: /hello\n\n\n\n=== TEST 3: hit route (with proxy-rewrite and args)\n--- request\nGET /nope?a=b&b=c&c=d\n--- response_body\nhello world\n--- error_log\nuri: /hello?a=b&b=c&c=d\n"
  },
  {
    "path": "t/plugin/proxy-mirror3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"POST /hello\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: grpc mirror\n--- log_level: debug\n--- http2\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uris:\n        - /helloworld.Greeter/SayHello\n    methods: [\n        POST\n    ]\n    plugins:\n        proxy-mirror:\n            host: grpc://127.0.0.1:19797\n            sample_ratio: 1\n    upstream:\n      scheme: grpc\n      nodes:\n        \"127.0.0.1:10051\": 1\n      type: roundrobin\n#END\n--- exec\ngrpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello\n--- response_body\n{\n  \"message\": \"Hello apisix\"\n}\n--- error_log eval\nqr/Connection refused\\) while connecting to upstream/\n"
  },
  {
    "path": "t/plugin/proxy-rewrite.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.proxy-rewrite\")\n            local ok, err = plugin.check_schema({\n                uri = '/apisix/home',\n                host = 'apisix.iresty.com'\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/test/add\",\n                            \"host\": \"apisix.iresty.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: update plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/test/update\",\n                            \"host\": \"apisix.iresty.com\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: disable plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: set route(rewrite host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite\",\n                                \"host\": \"apisix.iresty.com\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: rewrite host\n--- request\nGET /hello HTTP/1.1\n--- response_body\nuri: /plugin_proxy_rewrite\nhost: apisix.iresty.com\nscheme: http\n\n\n\n=== TEST 7: set route(rewrite host + upstream scheme is https)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite\",\n                                \"host\": \"test.com\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"scheme\": \"https\",\n                            \"nodes\": {\n                                \"127.0.0.1:1983\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: rewrite host + upstream scheme is https\n--- request\nGET /hello HTTP/1.1\n--- response_body\nuri: /plugin_proxy_rewrite\nhost: test.com\nscheme: https\n\n\n\n=== TEST 9: set route(rewrite headers)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api-Version\": \"v2\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: rewrite headers\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nX-Api-Version:v1\n--- response_body\nuri: /uri/plugin_proxy_rewrite\nhost: localhost\nx-api-version: v2\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 11: set route(add headers)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api-Engine\": \"apisix\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: add headers\n--- request\nGET /hello HTTP/1.1\n--- response_body\nuri: /uri/plugin_proxy_rewrite\nhost: localhost\nx-api-engine: apisix\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 13: set route(rewrite empty headers)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api-Test\": \"hello\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: rewrite empty headers\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nX-Api-Test:\n--- response_body\nuri: /uri/plugin_proxy_rewrite\nhost: localhost\nx-api-test: hello\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 15: set route(rewrite uri args)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite_args\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: rewrite uri args\n--- request\nGET /hello?q=apisix&a=iresty HTTP/1.1\n--- response_body\nuri: /plugin_proxy_rewrite_args\na: iresty\nq: apisix\n\n\n\n=== TEST 17: set route(rewrite uri empty args)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite_args\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: rewrite uri empty args\n--- request\nGET /hello HTTP/1.1\n--- response_body\nuri: /plugin_proxy_rewrite_args\n\n\n\n=== TEST 19: remove header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api-Engine\": \"APISIX\",\n                                    \"X-Api-Test\": \"\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: remove header\n--- request\nGET /hello HTTP/1.1\n--- more_headers\nX-Api-Test: foo\nX-Api-Engine: bar\n--- response_body\nuri: /uri/plugin_proxy_rewrite\nhost: localhost\nx-api-engine: APISIX\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 21: set route(only using regex_uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"regex_uri\": [\"^/test/(.*)/(.*)/(.*)\", \"/$1_$2_$3\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/test/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: hit route(rewrite uri using regex_uri)\n--- request\nGET /test/plugin/proxy/rewrite HTTP/1.1\n--- response_body\nuri: /plugin_proxy_rewrite\nhost: localhost\nscheme: http\n\n\n\n=== TEST 23: hit route(404 not found)\n--- request\nGET /test/not/found HTTP/1.1\n--- error_code: 404\n\n\n\n=== TEST 24: set route(Using both uri and regex_uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/hello\",\n                                \"regex_uri\": [\"^/test/(.*)\", \"/${1}1\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/test/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 25: hit route(rewrite uri using uri & regex_uri property)\n--- request\nGET /test/hello HTTP/1.1\n--- response_body\nhello world\n\n\n\n=== TEST 26: set route(invalid regex_uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"regex_uri\": [\"^/test/(.*)\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/test/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n\n\n\n=== TEST 27: set route(invalid regex syntax for the first element)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"regex_uri\": [\"[^/test/(.*)\", \"/$1\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/test/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid regex_uri/\n\n\n\n=== TEST 28: set route(invalid regex syntax for the second element)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"regex_uri\": [\"^/test/(.*)\", \"/$`1\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/test/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- error_log\ninvalid capturing variable name found\n\n\n\n=== TEST 29: set route(invalid uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"hello\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/failed to match pattern/\n\n\n\n=== TEST 30: wrong value of uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.proxy-rewrite\")\n            local ok, err = plugin.check_schema({\n                uri = 'home'\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"uri\" validation failed: failed to match pattern \"^\\\\/.*\" with \"home\"\n\n\n\n=== TEST 31: set route(invalid header field)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api:Version\": \"v2\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid field character/\n--- error_log\nheader field: X-Api:Version\n\n\n\n=== TEST 32: set route(invalid header value)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri/plugin_proxy_rewrite\",\n                                \"headers\": {\n                                    \"X-Api-Version\": \"v2\\r\\n\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/invalid value character/\n\n\n\n=== TEST 33: set route(rewrite uri with args)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"plugins\": {\n                          \"proxy-rewrite\": {\n                              \"uri\": \"/plugin_proxy_rewrite_args?q=apisix\"\n                          }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:1980\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      },\n                      \"uri\": \"/hello\"\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 34: rewrite uri with args\n--- request\nGET /hello?a=iresty\n--- response_body_like eval\nqr/uri: \\/plugin_proxy_rewrite_args(\nq: apisix\na: iresty|\na: iresty\nq: apisix)\n/\n\n\n\n=== TEST 35: print the plugin `conf` in etcd, no dirty data\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local encode_with_keys_sorted = require(\"toolkit.json\").encode\n\n            local code, _, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/uri/plugin_proxy_rewrite\",\n                            \"headers\": {\n                                \"X-Api\": \"v2\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local resp_data = core.json.decode(body)\n            ngx.say(encode_with_keys_sorted(resp_data.value.plugins))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"proxy-rewrite\":{\"headers\":{\"X-Api\":\"v2\"},\"uri\":\"/uri/plugin_proxy_rewrite\"}}\n\n\n\n=== TEST 36: set route(header contains nginx variables)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/uri\",\n                            \"headers\": {\n                                \"x-api\": \"$remote_addr\",\n                                \"name\": \"$arg_name\",\n                                \"x-key\": \"$http_key\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 37: hit route(header supports nginx variables)\n--- request\nGET /hello?name=Bill HTTP/1.1\n--- more_headers\nkey: X-APISIX\n--- response_body\nuri: /uri\nhost: localhost\nkey: X-APISIX\nname: Bill\nx-api: 127.0.0.1\nx-key: X-APISIX\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 38: set route(nginx variable does not exist)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"uri\": \"/uri\",\n                            \"headers\": {\n                                \"x-api\": \"$helle\",\n                                \"name\": \"$arg_world\",\n                                \"x-key\": \"$http_key\",\n                                \"Version\": \"nginx_var_does_not_exist\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 39: hit route(get nginx variable is nil)\n--- request\nGET /hello HTTP/1.1\n--- response_body\nuri: /uri\nhost: localhost\nversion: nginx_var_does_not_exist\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 40: set route(rewrite uri based on ctx.var)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/$arg_new_uri\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/test\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 41: hit route(upstream uri: should be /hello)\n--- request\nGET /test?new_uri=hello\n--- response_body\nhello world\n\n\n\n=== TEST 42: host with port\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.proxy-rewrite\")\n            local ok, err = plugin.check_schema({\n                host = 'apisix.iresty.com:6443',\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 43: set route(rewrite host with port), ensure ngx.var.uri matched the rewritten version\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/uri\",\n                                \"host\": \"test.com:6443\"\n                            },\n                            \"serverless-post-function\": {\n                                \"phase\": \"access\",\n                                \"functions\" : [\"return function(conf, ctx)\n                                    assert(ngx.var.uri == \\\"/uri\\\", \\\"proxy-rewrite do not call ngx.req.set_uri\\\")\n                                end\"]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 44: rewrite host with port\n--- request\nGET /hello\n--- response_body\nuri: /uri\nhost: test.com:6443\nx-real-ip: 127.0.0.1\n"
  },
  {
    "path": "t/plugin/proxy-rewrite2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = $block->yaml_config // <<_EOC_;\napisix:\n    node_listen: 1984\n    trusted_addresses:\n        - \"127.0.0.1\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /hello\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: access $upstream_uri before proxy-rewrite\n--- apisix_yaml\nglobal_rules:\n  -\n    id: 1\n    plugins:\n      serverless-pre-function:\n        phase: rewrite\n        functions:\n            - \"return function() ngx.log(ngx.WARN, 'serverless [', ngx.var.upstream_uri, ']') end\"\nroutes:\n  -\n    id: 1\n    uri: /hello\n    plugins:\n        proxy-rewrite:\n            uri: \"/plugin_proxy_rewrite\"\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- error_log\nserverless []\n--- response_body\nuri: /plugin_proxy_rewrite\nhost: localhost\nscheme: http\n\n\n\n=== TEST 2: default X-Forwarded-Proto\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- response_headers\nX-Forwarded-Proto: http\n\n\n\n=== TEST 3: pass X-Forwarded-Proto\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- more_headers\nX-Forwarded-Proto: https\n--- response_headers\nX-Forwarded-Proto: https\n\n\n\n=== TEST 4: customize X-Forwarded-Proto\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    plugins:\n        proxy-rewrite:\n            headers:\n                X-Forwarded-Proto: https\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- more_headers\nX-Forwarded-Proto: grpc\n--- response_headers\nX-Forwarded-Proto: https\n\n\n\n=== TEST 5: make sure X-Forwarded-Proto hit the `core.request.header` cache\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    plugins:\n        serverless-pre-function:\n            phase: rewrite\n            functions:\n              - return function(conf, ctx) local core = require(\"apisix.core\"); ngx.log(ngx.ERR, core.request.header(ctx, \"host\")); end\n        proxy-rewrite:\n            headers:\n                X-Forwarded-Proto: https-rewrite\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- more_headers\nX-Forwarded-Proto: grpc\n--- response_headers\nX-Forwarded-Proto: https-rewrite\n--- error_log\nlocalhost\n\n\n\n=== TEST 6: pass duplicate X-Forwarded-Proto\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- more_headers\nX-Forwarded-Proto: http\nX-Forwarded-Proto: grpc\n--- response_headers\nX-Forwarded-Proto: http, grpc\n\n\n\n=== TEST 7: customize X-Forwarded-Port\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    plugins:\n        proxy-rewrite:\n            headers:\n                X-Forwarded-Port: 10080\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- more_headers\nX-Forwarded-Port: 8080\n--- response_headers\nX-Forwarded-Port: 10080\n\n\n\n=== TEST 8: pass duplicate X-Forwarded-Proto, but not configured trusted_addresses\n--- yaml_config\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /echo\n    upstream_id: 1\nupstreams:\n  -\n    id: 1\n    nodes:\n        \"127.0.0.1:1980\": 1\n    type: roundrobin\n#END\n--- request\nGET /echo\n--- more_headers\nX-Forwarded-Proto: http\nX-Forwarded-Proto: grpc\n--- response_headers\nX-Forwarded-Proto: http\n"
  },
  {
    "path": "t/plugin/proxy-rewrite3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(rewrite method)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite\",\n                                \"method\": \"POST\",\n                                \"host\": \"apisix.iresty.com\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route(upstream uri: should be /hello)\n--- request\nGET /hello\n--- error_log\nplugin_proxy_rewrite get method: POST\n\n\n\n=== TEST 3: set route(update rewrite method)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite\",\n                                \"method\": \"GET\",\n                                \"host\": \"apisix.iresty.com\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route(upstream uri: should be /hello)\n--- request\nGET /hello\n--- error_log\nplugin_proxy_rewrite get method: GET\n\n\n\n=== TEST 5: wrong value of method key\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.proxy-rewrite\")\n            local ok, err = plugin.check_schema({\n                uri = '/apisix/home',\n                method = 'GET1',\n                host = 'apisix.iresty.com'\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"method\" validation failed: matches none of the enum values\ndone\n\n\n\n=== TEST 6: set route(rewrite method with headers)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/plugin_proxy_rewrite\",\n                                \"method\": \"POST\",\n                                \"host\": \"apisix.iresty.com\",\n                                \"headers\":{\n                                    \"x-api-version\":\"v1\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route(with header)\n--- request\nGET /hello\n--- error_log\nplugin_proxy_rewrite get method: POST\n\n\n\n=== TEST 8: set route(unsafe uri not normalized at request)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"use_real_request_uri_unsafe\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/print_uri_detailed\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: unsafe uri not normalized at request\n--- request\nGET /print%5Furi%5Fdetailed HTTP/1.1\n--- response_body\nngx.var.uri: /print_uri_detailed\nngx.var.request_uri: /print%5Furi%5Fdetailed\n\n\n\n=== TEST 10: set route(safe uri not normalized at request)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"use_real_request_uri_unsafe\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/print_uri_detailed\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: safe uri not normalized at request\n--- request\nGET /print_uri_detailed HTTP/1.1\n--- response_body\nngx.var.uri: /print_uri_detailed\nngx.var.request_uri: /print_uri_detailed\n\n\n\n=== TEST 12: set route(rewrite X-Forwarded-Host)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"headers\": {\n                                    \"X-Forwarded-Host\": \"test.com\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: rewrite X-Forwarded-Host\n--- request\nGET /echo HTTP/1.1\n--- more_headers\nX-Forwarded-Host: apisix.ai\n--- response_headers\nX-Forwarded-Host: test.com\n\n\n\n=== TEST 14: set route header test\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"headers\": {\n                                    \"add\":{\"test\": \"123\"},\n                                    \"set\":{\"test2\": \"2233\"},\n                                    \"remove\":[\"hello\"]\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: add exist header in muti-header\n--- request\nGET /echo HTTP/1.1\n--- more_headers\ntest: sssss\ntest: bbb\n--- response_headers\ntest: sssss, bbb, 123\n\n\n\n=== TEST 16: add header to exist header\n--- request\nGET /echo HTTP/1.1\n--- more_headers\ntest: sssss\n--- response_headers\ntest: sssss, 123\n\n\n\n=== TEST 17: remove header\n--- request\nGET /echo HTTP/1.1\n--- more_headers\nhello: word\n--- response_headers\nhello:\n\n\n\n=== TEST 18: set header success\n--- request\nGET /echo HTTP/1.1\n--- response_headers\ntest2: 2233\n\n\n\n=== TEST 19: header priority test\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"headers\": {\n                                    \"add\":{\"test\": \"test_in_add\"},\n                                    \"set\":{\"test\": \"test_in_set\"}\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: set and test priority test & deprecated calls test\n--- request\nGET /echo HTTP/1.1\n--- response_headers\ntest: test_in_set\n--- no_error_log\nDEPRECATED: use add_header(ctx, header_name, header_value) instead\nDEPRECATED: use set_header(ctx, header_name, header_value) instead\n\n\n\n=== TEST 21: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"plugins\": {\n                          \"proxy-rewrite\": {\n                              \"host\": \"test.xxxx.com\"\n                          }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:8125\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      },\n                      \"uri\": \"/hello*\"\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: hit with CRLF\n--- request\nGET /hello%3f0z=700%26a=c%20HTTP/1.1%0D%0AHost:google.com%0d%0a%0d%0a\n--- http_config\n    server {\n        listen 8125;\n        location / {\n            content_by_lua_block {\n                ngx.say(ngx.var.host)\n                ngx.say(ngx.var.request_uri)\n            }\n        }\n    }\n--- response_body\ntest.xxxx.com\n/hello%3F0z=700&a=c%20HTTP/1.1%0D%0AHost:google.com%0D%0A%0D%0A\n\n\n\n=== TEST 23: set route with uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"plugins\": {\n                          \"proxy-rewrite\": {\n                              \"uri\": \"/$uri/remain\",\n                              \"host\": \"test.xxxx.com\"\n                          }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:8125\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      },\n                      \"uri\": \"/hello*\"\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: hit with CRLF\n--- request\nGET /hello%3f0z=700%26a=c%20HTTP/1.1%0D%0AHost:google.com%0d%0a%0d%0a\n--- http_config\n    server {\n        listen 8125;\n        location / {\n            content_by_lua_block {\n                ngx.say(ngx.var.host)\n                ngx.say(ngx.var.request_uri)\n            }\n        }\n    }\n--- response_body\ntest.xxxx.com\n//hello%253F0z=700&a=c%20HTTP/1.1%0D%0AHost:google.com%0D%0A%0D%0A/remain\n\n\n\n=== TEST 25: regex_uri with args\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"plugins\": {\n                          \"proxy-rewrite\": {\n                              \"regex_uri\": [\"^/test/(.*)/(.*)/(.*)\", \"/$1_$2_$3?a=c\"]\n                          }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:8125\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      },\n                      \"uri\": \"/test/*\"\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: hit\n--- request\nGET /test/plugin/proxy/rewrite HTTP/1.1\n--- http_config\n    server {\n        listen 8125;\n        location / {\n            content_by_lua_block {\n                ngx.say(ngx.var.request_uri)\n            }\n        }\n    }\n--- response_body\n/plugin_proxy_rewrite?a=c\n\n\n\n=== TEST 27: use variables in headers when captured by regex_uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"uri\": \"/test/*\",\n                      \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"regex_uri\": [\"^/test/(.*)/(.*)/(.*)\", \"/echo\"],\n                            \"headers\": {\n                                \"add\": {\n                                    \"X-Request-ID\": \"$1/$2/$3\"\n                                }\n                            }\n                        }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:1980\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      }\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: hit\n--- request\nGET /test/plugin/proxy/rewrite HTTP/1.1\n--- response_headers\nX-Request-ID: plugin/proxy/rewrite\n\n\n\n=== TEST 29: use variables in header when not matched regex_uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"uri\": \"/echo*\",\n                      \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"regex_uri\": [\"^/test/(.*)/(.*)/(.*)\", \"/echo\"],\n                            \"headers\": {\n                                \"add\": {\n                                    \"X-Request-ID\": \"$1/$2/$3\"\n                                }\n                            }\n                        }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:1980\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      }\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: hit\n--- request\nGET /echo HTTP/1.1\n--- more_headers\nX-Foo: Foo\n--- response_headers\nX-Foo: Foo\n\n\n\n=== TEST 31: use variables in headers when captured by regex_uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"uri\": \"/test/*\",\n                      \"plugins\": {\n                        \"proxy-rewrite\": {\n                            \"regex_uri\": [\"^/test/(not_matched)?.*\", \"/echo\"],\n                            \"headers\": {\n                                \"add\": {\n                                    \"X-Request-ID\": \"test1/$1/$2/test2\"\n                                }\n                            }\n                        }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:1980\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      }\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 32: hit\n--- request\nGET /test/plugin/proxy/rewrite HTTP/1.1\n--- response_headers\nX-Request-ID: test1///test2\n\n\n\n=== TEST 33: set route (test if X-Forwarded-Port can be set before proxy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"headers\": {\n                                    \"X-Forwarded-Port\": \"9882\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 34: test if X-Forwarded-Port can be set before proxy\n--- request\nGET /echo HTTP/1.1\n--- more_headers\nX-Forwarded-Port: 9881\n--- response_headers\nX-Forwarded-Port: 9882\n\n\n\n=== TEST 35: set route (test if X-Forwarded-For can be set before proxy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\": {\n                                \"headers\": {\n                                    \"X-Forwarded-For\": \"22.22.22.22\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 36: test if X-Forwarded-For can be set before proxy\n--- request\nGET /echo HTTP/1.1\n--- more_headers\nX-Forwarded-For: 11.11.11.11\n--- response_headers\nX-Forwarded-For: 22.22.22.22, 127.0.0.1\n\n\n\n=== TEST 37: setting multiple regex_uris\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"plugins\": {\n                          \"proxy-rewrite\": {\n                              \"regex_uri\": [\n                                  \"^/test/(.*)/(.*)/(.*)/hello\",\n                                  \"/hello/$1_$2_$3\",\n                                  \"^/test/(.*)/(.*)/(.*)/world\",\n                                  \"/world/$1_$2_$3\"\n                              ]\n                          }\n                      },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:8125\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      },\n                      \"uri\": \"/test/*\"\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 38: hit\n--- request\nGET /test/plugin/proxy/rewrite/hello HTTP/1.1\n--- http_config\n    server {\n        listen 8125;\n        location / {\n            content_by_lua_block {\n                ngx.say(ngx.var.request_uri)\n            }\n        }\n    }\n--- response_body\n/hello/plugin_proxy_rewrite\n\n\n\n=== TEST 39: hit\n--- request\nGET /test/plugin/proxy/rewrite/world HTTP/1.1\n--- http_config\n    server {\n        listen 8125;\n        location / {\n            content_by_lua_block {\n                ngx.say(ngx.var.request_uri)\n            }\n        }\n    }\n--- response_body\n/world/plugin_proxy_rewrite\n\n\n\n=== TEST 40: use regex uri with unsafe allowed\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                      \"plugins\": {\n                          \"proxy-rewrite\": {\n                              \"regex_uri\": [\n                                  \"/hello/(.+)\",\n                                  \"/hello?unsafe_variable=$1\"\n                              ],\n                              \"use_real_request_uri_unsafe\": true\n                           }\n                        },\n                      \"upstream\": {\n                          \"nodes\": {\n                              \"127.0.0.1:8125\": 1\n                          },\n                          \"type\": \"roundrobin\"\n                      },\n                      \"uri\": \"/hello/*\"\n                 }]]\n                 )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 41: hit\n--- request\nGET /hello/%ED%85%8C%EC%8A%A4%ED%8A%B8 HTTP/1.1\n--- http_config\n    server {\n        listen 8125;\n        location / {\n            content_by_lua_block {\n                ngx.say(ngx.var.request_uri)\n            }\n        }\n    }\n--- response_body\n/hello?unsafe_variable=%ED%85%8C%EC%8A%A4%ED%8A%B8\n"
  },
  {
    "path": "t/plugin/public-api.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {uri = \"/apisix/plugin/wolf-rbac/user_info\"},\n                {uri = 3233}\n            }\n            local plugin = require(\"apisix.plugins.public-api\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"uri\" validation failed: wrong type: expected string, got number\n\n\n\n=== TEST 2: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    uri = \"/apisix/admin/consumers\",\n                    data = [[{\n                        \"username\": \"alice\",\n                        \"plugins\": {\n                            \"jwt-auth\": {\n                                \"key\": \"user-key\",\n                                \"algorithm\": \"HS256\",\n                                \"secret\": \"my-secret-key\"\n                            }\n                        }\n                    }]]\n                },\n                {\n                    uri = \"/apisix/admin/routes/direct-wolf-rbac-userinfo\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {},\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\": [\"return function(conf, ctx) require(\\\"apisix.core\\\").log.warn(\\\"direct-wolf-rbac-userinfo was triggered\\\"); end\"]\n                            }\n                        },\n                        \"uri\": \"/apisix/plugin/wolf-rbac/user_info\"\n                    }]],\n                },\n                {\n                    uri = \"/apisix/admin/routes/wrong-public-api\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {\n                                \"uri\": \"/apisix/plugin/balalbala\"\n                            }\n                        },\n                        \"uri\": \"/wrong-public-api\"\n                    }]]\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.uri, ngx.HTTP_PUT, data.data)\n                ngx.say(code..body)\n            end\n        }\n    }\n--- response_body eval\n\"201passed\\n\" x 3\n\n\n\n=== TEST 3: hit route (direct-wolf-rbac-userinfo)\n--- request\nGET /apisix/plugin/wolf-rbac/user_info\n--- error_code: 401\n--- error_log\ndirect-wolf-rbac-userinfo was triggered\n\n\n\n=== TEST 4: missing route (non-exist public API)\n--- request\nGET /apisix/plugin/balalbala\n--- error_code: 404\n\n\n\n=== TEST 5: hit route (wrong public-api uri)\n--- request\nGET /wrong-public-api\n--- error_code: 404\n\n\n\n=== TEST 6: setup route (protect public API)\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    uri = \"/apisix/admin/consumers\",\n                    data = [[{\n                        \"username\": \"bob\",\n                        \"plugins\": {\n                            \"key-auth\": {\n                                \"key\": \"testkey\"\n                            }\n                        }\n                    }]]\n                },\n                {\n                    uri = \"/apisix/admin/routes/custom-user-info\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {\n                                \"uri\": \"/apisix/plugin/wolf-rbac/user_info\"\n                            },\n                            \"key-auth\": {},\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\": [\"return function(conf, ctx) require(\\\"apisix.core\\\").log.warn(\\\"direct-wolf-rbac-userinfo was triggered\\\"); end\"]\n                            }\n                        },\n                        \"uri\": \"/get_user_info\"\n                    }]],\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.uri, ngx.HTTP_PUT, data.data)\n                ngx.say(code..body)\n            end\n        }\n    }\n--- response_body\n201passed\n201passed\n\n\n\n=== TEST 7: hit route (with key-auth header)\n--- request\nGET /get_user_info?key=user-key\n--- more_headers\napikey: testkey\n--- error_code: 401\n--- error_log\ndirect-wolf-rbac-userinfo was triggered\n\n\n\n=== TEST 8: hit route (without key-auth header)\n--- request\nGET /get_user_info?key=user-key\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing API key in request\"}\n"
  },
  {
    "path": "t/plugin/real-ip.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {input = {}},\n                {input = {\n                    source = \"http_xff\",\n                    trusted_addresses = {\"127.0.0.1/33\"}\n                }},\n                {input = {\n                    source = \"http_xff\",\n                    trusted_addresses = {\"::1/129\"}\n                }},\n            }) do\n                local code, body = t('/apisix/admin/routes/1',\n                    ngx.HTTP_PUT,\n                    {\n                        uri = \"/hello\",\n                        upstream = {\n                            type = \"roundrobin\",\n                            nodes = {\n                                [\"127.0.0.1:1980\"] = 1\n                            }\n                        },\n                        plugins = {\n                            [\"real-ip\"] = case.input\n                        }\n                    }\n                )\n                ngx.print(body)\n            end\n    }\n}\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin real-ip err: property \\\"source\\\" is required\"}\n{\"error_msg\":\"failed to check the configuration of plugin real-ip err: property \\\"trusted_addresses\\\" validation failed: failed to validate item 1: object matches none of the required\"}\n{\"error_msg\":\"failed to check the configuration of plugin real-ip err: invalid ip address: ::1/129\"}\n\n\n\n=== TEST 2: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"source\": \"http_xff\"\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 3: hit\n--- request\nGET /hello\n--- more_headers\nXFF: 1.1.1.1\n\n\n\n=== TEST 4: with port\n--- request\nGET /hello\n--- more_headers\nXFF: 1.1.1.1:80\n\n\n\n=== TEST 5: miss address\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 6: bad address\n--- request\nGET /hello\n--- more_headers\nXFF: 1.1.1.1.1\n--- error_code: 403\n\n\n\n=== TEST 7: bad port\n--- request\nGET /hello\n--- more_headers\nXFF: 1.1.1.1:65536\n--- error_code: 403\n\n\n\n=== TEST 8: ipv6\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"source\": \"http_xff\"\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"::2\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 9: hit\n--- request\nGET /hello\n--- more_headers\nXFF: ::2\n\n\n\n=== TEST 10: with port\n--- request\nGET /hello\n--- more_headers\nXFF: [::2]:80\n\n\n\n=== TEST 11: with bracket\n--- request\nGET /hello\n--- more_headers\nXFF: [::2]\n\n\n\n=== TEST 12: check port\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"source\": \"http_xff\"\n                        },\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"remote_port\": \"$remote_port\"\n                            }\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 13: hit\n--- request\nGET /hello\n--- more_headers\nXFF: 1.1.1.1:7090\n--- response_headers\nremote_port: 7090\n\n\n\n=== TEST 14: X-Forwarded-For\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"source\": \"http_x_forwarded_for\"\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"::2\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 15: hit\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: ::1, ::2\n\n\n\n=== TEST 16: hit (multiple X-Forwarded-For)\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: ::1\nX-Forwarded-For: ::2\n\n\n\n=== TEST 17: miss address\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 18: trusted addresses (not trusted)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"trusted_addresses\": [\"192.128.0.0/16\"],\n                            \"source\": \"http_x_forwarded_for\"\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 19: hit\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: 1.1.1.1\n--- error_code: 403\n\n\n\n=== TEST 20: trusted addresses (trusted)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"trusted_addresses\": [\"192.128.0.0/16\", \"127.0.0.0/24\"],\n                            \"source\": \"http_x_forwarded_for\"\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 21: hit\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: 1.1.1.1\n\n\n\n=== TEST 22: X-Forwarded-For and recursive\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"real-ip\": {\n                            \"trusted_addresses\": [\"192.128.0.0/16\", \"127.0.0.0/24\"],\n                            \"source\": \"http_x_forwarded_for\",\n                            \"recursive\": true\n                        },\n                        \"ip-restriction\": {\n                            \"whitelist\": [\"1.1.1.1\"]\n                        }\n                    }\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 23: hit\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: 1.1.1.1, 192.128.1.1, 127.0.0.1\n\n\n\n=== TEST 24: trusted in real-ip, but not trusted by `apisix.trusted_addresses`\nshould be rejected\n--- yaml_config\napisix:\n    node_listen: 1984\n    enable_admin: true\n    trusted_addresses:\n        - \"192.128.0.0/16\"\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n--- apisix_yaml\nroutes:\n  -\n    id: 1\n    uri: /hello\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    plugins:\n      real-ip:\n        trusted_addresses: [\"192.128.0.0/16\", \"127.0.0.0/24\"]\n        source: http_x_forwarded_for\n      ip-restriction:\n        whitelist: [\"1.1.1.1\"]\n#END\n--- request\nGET /hello\n--- more_headers\nX-Forwarded-For: 1.1.1.1\n--- error_code: 403\n"
  },
  {
    "path": "t/plugin/redirect.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nlog_level('info');\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.redirect\")\n            local ok, err = plugin.check_schema({\n                ret_code = 302,\n                uri = '/foo',\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: default ret_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.redirect\")\n            local ok, err = plugin.check_schema({\n                -- ret_code = 302,\n                uri = '/foo',\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 3: add plugin with new uri: /test/add\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"/test/add\",\n                            \"ret_code\": 301\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: redirect\n--- request\nGET /hello\n--- response_headers\nLocation: /test/add\n--- error_code: 301\n\n\n\n=== TEST 5: add plugin with new uri: $uri/test/add\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"$uri/test/add\",\n                            \"ret_code\": 301\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: redirect\n--- request\nGET /hello\n--- response_headers\nLocation: /hello/test/add\n--- error_code: 301\n\n\n\n=== TEST 7: add plugin with new uri: $uri/test/a${arg_name}c\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"$uri/test/a${arg_name}c\",\n                            \"ret_code\": 302\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: redirect\n--- request\nGET /hello?name=json\n--- response_headers\nLocation: /hello/test/ajsonc\n--- error_code: 302\n\n\n\n=== TEST 9: add plugin with new uri: /foo$$uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"/foo$$uri\",\n                            \"ret_code\": 302\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: redirect\n--- request\nGET /hello?name=json\n--- response_headers\nLocation: /foo$/hello\n--- error_code: 302\n\n\n\n=== TEST 11: add plugin with new uri: \\\\$uri/foo$uri\\\\$uri/bar\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"\\\\$uri/foo$uri\\\\$uri/bar\",\n                            \"ret_code\": 301\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: redirect\n--- request\nGET /hello\n--- response_headers\nLocation: \\$uri/foo/hello\\$uri/bar\n--- error_code: 301\n\n\n\n=== TEST 13: add plugin with new uri: $uri/$bad_var/bar\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"$uri/$bad_var/bar\",\n                            \"ret_code\": 301\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: redirect\n--- request\nGET /hello\n--- response_headers\nLocation: /hello//bar\n--- error_code: 301\n\n\n\n=== TEST 15: http -> https redirect\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"vars\": [\n                        [\n                            \"scheme\",\n                            \"==\",\n                            \"http\"\n                        ]\n                    ],\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"https://$host$request_uri\",\n                            \"ret_code\": 301\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: redirect\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com/hello\n\n\n\n=== TEST 17: enable http_to_https\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: redirect(port using `plugin_attr.redirect.https_port`)\n--- extra_yaml_config\nplugin_attr:\n    redirect:\n        https_port: 8443\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com:8443/hello\n\n\n\n=== TEST 19: redirect(port using `apisix.ssl.listen`)\n--- yaml_config\napisix:\n    ssl:\n        enable: true\n        listen:\n            - port: 9445\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com:9445/hello\n\n\n\n=== TEST 20: redirect(port using `apisix.ssl.listen` when listen length is one)\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com:9443/hello\n\n\n\n=== TEST 21: redirect(port using `apisix.ssl.listen` when listen length more than one)\n--- yaml_config\napisix:\n    ssl:\n        enable: true\n        listen:\n            - port: 6443\n            - port: 7443\n            - port: 8443\n            - port: 9443\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers_like\nLocation: https://foo.com:[6-9]443/hello\n\n\n\n=== TEST 22: redirect(port using `https default port`)\n--- yaml_config\napisix:\n    ssl:\n        enable: null\n--- extra_yaml_config\nplugin_attr:\n    redirect: null\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com/hello\n\n\n\n=== TEST 23: enable http_to_https with ret_code(not take effect)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true,\n                            \"ret_code\": 302\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: redirect\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com:9443/hello\n\n\n\n=== TEST 25: wrong configure, enable http_to_https with uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true,\n                            \"uri\": \"/hello\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/error_msg\":\"failed to check the configuration of plugin redirect err: value should match only one schema, but matches both schemas 1 and 3/\n\n\n\n=== TEST 26: enable http_to_https with upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"test.com\",\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: redirect\n--- request\nGET /hello\n--- more_headers\nHost: test.com\n--- error_code: 301\n--- response_headers\nLocation: https://test.com:9443/hello\n\n\n\n=== TEST 28: set ssl(sni: test.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: client https request\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"test.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 58 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 30: add plugin with new uri: /test/add\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"methods\":[\"POST\",\"GET\",\"HEAD\"],\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true,\n                            \"ret_code\": 307\n                        }\n                    },\n                    \"host\": \"test.com\",\n                    \"uri\": \"/hello-https\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 31: http to https post redirect\n--- request\nPOST /hello-https\n--- more_headers\nHost: test.com\n--- response_headers\nLocation: https://test.com:9443/hello-https\n--- error_code: 308\n\n\n\n=== TEST 32: http to https get redirect\n--- request\nGET /hello-https\n--- more_headers\nHost: test.com\n--- response_headers\nLocation: https://test.com:9443/hello-https\n--- error_code: 301\n\n\n\n=== TEST 33: http to https head redirect\n--- request\nHEAD /hello-https\n--- more_headers\nHost: test.com\n--- response_headers\nLocation: https://test.com:9443/hello-https\n--- error_code: 301\n\n\n\n=== TEST 34: add plugin with new regex_uri: /test/1 redirect to http://test.com/1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"regex_uri\": [\"^/test/(.*)\", \"http://test.com/${1}\"],\n                            \"ret_code\": 301\n                        }\n                    },\n                    \"uris\": [\"/test/*\", \"/hello\"],\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 35: regex_uri redirect\n--- request\nGET /test/1\n--- response_headers\nLocation: http://test.com/1\n--- error_code: 301\n\n\n\n=== TEST 36: regex_uri not match, get response from upstream\n--- request\nGET /hello\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 37: add plugin with new regex_uri: encode_uri = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"regex_uri\": [\"^/test/(.*)\", \"http://test.com/${1}\"],\n                            \"ret_code\": 301,\n                            \"encode_uri\": true\n                        }\n                    },\n                    \"uri\": \"/test/*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 38: regex_uri redirect with special characters\n--- request\nGET /test/with%20space\n--- error_code: 200\n--- response_headers\nLocation: http://test.com/with%20space\n--- error_code: 301\n\n\n\n=== TEST 39: add plugin with new uri: encode_uri = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"$uri\",\n                            \"ret_code\": 301,\n                            \"encode_uri\": true\n                        }\n                    },\n                    \"uri\": \"/hello*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 40: redirect with special characters\n--- request\nGET /hello/with%20space\n--- response_headers\nLocation: /hello/with%20space\n--- error_code: 301\n\n\n\n=== TEST 41: add plugin with new uri: $uri (append_query_string = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"$uri\",\n                            \"ret_code\": 302,\n                            \"append_query_string\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 42: redirect\n--- request\nGET /hello?name=json\n--- response_headers\nLocation: /hello?name=json\n--- error_code: 302\n\n\n\n=== TEST 43: add plugin with new uri: $uri?type=string (append_query_string = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"uri\": \"$uri?type=string\",\n                            \"ret_code\": 302,\n                            \"append_query_string\": true\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 44: redirect\n--- request\nGET /hello?name=json\n--- response_headers\nLocation: /hello?type=string&name=json\n--- error_code: 302\n\n\n\n=== TEST 45: enable http_to_https (pass X-Forwarded-Proto)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"vars\": [\n                        [\n                            \"scheme\",\n                            \"==\",\n                            \"http\"\n                        ]\n                    ],\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 46: enable http_to_https (pass X-Forwarded-Proto)\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\nX-Forwarded-Proto: http\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com:9443/hello\n\n\n\n=== TEST 47: pass wrong X-Forwarded-Proto, should not affect the redirect\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\nX-Forwarded-Proto: any\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com:9443/hello\n\n\n\n=== TEST 48: wrong configure, enable http_to_https with append_query_string\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true,\n                            \"append_query_string\": true\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body eval\nqr/error_msg\":\"failed to check the configuration of plugin redirect err: only one of `http_to_https` and `append_query_string` can be configured.\"/\n"
  },
  {
    "path": "t/plugin/redirect2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set use regex_uri redirect and enable append_query_string route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"regex_uri\": [\"^/test/(.*)\", \"http://test.com/${1}?q=apisix\"],\n                            \"append_query_string\": true\n                        }\n                    },\n                    \"uri\": \"/test/*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: set use regex_uri redirect and enable append_query_string route\n--- request\nGET /test/hello?o=apache\n--- response_headers\nLocation: http://test.com/hello?q=apisix&o=apache\n--- error_code: 302\n\n\n\n=== TEST 3: compatible with old version configuration\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"plugins\": {\n                        \"redirect\": {\n                            \"http_to_https\": true,\n                            \"append_query_string\": false\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/referer-restriction.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"no_error_log\", \"[error]\");\n\n    $block;\n});\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"referer-restriction\": {\n                                 \"whitelist\": [\n                                     \"*.xx.com\",\n                                     \"yy.com\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route and in the whitelist (wildcard)\n--- request\nGET /hello\n--- more_headers\nReferer: http://www.xx.com\n--- response_body\nhello world\n\n\n\n=== TEST 3: hit route and in the whitelist\n--- request\nGET /hello\n--- more_headers\nReferer: https://yy.com/am\n--- response_body\nhello world\n\n\n\n=== TEST 4: hit route and not in the whitelist\n--- request\nGET /hello\n--- more_headers\nReferer: https://www.yy.com/am\n--- error_code: 403\n\n\n\n=== TEST 5: hit route and without Referer\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 6: set whitelist, allow Referer missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"referer-restriction\": {\n                                \"bypass_missing\": true,\n                                 \"whitelist\": [\n                                     \"*.xx.com\",\n                                     \"yy.com\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route and without Referer\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 8: malformed Referer is treated as missing\n--- request\nGET /hello\n--- more_headers\nReferer: www.yy.com\n--- response_body\nhello world\n\n\n\n=== TEST 9: invalid schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.referer-restriction\")\n            local cases = {\n                \"x.*\",\n                \"~y.xn\",\n            }\n            for _, c in ipairs(cases) do\n                local ok, err = plugin.check_schema({\n                    whitelist = {c}\n                })\n                if ok then\n                    ngx.log(ngx.ERR, c)\n                end\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 10: set blacklist with reject message\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"referer-restriction\": {\n                                 \"blacklist\": [\n                                     \"*.xx.com\"\n                                 ],\n                                 \"message\": \"Your referer host is deny\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route and in the blacklist\n--- request\nGET /hello\n--- more_headers\nReferer: http://www.xx.com\n--- error_code: 403\n--- response_body\n{\"message\":\"Your referer host is deny\"}\n\n\n\n=== TEST 12: hit route and not in the blacklist\n--- request\nGET /hello\n--- more_headers\nReferer: https://yy.com\n--- response_body\nhello world\n\n\n\n=== TEST 13: whitelist and blacklist mutual exclusive\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.referer-restriction\")\n            local ok, err = plugin.check_schema({whitelist={\"xx.com\"}, blacklist={\"yy.com\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches both schemas 1 and 2\ndone\n"
  },
  {
    "path": "t/plugin/request-id.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nworker_connections(1024);\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.request-id\")\n            local ok, err = plugin.check_schema({})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong type\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.request-id\")\n            local ok, err = plugin.check_schema({include_in_response = \"bad_type\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"include_in_response\" validation failed: wrong type: expected boolean, got string\ndone\n\n\n\n=== TEST 3: add plugin with include_in_response true (default true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: check for request id in response header (default header name)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                })\n\n            if res.headers[\"X-Request-Id\"] then\n                ngx.say(\"request header present\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\nrequest header present\n\n\n\n=== TEST 5: check for unique id\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local t = {}\n            local ids = {}\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function()\n                    local httpc = http.new()\n                    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n                    local res, err = httpc:request_uri(uri,\n                        {\n                            method = \"GET\",\n                            headers = {\n                                [\"Content-Type\"] = \"application/json\",\n                            }\n                        }\n                    )\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n\n                    local id = res.headers[\"X-Request-Id\"]\n                    if not id then\n                        return -- ignore if the data is not synced yet.\n                    end\n\n                    if ids[id] == true then\n                        ngx.say(\"ids not unique\")\n                        return\n                    end\n                    ids[id] = true\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n\n            ngx.say(\"true\")\n        }\n    }\n--- wait: 5\n--- response_body\ntrue\n\n\n\n=== TEST 6: add plugin with custom header name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"header_name\": \"Custom-Header-Name\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: check for request id in response header (custom header name)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                })\n\n            if res.headers[\"Custom-Header-Name\"] then\n                ngx.say(\"request header present\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\nrequest header present\n\n\n\n=== TEST 8: add plugin with include_in_response false (default true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"include_in_response\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: check for request id is not present in the response header\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                })\n\n            if not res.headers[\"X-Request-Id\"] then\n                ngx.say(\"request header not present\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\nrequest header not present\n\n\n\n=== TEST 10: add plugin with custom header name in global rule and add plugin with default header name in specific route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                     [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"header_name\":\"Custom-Header-Name\"\n                            }\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                    [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: check for multiple request-ids in the response header are different\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                })\n\n            if res.headers[\"X-Request-Id\"] ~= res.headers[\"Custom-Header-Name\"] then\n                ngx.say(\"X-Request-Id and Custom-Header-Name are different\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\nX-Request-Id and Custom-Header-Name are different\n\n\n\n=== TEST 12: wrong algorithm type\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.request-id\")\n            local ok, err = plugin.check_schema({algorithm = \"bad_algorithm\"})\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"algorithm\" validation failed: matches none of the enum values\ndone\n\n\n\n=== TEST 13: add plugin with include_in_response true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"include_in_response\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: echo back the client's header if given\n--- request\nGET /opentracing\n--- more_headers\nX-Request-ID: 123\n--- response_headers\nX-Request-ID: 123\n\n\n\n=== TEST 15: add plugin with algorithm nanoid (default uuid)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local v = {}\n            local ids = {}\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"algorithm\": \"nanoid\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.say(\"algorithm nanoid is error\")\n            end\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function()\n                    local httpc = http.new()\n                    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n                    local res, err = httpc:request_uri(uri,\n                        {\n                            method = \"GET\",\n                            headers = {\n                                [\"Content-Type\"] = \"application/json\",\n                            }\n                        }\n                    )\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    local id = res.headers[\"X-Request-Id\"]\n                    if not id then\n                        return -- ignore if the data is not synced yet.\n                    end\n                    if ids[id] == true then\n                        ngx.say(\"ids not unique\")\n                        return\n                    end\n                    ids[id] = true\n                end, i))\n                table.insert(v, th)\n            end\n            for i, th in ipairs(v) do\n                ngx.thread.wait(th)\n            end\n            ngx.say(\"true\")\n        }\n    }\n--- wait: 5\n--- response_body\ntrue\n\n\n\n=== TEST 16: check for request id in response header when request id is empty in request\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                        [\"X-Request-Id\"] = \"\"\n                    }\n                })\n\n            if res.headers[\"X-Request-Id\"] and res.headers[\"X-Request-Id\"] ~= \"\" then\n                ngx.say(\"request header present and is not empty\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- response_body\nrequest header present and is not empty\n"
  },
  {
    "path": "t/plugin/request-id2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nworker_connections(1024);\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: check config with algorithm range_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"algorithm\": \"range_id\",\n                                \"range_id\": {\n                                    \"char_set\": \"abcdefg\",\n                                    \"length\": 20\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: add plugin with algorithm range_id (set automatic default)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"algorithm\": \"range_id\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: hit\n--- request\nGET /opentracing\n\n\n\n=== TEST 4: add plugin with algorithm range_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local v = {}\n            local ids = {}\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                       \"plugins\": {\n                            \"request-id\": {\n                                \"algorithm\": \"range_id\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.say(\"algorithm range_id is error\")\n            end\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function()\n                    local httpc = http.new()\n                    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n                    local res, err = httpc:request_uri(uri,\n                        {\n                            method = \"GET\",\n                            headers = {\n                                [\"Content-Type\"] = \"application/json\",\n                            }\n                        }\n                    )\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    local id = res.headers[\"X-Request-Id\"]\n                    if not id then\n                        return -- ignore if the data is not synced yet.\n                    end\n                    if #id ~= 16 then\n                        ngx.say(id)\n                        ngx.say(\"incorrect length for id\")\n                        return\n                    end\n                    local start, en = string.find(id, '[a-zA-Z0-9]*')\n                    if start ~= 1 or en ~= 16 then\n                        ngx.say(\"incorrect char set for id\")\n                        ngx.say(id)\n                        return\n                    end\n                    if ids[id] == true then\n                        ngx.say(\"ids not unique\")\n                        return\n                    end\n                    ids[id] = true\n                end, i))\n                table.insert(v, th)\n            end\n            for i, th in ipairs(v) do\n                ngx.thread.wait(th)\n            end\n            ngx.say(\"true\")\n        }\n    }\n--- wait: 5\n--- response_body\ntrue\n"
  },
  {
    "path": "t/plugin/request-id3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nworker_connections(1024);\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: check config with algorithm ksuid\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"algorithm\": \"ksuid\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nGET /opentracing\n--- error_log\nX-Request-Id\n--- no_error_log\n[error]\n\n\n\n=== TEST 3: add plugin with algorithm ksuid\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local http = require \"resty.http\"\n            local v = {}\n            local ids = {}\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                       \"plugins\": {\n                            \"request-id\": {\n                                \"algorithm\": \"ksuid\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.say(\"algorithm ksuid is error\")\n            end\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function()\n                    local httpc = http.new()\n                    local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n                    local res, err = httpc:request_uri(uri,\n                        {\n                            method = \"GET\",\n                            headers = {\n                                [\"Content-Type\"] = \"application/json\",\n                            }\n                        }\n                    )\n                    if not res then\n                        ngx.log(ngx.ERR, err)\n                        return\n                    end\n                    local id = res.headers[\"X-Request-Id\"]\n                    if not id then\n                        return -- ignore if the data is not synced yet.\n                    end\n                    if #id ~= 27 then\n                        ngx.say(id)\n                        ngx.say(\"incorrect length for id\")\n                        return\n                    end\n                    local start, en = string.find(id, '[a-zA-Z0-9]*')\n                    if start ~= 1 or en ~= 27 then\n                        ngx.say(\"incorrect char set for id\")\n                        ngx.say(id)\n                        return\n                    end\n                    if ids[id] == true then\n                        ngx.say(\"ids not unique\")\n                        return\n                    end\n                    ids[id] = true\n                end, i))\n                table.insert(v, th)\n            end\n            for i, th in ipairs(v) do\n                ngx.thread.wait(th)\n            end\n            ngx.say(\"true\")\n        }\n    }\n--- wait: 5\n--- response_body\ntrue\n\n\n\n=== TEST 4: enable request-id plugin\n--- yaml_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"request-id\": {\n                                \"header_name\": \"X-Request-Id\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1999\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: check request-id\n--- request\nGET /opentracing\n--- more_headers\nX-Request-Id: abctesting\n--- grep_error_log eval\nqr/request_id: \"abctesting\"/\n--- grep_error_log_out\nrequest_id: \"abctesting\"\nrequest_id: \"abctesting\"\nrequest_id: \"abctesting\"\n--- error_code: 502\n"
  },
  {
    "path": "t/plugin/request-validation.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.request-validation\")\n            local ok, err = plugin.check_schema({body_schema = {}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: missing schema for header and body\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.request-validation\")\n            local ok, err = plugin.check_schema({})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/object matches none of the required/\n\n\n\n=== TEST 3: add plugin with all combinations\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                plugins = {\n                    [\"request-validation\"] = {\n                    body_schema = {\n                        type = \"object\",\n                        required = { \"required_payload\" },\n                        properties = {\n                        required_payload = {\n                            type = \"string\"\n                        },\n                        boolean_payload = {\n                            type = \"boolean\"\n                        },\n                        timeouts = {\n                            type = \"integer\",\n                            minimum = 1,\n                            maximum = 254,\n                            default = 3\n                        },\n                        req_headers = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                                type = \"string\"\n                            }\n                        }\n                        }\n                    }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1982\"] = 1\n                    },\n                    type = \"roundrobin\"\n                },\n                uri = \"/opentracing\"\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: required payload missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = '{\"boolean-payload\": true}',\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                })\n\n            if res.status == 400 then\n                ngx.say(\"required field missing\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nrequired field missing\n--- error_log\nproperty \"required_payload\" is required\n\n\n\n=== TEST 5: required payload added\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"POST\",\n                    body = '{\"boolean-payload\": true,' ..\n                    '\"required_payload\": \"hello\"}',\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                    }\n                })\n\n            if res.status == 200 then\n                ngx.say(\"hello1 world\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\n--- no_error_log\n\n\n\n=== TEST 6: Add plugin with header_schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                plugins = {\n                    [\"request-validation\"] = {\n                    header_schema = {\n                        type = \"object\",\n                        required = { \"required_payload\" },\n                        properties = {\n                        required_payload = {\n                            type = \"string\"\n                        },\n                        boolean_payload = {\n                            type = \"boolean\"\n                        },\n                        timeouts = {\n                            type = \"integer\",\n                            minimum = 1,\n                            maximum = 254,\n                            default = 3\n                        },\n                        req_headers = {\n                            type = \"array\",\n                            minItems = 1,\n                            items = {\n                            type = \"string\"\n                            }\n                        }\n                        }\n                    }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                    [\"127.0.0.1:1982\"] = 1\n                    },\n                    type = \"roundrobin\"\n                },\n                uri = \"/opentracing\"\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: required header payload missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\"\n                    }\n                })\n\n            if res.status == 400 then\n                ngx.say(\"required field missing\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nrequired field missing\n--- error_log\nproperty \"required_payload\" is required\n\n\n\n=== TEST 8: required header added in header\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri,\n                {\n                    method = \"GET\",\n                    headers = {\n                        [\"Content-Type\"] = \"application/json\",\n                        [\"required_payload\"] = \"test payload\"\n                    }\n                })\n\n            if res.status == 200 then\n                ngx.say(\"hello1 world\")\n            else\n                ngx.say(\"failed\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\n\n\n\n=== TEST 9: add route (test request validation `body_schema`)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\",\n                                \"required\": [\"required_payload\"],\n                                \"properties\": {\n                                    \"required_payload\": {\"type\": \"string\"},\n                                    \"boolean_payload\": {\"type\": \"boolean\"},\n                                    \"timeouts\": {\n                                       \"type\": \"integer\",\n                                        \"minimum\": 1,\n                                        \"maximum\": 254,\n                                        \"default\": 3\n                                    },\n                                    \"req_headers\": {\n                                        \"type\": \"array\",\n                                        \"minItems\": 1,\n                                        \"items\": {\n                                            \"type\": \"string\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    },]] .. [[\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: add route (test request validation `body_schema.type` is object)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: add route (test request validation `body_schema.type` is array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"array\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: add route (test request validation `body_schema.type` is string)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: add route (test request validation `body_schema.type` is number)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"number\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: add route (test request validation `body_schema.type` is integer)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: add route (test request validation `body_schema.type` is table)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"table\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: add route (test request validation `body_schema.type` is function)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"function\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: add route (test request validation `body_schema.type` failure)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"test\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/invalid JSON type: test/\n--- error_code chomp\n400\n\n\n\n=== TEST 18: add route (test request validation `body_schema.enum` failure)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"string\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": \"test-enum\"\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/table expected, got string/\n--- error_code chomp\n400\n\n\n\n=== TEST 19: add route (test request validation `body_schema.enum` success)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"string\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: add route (test request validation `body_schema.required` failure)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": \"test-required\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/table expected, got string/\n--- error_code chomp\n400\n\n\n\n=== TEST 21: add route (test request validation `body_schema.required` success)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: add route (test request validation `header_schema`)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\",\n                                \"required\": [\"required_payload\"],\n                                \"properties\": {\n                                    \"required_payload\": {\"type\": \"string\"},\n                                    \"boolean_payload\": {\"type\": \"boolean\"},\n                                    \"timeouts\": {\n                                       \"type\": \"integer\",\n                                        \"minimum\": 1,\n                                        \"maximum\": 254,\n                                        \"default\": 3\n                                    },\n                                    \"req_headers\": {\n                                        \"type\": \"array\",\n                                        \"minItems\": 1,\n                                        \"items\": {\n                                            \"type\": \"string\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    },]] .. [[\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 23: add route (test request validation `header_schema.type` is object)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: add route (test request validation `header_schema.type` is array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"array\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 25: add route (test request validation `header_schema.type` is string)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: add route (test request validation `header_schema.type` is number)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"number\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 27: add route (test request validation `header_schema.type` is integer)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"integer\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 28: add route (test request validation `header_schema.type` is table)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"table\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 29: add route (test request validation `header_schema.type` is function)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"function\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 30: add route (test request validation `header_schema.type` failure)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"test\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/invalid JSON type: test/\n--- error_code chomp\n400\n\n\n\n=== TEST 31: add route (test request validation `header_schema.enum` failure)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"string\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": \"test-enum\"\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/table expected, got string/\n--- error_code chomp\n400\n\n\n\n=== TEST 32: add route (test request validation `header_schema.enum` success)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"string\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 33: add route (test request validation `header_schema.required` failure)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": \"test-required\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/table expected, got string/\n--- error_code chomp\n400\n\n\n\n=== TEST 34: add route (test request validation `header_schema.required` success)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 35: add route (test request validation `header_schema.required` success with custom reject message)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            },\n                            \"rejected_msg\": \"customize reject message for header_schema.required\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 36: use empty header to hit `header_schema.required with custom reject message` rule\n--- request\nGET /opentracing\n--- error_code: 400\n--- response_body chomp\ncustomize reject message for header_schema.required\n--- error_log eval\nqr/schema validation failed/\n\n\n\n=== TEST 37: use bad header value to hit `header_schema.required with custom reject message` rule\n--- request\nGET /opentracing\n--- more_headers\ntest: abc\n--- error_code: 400\n--- response_body chomp\ncustomize reject message for header_schema.required\n--- error_log eval\nqr/schema validation failed/\n\n\n\n=== TEST 38: pass `header_schema.required with custom reject message` rule\n--- request\nGET /opentracing\n--- more_headers\ntest: a\n--- error_code: 200\n--- response_body eval\nqr/opentracing/\n\n\n\n=== TEST 39: add route (test request validation `body_schema.required` success with custom reject message)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            },\n                            \"rejected_msg\": \"customize reject message for body_schema.required\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 40: use empty body to hit `body_schema.required with custom reject message` rule\n--- request\nGET /opentracing\n--- error_code: 400\n--- response_body chomp\ncustomize reject message for body_schema.required\n\n\n\n=== TEST 41: use bad body value to hit `body_schema.required with custom reject message` rule\n--- request\nPOST /opentracing\n{\"test\":\"abc\"}\n--- error_code: 400\n--- response_body chomp\ncustomize reject message for body_schema.required\n--- error_log eval\nqr/schema validation failed/\n\n\n\n=== TEST 42: pass `body_schema.required with custom reject message` rule\n--- request\nPOST /opentracing\n{\"test\":\"a\"}\n--- error_code: 200\n--- response_body eval\nqr/opentracing/\n\n\n\n=== TEST 43: add route (test request validation `header_schema.required` failure with custom reject message)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            },\n                            \"rejected_msg\": \"customize reject message customize reject message customize reject message customize reject message customize reject message customize reject message customize reject message customize reject message customize reject message customize reject message customize reject message\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/string too long/\n--- error_code: 400\n\n\n\n=== TEST 44: add route (test request validation schema with custom reject message only)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"rejected_msg\": \"customize reject message\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/object matches none of the required/\n--- error_code: 400\n\n\n\n=== TEST 45: add route (test request validation `body_schema.required` success with custom reject code)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            },\n                            \"rejected_code\": 505\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 46: use empty body to hit custom rejected code rule\n--- request\nGET /opentracing\n--- error_code: 505\n\n\n\n=== TEST 47: use bad body value to hit custom rejected code rule\n--- request\nPOST /opentracing\n{\"test\":\"abc\"}\n--- error_code: 505\n--- error_log eval\nqr/schema validation failed/\n\n\n\n=== TEST 48: pass custom rejected code rule\n--- request\nPOST /opentracing\n{\"test\":\"a\"}\n--- error_code: 200\n--- response_body eval\nqr/opentracing/\n\n\n\n=== TEST 49: add route (test request validation `header_schema.required` failure with custom reject code)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"header_schema\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                    \"test\": {\n                                        \"type\": \"string\",\n                                        \"enum\": [\"a\", \"b\", \"c\"]\n                                    }\n                                },\n                                \"required\": [\"test\"]\n                            },\n                            \"rejected_code\": 10000\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/expected 10000 to be at most 599/\n--- error_code: 400\n\n\n\n=== TEST 50: add route (test request validation schema with custom reject code only)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"rejected_code\": 505\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/plugin/request/validation\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body_like eval\nqr/object matches none of the required/\n--- error_code: 400\n\n\n\n=== TEST 51: add route for urlencoded post data validation\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"request-validation\": {\n                            \"body_schema\": {\n                                \"type\": \"object\",\n                                \"required\": [\"required_payload\"],\n                                \"properties\": {\n                                    \"required_payload\": {\"type\": \"string\"}\n                                },\n                                \"rejected_msg\": \"customize reject message\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]])\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 52: test urlencoded post data\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- request eval\n\"POST /echo\n\" . \"a=b&\" x 101 . \"required_payload=101-hello\"\n--- response_body eval\nqr/101-hello/\n\n\n\n=== TEST 53: test urlencoded post data with charset header\n--- more_headers\nContent-Type: application/x-www-form-urlencoded; charset=utf-8\n--- request eval\n\"POST /echo\n\" . \"a=b&\" x 101 . \"required_payload=101-hello\"\n--- response_body eval\nqr/101-hello/\n"
  },
  {
    "path": "t/plugin/request-validation2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: json body with duplicate key\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                plugins = {\n                    [\"request-validation\"] = {\n                        body_schema = {\n                            type = \"object\",\n                            properties = {\n                                k = {pattern = \"^good$\"}\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                },\n                uri = \"/echo\"\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nPOST /echo\n{\"k\":\"bad\",\"k\":\"good\"}\n--- response_body chomp\n{\"k\":\"good\"}\n"
  },
  {
    "path": "t/plugin/response-rewrite.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1:  add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                body = 'Hello world',\n                headers = {\n                    [\"X-Server-id\"] = 3\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2:  add plugin with wrong status_code\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                status_code = 599\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"status_code\" validation failed: expected 599 to be at most 598\n\n\n\n=== TEST 3:  add plugin fail\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                body = 2,\n                headers = {\n                    [\"X-Server-id\"] = \"3\"\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"body\" validation failed: wrong type: expected string, got number\n\n\n\n=== TEST 4: set header(rewrite header and body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\" : {\n                                \"X-Server-id\": 3,\n                                \"X-Server-status\": \"on\",\n                                \"Content-Type\": \"\"\n                            },\n                            \"body\": \"new body\\n\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/with_header\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: check body with deleted header\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/with_header\"\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            if res.headers['Content-Type'] then\n                ngx.say('fail content-type should not be exist, now is'..res.headers['Content-Type'])\n                return\n            end\n\n            if res.headers['X-Server-status'] ~= 'on' then\n                ngx.say('fail X-Server-status needs to be on')\n                return\n            end\n\n            if res.headers['X-Server-id'] ~= '3' then\n                ngx.say('fail X-Server-id needs to be 3')\n                return\n            end\n\n            ngx.print(res.body)\n            ngx.exit(200)\n        }\n    }\n--- request\nGET /t\n--- response_body\nnew body\n\n\n\n=== TEST 6: set body only and keep header the same\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"new body2\\n\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/with_header\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: check body and header not changed\n--- request\nGET /with_header\n--- more_headers\nresp-X-Server-id: 100\nresp-Content-Type: application/xml\nresp-Content-Encoding: gzip\nresp-Content-Length: 4\nresp-Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT\nresp-ETag: \"33a64df551425fcc55e4d42a148795d9f25f89d4\"\n--- response_body\nnew body2\n--- response_headers\nX-Server-id: 100\nContent-Type: application/xml\nContent-Length:\nContent-Encoding:\nLast-Modified:\nETag:\n\n\n\n=== TEST 8: set location header with 302 code\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"Location\":\"https://www.iresty.com\"\n                            },\n                            \"status_code\":302\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: check 302 redirect\n--- request\nGET /hello\n--- error_code eval\n302\n--- response_headers\nLocation: https://www.iresty.com\n\n\n\n=== TEST 10:  empty string in header field\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                status_code = 200,\n                headers = {\n                    [\"\"] = 2\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ninvalid field length in header\n\n\n\n=== TEST 11: array in header value\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                status_code = 200,\n                headers = {\n                    [\"X-Name\"] = {}\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ninvalid type as header value\n\n\n\n=== TEST 12: set body in base64\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"SGVsbG8K\",\n                            \"body_base64\": true\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: check base64 content\n--- request\nGET /hello\n--- response_body\nHello\n\n\n\n=== TEST 14: set body with not well formed base64\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                            body = \"1\",\n                            body_base64 =  true\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ninvalid base64 content\n\n\n\n=== TEST 15: print the plugin `conf` in etcd, no dirty data\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\").test\n            local encode_with_keys_sorted = require(\"toolkit.json\").encode\n\n            local code, _, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\" : {\n                                \"X-Server-id\": 3,\n                                \"X-Server-status\": \"on\",\n                                \"Content-Type\": \"\"\n                            },\n                            \"body\": \"new body\\n\"\n                        }\n                    },\n                    \"uri\": \"/with_header\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local resp_data = core.json.decode(body)\n            ngx.say(encode_with_keys_sorted(resp_data.value.plugins))\n        }\n    }\n--- request\nGET /t\n--- response_body\n{\"response-rewrite\":{\"body\":\"new body\\n\",\"headers\":{\"Content-Type\":\"\",\"X-Server-id\":3,\"X-Server-status\":\"on\"}}}\n\n\n\n=== TEST 16: add validate vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                vars = {\n                    {\"status\",\"==\",200}\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 17: add plugin with invalidate vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                vars = {\n                    {}\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to validate the 'vars' expression: rule too short\n\n\n\n=== TEST 18: set route with http status code as expr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"new body3\\n\",\n                            \"status_code\": 403,\n                            \"vars\": [\n                                [\"status\",\"==\",500]\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/server_error\",\"/hello\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: check http code that matches http_status\n--- request\nGET /server_error\n--- response_body\nnew body3\n--- error_code eval\n403\n--- error_log\n500 Internal Server Error\n\n\n\n=== TEST 20: check http code that not matches http_status\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_code eval\n200\n\n\n\n=== TEST 21: set an empty body with setting body_base64 to true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                            body = \"\",\n                            body_base64 = true\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ninvalid base64 content\n\n\n\n=== TEST 22: set an nil body with setting body_base64 to true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                            body_base64 = true\n            })\n            if not ok then\n                ngx.say(err)\n                return\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ninvalid base64 content\n\n\n\n=== TEST 23: rewrite header with variables\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\" : {\n                                \"X-A\": \"$remote_addr\",\n                                \"X-B\": \"from $remote_addr to $balancer_ip:$balancer_port\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/with_header\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: hit\n--- request\nGET /with_header\n--- response_headers\nX-A: 127.0.0.1\nX-B: from 127.0.0.1 to 127.0.0.1:1980\n\n\n\n=== TEST 25: set empty body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"body\": \"\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: hit set empty body\n--- request\nGET /hello\n--- response_body\n\n\n\n=== TEST 27: test add header with one word\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"add\": [\n                                    \"X-Server-test:a\"\n                                ]\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/response-rewrite2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {body = \"test\"},\n                {filters = {\n                    {\n                        regex = \"l\",\n                        replace = \"m\",\n                    },\n                }},\n                {body = \"test\", filters = {\n                    {\n                        regex = \"l\",\n                        replace = \"m\",\n                    },\n                }},\n                {filters = {}},\n                {filters = {\n                    {regex = \"l\"},\n                }},\n                {filters = {\n                    {\n                        regex = \"\",\n                        replace = \"m\",\n                    },\n                }},\n                {filters = {\n                    {\n                        regex = \"l\",\n                        replace = \"m\",\n                        scope = \"\"\n                    },\n                }},\n            }\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n\n            for _, case in ipairs(test_cases) do\n                local ok, err = plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body eval\nqr/done\ndone\nfailed to validate dependent schema for \"filters|body\": value wasn't supposed to match schema\nproperty \"filters\" validation failed: expect array to have at least 1 items\nproperty \"filters\" validation failed: failed to validate item 1: property \"replace\" is required\nproperty \"filters\" validation failed: failed to validate item 1: property \"regex\" validation failed: string too short, expected at least 1, got 0\nproperty \"filters\" validation failed: failed to validate item 1: property \"scope\" validation failed: matches none of the enum values/\n\n\n\n=== TEST 2: add plugin with valid filters\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                filters = {\n                    {\n                        regex = \"Hello\",\n                        scope = \"global\",\n                        replace = \"World\",\n                        options = \"jo\"\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 3:  add plugin with invalid filter required filed\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                filters = {\n                    {\n                        regex = \"Hello\",\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\nproperty \"filters\" validation failed: failed to validate item 1: property \"replace\" is required\n\n\n\n=== TEST 4:  add plugin with invalid filter scope\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                filters = {\n                    {\n                        regex = \"Hello\",\n                        scope = \"two\",\n                        replace = \"World\",\n                        options = \"jo\"\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\nproperty \"filters\" validation failed: failed to validate item 1: property \"scope\" validation failed: matches none of the enum values\n\n\n\n=== TEST 5:  add plugin with invalid filter empty value\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                filters = {\n                    {\n                        regex = \"\",\n                        replace = \"world\"\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- response_body\nproperty \"filters\" validation failed: failed to validate item 1: property \"regex\" validation failed: string too short, expected at least 1, got 0\n\n\n\n=== TEST 6:  add plugin with invalid filter regex options\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.response-rewrite\")\n            local ok, err = plugin.check_schema({\n                filters = {\n                    {\n                        regex = \"hello\",\n                        replace = \"HELLO\",\n                        options = \"h\"\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- error_code eval\n200\n--- response_body\nregex \"hello\" validation failed: unknown flag \"h\" (flags \"h\")\n\n\n\n=== TEST 7: set route with filters and vars expr\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"hello\",\n                                    \"replace\": \"test\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: check http body that matches filters\n--- request\nGET /hello\n--- response_body\ntest world\n\n\n\n=== TEST 9: filter substitute global\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"l\",\n                                    \"replace\": \"t\",\n                                    \"scope\": \"global\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: check http body that substitute global\n--- request\nGET /hello\n--- response_body\nhetto wortd\n\n\n\n=== TEST 11: filter replace with empty\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"hello\",\n                                    \"replace\": \"\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: check http body that replace with empty\n--- request\nGET /hello\n--- response_body\n world\n\n\n\n=== TEST 13: filter replace with words\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"\\\\w\\\\S+$\",\n                                    \"replace\": \"*\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: check http body that replace with words\n--- request\nGET /hello\n--- response_body\nhello *\n\n\n\n=== TEST 15: set multiple filters\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"hello\",\n                                    \"replace\": \"HELLO\"\n                                },\n                                {\n                                    \"regex\": \"L\",\n                                    \"replace\": \"T\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: check http body that set multiple filters\n--- request\nGET /hello\n--- response_body\nHETLO world\n\n\n\n=== TEST 17: filters no any match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"test\",\n                                    \"replace\": \"TEST\"\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: check http body that filters no any match\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 19: schema check for headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            for _, case in ipairs({\n                {add = {\n                    {\"headers:\"}\n                }},\n                {remove = {\n                    {\"headers:\"}\n                }},\n                {set = {\n                    {\"headers\"}\n                }},\n                {set = {\n                    {[\"\"] = 1}\n                }},\n                {set = {\n                    {[\"a\"] = true}\n                }},\n            }) do\n                local plugin = require(\"apisix.plugins.response-rewrite\")\n                local ok, err = plugin.check_schema({headers = case})\n                if not ok then\n                    ngx.say(err)\n                else\n                    ngx.say(\"done\")\n                end\n            end\n    }\n}\n--- response_body eval\n\"property \\\"headers\\\" validation failed: object matches none of the required\\n\" x 5\n\n\n\n=== TEST 20: add headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"add\": [\n                                    \"Cache-Control: no-cache\",\n                                    \"Cache-Control : max-age=0, must-revalidate\"\n                                ]\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 21: hit\n--- request\nGET /hello\n--- response_headers\nCache-Control: no-cache, max-age=0, must-revalidate\n\n\n\n=== TEST 22: set headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"add\": [\n                                    \"Cache-Control: no-cache\"\n                                ],\n                                \"set\": {\n                                    \"Cache-Control\": \"max-age=0, must-revalidate\"\n                                }\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 23: hit\n--- request\nGET /hello\n--- response_headers\nCache-Control: max-age=0, must-revalidate\n\n\n\n=== TEST 24: remove headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"headers\": {\n                                \"add\": [\n                                    \"Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>\"\n                                ],\n                                \"set\": {\n                                    \"Cache-Control\": \"max-age=0, must-revalidate\"\n                                },\n                                \"remove\": [\n                                    \"Set-Cookie\",\n                                    \"Cache-Control\"\n                                ]\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello\"]\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 25: hit\n--- request\nGET /hello\n--- response_headers\nCache-Control:\nSet-Cookie:\n"
  },
  {
    "path": "t/plugin/response-rewrite3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    server {\n        listen 11451;\n        gzip on;\n        gzip_types *;\n        gzip_min_length 1;\n        location /gzip_hello {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local s = \"hello world\"\n                ngx.header['Content-Length'] = #s + 1\n                ngx.say(s)\n            }\n        }\n    }\n\n    server {\n        listen 11452;\n        location /brotli_hello {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local s = \"hello world hello world hello world\"\n                ngx.header['Content-Length'] = #s + 1\n                ngx.say(s)\n            }\n            header_filter_by_lua_block {\n                local conf = {\n                    comp_level = 6,\n                    http_version = 1.1,\n                    lgblock = 0,\n                    lgwin = 19,\n                    min_length = 1,\n                    mode = 0,\n                    types = \"*\",\n                }\n                local brotli = require(\"apisix.plugins.brotli\")\n                brotli.header_filter(conf, ngx.ctx)\n            }\n            body_filter_by_lua_block {\n                local conf = {\n                    comp_level = 6,\n                    http_version = 1.1,\n                    lgblock = 0,\n                    lgwin = 19,\n                    min_length = 1,\n                    mode = 0,\n                    types = \"*\",\n                }\n                local brotli = require(\"apisix.plugins.brotli\")\n                brotli.body_filter(conf, ngx.ctx)\n            }\n        }\n    }\n\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route use gzip upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/gzip_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11451\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: should return gzip body\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- response_headers\nContent-Encoding: gzip\n\n\n\n=== TEST 3: set route use gzip upstream and response-rewrite body conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/gzip_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11451\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"body\": \"new body\\n\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: should rewrite body and clear Content-Encoding header\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- response_body\nnew body\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 5: set route use gzip upstream and response-rewrite filter conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/gzip_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11451\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"hello\",\n                                    \"replace\": \"test\"\n                                }\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: gzip decode support, should rewrite body and clear Content-Encoding header\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- response_body\ntest world\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 7: set route use response-write body conf, and mock unsupported compression encoding type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"body\": \"new body\\n\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: use body conf will ignore encoding, should rewrite body and clear Content-Encoding header\n--- request\nPOST /echo\nfake body with mock content encoding header\n--- more_headers\nContent-Encoding: deflate\n--- response_body\nnew body\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 9: set route use response-write filter conf, and mock unsupported compression encoding type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"hello\",\n                                    \"replace\": \"test\"\n                                }\n                            ]\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: use filter conf will report unsupported encoding type error\n--- request\nPOST /echo\nfake body with mock content encoding header\n--- more_headers\nContent-Encoding: deflate\n--- response_headers\nContent-Encoding:\n--- error_log\nfilters may not work as expected due to unsupported compression encoding type: deflate\n\n\n\n=== TEST 11: set route use response-write plugin but not use filter conf or body conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/gzip_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11451\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"headers\": {\n                                \"set\": {\n                                    \"X-Server-id\": 3,\n                                    \"X-Server-status\": \"on\",\n                                    \"Content-Type\": \"\"\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: should keep Content-Encoding\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- response_headers\nContent-Encoding: gzip\nX-Server-id: 3\nX-Server-status: on\nContent-Type:\n\n\n\n=== TEST 13: response-write without filter conf or body conf, and mock unsupported compression encoding type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/echo\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"headers\": {\n                                \"set\": {\n                                    \"X-Server-id\": 3,\n                                    \"X-Server-status\": \"on\",\n                                    \"Content-Type\": \"\"\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: should keep Content-Encoding\n--- request\nPOST /echo\nfake body with mock content encoding header\n--- more_headers\nContent-Encoding: deflate\n--- response_headers\nContent-Encoding: deflate\nX-Server-id: 3\nX-Server-status: on\nContent-Type:\n\n\n\n=== TEST 15: set route use brotli upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/brotli_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11452\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: should return brotli body\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- response_headers\nContent-Encoding: br\n\n\n\n=== TEST 17: set route use brotli upstream and response-rewrite body conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/brotli_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11452\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"body\": \"new body\\n\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: should rewrite body and clear Content-Encoding header\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- response_body\nnew body\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 19: set route use brotli upstream and response-rewrite filter conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/brotli_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11452\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"filters\": [\n                                {\n                                    \"regex\": \"hello\",\n                                    \"replace\": \"test\"\n                                }\n                            ]\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: brotli decode support, should rewrite body and clear Content-Encoding header\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- response_body\ntest world hello world hello world\n--- response_headers\nContent-Encoding:\n\n\n\n=== TEST 21: set route use response-write plugin but not use filter conf or body conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/brotli_hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:11452\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"response-rewrite\": {\n                            \"vars\": [\n                                [\"status\",\"==\",200]\n                            ],\n                            \"headers\": {\n                                \"set\": {\n                                    \"X-Server-id\": 3,\n                                    \"X-Server-status\": \"on\",\n                                    \"Content-Type\": \"\"\n                                }\n                            }\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: should keep Content-Encoding\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- response_headers\nContent-Encoding: br\nX-Server-id: 3\nX-Server-status: on\nContent-Type:\n"
  },
  {
    "path": "t/plugin/rocketmq-logger-log-format.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/rocketmq-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"@timestamp\": \"$time_iso8601\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1), batch_max_size=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"tag\" : \"tag1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: hit route and report rocketmq logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- error_log eval\nqr/send data to rocketmq: \\{.*\"host\":\"localhost\"/\n\n\n\n=== TEST 4: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"tag\" : \"tag1\",\n                                \"log_format\": {\n                                    \"x_ip\": \"$remote_addr\"\n                                },\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route and report logger\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- error_log eval\nqr/send data to rocketmq: \\{.*\"x_ip\":\"127.0.0.1\".*\\}/\n"
  },
  {
    "path": "t/plugin/rocketmq-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                 topic = \"test\",\n                 key = \"key1\",\n                 nameserver_list = {\n                    \"127.0.0.1:3\"\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: missing nameserver list\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({topic = \"test\", key= \"key1\"})\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"nameserver_list\" is required\ndone\n\n\n\n=== TEST 3: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                nameserver_list = {\n                    \"127.0.0.1:3000\"\n                },\n                timeout = \"10\",\n                topic =\"test\",\n                key= \"key1\"\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"timeout\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 4: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: access\n--- request\nGET /hello\n--- response_body\nhello world\n\n--- wait: 2\n\n\n\n=== TEST 6: unavailable nameserver\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                             \"rocketmq-logger\": {\n                                    \"nameserver_list\" : [ \"127.0.0.1:9877\" ],\n                                    \"topic\" : \"test2\",\n                                    \"producer_type\": \"sync\",\n                                    \"key\" : \"key1\",\n                                    \"batch_max_size\": 1\n                             }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n        }\n    }\n--- error_log\nfailed to send data to rocketmq topic\n[error]\n--- wait: 1\n\n\n\n=== TEST 7: set route(meta_format = origin, include_req_body = true)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true,\n                                \"meta_format\": \"origin\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route, report log to rocketmq\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n\n--- error_log\nsend data to rocketmq: GET /hello?ab=cd HTTP/1.1\nhost: localhost\ncontent-length: 6\nconnection: close\n\nabcdef\n--- wait: 2\n\n\n\n=== TEST 9: set route(meta_format = origin, include_req_body = false)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false,\n                                \"meta_format\": \"origin\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route, report log to rocketmq\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n\n--- error_log\nsend data to rocketmq: GET /hello?ab=cd HTTP/1.1\nhost: localhost\ncontent-length: 6\nconnection: close\n--- wait: 2\n\n\n\n=== TEST 11: set route(meta_format = default)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" :  [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route, report log to rocketmq\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n\n--- error_log_like eval\nqr/send data to rocketmq: \\{.*\"upstream\":\"127.0.0.1:1980\"/\n--- wait: 2\n\n\n\n=== TEST 13: set route(id: 1), missing key field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: access, test key field is optional\n--- request\nGET /hello\n--- response_body\nhello world\n\n--- wait: 2\n\n\n\n=== TEST 15: set route(meta_format = default), missing key field\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" :  [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route, report log to rocketmq\n--- request\nGET /hello?ab=cd\nabcdef\n--- response_body\nhello world\n\n--- error_log_like eval\nqr/send data to rocketmq: \\{.*\"upstream\":\"127.0.0.1:1980\"/\n--- wait: 2\n\n\n\n=== TEST 17: use the topic with 3 partitions\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" :  [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test3\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: report log to rocketmq by different partitions\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" :  [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test3\",\n                                \"producer_type\": \"sync\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n        }\n    }\n--- timeout: 5s\n--- ignore_response\n\n--- error_log eval\n[qr/queue: 1/,\nqr/queue: 0/,\nqr/queue: 2/]\n\n\n\n=== TEST 19: report log to rocketmq by different partitions in async mode\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" :  [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test3\",\n                                \"producer_type\": \"async\",\n                                \"timeout\" : 1,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n        }\n    }\n--- timeout: 5s\n--- ignore_response\n\n--- error_log eval\n[qr/queue: 1/,\nqr/queue: 0/,\nqr/queue: 2/]\n"
  },
  {
    "path": "t/plugin/rocketmq-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: update the nameserver_list, generate different rocketmq producers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            ngx.sleep(0.5)\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            code, body = t('/apisix/admin/routes/1/plugins',\n                ngx.HTTP_PATCH,\n                 [[{\n                        \"rocketmq-logger\": {\n                            \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                            \"topic\" : \"test2\",\n                            \"timeout\" : 1,\n                            \"batch_max_size\": 1,\n                            \"include_req_body\": false\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n\n            code, body = t('/apisix/admin/routes/1/plugins',\n                ngx.HTTP_PATCH,\n                 [[{\n                        \"rocketmq-logger\": {\n                            \"nameserver_list\" :  [ \"127.0.0.1:19876\" ],\n                            \"topic\" : \"test4\",\n                            \"timeout\" : 1,\n                            \"batch_max_size\": 1,\n                            \"include_req_body\": false\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n\n            ngx.sleep(2)\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 10\n--- response\npassed\n--- wait: 5\n--- error_log\nphase_func(): rocketmq nameserver_list[1]: 127.0.0.1:9876\nphase_func(): rocketmq nameserver_list[1]: 127.0.0.1:19876\n--- no_error_log eval\nqr/not found topic/\n\n\n\n=== TEST 2: use the topic that does not exist on rocketmq(even if rocketmq allows auto create topics, first time push messages to rocketmq would got this error)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1/plugins',\n                ngx.HTTP_PATCH,\n                 [[{\n                        \"rocketmq-logger\": {\n                            \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                            \"topic\" : \"undefined_topic\",\n                            \"timeout\" : 1,\n                            \"batch_max_size\": 1,\n                            \"include_req_body\": false\n                        }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            t('/hello',ngx.HTTP_GET)\n            ngx.sleep(0.5)\n\n            ngx.sleep(2)\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 5\n--- response\npassed\n--- error_log eval\nqr/getTopicRouteInfoFromNameserver return TOPIC_NOT_EXIST, No topic route info in name server for the topic: undefined_topic/\n\n\n\n=== TEST 3: rocketmq nameserver list info in log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                             \"rocketmq-logger\": {\n                                    \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                    \"topic\" : \"test2\",\n                                    \"producer_type\": \"sync\",\n                                    \"key\" : \"key1\",\n                                    \"batch_max_size\": 1\n                             }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n        }\n    }\n--- error_log_like eval\nqr/create new rocketmq producer instance, nameserver_list: \\[\\{\"port\":9876,\"host\":\"127.0.0.127\"}]/\nqr/failed to send data to rocketmq topic: .*, nameserver_list: \\{\"127.0.0.127\":9876}/\n\n\n\n=== TEST 4: delete plugin metadata, tests would fail if run rocketmq-logger-log-format.t and plugin metadata is added\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/rocketmq-logger',\n                ngx.HTTP_DELETE\n            )\n        }\n    }\n--- response_body\n\n\n\n=== TEST 5: set route(id: 1,include_req_body = true,include_req_body_expr = array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_req_body\": true,\n                                \"include_req_body_expr\": [\n                                    [\n                                      \"arg_name\",\n                                      \"==\",\n                                      \"qwerty\"\n                                    ]\n                                ],\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route, expr eval success\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n\n--- error_log eval\nqr/send data to rocketmq: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n\n\n\n=== TEST 7: hit route,expr eval fail\n--- request\nPOST /hello?name=zcxv\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\nqr/send data to rocketmq: \\{.*\"body\":\"abcdef\"/\n--- wait: 2\n\n\n\n=== TEST 8: check log schema(include_req_body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                 topic = \"test\",\n                 key = \"key1\",\n                 nameserver_list = {\n                    \"127.0.0.1:3\"\n                 },\n                 include_req_body = true,\n                 include_req_body_expr = {\n                     {\"bar\", \"<>\", \"foo\"}\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nfailed to validate the 'include_req_body_expr' expression: invalid operator '<>'\ndone\n\n\n\n=== TEST 9: check log schema(include_resp_body)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                 topic = \"test\",\n                 key = \"key1\",\n                 nameserver_list = {\n                    \"127.0.0.1:3\"\n                 },\n                 include_resp_body = true,\n                 include_resp_body_expr = {\n                     {\"bar\", \"<!>\", \"foo\"}\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nfailed to validate the 'include_resp_body_expr' expression: invalid operator '<!>'\ndone\n\n\n\n=== TEST 10: set route(id: 1,include_resp_body = true,include_resp_body_expr = array)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_resp_body\": true,\n                                \"include_resp_body_expr\": [\n                                    [\n                                      \"arg_name\",\n                                      \"==\",\n                                      \"qwerty\"\n                                    ]\n                                ],\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route, expr eval success\n--- request\nPOST /hello?name=qwerty\nabcdef\n--- response_body\nhello world\n--- error_log eval\nqr/send data to rocketmq: \\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 12: hit route, expr eval fail\n--- request\nPOST /hello?name=zcxv\nabcdef\n--- response_body\nhello world\n--- no_error_log eval\nqr/send data to rocketmq: \\{.*\"body\":\"hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 13: set route include_resp_body = true - gzip\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_resp_body\": true,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:11451\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/gzip_hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 14: hit\n--- http_config\nserver {\n    listen 11451;\n    gzip on;\n    gzip_types *;\n    gzip_min_length 1;\n    location /gzip_hello {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local s = \"gzip hello world\"\n            ngx.header['Content-Length'] = #s + 1\n            ngx.say(s)\n        }\n    }\n}\n--- request\nGET /gzip_hello\n--- more_headers\nAccept-Encoding: gzip\n--- error_log eval\nqr/send data to rocketmq: \\{.*\"body\":\"gzip hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 15: set route include_resp_body - brotli\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"key\" : \"key1\",\n                                \"timeout\" : 1,\n                                \"include_resp_body\": true,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:11452\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/brotli_hello\"\n                }]=]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 16: hit route, expr eval success\n--- http_config\nserver {\n    listen 11452;\n    location /brotli_hello {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local s = \"brotli hello world\"\n            ngx.header['Content-Length'] = #s + 1\n            ngx.say(s)\n        }\n        header_filter_by_lua_block {\n            local conf = {\n                comp_level = 6,\n                http_version = 1.1,\n                lgblock = 0,\n                lgwin = 19,\n                min_length = 1,\n                mode = 0,\n                types = \"*\",\n            }\n            local brotli = require(\"apisix.plugins.brotli\")\n            brotli.header_filter(conf, ngx.ctx)\n        }\n        body_filter_by_lua_block {\n            local conf = {\n                comp_level = 6,\n                http_version = 1.1,\n                lgblock = 0,\n                lgwin = 19,\n                min_length = 1,\n                mode = 0,\n                types = \"*\",\n            }\n            local brotli = require(\"apisix.plugins.brotli\")\n            brotli.body_filter(conf, ngx.ctx)\n        }\n    }\n}\n--- request\nGET /brotli_hello\n--- more_headers\nAccept-Encoding: br\n--- error_log eval\nqr/send data to rocketmq: \\{.*\"body\":\"brotli hello world\\\\n\"/\n--- wait: 2\n\n\n\n=== TEST 17: multi level nested expr conditions\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                 topic = \"test\",\n                 key = \"key1\",\n                 nameserver_list = {\n                    \"127.0.0.1:3\"\n                 },\n                 include_req_body = true,\n                 include_req_body_expr = {\n                    {\"request_length\", \"<\", 1024},\n                    {\"http_content_type\", \"in\", {\"application/xml\", \"application/json\", \"text/plain\", \"text/xml\"}}\n                 },\n                 include_resp_body = true,\n                 include_resp_body_expr = {\n                    {\"http_content_length\", \"<\", 1024},\n                    {\"http_content_type\", \"in\", {\"application/xml\", \"application/json\", \"text/plain\", \"text/xml\"}}\n                 }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 18: data encryption for secret_key\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"rocketmq-logger\": {\n                                \"nameserver_list\" : [ \"127.0.0.1:9876\" ],\n                                \"topic\" : \"test2\",\n                                \"access_key\": \"foo\",\n                                \"secret_key\": \"bar\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n             )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"rocketmq-logger\"].secret_key)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"rocketmq-logger\"].secret_key)\n        }\n    }\n--- response_body\nbar\n77+NmbYqNfN+oLm0aX5akg==\n"
  },
  {
    "path": "t/plugin/security-warning.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\nrun_tests();\n\n__DATA__\n\n=== TEST 1: authz-casdoor no https\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local fake_uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            local callback_url = \"http://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback\"\n            local conf = {\n                callback_url = callback_url,\n                endpoint_addr = fake_uri,\n                client_id = \"7ceb9b7fda4a9061ec1c\",\n                client_secret = \"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing authz-casdoor endpoint_addr with no TLS is a security risk\nUsing authz-casdoor callback_url with no TLS is a security risk\n\n\n\n=== TEST 2: authz-casdoor with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.authz-casdoor\")\n            local fake_uri = \"https://127.0.0.1:\" .. ngx.var.server_port\n            local callback_url = \"https://127.0.0.1:\" .. ngx.var.server_port ..\n                                    \"/anything/callback\"\n            local conf = {\n                callback_url = callback_url,\n                endpoint_addr = fake_uri,\n                client_id = \"7ceb9b7fda4a9061ec1c\",\n                client_secret = \"3416238e1edf915eac08b8fe345b2b95cdba7e04\"\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing authz-casdoor endpoint_addr with no TLS is a security risk\nUsing authz-casdoor callback_url with no TLS is a security risk\n\n\n\n=== TEST 3: authz keycloak with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local check = {\"discovery\", \"token_endpoint\", \"resource_registration_endpoint\", \"access_denied_redirect_uri\"}\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                discovery = \"http://host.domain/realms/foo/protocol/openid-connect/token\",\n                                token_endpoint = \"http://token_endpoint.domain\",\n                                resource_registration_endpoint = \"http://resource_registration_endpoint.domain\",\n                                access_denied_redirect_uri = \"http://access_denied_redirect_uri.domain\"\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nUsing authz-keycloak discovery with no TLS is a security risk\nUsing authz-keycloak token_endpoint with no TLS is a security risk\nUsing authz-keycloak resource_registration_endpoint with no TLS is a security\nUsing authz-keycloak access_denied_redirect_uri with no TLS is a security risk\n\n\n\n=== TEST 4: authz keycloak with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local check = {\"discovery\", \"token_endpoint\", \"resource_registration_endpoint\", \"access_denied_redirect_uri\"}\n            local plugin = require(\"apisix.plugins.authz-keycloak\")\n            local ok, err = plugin.check_schema({\n                                client_id = \"foo\",\n                                discovery = \"https://host.domain/realms/foo/protocol/openid-connect/token\",\n                                token_endpoint = \"https://token_endpoint.domain\",\n                                resource_registration_endpoint = \"https://resource_registration_endpoint.domain\",\n                                access_denied_redirect_uri = \"https://access_denied_redirect_uri.domain\"\n                            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nUsing authz-keycloak discovery with no TLS is a security risk\nUsing authz-keycloak token_endpoint with no TLS is a security risk\nUsing authz-keycloak resource_registration_endpoint with no TLS is a security\nUsing authz-keycloak access_denied_redirect_uri with no TLS is a security risk\n\n\n\n=== TEST 5: cas auth with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cas-auth\")\n            local ok, err = plugin.check_schema({\n                idp_uri = \"http://a.com\",\n                cas_callback_uri = \"/a/b\",\n                logout_uri = \"/c/d\"\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n--- error_log\nrisk\n\n\n\n=== TEST 6: cas auth with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.cas-auth\")\n            local ok, err = plugin.check_schema({\n                idp_uri = \"https://a.com\",\n                cas_callback_uri = \"/a/b\",\n                logout_uri = \"/c/d\"\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nrisk\n\n\n\n=== TEST 7: clickhouse logger with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.clickhouse-logger\")\n            local ok, err = plugin.check_schema({\n                timeout = 3,\n                retry_delay = 1,\n                batch_max_size = 500,\n                user = \"default\",\n                password = \"a\",\n                database = \"default\",\n                logtable = \"t\",\n                endpoint_addrs = {\n                    \"http://127.0.0.1:1980/clickhouse_logger_server\",\n                    \"http://127.0.0.2:1980/clickhouse_logger_server\",\n                },\n                max_retry_count = 1,\n                name = \"clickhouse logger\",\n                ssl_verify = false\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n--- error_log\nUsing clickhouse-logger endpoint_addrs with no TLS is a security risk\n\n\n\n=== TEST 8: clickhouse logger with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.clickhouse-logger\")\n            local ok, err = plugin.check_schema({\n                timeout = 3,\n                retry_delay = 1,\n                batch_max_size = 500,\n                user = \"default\",\n                password = \"a\",\n                database = \"default\",\n                logtable = \"t\",\n                endpoint_addrs = {\n                    \"https://127.0.0.1:1980/clickhouse_logger_server\",\n                    \"https://127.0.0.2:1980/clickhouse_logger_server\",\n                },\n                max_retry_count = 1,\n                name = \"clickhouse logger\",\n                ssl_verify = false\n            })\n\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nUsing clickhouse-logger endpoint_addrs with no TLS is a security risk\n\n\n\n=== TEST 9: elastic search logger with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local ok, err\n            local plugin = require(\"apisix.plugins.elasticsearch-logger\")\n                ok, err = plugin.check_schema({\n                    endpoint_addrs = {\n                        \"http://127.0.0.1:9200\"\n                    },\n                    field = {\n                        index = \"services\"\n                    }\n                })\n                if err then\n                    ngx.say(err)\n                else\n                    ngx.say(\"passed\")\n                end\n\n        }\n    }\n--- response_body_like\npassed\n--- error_log\nUsing elasticsearch-logger endpoint_addrs with no TLS is a security risk\n\n\n\n=== TEST 10: elastic search logger with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local ok, err\n            local plugin = require(\"apisix.plugins.elasticsearch-logger\")\n                ok, err = plugin.check_schema({\n                    endpoint_addrs = {\n                        \"https://127.0.0.1:9200\"\n                    },\n                    field = {\n                        index = \"services\"\n                    }\n                })\n                if err then\n                    ngx.say(err)\n                else\n                    ngx.say(\"passed\")\n                end\n\n        }\n    }\n--- response_body_like\npassed\n--- no_error_log\nUsing elasticsearch-logger endpoint_addrs with no TLS is a security risk\n\n\n\n=== TEST 11: error log logger with tcp.tls = false\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.error-log-logger\")\n            local ok, err = plugin.check_schema({\n                tcp = {\n                    host = \"host.com\",\n                    port = \"99\",\n                    tls = false,\n                },\n                skywalking = {\n                    endpoint_addr = \"http://a.bcd\"\n                },\n                clickhouse = {\n                    endpoint_addr = \"http://some.com\",\n                    user = \"user\",\n                    password = \"secret\",\n                    database = \"yes\",\n                    logtable = \"some\"\n                },\n            })\n            ngx.say(ok and \"done\" or err)\n\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nUsing error-log-logger skywalking.endpoint_addr with no TLS is a security risk\nUsing error-log-logger clickhouse.endpoint_addr with no TLS is a security risk\nKeeping tcp.tls disabled in error-log-logger configuration is a security risk\n\n\n\n=== TEST 12: error log logger with tcp.tls = true\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.error-log-logger\")\n            local ok, err = plugin.check_schema({\n                tcp = {\n                    host = \"host.com\",\n                    port = \"99\",\n                    tls = true,\n                },\n                skywalking = {\n                    endpoint_addr = \"https://a.bcd\"\n                },\n                clickhouse = {\n                    endpoint_addr = \"https://some.com\",\n                    user = \"user\",\n                    password = \"secret\",\n                    database = \"yes\",\n                    logtable = \"some\"\n                },\n            })\n            ngx.say(ok and \"done\" or err)\n\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nUsing error-log-logger skywalking.endpoint_addr with no TLS is a security risk\nUsing error-log-logger clickhouse.endpoint_addr with no TLS is a security risk\nKeeping tcp.tls disabled in error-log-logger configuration is a security risk\n\n\n\n=== TEST 13: forward auth with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.forward-auth\")\n\n            local ok, err = plugin.check_schema({uri = \"http://127.0.0.1:8199\"})\n            ngx.say(ok and \"done\" or err)\n\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing forward-auth uri with no TLS is a security risk\nUsing forward-auth uri with no TLS is a security risk\n\n\n\n=== TEST 14: forward auth with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.forward-auth\")\n\n            local ok, err = plugin.check_schema({uri = \"https://127.0.0.1:8199\"})\n            ngx.say(ok and \"done\" or err)\n\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing forward-auth uri with no TLS is a security risk\n\n\n\n=== TEST 15: http-logger with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({uri = \"http://127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing http-logger uri with no TLS is a security risk\n\n\n\n=== TEST 16: http-logger with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.http-logger\")\n            local ok, err = plugin.check_schema({uri = \"https://127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing http-logger uri with no TLS is a security risk\n\n\n\n=== TEST 17: ldap auth with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.ldap-auth\")\n            local ok, err = plugin.check_schema(\n                {\n                    base_dn = \"123\",\n                    ldap_uri = \"127.0.0.1:1389\",\n                    tls_verify = false,\n                    use_tls = false\n                })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nKeeping tls_verify disabled in ldap-auth configuration is a security risk\nKeeping use_tls disabled in ldap-auth configuration is a security risk\n\n\n\n=== TEST 18: ldap auth with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local plugin = require(\"apisix.plugins.ldap-auth\")\n            local ok, err = plugin.check_schema({base_dn = \"123\", ldap_uri = \"127.0.0.1:1389\", use_tls = true})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing LDAP auth with TLS disabled is a security risk\n\n\n\n=== TEST 19: loki-logger with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.loki-logger\")\n\n            local ok, err = plugin.check_schema({endpoint_addrs = {\"http://127.0.0.1:8199\"}})\n            ngx.say(ok and \"done\" or err)\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing loki-logger endpoint_addrs with no TLS is a security risk\nUsing loki-logger endpoint_addrs with no TLS is a security risk\nUsing loki-logger endpoint_addrs with no TLS is a security risk\n\n\n\n=== TEST 20: loki logger with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.loki-logger\")\n\n            local ok, err = plugin.check_schema({endpoint_addrs = {\"https://127.0.0.1:8199\"}})\n            ngx.say(ok and \"done\" or err)\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing loki-logger endpoint_addrs with no TLS is a security risk\n"
  },
  {
    "path": "t/plugin/security-warning2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\nrun_tests();\n\n__DATA__\n\n=== TEST 1: opa with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.opa\")\n            local ok, err = plugin.check_schema({host = \"http://127.0.0.1:8181\", policy = \"example/allow\"})\n            ngx.say(ok and \"done\" or err)\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing opa host with no TLS is a security risk\n\n\n\n=== TEST 2: opa with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.opa\")\n            local ok, err = plugin.check_schema({host = \"https://127.0.0.1:8181\", policy = \"example/allow\"})\n            ngx.say(ok and \"done\" or err)\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing opa host with no TLS is a security risk\n\n\n\n=== TEST 3: openid-connect with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n\n            local plugin = require(\"apisix.plugins.openid-connect\")\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"http://a.com\",\n                introspection_endpoint = \"http://b.com\",\n                redirect_uri = \"http://c.com\",\n                post_logout_redirect_uri = \"http://d.com\",\n                proxy_opts = {\n                    http_proxy = \"http://e.com\"\n                },\n                session = {\n                    secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing openid-connect discovery with no TLS is a security risk\nUsing openid-connect introspection_endpoint with no TLS is a security risk\nUsing openid-connect redirect_uri with no TLS is a security risk\nUsing openid-connect post_logout_redirect_uri with no TLS is a security risk\nUsing openid-connect proxy_opts.http_proxy with no TLS is a security risk\n\n\n\n=== TEST 4: openid-connect with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openid-connect\")\n\n            local ok, err = plugin.check_schema({\n                client_id = \"a\",\n                client_secret = \"b\",\n                discovery = \"https://a.com\",\n                introspection_endpoint = \"https://b.com\",\n                redirect_uri = \"https://c.com\",\n                post_logout_redirect_uri = \"https://d.com\",\n                proxy_opts = {\n                    http_proxy = \"https://e.com\"\n                },\n                session = {\n                    secret = \"jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK\"\n                }\n            })\n\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing openid-connect discovery with no TLS is a security risk\nUsing openid-connect introspection_endpoint with no TLS is a security risk\nUsing openid-connect redirect_uri with no TLS is a security risk\nUsing openid-connect post_logout_redirect_uri with no TLS is a security risk\nUsing openid-connect proxy_opts.http_proxy with no TLS is a security risk\n\n\n\n=== TEST 5: opentelemetry with no TLS\n--- extra_yaml_config\nplugins:\n    - opentelemetry\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"trace_id_source\": \"x-request-id\",\n                    \"collector\": {\n                        \"address\": \"http://127.0.0.1:4318\",\n                        \"request_timeout\": 3,\n                        \"request_headers\": {\n                            \"foo\": \"bar\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            --- deleting this data so this doesn't effect when metadata schema is validated\n            --- at init in next test.\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_log\nUsing opentelemetry collector.address with no TLS is a security risk\n\n\n\n=== TEST 6: opentelemetery with TLS\n--- extra_yaml_config\nplugins:\n    - opentelemetry\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',\n                ngx.HTTP_PUT,\n                [[{\n                    \"batch_span_processor\": {\n                        \"max_export_batch_size\": 1,\n                        \"inactive_timeout\": 0.5\n                    },\n                    \"trace_id_source\": \"x-request-id\",\n                    \"collector\": {\n                        \"address\": \"https://127.0.0.1:4318\",\n                        \"request_timeout\": 3,\n                        \"request_headers\": {\n                            \"foo\": \"bar\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"opentelemetry\": {\n                            \"sampler\": {\n                                \"name\": \"always_on\"\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- no_error_log\nUsing opentelemetry collector.address with no TLS is a security risk\n\n\n\n=== TEST 7: openwhisk with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openwhisk\")\n            local ok, err = plugin.check_schema({\n                api_host = \"http://127.0.0.1:3233\",\n                service_token = \"test:test\",\n                namespace = \"test\",\n                action = \"test\"\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing openwhisk api_host with no TLS is a security risk\n\n\n\n=== TEST 8: openwhisk with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.openwhisk\")\n            local ok, err = plugin.check_schema({api_host = \"https://127.0.0.1:3233\", service_token = \"test:test\", namespace = \"test\", action = \"test\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing openwhisk api_host with no TLS is a security risk\n\n\n\n=== TEST 9: rocketmq with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                 topic = \"test\",\n                 key = \"key1\",\n                 nameserver_list = {\n                    \"127.0.0.1:3\"\n                 },\n                 use_tls = false\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nKeeping use_tls disabled in rocketmq-logger configuration is a security risk\n\n\n\n=== TEST 10: rocketmq with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.rocketmq-logger\")\n            local ok, err = plugin.check_schema({\n                 topic = \"test\",\n                 key = \"key1\",\n                 nameserver_list = {\n                    \"127.0.0.1:3\"\n                 },\n                 use_tls = true\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nKeeping use_tls disabled in rocketmq-logger configuration is a security risk\n\n\n\n=== TEST 11: skywalking-logger with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking-logger\")\n            local ok, err = plugin.check_schema({endpoint_addr = \"http://127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- error_log\nUsing skywalking-logger endpoint_addr with no TLS is a security risk\n\n\n\n=== TEST 12: skywalking-logger with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking-logger\")\n            local ok, err = plugin.check_schema({endpoint_addr = \"https://127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n--- no_error_log\nUsing skywalking-logger endpoint_addr with no TLS is a security risk\n\n\n\n=== TEST 13: skywalking with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking\")\n            local ok, err = plugin.check_schema({endpoint_addr = \"http://127.0.0.1:12800\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nUsing skywalking endpoint_addr with no TLS is a security risk\n\n\n\n=== TEST 14: skywalking with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking\")\n            local ok, err = plugin.check_schema({endpoint_addr = \"https://127.0.0.1:12800\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nUsing skywalking endpoint_addr with no TLS is a security risk\n\n\n\n=== TEST 15: syslog with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.syslog\")\n            local ok, err = plugin.check_schema({\n                 host = \"127.0.0.1\",\n                 port = 5140,\n                 tls = false\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nKeeping tls disabled in syslog configuration is a security risk\n\n\n\n=== TEST 16: syslog with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.syslog\")\n            local ok, err = plugin.check_schema({\n                 host = \"127.0.0.1\",\n                 port = 5140,\n                 tls = true\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nKeeping tls disabled in syslog configuration is a security risk\n\n\n\n=== TEST 17: tcp-logger with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tcp-logger\")\n            local ok, err = plugin.check_schema({host = \"127.0.0.1\", port = 3000, tls = false})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nKeeping tls disabled in tcp-logger configuration is a security risk\n\n\n\n=== TEST 18: tcp-logger with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 3000,\n                                \"tls\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- no_error_log\nKeeping tls disabled in tcp-logger configuration is a security risk\n\n\n\n=== TEST 19: wolf-rbac with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.wolf-rbac\")\n            local conf = {\n                server = \"http://127.0.0.1:12180\"\n            }\n\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- response_body_like eval\nqr/\\{\"appid\":\"unset\",\"header_prefix\":\"X-\",\"server\":\"http:\\/\\/127\\.0\\.0\\.1:12180\"\\}/\n--- error_log\nUsing wolf-rbac server with no TLS is a security risk\n\n\n\n=== TEST 20: wolf-rbac with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"wolf_rbac_unit_test\",\n                    \"plugins\": {\n                        \"wolf-rbac\": {\n                            \"appid\": \"wolf-rbac-app\",\n                            \"server\": \"https://127.0.0.1:1982\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\nUsing wolf-rbac server with no TLS is a security risk\n\n\n\n=== TEST 21: zipkin with no TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.zipkin\")\n            local ok, err = plugin.check_schema({endpoint = 'http://127.0.0.1', sample_ratio = 0.001})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nUsing zipkin endpoint with no TLS is a security risk\n\n\n\n=== TEST 22: zipkin with TLS\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.zipkin\")\n            local ok, err = plugin.check_schema({endpoint = 'https://127.0.0.1', sample_ratio = 0.001})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nUsing zipkin endpoint with no TLS is a security risk\n"
  },
  {
    "path": "t/plugin/server-info.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nour $SkipReason;\n\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\nuse Test::Nginx::Socket::Lua $SkipReason ? (skip_all => $SkipReason) : ();\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity check\n--- yaml_config\napisix:\n    id: 123456\nplugins:\n    - server-info\nplugin_attr:\n    server-info:\n        report_ttl: 60\n--- config\nlocation /t {\n    content_by_lua_block {\n        ngx.sleep(2)\n        local core = require(\"apisix.core\")\n        local key = \"/data_plane/server_info/\" .. core.id.get()\n        local res, err = core.etcd.get(key)\n        if err ~= nil then\n            ngx.status = 500\n            ngx.say(err)\n            return\n        end\n\n        local value = res.body.node.value\n        local json = require(\"toolkit.json\")\n        ngx.say(json.encode(value))\n    }\n}\n--- response_body eval\nqr/^{\"boot_time\":\\d+,\"etcd_version\":\"[\\d\\.]+\",\"hostname\":\"[a-zA-Z\\-0-9\\.]+\",\"id\":[a-zA-Z\\-0-9]+,\"version\":\"[\\d\\.]+\"}$/\n\n\n\n=== TEST 2: get server_info from plugin control API\n--- yaml_config\napisix:\n    id: 123456\nplugins:\n    - server-info\n--- config\nlocation /t {\n    content_by_lua_block {\n        local json = require(\"toolkit.json\")\n        local t = require(\"lib.test_admin\").test\n        local code, _, body = t(\"/v1/server_info\")\n        if code >= 300 then\n            ngx.status = code\n        end\n\n        body = json.decode(body)\n        ngx.say(json.encode(body))\n    }\n}\n--- response_body eval\nqr/^{\"boot_time\":\\d+,\"etcd_version\":\"[\\d\\.]+\",\"hostname\":\"[a-zA-Z\\-0-9\\.]+\",\"id\":[a-zA-Z\\-0-9]+,\"version\":\"[\\d\\.]+\"}$/\n"
  },
  {
    "path": "t/plugin/serverless.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: use default phase\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.serverless-pre-function\")\n            local schema =  {functions = {\"return function() ngx.log(ngx.ERR, 'serverless post function'); ngx.exit(201); end\"}}\n            local ok, err = plugin.check_schema(schema)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(schema.phase)\n        }\n    }\n--- request\nGET /t\n--- response_body\naccess\n\n\n\n=== TEST 2: phase is rewrite\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.serverless-pre-function\")\n            local ok, err = plugin.check_schema({phase = 'rewrite', functions = {\"return function() ngx.log(ngx.ERR, 'serverless post function'); ngx.exit(201); end\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 3: phase is log for post function\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.serverless-post-function\")\n            local ok, err = plugin.check_schema({phase = 'log', functions = {\"return function() ngx.log(ngx.ERR, 'serverless post function'); ngx.exit(201); end\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 4: invalid phase\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.serverless-pre-function\")\n            local ok, err = plugin.check_schema({phase = 'abc', functions = {\"return function() ngx.log(ngx.ERR, 'serverless post function'); ngx.exit(201); end\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"phase\" validation failed: matches none of the enum values\ndone\n\n\n\n=== TEST 5: only accept function\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.serverless-pre-function\")\n            local ok, err = plugin.check_schema({functions = {\"local a = 123;\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nonly accept Lua function, the input code type is nil\ndone\n\n\n\n=== TEST 6: invalid lua code\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.serverless-pre-function\")\n            local ok, err = plugin.check_schema({functions = {\"a\"}})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to loadstring: [string \"a\"]:1: '=' expected near '<eof>'\ndone\n\n\n\n=== TEST 7: set route and serverless-post-function plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function() ngx.log(ngx.ERR, 'serverless post function'); ngx.exit(201); end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: check plugin\n--- request\nGET /hello\n--- error_code: 201\n--- error_log\nserverless post function\n\n\n\n=== TEST 9: set route and serverless-pre-function plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"functions\" : [\"return function() ngx.log(ngx.ERR, 'serverless pre function'); ngx.exit(201); end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: check plugin\n--- request\nGET /hello\n--- error_code: 201\n--- error_log\nserverless pre function\n\n\n\n=== TEST 11: serverless-pre-function and serverless-post-function\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"functions\" : [\"return function() ngx.log(ngx.ERR, 'serverless pre function'); end\"]\n                        },\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function() ngx.log(ngx.ERR, 'serverless post function'); ngx.exit(201); end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: check plugin\n--- request\nGET /hello\n--- error_code: 201\n--- error_log\nserverless pre function\nserverless post function\n\n\n\n=== TEST 13: log phase and serverless-pre-function plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"log\",\n                            \"functions\" : [\"return function() ngx.log(ngx.ERR, 'serverless pre function'); end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: check plugin\n--- request\nGET /hello\n--- error_log\nserverless pre function\n\n\n\n=== TEST 15: functions\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\" : [\"return function() ngx.log(ngx.ERR, 'one'); end\", \"return function() ngx.log(ngx.ERR, 'two'); end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: check plugin\n--- request\nGET /hello\n--- error_log\none\ntwo\n\n\n\n=== TEST 17: closure\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"log\",\n                            \"functions\" : [\"local count = 1; return function() count = count + 1;ngx.log(ngx.ERR, 'serverless pre function:', count); end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: check plugin\n--- request\nGET /hello\n--- error_log\nserverless pre function:2\n\n\n\n=== TEST 19: http -> https redirect\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"functions\" : [\"return function() if ngx.var.scheme == \\\"http\\\" and ngx.var.host == \\\"foo.com\\\" then ngx.header[\\\"Location\\\"] = \\\"https://foo.com\\\" .. ngx.var.request_uri; ngx.exit(ngx.HTTP_MOVED_PERMANENTLY); end; end\"]\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- more_headers\nHost: foo.com\n--- response_body\npassed\n\n\n\n=== TEST 20: check plugin\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- error_code: 301\n--- response_headers\nLocation: https://foo.com/hello\n\n\n\n=== TEST 21: access conf & ctx in serverless\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                        \"functions\" : [\"return function(conf, ctx) ngx.log(ngx.WARN, 'default phase: ', conf.phase);\n                                       ngx.log(ngx.WARN, 'match uri ', ctx.curr_req_matched and ctx.curr_req_matched._path);\n                                       ctx.var.upstream_uri = '/server_port' end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 22: check plugin\n--- request\nGET /hello\n--- response_body chomp\n1980\n--- error_log\ndefault phase: access\nmatch uri /hello\n\n\n\n=== TEST 23: add args parse test for serverless\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                        \"functions\" : [\"return function(conf, ctx) local net_url = require(\\\"net.url\\\");\n                                        local args = ngx.var.args;\n                                        ngx.print(net_url.parse(args).path);\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 24: check args parse test\n--- request\nGET /echo?args=%40%23%24%25%5E%26\n--- response_body chomp\nargs=@%23$%25%5E&\n\n\n\n=== TEST 25: return status code should exit the request like other plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                        \"functions\" : [\"return function(conf, ctx) return 403, 'forbidden' end\",\n                                       \"return function(conf, ctx) ngx.log(ngx.ERR, 'unreachable') end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 26: check plugin\n--- request\nGET /hello\n--- error_code: 403\n--- response_body chomp\nforbidden\n"
  },
  {
    "path": "t/plugin/skywalking-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $http_config = $block->http_config // <<_EOC_;\n\n    server {\n        listen 1986;\n        server_tokens off;\n\n        location /v3/logs {\n            content_by_lua_block {\n                local core = require(\"apisix.core\")\n                ngx.req.read_body()\n                local data = ngx.req.get_body_data()\n                local headers = ngx.req.get_headers()\n                ngx.log(ngx.WARN, \"skywalking-logger body: \", data)\n                core.log.warn(core.json.encode(core.request.get_body(), true))\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking-logger\")\n            local ok, err = plugin.check_schema({endpoint_addr = \"http://127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n\n\n\n=== TEST 2: full schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking-logger\")\n            local ok, err = plugin.check_schema({endpoint_addr = \"http://127.0.0.1\",\n                                                 timeout = 3,\n                                                 name = \"skywalking-logger\",\n                                                 max_retry_count = 2,\n                                                 retry_delay = 2,\n                                                 buffer_duration = 2,\n                                                 inactive_timeout = 2,\n                                                 batch_max_size = 500,\n                                                 })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n\n\n\n=== TEST 3: uri is missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.skywalking-logger\")\n            local ok, err = plugin.check_schema({timeout = 3,\n                                                 name = \"skywalking-logger\",\n                                                 max_retry_count = 2,\n                                                 retry_delay = 2,\n                                                 buffer_duration = 2,\n                                                 inactive_timeout = 2,\n                                                 batch_max_size = 500,\n                                                 })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"endpoint_addr\" is required\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"skywalking-logger\": {\n                                \"endpoint_addr\": \"http://127.0.0.1:1986\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"service_instance_name\": \"$hostname\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: access local server\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nBatch Processor[skywalking logger] successfully processed the entries\n--- wait: 0.5\n\n\n\n=== TEST 6: test trace context header\n--- request\nGET /opentracing\n--- more_headers\nsw8: 1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-1-QVBJU0lY-QVBJU0lYIEluc3RhbmNlIE5hbWU=-L2dldA==-dXBzdHJlYW0gc2VydmljZQ==\n--- response_body\nopentracing\n--- error_log eval\nqr/.*\\\\\\\"traceContext\\\\\\\":\\{(\\\\\\\"traceSegmentId\\\\\\\":\\\\\\\"ae709769-6e20-4c68-9733-10f455216cc5\\\\\\\"|\\\\\\\"traceId\\\\\\\":\\\\\\\"ae709769-6e20-4c68-9733-10f455216cc5\\\\\\\"|\\\\\\\"spanId\\\\\\\":1|,){5}\\}.*/\n--- wait: 0.5\n\n\n\n=== TEST 7: test wrong trace context header\n--- request\nGET /opentracing\n--- more_headers\nsw8: 1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-1-QVBJU0lY-QVBJU0lYIEluc3RhbmNlIE5hbWU=-L2dldA==\n--- response_body\nopentracing\n--- error_log eval\nqr/failed to parse trace_context header:/\n--- wait: 0.5\n\n\n\n=== TEST 8: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/skywalking-logger',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"@timestamp\": \"$time_iso8601\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: access local server and test log format\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log eval\nqr/.*\\{\\\\\\\"json\\\\\\\":\\\\\\\"\\{(\\\\\\\\\\\\\\\"\\@timestamp\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\".*\\\\\\\\\\\\\\\"|\\\\\\\\\\\\\\\"client_ip\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"127\\.0\\.0\\.1\\\\\\\\\\\\\\\"|\\\\\\\\\\\\\\\"host\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"localhost\\\\\\\\\\\\\\\"|\\\\\\\\\\\\\\\"route_id\\\\\\\\\\\\\\\":\\\\\\\\\\\\\\\"1\\\\\\\\\\\\\\\"|,){7}\\}/\n--- wait: 0.5\n\n\n\n=== TEST 10: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"skywalking-logger\": {\n                                \"endpoint_addr\": \"http://127.0.0.1:1986\",\n                                \"log_format\": {\n                                    \"my_ip\": \"$remote_addr\"\n                                },\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: access local server and test log format\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log eval\nqr/.*\\{\\\\\\\"json\\\\\\\":.*\\\\\\\\\\\\\"my_ip\\\\\\\\\\\\\":\\\\\\\\\\\\\"127\\.0\\.0\\.1\\\\\\\\\\\\\".*\\}/\n--- wait: 0.5\n\n\n\n=== TEST 12: test serviceInstance $hostname\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- no_error_log eval\nqr/\\\\\\\"serviceInstance\\\\\\\":\\\\\\\"\\$hostname\\\\\\\"/\nqr/\\\\\\\"serviceInstance\\\\\\\":\\\\\\\"\\\\\\\"/\n--- wait: 0.5\n\n\n\n=== TEST 13: add plugin with 'include_req_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/skywalking-logger', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"skywalking-logger\": {\n                                \"endpoint_addr\": \"http://127.0.0.1:1986\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body = t(\"/opentracing\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- error_log\n\\\"body\\\":\\\"{\\\\\\\"sample_payload\\\\\\\":\\\\\\\"hello\\\\\\\"}\\\"\n\n\n\n=== TEST 14: add plugin with 'include_resp_body' setting, collect response log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/skywalking-logger', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"skywalking-logger\": {\n                                \"endpoint_addr\": \"http://127.0.0.1:1986\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2,\n                                \"include_req_body\": true,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body = t(\"/opentracing\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- error_log\n\\\"body\\\":\\\"opentracing\\\\n\\\"\n"
  },
  {
    "path": "t/plugin/skywalking-logger2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: should drop entries when max_pending_entries is exceededA\n--- extra_yaml_config\nplugins:\n  - skywalking-logger\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"skywalking-logger\"] = {\n                            endpoint_addr = \"http://127.0.0.1:1234/v3/logs\",\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/skywalking-logger', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/skywalking.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - example-plugin\n    - key-auth\n    - skywalking\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    my $extra_init_by_lua = <<_EOC_;\n    -- reduce default report interval\n    local client = require(\"skywalking.client\")\n    client.backendTimerDelay = 0.5\n\n    local sw_tracer = require(\"skywalking.tracer\")\n    local inject = function(mod, name)\n        local old_f = mod[name]\n        mod[name] = function (...)\n            ngx.log(ngx.WARN, \"skywalking run \", name)\n            return old_f(...)\n        end\n    end\n\n    inject(sw_tracer, \"start\")\n    inject(sw_tracer, \"finish\")\n    inject(sw_tracer, \"prepareForReport\")\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n\n    $block;\n});\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: trigger skywalking\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/skywalking run \\w+/\n--- grep_error_log_out\nskywalking run start\nskywalking run finish\nskywalking run prepareForReport\n--- wait: 1\n\n\n\n=== TEST 3: change sample ratio\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                            \"sample_ratio\": 0.00001\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: not trigger skywalking\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/skywalking run \\w+/\n--- grep_error_log_out\n\n\n\n=== TEST 5: disabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: not trigger skywalking\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- no_error_log\nrewrite phase of skywalking plugin\n\n\n\n=== TEST 7: enable skywalking(sample_ratio=1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"skywalking\": {\n                                \"sample_ratio\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: test segments report\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/skywalking run \\w+/\n--- grep_error_log_out\nskywalking run start\nskywalking run finish\nskywalking run prepareForReport\n--- wait: 1\n\n\n\n=== TEST 9: enable at both global and route levels\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: run once\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/skywalking run \\w+/\n--- grep_error_log_out\nskywalking run start\nskywalking run finish\nskywalking run prepareForReport\n\n\n\n=== TEST 11: enable at global but disable at route levels\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                            \"_meta\": {\n                                \"disable\": true\n                            }\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: run once\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- grep_error_log eval\nqr/skywalking run \\w+/\n--- grep_error_log_out\nskywalking run start\nskywalking run finish\nskywalking run prepareForReport\n\n\n\n=== TEST 13: delete global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1', ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: trace request rejected by auth\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                        \"key-auth\": {\n                            \"key\": \"auth-one\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"key-auth\": {},\n                        \"example-plugin\": {\"i\": 1},\n                        \"skywalking\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit\n--- request\nGET /opentracing\n--- error_code: 401\n--- grep_error_log eval\nqr/(skywalking run \\w+|plugin body_filter phase)/\n--- grep_error_log_out\nskywalking run start\nplugin body_filter phase\nplugin body_filter phase\nskywalking run finish\nskywalking run prepareForReport\n"
  },
  {
    "path": "t/plugin/skywalking2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_yaml_config = <<_EOC_;\nplugins:\n    - skywalking\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    my $extra_init_by_lua = <<_EOC_;\n    -- reduce default report interval\n    local client = require(\"skywalking.client\")\n    client.backendTimerDelay = 0.5\n    local initialized = false\n    client.startBackendTimer = function(self, backend_http_uri)\n        initialized = true\n        self.stopped = false\n        local metadata_buffer = ngx.shared.tracing_buffer\n\n        -- The codes of timer setup is following the OpenResty timer doc\n        local new_timer = ngx.timer.at\n        local check\n\n        local log = ngx.log\n        local ERR = ngx.ERR\n\n        check = function(premature)\n            if not premature and not self.stopped then\n                log(ngx.INFO, \"running timer\")\n                local instancePropertiesSubmitted = metadata_buffer:get('instancePropertiesSubmitted')\n                if (instancePropertiesSubmitted == nil or instancePropertiesSubmitted == false) then\n                    self:reportServiceInstance(metadata_buffer, backend_http_uri)\n                else\n                    self:ping(metadata_buffer, backend_http_uri)\n                end\n\n                self:reportTraces(metadata_buffer, backend_http_uri)\n\n                -- do the health check\n                local ok, err = new_timer(self.backendTimerDelay, check)\n                if not ok then\n                    log(ERR, \"failed to create timer: \", err)\n                    return\n                end\n            end\n        end\n\n        if 0 == ngx.worker.id() then\n            -- patch: skywalking2\n            -- Ensure that it is executed only once\n            ngx.log(ngx.INFO, \"start skywalking backend timer\")\n            local ok, err = new_timer(self.backendTimerDelay, check)\n            if not ok then\n                log(ERR, \"failed to create timer: \", err)\n                return\n            end\n        end\n    end\n\n\n    local sw_tracer = require(\"skywalking.tracer\")\n    local inject = function(mod, name)\n        local old_f = mod[name]\n        mod[name] = function (...)\n            ngx.log(ngx.WARN, \"skywalking run \", name)\n            return old_f(...)\n        end\n    end\n\n    inject(sw_tracer, \"start\")\n    inject(sw_tracer, \"finish\")\n    inject(sw_tracer, \"prepareForReport\")\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n\n    $block;\n});\n\nworkers(4);\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"debug\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"skywalking\": {\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: trigger skywalking\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/opentracing\"\n            local ports_count = {}\n            for i = 1, 12 do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n                if not res then\n                    ngx.say(\"failed to request: \", err)\n                    ngx.exit(500)\n                end\n                if res.status ~= 200 then\n                    ngx.say(\"failed to request: \", res.status)\n                    ngx.exit(500)\n                end\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- grep_error_log eval\nqr/start skywalking backend timer/\n--- grep_error_log_out\nstart skywalking backend timer\n--- wait: 1\n"
  },
  {
    "path": "t/plugin/sls-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.sls-logger\")\n            local ok, err = plugin.check_schema({host = \"cn-zhangjiakou-intranet.log.aliyuncs.com\", port = 10009, project = \"your-project\", logstore = \"your-logstore\"\n            , access_key_id = \"your_access_key\", access_key_secret = \"your_access_secret\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: missing access_key_secret\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.sls-logger\")\n            local ok, err = plugin.check_schema({host = \"cn-zhangjiakou-intranet.log.aliyuncs.com\", port = 10009, project = \"your-project\", logstore = \"your-logstore\"\n            , access_key_id = \"your_access_key\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"access_key_secret\" is required\ndone\n\n\n\n=== TEST 3: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.sls-logger\")\n            local ok, err = plugin.check_schema({host = \"cn-zhangjiakou-intranet.log.aliyuncs.com\", port = 10009, project = \"your_project\", logstore = \"your_logstore\"\n            , access_key_id = \"your_access_key\", access_key_secret = \"your_access_secret\", timeout = \"10\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"timeout\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"sls-logger\": {\n                                \"host\": \"100.100.99.135\",\n                                \"port\": 10009,\n                                \"project\": \"your_project\",\n                                \"logstore\": \"your_logstore\",\n                                \"access_key_id\": \"your_access_key_id\",\n                                \"access_key_secret\": \"your_access_key_secret\",\n                                \"timeout\": 30000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: access\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 1\n\n\n\n=== TEST 6: test combine log\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.sls-logger\")\n            local entities = {}\n            table.insert(entities, {data = \"1\"})\n            table.insert(entities, {data = \"2\"})\n            table.insert(entities, {data = \"3\"})\n            local data =  plugin.combine_syslog(entities)\n            ngx.say(data)\n        }\n    }\n--- response_body\n123\n\n\n\n=== TEST 7: sls log get milliseconds\n--- config\n    location /t {\n        content_by_lua_block {\n            local function get_syslog_timestamp_millisecond(log_entry)\n                local first_idx = string.find(log_entry, \" \") + 1\n                local last_idx2 = string.find(log_entry, \" \", first_idx)\n                local rfc3339_date = string.sub(log_entry, first_idx, last_idx2)\n                local rfc3339_len = #rfc3339_date\n                local rfc3339_millisecond = string.sub(rfc3339_date, rfc3339_len - 4, rfc3339_len - 2)\n                return tonumber(rfc3339_millisecond)\n            end\n\n            math.randomseed(os.time())\n            local rfc5424 = require(\"apisix.utils.rfc5424\")\n            local m = 0\n            -- because the millisecond value obtained by `ngx.now` may be `0`\n            -- it is executed multiple times to ensure the accuracy of the test\n            for i = 1, 5 do\n                ngx.sleep(string.format(\"%0.3f\", math.random()))\n                local structured_data = {\n                    {name = \"project\", value = \"apisix.apache.org\"},\n                    {name = \"logstore\", value = \"apisix.apache.org\"},\n                    {name = \"access-key-id\", value = \"apisix.sls.logger\"},\n                    {name = \"access-key-secret\", value = \"BD274822-96AA-4DA6-90EC-15940FB24444\"}\n                }\n                local log_entry = rfc5424.encode(\"SYSLOG\", \"INFO\", \"localhost\", \"apisix\",\n                                                 123456, \"hello world\", structured_data)\n                m = get_syslog_timestamp_millisecond(log_entry) + m\n            end\n\n            if m > 0 then\n                ngx.say(\"passed\")\n            end\n        }\n    }\n--- response_body\npassed\n--- timeout: 5\n\n\n\n=== TEST 8: add log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/sls-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: access\n--- extra_init_by_lua\n    local json = require(\"toolkit.json\")\n    local rfc5424 = require(\"apisix.utils.rfc5424\")\n    local old_f = rfc5424.encode\n    rfc5424.encode = function(facility, severity, hostname, appname, pid, msg, structured_data)\n        local r = json.decode(msg)\n        assert(r.client_ip == \"127.0.0.1\", r.client_ip)\n        assert(r.host == \"localhost\", r.host)\n        return old_f(facility, severity, hostname, appname, pid, msg, structured_data)\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 10: delete exist routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- delete exist consumers\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: data encryption for access_key_secret\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"sls-logger\": {\n                                \"host\": \"100.100.99.135\",\n                                \"port\": 10009,\n                                \"project\": \"your_project\",\n                                \"logstore\": \"your_logstore\",\n                                \"access_key_id\": \"your_access_key_id\",\n                                \"access_key_secret\": \"your_access_key_secret\",\n                                \"timeout\": 30000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"sls-logger\"].access_key_secret)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"sls-logger\"].access_key_secret)\n        }\n    }\n--- response_body\nyour_access_key_secret\n1T6nR0fz4yhz/zTuRTvt7Xu3c9ASelDXG2//e/A5OiA=\n\n\n\n=== TEST 12: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"sls-logger\": {\n                                \"host\": \"100.100.99.135\",\n                                \"port\": 10009,\n                                \"project\": \"your_project\",\n                                \"logstore\": \"your_logstore\",\n                                \"access_key_id\": \"your_access_key_id\",\n                                \"access_key_secret\": \"your_access_key_secret\",\n                                \"log_format\": {\n                                    \"vip\": \"$remote_addr\"\n                                },\n                                \"timeout\": 30000\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: access\n--- extra_init_by_lua\n    local json = require(\"toolkit.json\")\n    local rfc5424 = require(\"apisix.utils.rfc5424\")\n    local old_f = rfc5424.encode\n    rfc5424.encode = function(facility, severity, hostname, appname, pid, msg, structured_data)\n        local r = json.decode(msg)\n        assert(r.vip == \"127.0.0.1\", r.vip)\n        return old_f(facility, severity, hostname, appname, pid, msg, structured_data)\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 14: add plugin with 'include_req_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/sls-logger', ngx.HTTP_DELETE)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"sls-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 10009,\n                                \"project\": \"your_project\",\n                                \"logstore\": \"your_logstore\",\n                                \"access_key_id\": \"your_access_key_id\",\n                                \"access_key_secret\": \"your_access_key_secret\",\n                                \"timeout\": 30000,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- error_log\n\"body\":\"{\\\"sample_payload\\\":\\\"hello\\\"}\n\n\n\n=== TEST 15: add plugin with 'include_resp_body' setting, collect response log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/sls-logger', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"sls-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 10009,\n                                \"project\": \"your_project\",\n                                \"logstore\": \"your_logstore\",\n                                \"access_key_id\": \"your_access_key_id\",\n                                \"access_key_secret\": \"your_access_key_secret\",\n                                \"timeout\": 30000,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- error_log\n\"body\":\"hello world\\n\"\n\n\n\n=== TEST 16: set incorrect plugin metadata, should have error log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local key = \"/plugin_metadata/sls-logger\"\n            local val = {\n                id = \"sls-logger\",\n                log_format = \"bad plugin metadata\"\n            }\n            local _, err = core.etcd.set(key, val)\n            ngx.sleep(1)\n            if err then\n                ngx.say(err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nsync_data(): failed to check item data of [/apisix/plugin_metadata]\nfailed to check the configuration of plugin sls-logger\n\n\n\n=== TEST 17: set correct plugin metadata, should no error log\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local key = \"/plugin_metadata/sls-logger\"\n            local val = {\n                id = \"sls-logger\",\n                log_format = {\n                    host = \"$host\",\n                    client_ip = \"$remote_addr\"\n                }\n            }\n            local _, err = core.etcd.set(key, val)\n            if err then\n                ngx.say(err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\n"
  },
  {
    "path": "t/plugin/splunk-hec-logging.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: configuration verification\n--- config\n    location /t {\n        content_by_lua_block {\n            local ok, err\n            local configs = {\n                -- full configuration\n                {\n                    endpoint = {\n                        uri = \"http://127.0.0.1:18088/services/collector\",\n                        token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                        channel = \"FE0ECFAD-13D5-401B-847D-77833BD77131\",\n                        timeout = 60\n                    },\n                    max_retry_count = 0,\n                    retry_delay = 1,\n                    buffer_duration = 60,\n                    inactive_timeout = 2,\n                    batch_max_size = 10,\n                },\n                -- minimize configuration\n                {\n                    endpoint = {\n                        uri = \"http://127.0.0.1:18088/services/collector\",\n                        token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                    }\n                },\n                -- property \"uri\" is required\n                {\n                    endpoint = {\n                        token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                    }\n                },\n                -- property \"token\" is required\n                {\n                    endpoint = {\n                        uri = \"http://127.0.0.1:18088/services/collector\",\n                    }\n                },\n                -- property \"uri\" validation failed\n                {\n                    endpoint = {\n                        uri = \"127.0.0.1:18088/services/collector\",\n                        token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                    }\n                }\n            }\n\n            local plugin = require(\"apisix.plugins.splunk-hec-logging\")\n            for i = 1, #configs do\n                ok, err = plugin.check_schema(configs[i])\n                if err then\n                    ngx.say(err)\n                else\n                    ngx.say(\"passed\")\n                end\n            end\n        }\n    }\n--- response_body_like\npassed\npassed\nproperty \"endpoint\" validation failed: property \"uri\" is required\nproperty \"endpoint\" validation failed: property \"token\" is required\nproperty \"endpoint\" validation failed: property \"uri\" validation failed.*\n\n\n\n=== TEST 2: set route (failed auth)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"splunk-hec-logging\"] = {\n                        endpoint = {\n                            uri = \"http://127.0.0.1:18088/services/collector\",\n                            token = \"BD274822-96AA-4DA6-90EC-18940FB24444\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: test route (failed auth)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n--- error_log\nBatch Processor[splunk-hec-logging] failed to process entries: failed to send splunk, Invalid authorization\nBatch Processor[splunk-hec-logging] exceeded the max_retry_count\n\n\n\n=== TEST 4: set route (success write)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"splunk-hec-logging\"] = {\n                        endpoint = {\n                            uri = \"http://127.0.0.1:18088/services/collector\",\n                            token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: test route (success write)\n--- request\nGET /hello\n--- wait: 2\n--- response_body\nhello world\n\n\n\n=== TEST 6: bad custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/splunk-hec-logging',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": \"'$host' '$time_iso8601'\"\n                 }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"log_format\\\" validation failed: wrong type: expected object, got string\"}\n\n\n\n=== TEST 7: set route to test custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/splunk-hec-logging',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"client_ip\": \"$remote_addr\",\n                            \"message_1\":\"test custom log format in plugin\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1982\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"splunk-hec-logging\"] = {\n                        endpoint = {\n                            uri = \"http://127.0.0.1:18088/services/collector\",\n                            token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n                        },\n                        batch_max_size = 3,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- wait: 5\n\n\n\n=== TEST 8: check splunk log\n--- exec\ntail -n 1 ci/pod/vector/splunk.log\n--- response_body eval\nqr/.*test custom log format in plugin.*/\n\n\n\n=== TEST 9: set route to test custom log format in route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/splunk-hec-logging',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"vip\": \"$remote_addr\",\n                            \"message_2\":\"logger format in plugin\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1982\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"splunk-hec-logging\"] = {\n                        endpoint = {\n                            uri = \"http://127.0.0.1:18088/services/collector\",\n                            token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n                        },\n                        batch_max_size = 3,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- wait: 5\n\n\n\n=== TEST 10: check splunk log\n--- exec\ntail -n 1 ci/pod/vector/splunk.log\n--- response_body eval\nqr/.*logger format in plugin.*/\n\n\n\n=== TEST 11: set route test batched data\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/splunk-hec-logging',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"vip\": \"$remote_addr\",\n                            \"message_3\":\"test batched data\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1982\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"splunk-hec-logging\"] = {\n                        endpoint = {\n                            uri = \"http://127.0.0.1:18088/services/collector\",\n                            token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n                        },\n                        batch_max_size = 3,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- wait: 5\n\n\n\n=== TEST 12: check splunk log\n--- exec\ntail -n 1 ci/pod/vector/splunk.log\n--- response_body eval\nqr/.*test batched data.*/\n\n\n\n=== TEST 13: set route with keepalive_timeout (success write)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                },\n                plugins = {\n                    [\"splunk-hec-logging\"] = {\n                        endpoint = {\n                            uri = \"http://127.0.0.1:18088/services/collector\",\n                            token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\",\n                            keepalive_timeout = 5000\n                        },\n                        batch_max_size = 1,\n                        inactive_timeout = 1\n                    }\n                }\n            })\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/plugin/splunk-hec-logging2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: should drop entries when max_pending_entries is exceeded\n--- extra_yaml_config\nplugins:\n  - splunk-hec-logging\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local httpc = http.new()\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"splunk-hec-logging\"] = {\n                            endpoint = {\n                                uri = \"http://127.0.0.1:1234/services/collector\",\n                                token = \"BD274822-96AA-4DA6-90EC-18940FB2414C\"\n                            },\n                            batch_max_size = 1,\n                            timeout = 1,\n                            max_retry_count = 10\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1980\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    },\n                    uri = \"/hello\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n\n        -- Set plugin metadata\n        local metadata = {\n            log_format = {\n                host = \"$host\",\n                [\"@timestamp\"] = \"$time_iso8601\",\n                client_ip = \"$remote_addr\"\n            },\n            max_pending_entries = 1\n        }\n\n        local code, body = t('/apisix/admin/plugin_metadata/splunk-hec-logging', ngx.HTTP_PUT, metadata)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        -- Create route\n        local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[1].input)\n        if code >= 300 then\n            ngx.status = code\n            ngx.say(body)\n            return\n        end\n\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        httpc:request_uri(uri, {\n            method = \"GET\",\n            keepalive_timeout = 1,\n            keepalive_pool = 1,\n        })\n        ngx.sleep(2)\n    }\n}\n--- error_log\nmax pending entries limit exceeded. discarding entry\n--- timeout: 5\n"
  },
  {
    "path": "t/plugin/syslog.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.syslog\")\n            local ok, err = plugin.check_schema({\n                 host = \"127.0.0.1\",\n                 port = 5140,\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: missing port\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.syslog\")\n            local ok, err = plugin.check_schema({host = \"127.0.0.1\"})\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"port\" is required\ndone\n\n\n\n=== TEST 3: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.syslog\")\n            local ok, err = plugin.check_schema({\n                 host = \"127.0.0.1\",\n                 port = \"5140\",\n            })\n            if not ok then\n                ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"port\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5140\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: access\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.2\n\n\n\n=== TEST 6: flush manually\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.syslog\")\n            local logger_socket = require(\"resty.logger.socket\")\n            local logger, err = logger_socket:new({\n                    host = \"127.0.0.1\",\n                    port = 5140,\n                    flush_limit = 100,\n            })\n\n            local bytes, err = logger:log(\"abc\")\n            if err then\n                ngx.log(ngx.ERR, err)\n            end\n\n            local bytes, err = logger:log(\"efg\")\n            if err then\n                ngx.log(ngx.ERR, err)\n            end\n\n            local ok, err = plugin.flush_syslog(logger)\n            if not ok then\n                ngx.say(\"failed to flush syslog: \", err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 7: small flush_limit, instant flush\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                -- before 2.13.0, timeout is incorrectly treated as inactive_timeout\n                [[{\n                    \"plugins\": {\n                        \"syslog\": {\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5140,\n                                \"flush_limit\" : 1,\n                                \"inactive_timeout\": 1\n                            }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            -- wait etcd sync\n            ngx.sleep(0.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n            if not res then\n                ngx.say(\"failed request: \", err)\n                return\n            end\n\n            if res.status >= 300 then\n                ngx.status = res.status\n            end\n            ngx.print(res.body)\n\n            -- wait flush log\n            ngx.sleep(2.5)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\nhello world\n--- error_log\ntry to lock with key route#1\nunlock with key route#1\n--- timeout: 5\n\n\n\n=== TEST 8: check log\n--- exec\ntail -n 1 ci/pod/vector/syslog-tcp.log\n--- response_body eval\nqr/.*apisix_latency.*/\n\n\n\n=== TEST 9: check plugin configuration updating\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 5044,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body2 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, body3 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 5045,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body4 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body1)\n            ngx.print(body2)\n            ngx.print(body3)\n            ngx.print(body4)\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body\npassedopentracing\npassedopentracing\n--- grep_error_log eval\nqr/sending a batch logs to 127.0.0.1:(\\d+)/\n--- grep_error_log_out\nsending a batch logs to 127.0.0.1:5044\nsending a batch logs to 127.0.0.1:5045\n\n\n\n=== TEST 10: add log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/syslog',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"log_format\": {\n                        \"host\": \"$host\",\n                        \"client_ip\": \"$remote_addr\",\n                        \"upstream\": \"$upstream_addr\"\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: Add route and Enable Syslog Plugin, batch_max_size=1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"batch_max_size\": 1,\n                                \"disable\": false,\n                                \"flush_limit\": 1,\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5140\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route and report sys logger\n--- extra_init_by_lua\n    local syslog = require(\"apisix.plugins.syslog.init\")\n    local json = require(\"apisix.core.json\")\n    local log = require(\"apisix.core.log\")\n    local old_f = syslog.push_entry\n    syslog.push_entry = function(conf, ctx, entry)\n        log.info(\"syslog-log-format => \" ..  json.encode(entry))\n        return old_f(conf, ctx, entry)\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- no_error_log\n[error]\n--- error_log eval\nqr/syslog-log-format.*\\{.*\"upstream\":\"127.0.0.1:\\d+\"/\n\n\n\n=== TEST 13: check log\n--- exec\ntail -n 1 ci/pod/vector/syslog-tcp.log\n--- response_body eval\nqr/.*\\\"host\\\":\\\"localhost\\\".*/\n\n\n\n=== TEST 14: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"batch_max_size\": 1,\n                                \"flush_limit\": 1,\n                                \"log_format\": {\n                                    \"vip\": \"$remote_addr\"\n                                },\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5140\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: access\n--- extra_init_by_lua\n    local syslog = require(\"apisix.plugins.syslog.init\")\n    local json = require(\"apisix.core.json\")\n    local log = require(\"apisix.core.log\")\n    local old_f = syslog.push_entry\n    syslog.push_entry = function(conf, ctx, entry)\n        assert(entry.vip == \"127.0.0.1\")\n        log.info(\"push_entry is called with data: \", json.encode(entry))\n        return old_f(conf, ctx, entry)\n    end\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 0.5\n--- no_error_log\n[error]\n--- error_log\npush_entry is called with data\n\n\n\n=== TEST 16: check log\n--- exec\ntail -n 1 ci/pod/vector/syslog-tcp.log\n--- response_body eval\nqr/.*vip.*/\n\n\n\n=== TEST 17: test udp mode\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"batch_max_size\": 1,\n                                \"disable\": false,\n                                \"flush_limit\": 1,\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5150,\n                                \"sock_type\": \"udp\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit\n--- request\nGET /hello\n\n\n\n=== TEST 19: check log\n--- exec\ntail -n 1 ci/pod/vector/syslog-udp.log\n--- response_body eval\nqr/.*upstream.*/\n\n\n\n=== TEST 20: add plugin with 'include_req_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/syslog', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"batch_max_size\": 1,\n                                \"flush_limit\": 1,\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5140,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"body\":\"{\\\"sample_payload\\\":\\\"hello\\\"}\"\n\n\n\n=== TEST 21: add plugin with 'include_resp_body' setting, collect response log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/syslog', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"batch_max_size\": 1,\n                                \"flush_limit\": 1,\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5140,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"body\":\"hello world\\n\"\n"
  },
  {
    "path": "t/plugin/tcp-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tcp-logger\")\n            local ok, err = plugin.check_schema({host = \"127.0.0.1\", port = 3000})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: missing host\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tcp-logger\")\n            local ok, err = plugin.check_schema({port = 3000})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"host\" is required\ndone\n\n\n\n=== TEST 3: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tcp-logger\")\n            local ok, err = plugin.check_schema({host= \"127.0.0.1\", port = 2000, timeout = \"10\",\n            tls = false, tls_options = \"tls options\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"timeout\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 3000,\n                                \"tls\": false\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: access\n--- request\nGET /hello\n--- response_body\nhello world\n--- wait: 1\n\n\n\n=== TEST 6: error log\n--- log_level: error\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"312.0.0.1\",\n                                \"port\": 2000,\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 2,\n                                \"retry_delay\": 0\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n        }\n    }\n--- request\nGET /t\n--- error_log\nfailed to connect to TCP server: host[312.0.0.1] port[2000]\n[error]\n--- wait: 3\n\n\n\n=== TEST 7: check plugin configuration updating\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 3000,\n                                \"tls\": false,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body2 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, body3 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 43000,\n                                \"tls\": false,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body4 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body1)\n            ngx.print(body2)\n            ngx.print(body3)\n            ngx.print(body4)\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body\npassedopentracing\npassedopentracing\n--- grep_error_log eval\nqr/sending a batch logs to 127.0.0.1:(\\d+)/\n--- grep_error_log_out\nsending a batch logs to 127.0.0.1:3000\nsending a batch logs to 127.0.0.1:43000\n\n\n\n=== TEST 8: bad custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/tcp-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": \"'$host' '$time_iso8601'\"\n                 }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"log_format\\\" validation failed: wrong type: expected object, got string\"}\n\n\n\n=== TEST 9: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 3000,\n                                \"tls\": false,\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_metadata/tcp-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"case name\": \"plugin_metadata\",\n                            \"host\": \"$host\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"client_ip\": \"$remote_addr\"\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, _, _ = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: log format in plugin_metadata\n--- exec\ntail -n 1 ci/pod/vector/tcp.log\n--- response_body eval\nqr/.*plugin_metadata.*/\n\n\n\n=== TEST 11: remove tcp logger metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/plugin_metadata/tcp-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {}\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 3000,\n                                \"tls\": false,\n                                \"log_format\": {\n                                    \"case name\": \"logger format in plugin\",\n                                    \"vip\": \"$remote_addr\"\n                                },\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, _, body2 = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body\npassed\n\n\n\n=== TEST 13: check tcp log\n--- exec\ntail -n 1 ci/pod/vector/tcp.log\n--- response_body eval\nqr/.*logger format in plugin.*/\n\n\n\n=== TEST 14: true tcp log with tls\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 43000,\n                                \"tls\": true,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body2 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body2)\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body\nopentracing\n\n\n\n=== TEST 15: check tls log\n--- exec\ntail -n 1 ci/pod/vector/tls-datas.log\n--- response_body eval\nqr/.*route_id.*1.*/\n\n\n\n=== TEST 16: add plugin with 'include_req_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/tcp-logger', ngx.HTTP_DELETE)\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 43000,\n                                \"tls\": true,\n                                \"batch_max_size\": 1,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body = t(\"/opentracing\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"body\":\"{\\\"sample_payload\\\":\\\"hello\\\"}\"\n\n\n\n=== TEST 17: add plugin with 'include_resp_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/tcp-logger', ngx.HTTP_DELETE)\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tcp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 43000,\n                                \"tls\": true,\n                                \"batch_max_size\": 1,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body = t(\"/opentracing\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"body\":\"opentracing\\n\"\n"
  },
  {
    "path": "t/plugin/tencent-cloud-cls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    server {\n        listen 10420;\n        location /structuredlog {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local data = ngx.req.get_body_data()\n                local headers = ngx.req.get_headers()\n                ngx.log(ngx.WARN, \"tencent-cloud-cls body: \", data)\n                for k, v in pairs(headers) do\n                    ngx.log(ngx.WARN, \"tencent-cloud-cls headers: \" .. k .. \":\" .. v)\n                end\n                ngx.say(\"ok\")\n            }\n        }\n    }\n    server {\n        listen 10421;\n        location /structuredlog {\n            content_by_lua_block {\n                ngx.exit(500)\n            }\n        }\n    }\n    server {\n        listen 10422 ssl;\n        ssl_certificate             cert/apisix.crt;\n        ssl_certificate_key         cert/apisix.key;\n        location /structuredlog {\n            content_by_lua_block {\n                ngx.req.read_body()\n                local data = ngx.req.get_body_data()\n                local headers = ngx.req.get_headers()\n                ngx.log(ngx.WARN, \"tencent-cloud-cls https body: \", data)\n                for k, v in pairs(headers) do\n                    ngx.log(ngx.WARN, \"tencent-cloud-cls https headers: \" .. k .. \":\" .. v)\n                end\n                ngx.say(\"ok\")\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tencent-cloud-cls\")\n            local ok, err = plugin.check_schema({\n                cls_host = \"ap-guangzhou.cls.tencentyun.com\",\n                cls_topic = \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                secret_id = \"secret_id\",\n                secret_key = \"secret_key\",\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\ndone\n\n\n\n=== TEST 2: cls config missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tencent-cloud-cls\")\n            local ok, err = plugin.check_schema({\n                cls_host = \"ap-guangzhou.cls.tencentyun.com\",\n                cls_topic = \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                secret_id = \"secret_id\",\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"secret_key\" is required\ndone\n\n\n\n=== TEST 3: add plugin for incorrect server\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"http\",\n                                \"cls_host\": \"127.0.0.1:10421\",\n                                \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                \"secret_id\": \"secret_id\",\n                                \"secret_key\": \"secret_key\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: incorrect server\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nBatch Processor[tencent-cloud-cls] failed to process entries [1/1]: got wrong status: 500\n--- wait: 0.5\n\n\n\n=== TEST 5: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"http\",\n                                \"cls_host\": \"127.0.0.1:10420\",\n                                \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                \"secret_id\": \"secret_id\",\n                                \"secret_key\": \"secret_key\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: access local server\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nBatch Processor[tencent-cloud-cls] successfully processed the entries\n--- wait: 0.5\n\n\n\n=== TEST 7: verify request\n--- extra_init_by_lua\n    local cls = require(\"apisix.plugins.tencent-cloud-cls.cls-sdk\")\n    cls.send_to_cls = function(self, logs)\n        if (#logs ~= 1) then\n            ngx.log(ngx.ERR, \"unexpected logs length: \", #logs)\n            return\n        end\n        return true\n    end\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nBatch Processor[tencent-cloud-cls] successfully processed the entries\n--- wait: 0.5\n\n\n\n=== TEST 8: verify cls api request\n--- extra_init_by_lua\n    local cls = require(\"apisix.plugins.tencent-cloud-cls.cls-sdk\")\n    cls.send_cls_request = function(self, pb_obj)\n        if (#pb_obj.logGroupList ~= 1) then\n            ngx.log(ngx.ERR, \"unexpected logGroupList length: \", #pb_obj.logGroupList)\n            return false\n        end\n        local log_group = pb_obj.logGroupList[1]\n        if #log_group.logs ~= 1 then\n            ngx.log(ngx.ERR, \"unexpected logs length: \", #log_group.logs)\n            return false\n        end\n        local log = log_group.logs[1]\n        if #log.contents == 0 then\n            ngx.log(ngx.ERR, \"unexpected contents length: \", #log.contents)\n            return false\n        end\n        return true\n    end\n--- request\nGET /opentracing\n--- response_body\nopentracing\n\n\n\n=== TEST 9: plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tencent-cloud-cls\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/tencent-cloud-cls',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"client_ip\": \"$remote_addr\"\n                        }\n                }]]\n                )\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: log use log_format\n--- extra_init_by_lua\n    local cls = require(\"apisix.plugins.tencent-cloud-cls.cls-sdk\")\n    cls.send_cls_request = function(self, pb_obj)\n        if (#pb_obj.logGroupList ~= 1) then\n            ngx.log(ngx.ERR, \"unexpected logGroupList length: \", #pb_obj.logGroupList)\n            return false\n        end\n        local log_group = pb_obj.logGroupList[1]\n        if #log_group.logs ~= 1 then\n            ngx.log(ngx.ERR, \"unexpected logs length: \", #log_group.logs)\n            return false\n        end\n        local log = log_group.logs[1]\n        if #log.contents == 0 then\n            ngx.log(ngx.ERR, \"unexpected contents length: \", #log.contents)\n            return false\n        end\n        local has_host, has_timestamp, has_client_ip = false, false, false\n        for i, tag in ipairs(log.contents) do\n            if tag.key == \"host\" then\n                has_host = true\n            end\n            if tag.key == \"@timestamp\" then\n                has_timestamp = true\n            end\n            if tag.key == \"client_ip\" then\n                has_client_ip = true\n            end\n        end\n        if not(has_host and has_timestamp and has_client_ip) then\n            return false\n        end\n        return true\n    end\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- wait: 0.5\n\n\n\n=== TEST 11: delete exist routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- delete exist consumers\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_DELETE)\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: data encryption for secret_key\n--- yaml_config\napisix:\n    data_encryption:\n        enable_encrypt_fields: true\n        keyring:\n            - edd1c9f0985e76a2\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"tencent-cloud-cls\": {\n                            \"scheme\": \"http\",\n                            \"cls_host\": \"127.0.0.1:10421\",\n                            \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                            \"secret_id\": \"secret_id\",\n                            \"secret_key\": \"secret_key\",\n                            \"batch_max_size\": 1,\n                            \"max_retry_count\": 1,\n                            \"retry_delay\": 2,\n                            \"buffer_duration\": 2,\n                            \"inactive_timeout\": 2\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            -- get plugin conf from admin api, password is decrypted\n            local code, message, res = t('/apisix/admin/routes/1',\n                ngx.HTTP_GET\n            )\n            res = json.decode(res)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(message)\n                return\n            end\n\n            ngx.say(res.value.plugins[\"tencent-cloud-cls\"].secret_key)\n\n            -- get plugin conf from etcd, password is encrypted\n            local etcd = require(\"apisix.core.etcd\")\n            local res = assert(etcd.get('/routes/1'))\n            ngx.say(res.body.node.value.plugins[\"tencent-cloud-cls\"].secret_key)\n        }\n    }\n--- response_body\nsecret_key\noshn8tcqE8cJArmEILVNPQ==\n\n\n\n=== TEST 13: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.tencent-cloud-cls\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"http\",\n                                \"cls_host\": \"127.0.0.1:10421\",\n                                \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                \"secret_id\": \"secret_id\",\n                                \"secret_key\": \"secret_key\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"inactive_timeout\": 1,\n                                \"log_format\": {\n                                    \"host\": \"$host\",\n                                    \"@timestamp\": \"$time_iso8601\",\n                                    \"vip\": \"$remote_addr\"\n                                }\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: log use log_format\n--- extra_init_by_lua\n    local cls = require(\"apisix.plugins.tencent-cloud-cls.cls-sdk\")\n    cls.send_cls_request = function(self, pb_obj)\n        if (#pb_obj.logGroupList ~= 1) then\n            ngx.log(ngx.ERR, \"unexpected logGroupList length: \", #pb_obj.logGroupList)\n            return false\n        end\n        local log_group = pb_obj.logGroupList[1]\n        if #log_group.logs ~= 1 then\n            ngx.log(ngx.ERR, \"unexpected logs length: \", #log_group.logs)\n            return false\n        end\n        local log = log_group.logs[1]\n        if #log.contents == 0 then\n            ngx.log(ngx.ERR, \"unexpected contents length: \", #log.contents)\n            return false\n        end\n        local has_host, has_timestamp, has_vip = false, false, false\n        for i, tag in ipairs(log.contents) do\n            if tag.key == \"host\" then\n                has_host = true\n            end\n            if tag.key == \"@timestamp\" then\n                has_timestamp = true\n            end\n            if tag.key == \"vip\" then\n                has_vip = true\n            end\n        end\n        if not(has_host and has_timestamp and has_vip) then\n            return false\n        end\n        return true\n    end\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- wait: 0.5\n\n\n\n=== TEST 15: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"http\",\n                                \"cls_host\": \"127.0.0.1:10420\",\n                                \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                \"secret_id\": \"secret_id\",\n                                \"secret_key\": \"secret_key\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: test resolvt e ip failed\n--- extra_init_by_lua\n    local socket = require(\"socket\")\n    socket.dns.toip = function(address)\n        return nil, \"address can't be resolved\"\n    end\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log eval\nqr/resolve ip failed, hostname: .*, error: address can't be resolved/\n--- wait: 0.5\n\n\n\n=== TEST 17: collect log with include_req_body_expr\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/tencent-cloud-cls', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"http\",\n                                \"cls_host\": \"127.0.0.1:10420\",\n                                     \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                     \"secret_id\": \"secret_id\",\n                                     \"secret_key\": \"secret_key\",\n                                     \"batch_max_size\": 1,\n                                     \"max_retry_count\": 1,\n                                     \"retry_delay\": 2,\n                                     \"buffer_duration\": 2,\n                                     \"inactive_timeout\": 2,\n                                     \"include_req_body\": true,\n                                     \"include_req_body_expr\": [\n                                         [\"arg_bar\", \"==\", \"bar\"]\n                                     ]\n                                 }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            -- this will include resp body\n            local code, _, body = t(\"/opentracing?bar=bar\", \"POST\", \"body-data\")\n        }\n    }\n--- error_log\n\"body\":\"body-data\"\n\n\n\n=== TEST 18: collect log with include_req_body_expr mismatch\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/opentracing?foo=bar\", \"POST\", \"body-data\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n        }\n    }\n--- no_error_log\n\"body\":\"body-data\"\n\n\n\n=== TEST 19: collect log with include_resp_body_expr\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"http\",\n                                \"cls_host\": \"127.0.0.1:10420\",\n                                     \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                     \"secret_id\": \"secret_id\",\n                                     \"secret_key\": \"secret_key\",\n                                     \"batch_max_size\": 1,\n                                     \"max_retry_count\": 1,\n                                     \"retry_delay\": 2,\n                                     \"buffer_duration\": 2,\n                                     \"inactive_timeout\": 2,\n                                     \"include_resp_body\": true,\n                                     \"include_resp_body_expr\": [\n                                         [\"arg_bar\", \"==\", \"bar\"]\n                                     ]\n                                 }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n            )\n\n            -- this will include resp body\n            local code, _, body = t(\"/opentracing?bar=bar\", \"GET\")\n        }\n    }\n--- error_log\n\"body\":\"opentracing\\n\"\n\n\n\n=== TEST 20: collect log with include_resp_body_expr mismatch\n--- log_level: debug\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, _, body = t(\"/opentracing?foo=bar\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            ngx.print(body)\n\n        }\n    }\n--- no_error_log\n\"body\":\"opentracing\\n\"\n\n\n\n=== TEST 21: add plugin with https scheme\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"tencent-cloud-cls\": {\n                                \"scheme\": \"https\",\n                                \"cls_host\": \"127.0.0.1:10422\",\n                                \"cls_topic\": \"143b5d70-139b-4aec-b54e-bb97756916de\",\n                                \"secret_id\": \"secret_id\",\n                                \"secret_key\": \"secret_key\",\n                                \"batch_max_size\": 1,\n                                \"max_retry_count\": 1,\n                                \"retry_delay\": 2,\n                                \"buffer_duration\": 2,\n                                \"inactive_timeout\": 2\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: access https endpoint\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- error_log\nBatch Processor[tencent-cloud-cls] successfully processed the entries\n--- wait: 0.5\n"
  },
  {
    "path": "t/plugin/traffic-split.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: schema validation passed\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.traffic-split\")\n            local ok, err = plugin.check_schema({\n                rules = {\n                    {\n                        match = {\n                            {\n                                vars = {\n                                    {\"arg_name\", \"==\", \"jack\"},\n                                    {\"arg_age\", \"!\", \"<\", \"16\"}\n                                }\n                            },\n                             {\n                                vars = {\n                                    {\"arg_name\", \"==\", \"rose\"},\n                                    {\"arg_age\", \"!\", \">\", \"32\"}\n                                }\n                            }\n                        },\n                        weighted_upstreams = {\n                            {\n                                upstream = {\n                                    name = \"upstream_A\",\n                                    type = \"roundrobin\",\n                                    nodes = {[\"127.0.0.1:1981\"]=2},\n                                    timeout = {connect = 15, send = 15, read = 15}\n                                },\n                                weight = 2\n                            },\n                            {\n                                upstream = {\n                                    name = \"upstream_B\",\n                                    type = \"roundrobin\",\n                                    nodes = {[\"127.0.0.1:1982\"]=2},\n                                    timeout = {connect = 15, send = 15, read = 15}\n                                },\n                                weight = 2\n                            },\n                            {\n                                weight = 1\n                            }\n                        }\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: schema validation passed, and `match` configuration is missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.traffic-split\")\n            local ok, err = plugin.check_schema({\n                rules = {\n                    {\n                        weighted_upstreams = {\n                            {\n                                upstream = {\n                                    name = \"upstream_A\",\n                                    type = \"roundrobin\",\n                                    nodes = {[\"127.0.0.1:1981\"]=2},\n                                    timeout = {connect = 15, send = 15, read = 15}\n                                },\n                                weight = 2\n                            },\n                            {\n                                weight = 1\n                            }\n                        }\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 3: schema validation failed, `vars` expression operator type is wrong\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.traffic-split\")\n            local ok, err = plugin.check_schema({\n                rules = {\n                    {\n                        match = {\n                            {\n                                vars = {\n                                    {\"arg_name\", 123, \"jack\"}\n                                }\n                            }\n                        },\n                        weighted_upstreams = {\n                            {\n                                upstream = {\n                                    name = \"upstream_A\",\n                                    type = \"roundrobin\",\n                                    nodes = {[\"127.0.0.1:1981\"]=2},\n                                    timeout = {connect = 15, send = 15, read = 15}\n                                },\n                                weight = 2\n                            },\n                            {\n                                weight = 1\n                            }\n                        }\n                    }\n                }\n            })\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/failed to validate the 'vars' expression: invalid operator '123'/\n\n\n\n=== TEST 4: missing `rules` configuration, the upstream of the default `route` takes effect\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port\",\n                    \"plugins\": {\n                        \"traffic-split\": {}\n                    },\n                    \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: the upstream of the default `route` takes effect\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 6 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1980, 1980, 1980, 1980, 1980\n\n\n\n=== TEST 6: when `weighted_upstreams` is empty, the upstream of `route` is used by default\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port\",\n                    \"plugins\": {\n                        \"traffic-split\": {\n                             \"rules\": [\n                                {\n                                    \"weighted_upstreams\": [{}]\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: the upstream of the default `route` takes effect\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 6 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1980, 1980, 1980, 1980, 1980\n\n\n\n=== TEST 8: single `vars` expression and single plugin `upstream`, and the upstream traffic on `route` accounts for 1/3\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                        match = { {\n                            vars = { { \"arg_name\", \"==\", \"jack\" }, { \"arg_age\", \"!\", \"<\", \"16\" } }\n                        } },\n                        weighted_upstreams = { {\n                            upstream = {\n                            name = \"upstream_A\",\n                            type = \"roundrobin\",\n                            nodes = {\n                                [\"127.0.0.1:1981\"] = 2\n                            },\n                            timeout = {\n                                connect = 15,\n                                send = 15,\n                                read = 15\n                            }\n                            },\n                            weight = 2\n                        }, {\n                            weight = 1\n                        } }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: expression validation failed, return to the default `route` upstream port `1980`\n--- request\nGET /server_port?name=jack&age=14\n--- response_body eval\n1980\n\n\n\n=== TEST 10: the expression passes and initiated multiple requests, the upstream traffic of `route` accounts for 1/3, and the upstream traffic of plugins accounts for 2/3\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 6 do\n            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1980, 1981, 1981, 1981, 1981\n\n\n\n=== TEST 11: Multiple vars rules and multiple plugin upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"jack\" }, { \"arg_age\", \"~~\", \"^[1-9]{1,2}\"}\n                                    }\n                                },\n                                {\n                                    vars = {\n                                        {\"arg_name2\", \"in\", {\"jack\", \"rose\"} }, { \"arg_age\", \"!\", \"<\", 18}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1981\"] = 20\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    upstream = {\n                                        name = \"upstream_B\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1982\"] = 10\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: expression validation failed, return to the default `route` upstream port `1980`\n--- request\nGET /server_port?name=jack&age=0\n--- response_body eval\n1980\n\n\n\n=== TEST 13: the expression passes and initiated multiple requests, the upstream traffic of `route` accounts for 1/5, and the upstream traffic of plugins accounts for 4/5\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1981, 1981, 1982, 1982\n\n\n\n=== TEST 14: Multiple vars rules and multiple plugin upstream, do not split traffic to the upstream of `route`\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"jack\" }, { \"arg_age\", \"~~\", \"^[1-9]{1,2}\"}\n                                    }\n                                },\n                                {\n                                    vars = {\n                                        {\"arg_name2\", \"in\", {\"jack\", \"rose\"} }, { \"arg_age\", \"!\", \"<\", 18}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1981\"] = 20\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    upstream = {\n                                        name = \"upstream_B\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1982\"] = 10\n                                        }\n                                    },\n                                    weight = 2\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: the expression passes and initiated multiple requests, do not split traffic to the upstream of `route`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 6 do\n            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1981, 1981, 1981, 1982, 1982, 1982\n\n\n\n=== TEST 16: support multiple ip configuration of `nodes`, and missing upstream configuration on `route`\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"jack\" }, { \"arg_age\", \"~~\", \"^[1-9]{1,2}\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1980\"] = 1,\n                                            [\"127.0.0.1:1981\"] = 2,\n                                            [\"127.0.0.1:1982\"] = 2\n                                        },\n                                        timeout = {\n                                            connect = 15,\n                                            send = 15,\n                                            read = 15\n                                        }\n                                    },\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: the expression passes and initiated multiple requests, roundrobin the ip of nodes\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?name=jack&age=22', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1981, 1981, 1982, 1982\n--- no_error_log\n\n\n\n=== TEST 18: host is domain name\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n              uri = \"/\",\n              plugins = {\n                [\"traffic-split\"] = {\n                  rules = { {\n                    weighted_upstreams = { {\n                      upstream = {\n                        name = \"upstream_A\",\n                        type = \"roundrobin\",\n                        pass_host = \"rewrite\",\n                        upstream_host = \"httpbin.local\",\n                        nodes = {\n                          [\"httpbin.local:8280\"] = 1\n                        }\n                      },\n                      weight = 100000\n                    }, {\n                      weight = 1\n                    } }\n                  } }\n                }\n              },\n              upstream = {\n                type = \"roundrobin\",\n                nodes = {\n                  [\"127.0.0.1:1980\"] = 1\n                }\n              }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: domain name resolved successfully\n--- pipelined_requests eval\n[\"GET /\", \"GET /\"]\n--- error_code eval\n[200, 200]\n--- error_log_like eval\nqr/(dns resolver domain: httpbin.local to 127.0.0.\\d+){2}/\n\n\n\n=== TEST 20: mock Grayscale Release\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {{\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {[\"127.0.0.1:1981\"] = 1}\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    weight = 1\n                                }\n                            }\n                        }}\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {[\"127.0.0.1:1980\"] = 1}\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 21: 2/3 request traffic hits the upstream of the plugin, 1/3 request traffic hits the upstream of `route`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 6 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- request\nGET /t\n--- response_body\n1980, 1980, 1981, 1981, 1981, 1981\n"
  },
  {
    "path": "t/plugin/traffic-split2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: vars rule with ! (set)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {{\n                            match = {\n                                {vars = {\n                                    {\"!AND\",\n                                        {\"arg_name\", \"==\", \"jack\"},\n                                        {\"arg_age\", \"!\", \"<\", \"18\"},\n                                    }\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {[\"127.0.0.1:1981\"] = 1}\n                                    },\n                                    weight = 1,\n                                }\n                            }\n                        }}\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {[\"127.0.0.1:1980\"] = 1}\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: vars rule with ! (hit)\n--- request\nGET /server_port?name=jack&age=17\n--- response_body chomp\n1981\n\n\n\n=== TEST 3: vars rule with ! (miss)\n--- request\nGET /server_port?name=jack&age=18\n--- response_body chomp\n1980\n\n\n\n=== TEST 4: the upstream node is IP and pass_host is `pass`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local data = {\n                uri = \"/uri\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {{\n                            match = { {\n                              vars = { { \"arg_name\", \"==\", \"jack\" } }\n                            } },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        type = \"roundrobin\",\n                                        pass_host = \"pass\",\n                                        nodes = {[\"127.0.0.1:1981\"] = 1}\n                                    }\n                                }\n                            }\n                        }}\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {[\"127.0.0.1:1980\"] = 1}\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: upstream_host is `127.0.0.1`\n--- request\nGET /uri?name=jack\n--- more_headers\nhost: 127.0.0.1\n--- response_body\nuri: /uri\nhost: 127.0.0.1\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 6: the upstream node is IP and pass_host is `rewrite`\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n              uri = \"/uri\",\n              plugins = {\n                [\"traffic-split\"] = {\n                  rules = { {\n                    match = { {\n                      vars = { { \"arg_name\", \"==\", \"jack\" } }\n                    } },\n                    weighted_upstreams = { {\n                      upstream = {\n                        type = \"roundrobin\",\n                        pass_host = \"rewrite\",\n                        upstream_host = \"test.com\",\n                        nodes = {\n                          [\"127.0.0.1:1981\"] = 1\n                        }\n                      }\n                    } }\n                  } }\n                }\n              },\n              upstream = {\n                type = \"roundrobin\",\n                nodes = {\n                  [\"127.0.0.1:1980\"] = 1\n                }\n              }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: upstream_host is test.com\n--- request\nGET /uri?name=jack\n--- response_body\nuri: /uri\nhost: test.com\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 8: the upstream node is IP and pass_host is `node`\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local data = {\n                uri = \"/uri\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {{\n                            match = { {\n                              vars = { { \"arg_name\", \"==\", \"jack\" } }\n                            } },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        type = \"roundrobin\",\n                                        pass_host = \"node\",\n                                        nodes = {[\"localhost:1981\"] = 1}\n                                    }\n                                }\n                            }\n                        }}\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {[\"127.0.0.1:1980\"] = 1}\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: upstream_host is localhost\n--- request\nGET /uri?name=jack\n--- more_headers\nhost: 127.0.0.1\n--- response_body\nuri: /uri\nhost: localhost\nx-real-ip: 127.0.0.1\n\n\n\n=== TEST 10: the upstream.type is `chash` and `key` is header\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"chash_test\",\n                                        type = \"chash\",\n                                        hash_on = \"header\",\n                                        key = \"custom_header\",\n                                        nodes = {\n                                            [\"127.0.0.1:1981\"] = 1,\n                                            [\"127.0.0.1:1982\"] = 1\n                                        }\n                                    },\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit routes, hash_on custom header\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        local headers2 = {}\n        headers[\"custom_header\"] = \"hello\"\n        headers2[\"custom_header\"] = \"world\"\n        for i = 1, 8, 2 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET, \"\", nil, headers2)\n            local _, _, body2 = t('/server_port', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n            bodys[i+1] = body2\n        end\n\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body eval\nqr/1981, 1982, 1981, 1982, 1981, 1982, 1981, 1982/\n--- grep_error_log eval\nqr/hash_on: header|chash_key: \"hello\"|chash_key: \"world\"/\n--- grep_error_log_out\nhash_on: header\nchash_key: \"world\"\nhash_on: header\nchash_key: \"hello\"\nhash_on: header\nchash_key: \"world\"\nhash_on: header\nchash_key: \"hello\"\nhash_on: header\nchash_key: \"world\"\nhash_on: header\nchash_key: \"hello\"\nhash_on: header\nchash_key: \"world\"\nhash_on: header\nchash_key: \"hello\"\n\n\n\n=== TEST 12: the plugin has multiple weighted_upstreams(upstream method)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                    rules = { {\n                        match = { {\n                            vars = { { \"arg_id\", \"==\", \"1\" } }\n                        } },\n                        weighted_upstreams = { {\n                            upstream = {\n                                name = \"upstream_A\",\n                                type = \"roundrobin\",\n                                nodes = {\n                                    [\"127.0.0.1:1981\"] = 1\n                                }\n                            },\n                            weight = 1\n                        } }\n                    }, {\n                        match = { {\n                            vars = { { \"arg_id\", \"==\", \"2\" } }\n                        } },\n                        weighted_upstreams = { {\n                            upstream = {\n                                name = \"upstream_B\",\n                                type = \"roundrobin\",\n                                nodes = {\n                                    [\"127.0.0.1:1982\"] = 1\n                                }\n                            },\n                            weight = 1\n                        } }\n                    } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit each upstream separately\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 9, 3 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            local _, _, body2 = t('/server_port?id=1', ngx.HTTP_GET)\n            local _, _, body3 = t('/server_port?id=2', ngx.HTTP_GET)\n            bodys[i] = body\n            bodys[i+1] = body2\n            bodys[i+2] = body3\n        end\n\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body eval\nqr/1980, 1981, 1982, 1980, 1981, 1982, 1980, 1981, 1982/\n\n\n\n=== TEST 14: the plugin has multiple weighted_upstreams and has a default routing weight in weighted_upstreams\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = { {\n                            vars = { { \"arg_id\", \"==\", \"1\" } }\n                            } },\n                            weighted_upstreams = { {\n                            upstream = {\n                                name = \"upstream_A\",\n                                type = \"roundrobin\",\n                                nodes = {\n                                    [\"127.0.0.1:1981\"] = 1\n                                }\n                            },\n                            weight = 1\n                            }, {\n                            weight = 1\n                            } }\n                        }, {\n                            match = { {\n                            vars = { { \"arg_id\", \"==\", \"2\" } }\n                            } },\n                            weighted_upstreams = { {\n                            upstream = {\n                                name = \"upstream_B\",\n                                type = \"roundrobin\",\n                                nodes = {\n                                    [\"127.0.0.1:1982\"] = 1\n                                }\n                            },\n                            weight = 1\n                            }, {\n                            weight = 1\n                            } }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 15: every weighted_upstreams in the plugin is hit\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 8, 2 do\n            local _, _, body = t('/server_port?id=1', ngx.HTTP_GET)\n            local _, _, body2 = t('/server_port?id=2', ngx.HTTP_GET)\n            bodys[i] = body\n            bodys[i+1] = body2\n        end\n\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body eval\nqr/1980, 1980, 1980, 1980, 1981, 1981, 1982, 1982/\n\n\n\n=== TEST 16: set upstream(upstream_id: 1, upstream_id: 2) and add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1981\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream A\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/upstreams/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1982\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream B\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n            end\n\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = {\n                                    {\n                                        vars = {\n                                            {\"arg_id\", \"==\", \"1\" }\n                                        }\n                                    }\n                                },\n                                weighted_upstreams = {\n                                    {\n                                        upstream_id = 1,\n                                        weight = 1\n                                    }\n                                }\n                            },\n                            {\n                                match = {\n                                    {\n                                        vars = {\n                                            {\"arg_id\", \"==\", \"2\" }\n                                        }\n                                    }\n                                },\n                                weighted_upstreams = {\n                                    {\n                                        upstream_id = 2,\n                                        weight = 1\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: hit each upstream separately\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 9, 3 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            local _, _, body2 = t('/server_port?id=1', ngx.HTTP_GET)\n            local _, _, body3 = t('/server_port?id=2', ngx.HTTP_GET)\n            bodys[i] = body\n            bodys[i+1] = body2\n            bodys[i+2] = body3\n        end\n\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body eval\nqr/1980, 1981, 1982, 1980, 1981, 1982, 1980, 1981, 1982/\n\n\n\n=== TEST 18: multi nodes with `node` mode to pass host\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"localhost:1979\": 1000,\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"pass_host\": \"node\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local data = {\n                uri = \"/uri\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = {\n                                    {\n                                        vars = {\n                                            {\"arg_id\", \"==\", \"1\" }\n                                        }\n                                    }\n                                },\n                                weighted_upstreams = {\n                                    {\n                                        upstream_id = 1,\n                                        weight = 1\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1978\"] = 1\n                    }\n                }\n            }\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: hit route\n--- request\nGET /uri?id=1\n--- response_body eval\nqr/host: 127.0.0.1/\n--- error_log\nproxy request to 127.0.0.1:1980\n\n\n\n=== TEST 20: invalid upstream_id should report failure\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n\n            local data = {\n                uri = \"/route\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                weighted_upstreams = {\n                                    {\n                                        upstream_id = \"invalid-id\",\n                                        weight = 1\n                                    }\n                                }\n                            },\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n\n            code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PATCH,\n                json.encode(data)\n            )\n            ngx.status, body = t('/route', ngx.HTTP_GET)\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 500\n--- error_log\nfailed to find upstream by id: invalid-id\n\n\n\n=== TEST 21: use upstream with https scheme\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                uri = \"/hello\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = { {\n                                    vars = { { \"arg_scheme\", \"==\", \"https\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            type = \"roundrobin\",\n                                            pass_host = \"node\",\n                                            nodes = {\n                                                [\"127.0.0.1:1983\"] = 1,\n                                            },\n                                            scheme = \"https\"\n                                        },\n                                        weight = 1\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: hit route\n--- request\nGET /hello?scheme=https\n--- error_code: 200\n"
  },
  {
    "path": "t/plugin/traffic-split3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: mock Blue-green Release\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n              uri = \"/server_port\",\n              plugins = {\n                [\"traffic-split\"] = {\n                  rules = { {\n                    match = { {\n                      vars = { { \"http_release\", \"==\", \"blue\" } }\n                    } },\n                    weighted_upstreams = { {\n                      upstream = {\n                        name = \"upstream_A\",\n                        type = \"roundrobin\",\n                        nodes = {\n                          [\"127.0.0.1:1981\"] = 1\n                        }\n                      }\n                    } }\n                  } }\n                }\n              },\n              upstream = {\n                type = \"roundrobin\",\n                nodes = {\n                  [\"127.0.0.1:1980\"] = 1\n                }\n              }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: release is equal to `blue`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"release\"] = \"blue\"\n        for i = 1, 6 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1981, 1981, 1981, 1981, 1981, 1981\n\n\n\n=== TEST 3: release is equal to `green`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"release\"] = \"green\"\n        for i = 1, 6 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1980, 1980, 1980, 1980, 1980\n\n\n\n=== TEST 4: mock Custom Release\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"jack\"},\n                                        {\"arg_age\", \">\", \"23\"},\n                                        {\"http_appkey\", \"~~\", \"[a-z]{1,5}\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1981\"] = 20\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    upstream = {\n                                        name = \"upstream_B\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            [\"127.0.0.1:1982\"] = 10\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: `match` rule passed\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"appkey\"] = \"api-key\"\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1981, 1981, 1982, 1982\n\n\n\n=== TEST 6: `match` rule failed, `age` condition did not match\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"release\"] = \"green\"\n        for i = 1, 6 do\n            local _, _, body = t('/server_port?name=jack&age=16', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1980, 1980, 1980, 1980, 1980\n\n\n\n=== TEST 7: upstream nodes are array type and node is the domain name\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"test.com\", port = 80, weight = 0}\n                                        }\n                                    },\n                                    weight = 2\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: domain name resolved successfully\n--- request\nGET /server_port\n--- error_code: 502\n--- error_log eval\nqr/dns resolver domain: test.com to \\d+.\\d+.\\d+.\\d+/\n\n\n\n=== TEST 9: the nodes of upstream are array type, with multiple nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"jack\"},\n                                        {\"arg_age\", \">\", \"23\"},\n                                        {\"http_appkey\", \"~~\", \"[a-z]{1,5}\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1981, weight = 2},\n                                            {host = \"127.0.0.1\", port = 1982, weight = 2}\n                                        }\n                                    },\n                                    weight = 4\n                                },\n                                {\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: `match` rule passed\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"appkey\"] = \"api-key\"\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1981, 1981, 1982, 1982\n\n\n\n=== TEST 11: the upstream node is an array type and has multiple upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"jack\"},\n                                        {\"arg_age\", \">\", \"23\"},\n                                        {\"http_appkey\", \"~~\", \"[a-z]{1,5}\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1981, weight = 2}\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    upstream = {\n                                        name = \"upstream_B\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1982, weight = 2}\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: `match` rule passed\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"appkey\"] = \"api-key\"\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?name=jack&age=36', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1981, 1981, 1982, 1982\n\n\n\n=== TEST 13: multi-upstream, test with unique upstream key\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1981, weight = 2}\n                                        }\n                                    },\n                                    weight = 2\n                                },\n                                {\n                                    upstream = {\n                                        name = \"upstream_B\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1982, weight = 2}\n                                        }\n                                    },\n                                    weight = 2\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: the upstream `key` is unique\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 2 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1981, 1982\n--- grep_error_log eval\nqr/upstream_key: roundrobin#route_1_\\d/\n--- grep_error_log_out eval\nqr/(upstream_key: roundrobin#route_1_1\nupstream_key: roundrobin#route_1_2\n|upstream_key: roundrobin#route_1_2\nupstream_key: roundrobin#route_1_1\n)/\n\n\n\n=== TEST 15: has empty upstream, test the upstream key is unique\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1981, weight = 2}\n                                        }\n                                    },\n                                    weight = 1\n                                },\n                                {\n                                 weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: the upstream `key` is unique\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 2 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1981\n--- grep_error_log eval\nqr/upstream_key: roundrobin#route_1_\\d/\n--- grep_error_log_out\nupstream_key: roundrobin#route_1_1\n\n\n\n=== TEST 17: the request header contains horizontal lines(\"-\")\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"http_x-api-appkey\", \"==\", \"api-key\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1981, weight = 2},\n                                            {host = \"127.0.0.1\", port = 1982, weight = 2}\n                                        }\n                                    },\n                                    weight = 4\n                                },\n                                {\n                                 weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: `match` rule passed\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"x-api-appkey\"] = \"api-key\"\n        for i = 1, 5 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1981, 1981, 1982, 1982\n\n\n\n=== TEST 19: request args and request headers contain horizontal lines(\"-\")\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_x-api-name\", \"==\", \"jack\"},\n                                        {\"arg_x-api-age\", \">\", \"23\"},\n                                        {\"http_x-api-appkey\", \"~~\", \"[a-z]{1,5}\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream = {\n                                        name = \"upstream_A\",\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1981, weight = 2},\n                                            {host = \"127.0.0.1\", port = 1982, weight = 2}\n                                        }\n                                    },\n                                    weight = 4\n                                },\n                                {\n                                 weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: `match` rule passed\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"x-api-appkey\"] = \"hello\"\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?x-api-name=jack&x-api-age=36', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.print(table.concat(bodys, \", \"))\n    }\n}\n--- response_body chomp\n1980, 1981, 1981, 1982, 1982\n"
  },
  {
    "path": "t/plugin/traffic-split4.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log &&\n        (defined $block->error_code && $block->error_code != 502))\n    {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1981\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: set upstream(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/2',\n                 ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1982\": 1\n                    },\n                    \"type\": \"roundrobin\",\n                    \"desc\": \"new upstream\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: set route(id: 1, upstream_id: 1, upstream_id in plugin: 2), and `weighted_upstreams` does not have a structure with only `weight`\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"James\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 2\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream_id = 1\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: when `match` rule passed, use the `upstream_id` in plugin, and when it failed, use the `upstream_id` in route\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n\n        for i = 1, 5, 2 do\n            -- match rule passed\n            local _, _, body = t('/server_port?name=James', ngx.HTTP_GET)\n            bodys[i] = body\n\n             -- match rule failed\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i+1] = body\n        end\n\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1981, 1981, 1981, 1982, 1982, 1982\n\n\n\n=== TEST 5: set route(use upstream for route and upstream_id for plugin), and `weighted_upstreams` does not have a structure with only `weight`\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_name\", \"==\", \"James\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: when `match` rule passed, use the `upstream_id` in plugin, and when it failed, use the `upstream` in route\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n\n        for i = 1, 5, 2 do\n            -- match rule passed\n            local _, _, body = t('/server_port?name=James', ngx.HTTP_GET)\n            bodys[i] = body\n\n             -- match rule failed\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i+1] = body\n        end\n\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1980, 1980, 1981, 1981, 1981\n\n\n\n=== TEST 7: set route(id: 1, upstream_id: 1, upstream_id in plugin: 2), and `weighted_upstreams` has a structure with only `weight`\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"uri\", \"==\", \"/server_port\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 2,\n                                    weight = 1\n                                },\n                                {\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream_id = 1\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: all requests `match` rule passed, proxy requests to the upstream of route based on the structure with only `weight` in `weighted_upstreams`\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 6 do\n            local _, _, body = t('/server_port', ngx.HTTP_GET)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1981, 1981, 1981, 1982, 1982, 1982\n\n\n\n=== TEST 9: the upstream_id is used in the plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_x-api-name\", \"==\", \"jack\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 1,\n                                    weight = 2\n                                },\n                                {\n                                    upstream_id = 2,\n                                    weight = 1\n                                },\n                                {\n                                    weight = 2\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: `match` rule passed(upstream_id)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?x-api-name=jack', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1980, 1981, 1981, 1982\n\n\n\n=== TEST 11: only use upstream_id in the plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_x-api-name\", \"==\", \"jack\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 1,\n                                    weight = 1\n                                },\n                                {\n                                    upstream_id = 2,\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: `match` rule passed(only use upstream_id)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        for i = 1, 4 do\n            local _, _, body = t('/server_port?x-api-name=jack', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1981, 1981, 1982, 1982\n\n\n\n=== TEST 13: use upstream and upstream_id in the plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = {\n                                {\n                                    vars = {\n                                        {\"arg_x-api-name\", \"==\", \"jack\"}\n                                    }\n                                }\n                            },\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 1,\n                                    weight = 2\n                                },\n                                {\n                                    upstream = {\n                                        type = \"roundrobin\",\n                                        nodes = {\n                                            {host = \"127.0.0.1\", port = 1982, weight = 1}\n                                        }\n                                    },\n                                    weight = 1\n                                },\n                                {\n                                    weight = 2\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 14: `match` rule passed(upstream + upstream_id)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local bodys = {}\n        local headers = {}\n        headers[\"x-api-appkey\"] = \"hello\"\n        for i = 1, 5 do\n            local _, _, body = t('/server_port?x-api-name=jack', ngx.HTTP_GET, \"\", nil, headers)\n            bodys[i] = body\n        end\n        table.sort(bodys)\n        ngx.say(table.concat(bodys, \", \"))\n    }\n}\n--- response_body\n1980, 1980, 1981, 1981, 1982\n\n\n\n=== TEST 15: set route + upstream (two upstream node: one healthy + one unhealthy)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local up_data = {\n                type = \"roundrobin\",\n                nodes = {\n                    [\"127.0.0.1:1981\"] = 1,\n                    [\"127.0.0.1:1970\"] = 1\n                },\n                checks = {\n                    active = {\n                        http_path = \"/status\",\n                        host = \"foo.com\",\n                        healthy = {\n                            interval = 1,\n                            successes = 1\n                        },\n                        unhealthy = {\n                            interval = 1,\n                            http_failures = 2\n                        }\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 json.encode(up_data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 1,\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: hit routes, ensure the checker is bound to the upstream\n--- config\nlocation /t {\n    content_by_lua_block {\n        local http = require \"resty.http\"\n        local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                    .. \"/server_port\"\n\n        do\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n        end\n\n        ngx.sleep(2.5)\n\n        local ports_count = {}\n        for i = 1, 6 do\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {method = \"GET\", keepalive = false})\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ports_count[res.body] = (ports_count[res.body] or 0) + 1\n        end\n\n        local ports_arr = {}\n        for port, count in pairs(ports_count) do\n            table.insert(ports_arr, {port = port, count = count})\n        end\n\n        local function cmd(a, b)\n            return a.port > b.port\n        end\n        table.sort(ports_arr, cmd)\n\n        ngx.say(require(\"toolkit.json\").encode(ports_arr))\n        ngx.exit(200)\n    }\n}\n--- response_body\n[{\"count\":6,\"port\":\"1981\"}]\n--- grep_error_log eval\nqr/\\([^)]+\\) unhealthy .* for '.*'/\n--- grep_error_log_out\n(upstream#/apisix/upstreams/1) unhealthy TCP increment (1/2) for 'foo.com(127.0.0.1:1970)'\n(upstream#/apisix/upstreams/1) unhealthy TCP increment (2/2) for 'foo.com(127.0.0.1:1970)'\n--- timeout: 10\n\n\n\n=== TEST 17: set upstream(id: 1), by default retries count = number of nodes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1\": 1,\n                        \"127.0.0.2:1\": 1,\n                        \"127.0.0.3:1\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: set route(id: 1, upstream_id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/hello\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            weighted_upstreams = {\n                                {\n                                    upstream_id = 1,\n                                    weight = 1\n                                }\n                            }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 19: hit routes\n--- request\nGET /hello\n--- error_code: 502\n--- grep_error_log eval\nqr/\\([^)]+\\) while connecting to upstream/\n--- grep_error_log_out\n(111: Connection refused) while connecting to upstream\n(111: Connection refused) while connecting to upstream\n(111: Connection refused) while connecting to upstream\n"
  },
  {
    "path": "t/plugin/traffic-split5.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $http_config = $block->http_config // <<_EOC_;\n    # fake server, only for test\n    server {\n        listen 1970;\n        location / {\n            content_by_lua_block {\n                ngx.say(1970)\n            }\n        }\n    }\n\n    server {\n        listen 1971;\n        location / {\n            content_by_lua_block {\n                ngx.say(1971)\n            }\n        }\n    }\n\n    server {\n        listen 1972;\n        location / {\n            content_by_lua_block {\n                ngx.say(1972)\n            }\n        }\n    }\n\n    server {\n        listen 1973;\n        location / {\n            content_by_lua_block {\n                ngx.say(1973)\n            }\n        }\n    }\n\n    server {\n        listen 1974;\n        location / {\n            content_by_lua_block {\n                ngx.say(1974)\n            }\n        }\n    }\n_EOC_\n\n    $block->set_value(\"http_config\", $http_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream(multiple rules, multiple nodes under each weighted_upstreams) and add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/hello\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = { {\n                                    vars = { { \"arg_id\", \"==\", \"1\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_A\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1970\"] = 1,\n                                                [\"127.0.0.1:1971\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    }\n                               }\n                            },\n                            {\n                                match = { {\n                                    vars = { { \"arg_id\", \"==\", \"2\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_B\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1972\"] = 1,\n                                                [\"127.0.0.1:1973\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    }\n                               }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1974\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit different weighted_upstreams by rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri)\n            local port = tonumber(res.body)\n            if port ~= 1974 then\n                ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR\n                ngx.say(\"failed while no arg_id\")\n                return\n            end\n\n            uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello?id=1\"\n            res, err = httpc:request_uri(uri)\n            port = tonumber(res.body)\n            if port ~= 1970 and port ~= 1971 then\n                ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR\n                ngx.say(\"failed while arg_id = 1\")\n                return\n            end\n\n            uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello?id=2\"\n            res, err = httpc:request_uri(uri)\n            port = tonumber(res.body)\n            if port ~= 1972 and port ~= 1973 then\n                ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR\n                ngx.say(\"failed while arg_id = 2\")\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: set upstream(multiple rules, multiple nodes with different weight under each weighted_upstreams) and add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/hello\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = { {\n                                    vars = { { \"arg_id\", \"==\", \"1\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_A\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1970\"] = 2,\n                                                [\"127.0.0.1:1971\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    }\n                               }\n                            },\n                            {\n                                match = { {\n                                    vars = { { \"arg_id\", \"==\", \"2\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_B\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1972\"] = 2,\n                                                [\"127.0.0.1:1973\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    }\n                               }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1974\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: pick different nodes by weight\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello?id=1\"\n            local ports = {}\n            local res, err\n            for i = 1, 3 do\n                res, err = httpc:request_uri(uri)\n                local port = tonumber(res.body)\n                ports[i] = port\n            end\n            table.sort(ports)\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello?id=2\"\n            for i = 4, 6 do\n                res, err = httpc:request_uri(uri)\n                local port = tonumber(res.body)\n                ports[i] = port\n            end\n            table.sort(ports)\n\n            ngx.say(table.concat(ports, \", \"))\n        }\n    }\n--- response_body\n1970, 1970, 1971, 1972, 1972, 1973\n\n\n\n=== TEST 5: set upstream(multiple rules, the first rule has the match attribute and the second rule does not) and add route\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/hello\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = { {\n                                    vars = { { \"arg_id\", \"==\", \"1\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_A\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1970\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    }\n                               }\n                            },\n                            {\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_B\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1971\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    },\n                                    {\n                                        weight = 1\n                                    }\n                               }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1972\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: first rule match failed and the second rule match success\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello?id=1\"\n            local ports = {}\n            local res, err\n            for i = 1, 2 do\n                res, err = httpc:request_uri(uri)\n                local port = tonumber(res.body)\n                ports[i] = port\n            end\n\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello?id=2\"\n            for i = 3, 4 do\n                res, err = httpc:request_uri(uri)\n                local port = tonumber(res.body)\n                ports[i] = port\n            end\n            table.sort(ports)\n\n            ngx.say(table.concat(ports, \", \"))\n        }\n    }\n--- response_body\n1970, 1970, 1971, 1972\n\n\n\n=== TEST 7: set up traffic-split rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/server_port\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = { {\n                            match = { {\n                                vars = { { \"arg_name\", \"==\", \"jack\" } }\n                            } },\n                            weighted_upstreams = { {\n                                upstream = {\n                                    type = \"roundrobin\",\n                                    nodes = {\n                                        [\"127.0.0.1:1979\"] = 1\n                                    },\n                                },\n                            } }\n                        } }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit and check default timeout\n--- http_config\nproxy_connect_timeout 12345s;\n--- request\nGET /server_port?name=jack\n--- log_level: debug\n--- error_log eval\nqr/event timer add: \\d+: 12345000:\\d+/\n--- error_code: 502\n\n\n\n=== TEST 9: set upstream for post_arg_id test case\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/hello\",\n                plugins = {\n                    [\"traffic-split\"] = {\n                        rules = {\n                            {\n                                match = { {\n                                    vars = { { \"post_arg_id\", \"==\", \"1\" } }\n                                } },\n                                weighted_upstreams = {\n                                    {\n                                        upstream = {\n                                            name = \"upstream_A\",\n                                            type = \"roundrobin\",\n                                            nodes = {\n                                                [\"127.0.0.1:1970\"] = 1\n                                            }\n                                        },\n                                        weight = 1\n                                    }\n                               }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1974\"] = 1\n                    }\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 10: post_arg_id = 1 without content-type charset\n--- request\nPOST /hello\nid=1\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- response_body\n1970\n\n\n\n=== TEST 11: post_arg_id = 1 with content-type charset\n--- request\nPOST /hello\nid=1\n--- more_headers\nContent-Type: application/x-www-form-urlencoded;charset=UTF-8\n--- response_body\n1970\n\n\n\n=== TEST 12: failure after plugin reload\n--- extra_yaml_config\nnginx_config:\n  worker_processes: 1\n\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1970\":10\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/upstreams/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"type\": \"roundrobin\",\n                    \"nodes\": {\n                        \"127.0.0.1:1971\":10\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"traffic-split\": {\n                            \"rules\": [\n                                {\n                                    \"weighted_upstreams\": [\n                                        {\n                                            \"upstream_id\": \"2\",\n                                            \"weight\": 1\n                                        },\n                                        {\n                                            \"weight\": 1\n                                        }\n                                    ]\n                                }\n                            ]\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t('/hello')\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugins/reload', ngx.HTTP_PUT)\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/hello')\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(\"passed.\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed.\n"
  },
  {
    "path": "t/plugin/ua-restriction.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set both allowlist and denylist\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n               allowlist = {\n                    \"my-bot1\",\n                    \"my-bot2\"\n               },\n               denylist = {\n                    \"my-bot1\",\n                    \"my-bot2\"\n               },\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- response_body\nvalue should match only one schema, but matches both schemas 1 and 2\n\n\n\n=== TEST 2: bypass_missing not boolean\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n                bypass_missing = \"foo\",\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"bypass_missing\" validation failed: wrong type: expected boolean, got string\ndone\n\n\n\n=== TEST 3: allowlist not array\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n                allowlist = \"my-bot1\",\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"allowlist\" validation failed: wrong type: expected array, got string\ndone\n\n\n\n=== TEST 4: denylist not array\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n                denylist = 100,\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"denylist\" validation failed: wrong type: expected array, got number\ndone\n\n\n\n=== TEST 5: message not string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n                message = 100,\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"message\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 6: set denylist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                 \"denylist\": [\n                                     \"my-bot1\",\n                                     \"(Baiduspider)/(\\\\d+)\\\\.(\\\\d+)\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route and user-agent in denylist\n--- request\nGET /hello\n--- more_headers\nUser-Agent:my-bot1\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 8: hit route and user-agent in denylist with multiple user-agent\n--- request\nGET /hello\n--- more_headers\nUser-Agent:my-bot1\nUser-Agent:my-bot2\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 9: hit route and user-agent in denylist with reverse order multiple user-agent\n--- request\nGET /hello\n--- more_headers\nUser-Agent:my-bot2\nUser-Agent:my-bot1\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 10: hit route and user-agent match denylist regex\n--- request\nGET /hello\n--- more_headers\nUser-Agent:Baiduspider/3.0\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 11: hit route and user-agent not in denylist\n--- request\nGET /hello\n--- more_headers\nUser-Agent:foo/bar\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 12: set allowlist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                 \"allowlist\": [\n                                     \"my-bot1\",\n                                     \"(Baiduspider)/(\\\\d+)\\\\.(\\\\d+)\"\n                                 ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route and user-agent in allowlist\n--- request\nGET /hello\n--- more_headers\nUser-Agent:my-bot1\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 14: hit route and user-agent match allowlist regex\n--- request\nGET /hello\n--- more_headers\nUser-Agent:Baiduspider/3.0\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 15: hit route and user-agent not in allowlist\n--- request\nGET /hello\n--- more_headers\nUser-Agent:foo/bar\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 16:  hit route and user-agent in allowlist with multiple user-agent\n--- request\nGET /hello\n--- more_headers\nUser-Agent:foo/bar\nUser-Agent:my-bot1\n--- response_body\nhello world\n\n\n\n=== TEST 17:  hit route and user-agent in allowlist with reverse order multiple user-agent\n--- request\nGET /hello\n--- more_headers\nUser-Agent:my-bot1\nUser-Agent:foo/bar\n--- response_body\nhello world\n\n\n\n=== TEST 18: message that do not reach the minimum range\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                 \"message\": \"\"\n                            }\n                        }\n                }]]\n                )\n\n            ngx.say(body)\n        }\n    }\n--- response_body_like eval\nqr/string too short, expected at least 1, got 0/\n\n\n\n=== TEST 19: exceeds the maximum limit of message\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local json = require(\"toolkit.json\")\n\n            local data = {\n                uri = \"/hello\",\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    }\n                },\n                plugins = {\n                    [\"ua-restriction\"] = {\n                        denylist = {\n                           \"my-bot1\",\n                        },\n                        message = (\"-1Aa#\"):rep(205)\n                    }\n                }\n            }\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            ngx.say(body)\n        }\n    }\n--- response_body_like eval\nqr/string too long, expected at most 1024, got 1025/\n\n\n\n=== TEST 20: set custom message\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                \"denylist\": [\n                                    \"(Baiduspider)/(\\\\d+)\\\\.(\\\\d+)\"\n                                ],\n                                \"message\": \"Do you want to do something bad?\"\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n\n--- response_body\npassed\n\n\n\n=== TEST 21: test custom message\n--- request\nGET /hello\n--- more_headers\nUser-Agent:Baiduspider/1.0\n--- error_code: 403\n--- response_body\n{\"message\":\"Do you want to do something bad?\"}\n\n\n\n=== TEST 22: test remove ua-restriction, add denylist(part 1)\n--- config\n    location /enable {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                \"denylist\": [\n                                     \"(Baiduspider)/(\\\\d+)\\\\.(\\\\d+)\"\n                                ]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /enable\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 23: test remove ua-restriction, fail(part 2)\n--- request\nGET /hello\n--- more_headers\nUser-Agent:Baiduspider/1.0\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 24: test remove ua-restriction, remove plugin(part 3)\n--- config\n    location /disable {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /disable\n--- error_code: 200\n--- response_body\npassed\n\n\n\n=== TEST 25: test remove ua-restriction, check spider User-Agent(part 4)\n--- request\nGET /hello\n--- more_headers\nUser-Agent:Baiduspider/1.0\n--- response_body\nhello world\n\n\n\n=== TEST 26: set disable=true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"plugins\": {\n                        \"ua-restriction\": {\n                            \"denylist\": [\n                                \"foo\"\n                            ],\n                            \"_meta\": {\n                                \"disable\": true\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 27: the element in allowlist is null\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n                allowlist = {\n                    \"userdata: NULL\",\n                    null,\n                    nil,\n                    \"\"\n                },\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"allowlist\" validation failed: wrong type: expected array, got table\ndone\n\n\n\n=== TEST 28: the element in denylist is null\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.ua-restriction\")\n            local conf = {\n                denylist = {\n                    \"userdata: NULL\",\n                    null,\n                    nil,\n                    \"\"\n                },\n            }\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"denylist\" validation failed: wrong type: expected array, got table\ndone\n\n\n\n=== TEST 29: test both allowlist and denylist are not exist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin ua-restriction err: value should match only one schema, but matches none\"}\n\n\n\n=== TEST 30: test bypass_missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                \"allowlist\": [\n                                    \"my-bot1\"\n                                ]\n                            }\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 31: hit\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"message\":\"Not allowed\"}\n\n\n\n=== TEST 32: test bypass_missing with true\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"uri\": \"/hello\",\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            }\n                        },\n                        \"plugins\": {\n                            \"ua-restriction\": {\n                                \"bypass_missing\": true,\n                                \"denylist\": [\n                                    \"my-bot1\"\n                                ]\n                            }\n                        }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 33: hit\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/plugin/udp-logger.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.udp-logger\")\n            local ok, err = plugin.check_schema({host = \"127.0.0.1\", port = 3000})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: missing host\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.udp-logger\")\n            local ok, err = plugin.check_schema({port = 3000})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"host\" is required\ndone\n\n\n\n=== TEST 3: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.udp-logger\")\n            local ok, err = plugin.check_schema({host= \"127.0.0.1\", port = 3000, timeout = \"10\"})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"timeout\" validation failed: wrong type: expected integer, got string\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 2000,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: access\n--- request\nGET /opentracing\n--- response_body\nopentracing\n--- wait: 1\n\n\n\n=== TEST 6: error log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"312.0.0.1\",\n                                \"port\": 2000,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/opentracing\"\n            local res, err = httpc:request_uri(uri, {method = \"GET\"})\n        }\n    }\n--- request\nGET /t\n--- error_log\nfailed to connect to UDP server: host[312.0.0.1] port[2000]\n[error]\n--- wait: 5\n\n\n\n=== TEST 7: check plugin configuration updating\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body1 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 2000,\n                                \"tls\": false,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body2 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, body3 = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 2002,\n                                \"tls\": false,\n                                \"batch_max_size\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local code, _, body4 = t(\"/opentracing\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            ngx.print(body1)\n            ngx.print(body2)\n            ngx.print(body3)\n            ngx.print(body4)\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body\npassedopentracing\npassedopentracing\n--- grep_error_log eval\nqr/sending a batch logs to 127.0.0.1:(\\d+)/\n--- grep_error_log_out\nsending a batch logs to 127.0.0.1:2000\nsending a batch logs to 127.0.0.1:2002\n\n\n\n=== TEST 8: bad custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/udp-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": \"'$host' '$time_iso8601'\"\n                 }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                ngx.print(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid configuration: property \\\"log_format\\\" validation failed: wrong type: expected object, got string\"}\n\n\n\n=== TEST 9: configure plugin and access route /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8127,\n                                \"tls\": false,\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_metadata/udp-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"case name\": \"plugin_metadata\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"client_ip\": \"$remote_addr\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n            local code, _, _ = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: check if log exists to confirm if logging server was hit\n--- exec\ntail -n 1 ci/pod/vector/udp.log\n--- response_body eval\nqr/.*plugin_metadata.*/\n\n\n\n=== TEST 11: configure plugin and access route /hello\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8127,\n                                \"tls\": false,\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/plugin_metadata/udp-logger',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"log_format\": {\n                            \"host\": \"$host\",\n                            \"case name\": \"logger format in plugin\",\n                            \"@timestamp\": \"$time_iso8601\",\n                            \"client_ip\": \"$remote_addr\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n            local code, _, _ = t(\"/hello\", \"GET\")\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: check log format from logging server\n--- exec\ntail -n 1 ci/pod/vector/udp.log\n--- response_body eval\nqr/.*logger format in plugin.*/\n\n\n\n=== TEST 13: add plugin with 'include_req_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/udp-logger', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8127,\n                                \"tls\": false,\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1,\n                                \"include_req_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"body\":\"{\\\"sample_payload\\\":\\\"hello\\\"}\"\n\n\n\n=== TEST 14: add plugin with 'include_resp_body' setting, collect request log\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            t('/apisix/admin/plugin_metadata/udp-logger', ngx.HTTP_DELETE)\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"udp-logger\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 8127,\n                                \"tls\": false,\n                                \"batch_max_size\": 1,\n                                \"inactive_timeout\": 1,\n                                \"include_resp_body\": true\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n\n            local code, _, body = t(\"/hello\", \"POST\", \"{\\\"sample_payload\\\":\\\"hello\\\"}\")\n        }\n    }\n--- request\nGET /t\n--- error_log\n\"body\":\"hello world\\n\"\n"
  },
  {
    "path": "t/plugin/uri-blocker.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: invalid regular expression\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\".+(\"]\n                    }\n                },\n                \"uri\": \"/hello\"\n            }]])\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.print(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin uri-blocker err: pcre2_compile() failed: missing closing parenthesis in \\\".+(\\\"\"}\n\n\n\n=== TEST 2: multiple valid rules\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"^a\", \"^b\"]\n                    }\n                },\n                \"uri\": \"/hello\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: multiple rules(include one invalid rule)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"^a\", \"^b(\"]\n                    }\n                },\n                \"uri\": \"/hello\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.print(body)\n    }\n}\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin uri-blocker err: pcre2_compile() failed: missing closing parenthesis in \\\"^b(\\\"\"}\n\n\n\n=== TEST 4: one block rule\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"aa\"]\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit block rule\n--- request\nGET /hello?aa=1\n--- error_code: 403\n--- error_log\nconcat block_rules: aa\n\n\n\n=== TEST 6: miss block rule\n--- request\nGET /hello?bb=2\n--- error_log\nconcat block_rules: aa\n\n\n\n=== TEST 7: multiple block rules\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"aa\", \"bb\", \"c\\\\d+\"]\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n        )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit block rule\n--- request\nGET /hello?x=bb\n--- error_code: 403\n--- error_log\nconcat block_rules: aa|bb|c\\d+,\n\n\n\n=== TEST 9: hit block rule\n--- request\nGET /hello?bb=2\n--- error_code: 403\n--- error_log\nconcat block_rules: aa|bb|c\\d+,\n\n\n\n=== TEST 10: hit block rule\n--- request\nGET /hello?c1=2\n--- error_code: 403\n\n\n\n=== TEST 11: not hit block rule\n--- request\nGET /hello?cc=2\n\n\n\n=== TEST 12: SQL injection\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"select.+(from|limit)\", \"(?:(union(.*?)select))\"]\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n        )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: hit block rule\n--- request\nGET /hello?name=;select%20from%20sys\n--- error_code: 403\n--- error_log\nconcat block_rules: select.+(from|limit)|(?:(union(.*?)select)),\n\n\n\n=== TEST 14: hit block rule\n--- request\nGET /hello?name=;union%20select%20\n--- error_code: 403\n\n\n\n=== TEST 15: not hit block rule\n--- request\nGET /hello?cc=2\n\n\n\n=== TEST 16: invalid rejected_msg length or type\n--- config\nlocation /t {\n    content_by_lua_block {\n        local data = {\n            {\n                input = {\n                    plugins = {\n                        [\"uri-blocker\"] = {\n                            block_rules = { \"^a\" },\n                            rejected_msg = \"\",\n                        },\n                    },\n                    uri = \"/hello\",\n                },\n                output = {\n                    error_msg = \"failed to check the configuration of plugin uri-blocker err: property \\\"rejected_msg\\\" validation failed: string too short, expected at least 1, got 0\",\n                },\n            },\n            {\n                input = {\n                    plugins = {\n                        [\"uri-blocker\"] = {\n                            block_rules = { \"^a\" },\n                            rejected_msg = true,\n                        },\n                    },\n                    uri = \"/hello\",\n                },\n                output = {\n                    error_msg = \"failed to check the configuration of plugin uri-blocker err: property \\\"rejected_msg\\\" validation failed: wrong type: expected string, got boolean\",\n                },\n            },\n        }\n\n        local t = require(\"lib.test_admin\").test\n        local err_count = 0\n        for i in ipairs(data) do\n            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[i].input, data[i].output)\n\n            if code >= 300 then\n                err_count = err_count + 1\n            end\n            ngx.print(body)\n        end\n\n        assert(err_count == #data)\n    }\n}\n--- request\nGET /t\n\n\n\n=== TEST 17: one block rule, with rejected_msg\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"aa\"],\n                        \"rejected_msg\": \"access is not allowed\"\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.print(body)\n    }\n}\n--- request\nGET /t\n\n\n\n=== TEST 18: hit block rule and return rejected_msg\n--- request\nGET /hello?aa=1\n--- error_code: 403\n--- response_body\n{\"error_msg\":\"access is not allowed\"}\n\n\n\n=== TEST 19: one block rule, with case insensitive\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"AA\"],\n                        \"rejected_msg\": \"access is not allowed\",\n                        \"case_insensitive\": true\n                    }\n                },\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1980\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                },\n                \"uri\": \"/hello\"\n            }]]\n            )\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.print(body)\n    }\n}\n--- request\nGET /t\n\n\n\n=== TEST 20: hit block rule\n--- request\nGET /hello?aa=1\n--- error_code: 403\n--- response_body\n{\"error_msg\":\"access is not allowed\"}\n\n\n\n=== TEST 21: add block rule with anchor\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        local code, body = t('/apisix/admin/routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"plugins\": {\n                    \"uri-blocker\": {\n                        \"block_rules\": [\"^/internal/\"]\n                    }\n                },\n                \"uri\": \"/internal/*\"\n            }]])\n\n        if code >= 300 then\n            ngx.status = code\n        end\n        ngx.print(body)\n    }\n}\n--- request\nGET /t\n\n\n\n=== TEST 22: can't bypass with url without normalization\n--- request\nGET /./internal/x?aa=1\n--- error_code: 403\n"
  },
  {
    "path": "t/plugin/wolf-rbac.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n}\n\nuse t::APISIX 'no_plan';\n\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.wolf-rbac\")\n            local conf = {}\n\n            local ok, err = plugin.check_schema(conf)\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(require(\"toolkit.json\").encode(conf))\n        }\n    }\n--- response_body_like eval\nqr/\\{\"appid\":\"unset\",\"header_prefix\":\"X-\",\"server\":\"http:\\/\\/127\\.0\\.0\\.1:12180\"\\}/\n\n\n\n=== TEST 2: wrong type of string\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.wolf-rbac\")\n            local ok, err = plugin.check_schema({appid = 123})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- response_body\nproperty \"appid\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 3: setup public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/wolf-login\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/wolf-rbac/login\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/wolf-userinfo\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/wolf-rbac/user_info\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/routes/wolf-change-pwd\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/plugin/wolf-rbac/change_pwd\"\n                    }]]\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\" x 3\n\n\n\n=== TEST 4: add consumer with username and plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"wolf_rbac_unit_test\",\n                    \"plugins\": {\n                        \"wolf-rbac\": {\n                            \"appid\": \"wolf-rbac-app\",\n                            \"server\": \"http://127.0.0.1:1982\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: enable wolf rbac plugin using admin api\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"wolf-rbac\": {}\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1982\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uris\": [\"/hello*\",\"/wolf/rbac/*\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: login failed, appid is missing\n--- request\nPOST /apisix/plugin/wolf-rbac/login\nusername=admin&password=123456\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 400\n--- response_body_like eval\nqr/appid is missing/\n\n\n\n=== TEST 7: login failed, appid not found\n--- request\nPOST /apisix/plugin/wolf-rbac/login\nappid=not-found&username=admin&password=123456\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 400\n--- response_body_like eval\nqr/appid not found/\n\n\n\n=== TEST 8: login failed, username missing\n--- request\nPOST /apisix/plugin/wolf-rbac/login\nappid=wolf-rbac-app&password=123456\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 200\n--- response_body\n{\"message\":\"request to wolf-server failed!\"}\n--- grep_error_log eval\nqr/ERR_USERNAME_MISSING/\n--- grep_error_log_out eval\nqr/ERR_USERNAME_MISSING/\n\n\n\n=== TEST 9: login failed, password missing\n--- request\nPOST /apisix/plugin/wolf-rbac/login\nappid=wolf-rbac-app&username=admin\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 200\n--- response_body\n{\"message\":\"request to wolf-server failed!\"}\n--- grep_error_log eval\nqr/ERR_PASSWORD_MISSING/\n--- grep_error_log_out eval\nqr/ERR_PASSWORD_MISSING/\n\n\n\n=== TEST 10: login failed, username not found\n--- request\nPOST /apisix/plugin/wolf-rbac/login\nappid=wolf-rbac-app&username=not-found&password=123456\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 200\n--- response_body\n{\"message\":\"request to wolf-server failed!\"}\n--- grep_error_log eval\nqr/ERR_USER_NOT_FOUND/\n--- grep_error_log_out eval\nqr/ERR_USER_NOT_FOUND/\n\n\n\n=== TEST 11: login failed, wrong password\n--- request\nPOST /apisix/plugin/wolf-rbac/login\nappid=wolf-rbac-app&username=admin&password=wrong-password\n--- more_headers\nContent-Type: application/x-www-form-urlencoded\n--- error_code: 200\n--- response_body\n{\"message\":\"request to wolf-server failed!\"}\n--- grep_error_log eval\nqr/ERR_PASSWORD_ERROR/\n--- grep_error_log_out eval\nqr/ERR_PASSWORD_ERROR/\n\n\n\n=== TEST 12: login successfully\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/plugin/wolf-rbac/login',\n                ngx.HTTP_POST,\n                [[\n                {\"appid\": \"wolf-rbac-app\", \"username\": \"admin\",\"password\": \"123456\"}\n                ]],\n                [[\n                {\"rbac_token\":\"V1#wolf-rbac-app#wolf-rbac-token\",\"user_info\":{\"nickname\":\"administrator\",\"username\":\"admin\",\"id\":\"100\"}}\n                ]],\n                {[\"Content-Type\"] = \"application/json\"}\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 13: verify, missing token\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing rbac token in request\"}\n\n\n\n=== TEST 14: verify: invalid rbac token\n--- request\nGET /hello\n--- error_code: 401\n--- more_headers\nx-rbac-token: invalid-rbac-token\n--- response_body\n{\"message\":\"invalid rbac token: parse failed\"}\n\n\n\n=== TEST 15: verify: invalid appid in rbac token\n--- request\nGET /hello\n--- error_code: 401\n--- more_headers\nx-rbac-token: V1#invalid-appid#rbac-token\n--- response_body\n{\"message\":\"Invalid appid in rbac token\"}\n--- error_log\nconsumer [invalid-appid] not found\n\n\n\n=== TEST 16: verify: failed\n--- request\nGET /hello1\n--- error_code: 403\n--- more_headers\nx-rbac-token: V1#wolf-rbac-app#wolf-rbac-token\n--- response_body\n{\"message\":\"ERR_ACCESS_DENIED\"}\n--- grep_error_log eval\nqr/ERR_ACCESS_DENIED */\n--- grep_error_log_out\nERR_ACCESS_DENIED\nERR_ACCESS_DENIED\nERR_ACCESS_DENIED\n\n\n\n=== TEST 17: verify (in argument)\n--- request\nGET /hello?rbac_token=V1%23wolf-rbac-app%23wolf-rbac-token\n--- response_headers\nX-UserId: 100\nX-Username: admin\nX-Nickname: administrator\n--- response_body\nhello world\n\n\n\n=== TEST 18: verify (in header Authorization)\n--- request\nGET /hello\n--- more_headers\nAuthorization: V1#wolf-rbac-app#wolf-rbac-token\n--- response_headers\nX-UserId: 100\nX-Username: admin\nX-Nickname: administrator\n--- response_body\nhello world\n\n\n\n=== TEST 19: verify (in header x-rbac-token)\n--- request\nGET /hello\n--- more_headers\nx-rbac-token: V1#wolf-rbac-app#wolf-rbac-token\n--- response_headers\nX-UserId: 100\nX-Username: admin\nX-Nickname: administrator\n--- response_body\nhello world\n\n\n\n=== TEST 20: verify (in cookie)\n--- request\nGET /hello\n--- more_headers\nCookie: x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token\n--- response_headers\nX-UserId: 100\nX-Username: admin\nX-Nickname: administrator\n--- response_body\nhello world\n\n\n\n=== TEST 21: get userinfo failed, missing token\n--- request\nGET /apisix/plugin/wolf-rbac/user_info\n--- error_code: 401\n--- response_body\n{\"message\":\"Missing rbac token in request\"}\n\n\n\n=== TEST 22: get userinfo failed, invalid rbac token\n--- request\nGET /apisix/plugin/wolf-rbac/user_info\n--- error_code: 401\n--- more_headers\nx-rbac-token: invalid-rbac-token\n--- response_body\n{\"message\":\"invalid rbac token: parse failed\"}\n\n\n\n=== TEST 23: get userinfo\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/plugin/wolf-rbac/user_info',\n                ngx.HTTP_GET,\n                nil,\n                [[\n{\"user_info\":{\"username\":\"admin\",\"id\":\"100\",\"nickname\":\"administrator\"}}\n                ]],\n                {Cookie = \"x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token\"}\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 24: change password failed, old password incorrect\n--- request\nPUT /apisix/plugin/wolf-rbac/change_pwd\n{\"oldPassword\": \"error\", \"newPassword\": \"abcdef\"}\n--- more_headers\nContent-Type: application/json\nCookie: x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token\n--- error_code: 200\n--- response_body\n{\"message\":\"request to wolf-server failed!\"}\n--- grep_error_log eval\nqr/ERR_OLD_PASSWORD_INCORRECT/\n--- grep_error_log_out eval\nqr/ERR_OLD_PASSWORD_INCORRECT/\n\n\n\n=== TEST 25: change password\n--- request\nPUT /apisix/plugin/wolf-rbac/change_pwd\n{\"oldPassword\":\"123456\", \"newPassword\": \"abcdef\"}\n--- more_headers\nContent-Type: application/json\nCookie: x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token\n--- error_code: 200\n--- response_body_like eval\nqr/success to change password/\n\n\n\n=== TEST 26: custom headers in request headers\n--- request\nGET /wolf/rbac/custom/headers?rbac_token=V1%23wolf-rbac-app%23wolf-rbac-token\n--- response_headers\nX-UserId: 100\nX-Username: admin\nX-Nickname: administrator\n--- response_body\nid:100,username:admin,nickname:administrator\n\n\n\n=== TEST 27: change password by post raw args\n--- request\nPUT /apisix/plugin/wolf-rbac/change_pwd\noldPassword=123456&newPassword=abcdef\n--- more_headers\nCookie: x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token\n--- error_code: 200\n--- response_body_like eval\nqr/success to change password/\n\n\n\n=== TEST 28: change password by post raw args, greater than 100 args is ok\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\")\n\n        local headers = {\n            [\"Cookie\"] = \"x-rbac-token=V1#wolf-rbac-app#wolf-rbac-token\"\n        }\n        local tbl = {}\n        for i=1, 100 do\n            tbl[i] = \"test\"..tostring(i)..\"=test&\"\n        end\n        tbl[101] = \"oldPassword=123456&newPassword=abcdef\"\n        local code, _, real_body = t.test('/apisix/plugin/wolf-rbac/change_pwd',\n            ngx.HTTP_PUT,\n            table.concat(tbl, \"\"),\n            nil,\n            headers\n        )\n        ngx.status = 200\n        ngx.say(real_body)\n    }\n}\n--- response_body_like eval\nqr/success to change password/\n\n\n\n=== TEST 29: verify: failed, server internal error\n--- request\nGET /hello/500\n--- error_code: 500\n--- more_headers\nx-rbac-token: V1#wolf-rbac-app#wolf-rbac-token\n--- response_body\n{\"message\":\"request to wolf-server failed, status:500\"}\n--- grep_error_log eval\nqr/request to wolf-server failed, status:500 */\n--- grep_error_log_out\nrequest to wolf-server failed, status:500\nrequest to wolf-server failed, status:500\n\n\n\n=== TEST 30: verify: failed, token is expired\n--- request\nGET /hello/401\n--- error_code: 401\n--- more_headers\nx-rbac-token: V1#wolf-rbac-app#wolf-rbac-token\n--- response_body\n{\"message\":\"ERR_TOKEN_INVALID\"}\n--- grep_error_log eval\nqr/ERR_TOKEN_INVALID */\n--- grep_error_log_out\nERR_TOKEN_INVALID\nERR_TOKEN_INVALID\nERR_TOKEN_INVALID\n\n\n\n=== TEST 31: set hmac-auth conf: appid uses secret ref\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n             -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"wolf_rbac_unit_test\",\n                    \"plugins\": {\n                        \"wolf-rbac\": {\n                            \"appid\": \"$secret://vault/test1/wolf_rbac_unit_test/appid\",\n                            \"server\": \"http://127.0.0.1:1982\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 32: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/wolf_rbac_unit_test appid=wolf-rbac-app\n--- response_body\nSuccess! Data written to: kv/apisix/wolf_rbac_unit_test\n\n\n\n=== TEST 33: login successfully\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/plugin/wolf-rbac/login',\n                ngx.HTTP_POST,\n                [[\n                {\"appid\": \"wolf-rbac-app\", \"username\": \"admin\",\"password\": \"123456\"}\n                ]],\n                [[\n                {\"rbac_token\":\"V1#wolf-rbac-app#wolf-rbac-token\",\"user_info\":{\"nickname\":\"administrator\",\"username\":\"admin\",\"id\":\"100\"}}\n                ]],\n                {[\"Content-Type\"] = \"application/json\"}\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 34: set hmac-auth conf with the token in an env var: appid uses secret ref\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n             -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\" : \"kv/apisix\",\n                    \"token\" : \"$ENV://VAULT_TOKEN\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"wolf_rbac_unit_test\",\n                    \"plugins\": {\n                        \"wolf-rbac\": {\n                            \"appid\": \"$secret://vault/test1/wolf_rbac_unit_test/appid\",\n                            \"server\": \"http://127.0.0.1:1982\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 35: login successfully\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/plugin/wolf-rbac/login',\n                ngx.HTTP_POST,\n                [[\n                {\"appid\": \"wolf-rbac-app\", \"username\": \"admin\",\"password\": \"123456\"}\n                ]],\n                [[\n                {\"rbac_token\":\"V1#wolf-rbac-app#wolf-rbac-token\",\"user_info\":{\"nickname\":\"administrator\",\"username\":\"admin\",\"id\":\"100\"}}\n                ]],\n                {[\"Content-Type\"] = \"application/json\"}\n                )\n            ngx.status = code\n        }\n    }\n\n\n\n=== TEST 36: add consumer with echo plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"wolf_rbac_with_other_plugins\",\n                    \"plugins\": {\n                        \"wolf-rbac\": {\n                            \"appid\": \"wolf-rbac-app\",\n                            \"server\": \"http://127.0.0.1:1982\"\n                        },\n                        \"echo\": {\n                            \"body\": \"consumer merge echo plugins\\n\"\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n--- no_error_log\n[error]\n\n\n\n=== TEST 37: verify echo plugin in consumer\n--- request\nGET /hello\n--- more_headers\nAuthorization: V1#wolf-rbac-app#wolf-rbac-token\n--- response_headers\nX-UserId: 100\nX-Username: admin\nX-Nickname: administrator\n--- response_body\nconsumer merge echo plugins\n--- no_error_log\n[error]\n"
  },
  {
    "path": "t/plugin/workflow-without-case.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n\n__DATA__\n\n=== TEST 1: set plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"workflow\": {\n                                \"rules\": [\n                                    {\n                                        \"actions\": [\n                                            [\n                                                \"return\",\n                                                {\n                                                    \"code\": 403\n                                                }\n                                            ]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: trigger workflow\n--- request\nGET /hello\n--- error_code: 403\n"
  },
  {
    "path": "t/plugin/workflow.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n\n__DATA__\n\n=== TEST 1: schema check\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.workflow\")\n            local data = {\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"return\",\n                                    {\n                                        code = 403\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"return\",\n                                    {\n                                        status = 403\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"return\",\n                                    {\n                                        code = \"403\"\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n\n                            },\n                            actions = {\n                                {\n                                    \"return\",\n                                    {\n                                        code = 403\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"fake\",\n                                    {\n                                        code = 403\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            for _, conf in ipairs(data) do\n                local ok, err = plugin.check_schema(conf)\n                if not ok then\n                    ngx.say(err)\n                else\n                    ngx.say(\"done\")\n                end\n            end\n        }\n    }\n--- response_body\ndone\nproperty \"rules\" validation failed: failed to validate item 1: property \"actions\" is required\nproperty \"rules\" validation failed: failed to validate item 1: property \"actions\" validation failed: failed to validate item 1: expect array to have at least 1 items\nfailed to validate the 'return' action: property \"code\" is required\nfailed to validate the 'return' action: property \"code\" validation failed: wrong type: expected integer, got string\nproperty \"rules\" validation failed: failed to validate item 1: property \"case\" validation failed: expect array to have at least 1 items\nunsupported action: fake\n\n\n\n=== TEST 2: set plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"workflow\": {\n                                \"rules\": [\n                                    {\n                                        \"case\": [\n                                            [\"uri\", \"==\", \"/hello\"]\n                                        ],\n                                        \"actions\": [\n                                            [\n                                                \"return\",\n                                                {\n                                                    \"code\": 403\n                                                }\n                                            ]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: trigger workflow\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 4: multiple conditions in one case\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"workflow\": {\n                                \"rules\": [\n                                    {\n                                        \"case\": [\n                                            [\"uri\", \"==\", \"/hello\"],\n                                            [\"arg_foo\", \"==\", \"bar\"]\n                                        ],\n                                        \"actions\": [\n                                            [\n                                                \"return\",\n                                                {\n                                                    \"code\": 403\n                                                }\n                                            ]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: missing match the only case\n--- request\nGET /hello?foo=bad\n\n\n\n=== TEST 6: trigger workflow\n--- request\nGET /hello?foo=bar\n--- error_code: 403\n--- response_body\n{\"error_msg\":\"rejected by workflow\"}\n\n\n\n=== TEST 7: multiple cases with different actions\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello\"}\n                                },\n                                actions = {\n                                    {\n                                        \"return\",\n                                        {\n                                            code = 403\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello2\"}\n                                },\n                                actions = {\n                                    {\n                                        \"return\",\n                                        {\n                                            code = 401\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: trigger one case\n--- request\nGET /hello\n--- error_code: 403\n\n\n\n=== TEST 9: trigger another case\n--- request\nGET /hello2\n--- error_code: 401\n\n\n\n=== TEST 10: match case in order\n# rules is an array, match in the order of the index of the array,\n# when cases are matched, actions are executed and do not continue\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"arg_foo\", \"==\", \"bar\"}\n                                },\n                                actions = {\n                                    {\n                                        \"return\",\n                                        {\n                                            code = 403\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello\"}\n                                },\n                                actions = {\n                                    {\n                                        \"return\",\n                                        {\n                                            code = 401\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: both case 1&2 matched, trigger the first cases\n--- request\nGET /hello?foo=bar\n--- error_code: 403\n\n\n\n=== TEST 12: case 1 mismatched, trigger the second cases\n--- request\nGET /hello?foo=bad\n--- error_code: 401\n\n\n\n=== TEST 13: all cases mismatched, pass to upstream\n--- request\nGET /hello1\n--- response_body\nhello1 world\n\n\n\n=== TEST 14: schema check(limit-count)\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.workflow\")\n            local data = {\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"limit-count\",\n                                    {count = 2, time_window = 60, rejected_code = 503, key = 'remote_addr'}\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"limit-count\",\n                                    {count = 2}\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"limit-count\",\n                                    {time_window = 60}\n                                }\n                            }\n                        }\n                    }\n                },\n                {\n                    rules = {\n                        {\n                            case = {\n                                {\"uri\", \"==\", \"/hello\"}\n                            },\n                            actions = {\n                                {\n                                    \"limit-count\",\n                                    {\n                                        count = 2,\n                                        time_window = 60,\n                                        rejected_code = 503,\n                                        group = \"services_1\"\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            for _, conf in ipairs(data) do\n                local ok, err = plugin.check_schema(conf)\n                if not ok then\n                    ngx.say(err)\n                else\n                    ngx.say(\"done\")\n                end\n            end\n        }\n    }\n--- response_body\ndone\nfailed to validate the 'limit-count' action: value should match only one schema, but matches none\nfailed to validate the 'limit-count' action: value should match only one schema, but matches none\nfailed to validate the 'limit-count' action: group is not supported\n\n\n\n=== TEST 15: set actions as limit-count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"workflow\": {\n                                \"rules\": [\n                                    {\n                                        \"case\": [\n                                            [\"uri\", \"==\", \"/hello\"]\n                                        ],\n                                        \"actions\": [\n                                            [\n                                                \"limit-count\",\n                                                {\n                                                    \"count\": 3,\n                                                    \"time_window\": 60,\n                                                    \"rejected_code\": 503,\n                                                    \"key\": \"remote_addr\"\n                                                }\n                                            ]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 16: up the limit\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello\", \"GET /hello\", \"GET /hello\"]\n--- error_code eval\n[200, 200, 200, 503]\n\n\n\n=== TEST 17: the conf in actions is isolation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 3,\n                                            time_window = 60,\n                                            rejected_code = 503,\n                                            key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello1\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 3,\n                                            time_window = 60,\n                                            rejected_code = 503,\n                                            key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 18: cross-hit case 1 and case 2, up limit by isolation\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello1\", \"GET /hello\", \"GET /hello1\",\n\"GET /hello\", \"GET /hello1\", \"GET /hello\", \"GET /hello1\"]\n--- error_code eval\n[200, 200, 200, 200, 200, 200, 503, 503]\n\n\n\n=== TEST 19: multiple conditions in one case\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"workflow\": {\n                                \"rules\": [\n                                    {\n                                        \"case\": [\n                                            \"OR\",\n                                            [\"arg_foo\", \"==\", \"bar\"],\n                                            [\"uri\", \"==\", \"/hello\"]\n                                        ],\n                                        \"actions\": [\n                                            [\n                                                \"return\",\n                                                {\n                                                    \"code\": 403\n                                                }\n                                            ]\n                                        ]\n                                    }\n                                ]\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: trigger workflow\n--- request\nGET /hello\n--- error_code: 403\n--- response_body\n{\"error_msg\":\"rejected by workflow\"}\n"
  },
  {
    "path": "t/plugin/workflow2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n\n__DATA__\n\n=== TEST 1: multiple cases with different actions(return & limit-count)\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello\"}\n                                },\n                                actions = {\n                                    {\n                                        \"return\",\n                                        {\n                                            code = 403\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello1\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 1,\n                                            time_window = 60,\n                                            rejected_code = 503\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: cross-hit case 1 and case 2, trigger actions by isolation\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello1\", \"GET /hello1\"]\n--- error_code eval\n[403, 200, 503]\n\n\n\n=== TEST 3: the conf in actions is isolation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 3,\n                                            time_window = 60,\n                                            rejected_code = 503,\n                                            key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello1\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 3,\n                                            time_window = 60,\n                                            rejected_code = 503,\n                                            key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: cross-hit case 1 and case 2, trigger actions by isolation\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello1\", \"GET /hello\", \"GET /hello1\"]\n--- error_code eval\n[200, 200, 200, 200]\n\n\n\n=== TEST 5: cross-hit case 1 and case 2, up limit by isolation 2\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello1\", \"GET /hello\", \"GET /hello1\"]\n--- error_code eval\n[200, 200, 503, 503]\n\n\n\n=== TEST 6: different actions with different limit count conf, up limit by isolation\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 1,\n                                            time_window = 60,\n                                            rejected_code = 503,\n                                            key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            },\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/hello1\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-count\",\n                                        {\n                                            count = 2,\n                                            time_window = 60,\n                                            rejected_code = 503,\n                                            key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: case 1 up limit, case 2 psssed\n--- pipelined_requests eval\n[\"GET /hello\", \"GET /hello1\", \"GET /hello\", \"GET /hello1\"]\n--- error_code eval\n[200, 200, 503, 200]\n\n\n\n=== TEST 8: test no rules\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.print(body)\n        }\n    }\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of plugin workflow err: property \\\"rules\\\" is required\"}\n"
  },
  {
    "path": "t/plugin/workflow3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nno_shuffle();\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n    my $port = $ENV{TEST_NGINX_SERVER_PORT};\n    my $config = $block->config // <<_EOC_;\n    location /access_root_dir {\n        content_by_lua_block {\n            local httpc = require \"resty.http\"\n            local hc = httpc:new()\n\n            local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn')\n            if res then\n                ngx.exit(res.status)\n            end\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/access_root_dir\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests();\n\n\n__DATA__\n\n=== TEST 1: limit-conn\n--- config\n    location /t {\n        content_by_lua_block {\n            local json = require(\"toolkit.json\")\n            local t = require(\"lib.test_admin\").test\n            local data = {\n                uri = \"/*\",\n                plugins = {\n                    workflow = {\n                        rules = {\n                            {\n                                case = {\n                                    {\"uri\", \"==\", \"/limit_conn\"}\n                                },\n                                actions = {\n                                    {\n                                        \"limit-conn\",\n                                        {\n                                          conn = 2,\n                                          burst = 1,\n                                          default_conn_delay = 0.1,\n                                          rejected_code = 503,\n                                          key = \"remote_addr\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: exceeding the burst\n--- request\nGET /test_concurrency\n--- timeout: 10s\n--- response_body\n200\n200\n200\n503\n503\n503\n503\n503\n503\n503\n"
  },
  {
    "path": "t/plugin/zipkin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.zipkin\")\n            local ok, err = plugin.check_schema({endpoint = 'http://127.0.0.1', sample_ratio = 0.001})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 2: wrong value of ratio\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.zipkin\")\n            local ok, err = plugin.check_schema({endpoint = 'http://127.0.0.1', sample_ratio = -0.1})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"sample_ratio\" validation failed: expected -0.1 to be at least 1e-05\ndone\n\n\n\n=== TEST 3: wrong value of ratio\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.zipkin\")\n            local ok, err = plugin.check_schema({endpoint = 'http://127.0.0.1', sample_ratio = 2})\n            if not ok then\n                ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"sample_ratio\" validation failed: expected 2 to be at most 1\ndone\n\n\n\n=== TEST 4: add plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:1980/mock_zipkin?server_addr=127.0.0.1\",\n                                \"sample_ratio\": 1,\n                                \"service_name\": \"APISIX\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: tiger zipkin\n--- request\nGET /opentracing\n--- wait: 10\n\n\n\n=== TEST 6: change sample ratio\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9999/mock_zipkin\",\n                                \"sample_ratio\": 0.00001\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: not tiger zipkin\n--- request\nGET /opentracing\n--- response_body\nopentracing\n\n\n\n=== TEST 8: disabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: not tiger zipkin\n--- request\nGET /opentracing\n--- response_body\nopentracing\n\n\n\n=== TEST 10: set plugin with external ip address\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:1980/mock_zipkin?server_addr=1.2.3.4\",\n                                \"sample_ratio\": 1,\n                                \"service_name\": \"apisix\",\n                                \"server_addr\": \"1.2.3.4\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: tiger zipkin\n--- request\nGET /opentracing\n--- wait: 10\n\n\n\n=== TEST 12: sanity server_addr\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.plugins.zipkin\")\n            local ok, err = plugin.check_schema({\n                endpoint = 'http://127.0.0.1',\n                sample_ratio = 0.001,\n                server_addr = 'badip'\n            })\n            if not ok then\n                ngx.say(err)\n            else\n                ngx.say(\"done\")\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nproperty \"server_addr\" validation failed: failed to match pattern \"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$\" with \"badip\"\n\n\n\n=== TEST 13: check zipkin headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9999/mock_zipkin\",\n                                \"sample_ratio\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: set x-b3-sampled if sampled\n--- request\nGET /echo\n--- response_headers\nx-b3-sampled: 1\n\n\n\n=== TEST 15: don't sample if disabled\n--- request\nGET /echo\n--- more_headers\nx-b3-sampled: 0\n--- response_headers\nx-b3-sampled: 0\n\n\n\n=== TEST 16: don't sample if disabled (old way)\n--- request\nGET /echo\n--- more_headers\nx-b3-sampled: false\n--- response_headers\nx-b3-sampled: 0\n\n\n\n=== TEST 17: sample according to the header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9999/mock_zipkin\",\n                                \"sample_ratio\": 0.00001\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: don't sample by default\n--- request\nGET /echo\n--- response_headers\nx-b3-sampled: 0\n\n\n\n=== TEST 19: sample if needed\n--- request\nGET /echo\n--- more_headers\nx-b3-sampled: 1\n--- response_headers\nx-b3-sampled: 1\n\n\n\n=== TEST 20: sample if debug\n--- request\nGET /echo\n--- more_headers\nx-b3-flags: 1\n--- response_headers\nx-b3-sampled: 1\n\n\n\n=== TEST 21: sample if needed (old way)\n--- request\nGET /echo\n--- more_headers\nx-b3-sampled: true\n--- response_headers\nx-b3-sampled: 1\n\n\n\n=== TEST 22: don't cache the per-req sample ratio\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/echo\"\n            -- force to trace\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    ['x-b3-sampled'] = 1\n                }\n            })\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.headers['x-b3-sampled'])\n\n            -- force not to trace\n            local res, err = httpc:request_uri(uri, {\n                method = \"GET\",\n                headers = {\n                    ['x-b3-sampled'] = 0\n                }\n            })\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.headers['x-b3-sampled'])\n        }\n    }\n--- request\nGET /t\n--- response_body\n1\n0\n\n\n\n=== TEST 23: no error in log phase while b3 header invalid\n--- request\nGET /echo\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7\n--- response_headers\nx-b3-sampled:\n--- error_code: 400\n--- error_log\ninvalid b3 header\n--- no_error_log\nattempt to index local 'opentracing' (a nil value)\n"
  },
  {
    "path": "t/plugin/zipkin2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /echo\");\n    }\n\n    if (!$block->no_error_log && !$block->error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $extra_init_by_lua = <<_EOC_;\n    local new = require(\"opentracing.tracer\").new\n    local tracer_mt = getmetatable(new()).__index\n    local orig_func = tracer_mt.start_span\n    tracer_mt.start_span = function (...)\n        local orig = orig_func(...)\n        local mt = getmetatable(orig).__index\n        local old_start_child_span = mt.start_child_span\n        mt.start_child_span = function(self, name, time)\n            ngx.log(ngx.WARN, \"zipkin start_child_span \", name, \" time: \", time)\n            return old_start_child_span(self, name, time)\n        end\n        return orig\n    end\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: b3 single header\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9999/mock_zipkin\",\n                                \"sample_ratio\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: sanity\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90\n--- response_headers\nx-b3-sampled: 1\nx-b3-traceid: 80f198ee56343ba864fe8b2a57d3eff7\n--- raw_response_headers_unlike\nb3:\n--- error_log\nnew span context: trace id: 80f198ee56343ba864fe8b2a57d3eff7, span id: e457b5a2e4d86bd1, parent span id: 05e3ac9a4f6e3b90\n--- grep_error_log eval\nqr/zipkin start_child_span apisix.response_span time: nil/\n--- grep_error_log_out\n\n\n\n=== TEST 3: invalid header\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7\n--- response_headers\nx-b3-sampled:\n--- error_code: 400\n--- error_log\ninvalid b3 header\n\n\n\n=== TEST 4: disable via b3\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-0-05e3ac9a4f6e3b90\n--- response_headers_like\nx-b3-sampled: 0\nx-b3-traceid: 80f198ee56343ba864fe8b2a57d3eff7\nx-b3-parentspanid: e457b5a2e4d86bd1\nx-b3-spanid: \\w+\n\n\n\n=== TEST 5: disable via b3 (abbr)\n--- more_headers\nb3: 0\n--- response_headers_like\nx-b3-sampled: 0\nx-b3-spanid: \\w+\nx-b3-traceid: \\w+\n\n\n\n=== TEST 6: debug via b3\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-d-05e3ac9a4f6e3b90\n--- response_headers\nx-b3-sampled: 1\nx-b3-flags: 1\n\n\n\n=== TEST 7: b3 without parent span id\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-d\n--- response_headers\nx-b3-sampled: 1\nx-b3-flags: 1\n--- error_log\nnew span context: trace id: 80f198ee56343ba864fe8b2a57d3eff7, span id: e457b5a2e4d86bd1, parent span id: nil\n\n\n\n=== TEST 8: b3 without sampled & parent span id\n--- more_headers\nb3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1\n--- response_headers\nx-b3-sampled: 1\n--- error_log\nnew span context: trace id: 80f198ee56343ba864fe8b2a57d3eff7, span id: e457b5a2e4d86bd1, parent span id: nil\n\n\n\n=== TEST 9: set plugin with span version 1\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:1980/mock_zipkin?span_version=1\",\n                                \"sample_ratio\": 1,\n                                \"service_name\": \"apisix\",\n                                \"span_version\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 10: tiger zipkin\n--- request\nGET /opentracing\n--- wait: 10\n--- grep_error_log eval\nqr/zipkin start_child_span apisix.response_span time: nil/\n--- grep_error_log_out\n\n\n\n=== TEST 11: check not error with limit count\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9999/mock_zipkin\",\n                                \"sample_ratio\": 1,\n                                \"service_name\": \"APISIX\"\n                            },\n                            \"limit-count\": {\n                                \"count\": 2,\n                                \"time_window\": 60,\n                                \"rejected_code\": 403,\n                                \"key\": \"remote_addr\"\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/opentracing\"\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- pipelined_requests eval\n[\"GET /t\", \"GET /opentracing\", \"GET /opentracing\", \"GET /opentracing\"]\n--- error_code eval\n[200, 200, 200, 403]\n"
  },
  {
    "path": "t/plugin/zipkin3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nplugins:\n    - zipkin\nplugin_attr:\n    zipkin:\n        set_ngx_var: true\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $upstream_server_config = $block->upstream_server_config // <<_EOC_;\n        set \\$zipkin_context_traceparent \"\";\n        set \\$zipkin_trace_id \"\";\n        set \\$zipkin_span_id \"\";\n_EOC_\n\n    $block->set_value(\"upstream_server_config\", $upstream_server_config);\n\n    my $extra_init_by_lua = <<_EOC_;\n    local zipkin = require(\"apisix.plugins.zipkin\")\n    local orig_func = zipkin.access\n    zipkin.access = function (...)\n        local traceparent = ngx.var.zipkin_context_traceparent\n        if traceparent == nil or traceparent == '' then\n           ngx.log(ngx.ERR,\"ngx_var.zipkin_context_traceparent is empty\")\n        else\n            ngx.log(ngx.ERR,\"ngx_var.zipkin_context_traceparent:\",ngx.var.zipkin_context_traceparent)\n        end\n\n        local trace_id = ngx.var.zipkin_trace_id\n        if trace_id == nil or trace_id == '' then\n           ngx.log(ngx.ERR,\"ngx_var.zipkin_trace_id is empty\")\n        else\n            ngx.log(ngx.ERR,\"ngx_var.zipkin_trace_id:\",ngx.var.zipkin_trace_id)\n        end\n\n        local span_id = ngx.var.zipkin_span_id\n        if span_id == nil or span_id == '' then\n           ngx.log(ngx.ERR,\"ngx_var.zipkin_span_id is empty\")\n        else\n            ngx.log(ngx.ERR,\"ngx_var.zipkin_span_id:\",ngx.var.zipkin_span_id)\n        end\n\n        local orig = orig_func(...)\n        return orig\n    end\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n\n    if (!$block->request) {\n            $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add plugin metadata\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"zipkin\": {\n                                \"endpoint\": \"http://127.0.0.1:9999/mock_zipkin\",\n                                \"sample_ratio\": 1\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/echo\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2:  trigger zipkin with open set variables\n--- request\nGET /echo\n--- error_log eval\nqr/ngx_var.zipkin_context_traceparent:00-\\w{32}-\\w{16}-01*/\n\n\n\n=== TEST 3: trigger zipkin with open set variables\n--- request\nGET /echo\n--- error_log eval\nqr/ngx_var.zipkin_trace_id:\\w{32}/\n\n\n\n=== TEST 4: trigger zipkin with open set variables\n--- request\nGET /echo\n--- error_log eval\nqr/ngx_var.zipkin_span_id:\\w{16}/\n\n\n\n=== TEST 5: trigger zipkin with disable set variables\n--- extra_yaml_config\nplugins:\n    - zipkin\nplugin_attr:\n    zipkin:\n        set_ngx_var: false\n--- request\nGET /echo\n--- error_log\nngx_var.zipkin_context_traceparent is empty\n"
  },
  {
    "path": "t/pubsub/kafka.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse Cwd qw(cwd);\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nmy $apisix_home = $ENV{APISIX_HOME} // cwd();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $block_init = <<_EOC_;\n    `ln -sf $apisix_home/apisix $apisix_home/t/servroot/apisix`;\n_EOC_\n\n    $block->set_value(\"init\", $block_init);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nadd_test_cleanup_handler(sub {\n    `rm -f $apisix_home/t/servroot/apisix`;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup all-in-one test\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/kafka\",\n                    data = [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:9092\": 1\n                            },\n                            \"type\": \"none\",\n                            \"scheme\": \"kafka\"\n                        },\n                        \"uri\": \"/kafka\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/kafka-invalid\",\n                    data = [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:59092\": 1\n                            },\n                            \"type\": \"none\",\n                            \"scheme\": \"kafka\"\n                        },\n                        \"uri\": \"/kafka-invalid\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/kafka-tlsv\",\n                    data = [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:9093\": 1\n                            },\n                            \"type\": \"none\",\n                            \"scheme\": \"kafka\",\n                            \"tls\": {\n                                \"verify\": true\n                            }\n                        },\n                        \"uri\": \"/kafka-tlsv\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/kafka-tls\",\n                    data = [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:9093\": 1\n                            },\n                            \"type\": \"none\",\n                            \"scheme\": \"kafka\",\n                            \"tls\": {\n                                \"verify\": false\n                            }\n                        },\n                        \"uri\": \"/kafka-tls\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/kafka-sasl\",\n                    data = [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:9094\": 1\n                            },\n                            \"type\": \"none\",\n                            \"scheme\": \"kafka\"\n                        },\n                        \"uri\": \"/kafka-sasl\",\n                        \"plugins\": {\n                            \"kafka-proxy\": {\n                                \"sasl\": {\n                                    \"username\": \"admin\",\n                                    \"password\": \"admin-secret\"\n                                }\n                            }\n                        }\n                    }]],\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\"x5\n\n\n\n=== TEST 2: hit route (with HTTP request)\n--- request\nGET /kafka\n--- error_code: 400\n--- error_log\nfailed to initialize pubsub module, err: bad \"upgrade\" request header: nil\n\n\n\n=== TEST 3: hit route (Kafka)\n--- config\n    # The messages used in this test are produced in the linux-ci-init-service.sh\n    # script that prepares the CI environment\n    location /t {\n        content_by_lua_block {\n            local pb         = require(\"pb\")\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/kafka\")\n            local data = {\n                {\n                    sequence = 0,\n                    cmd_kafka_list_offset = {\n                        topic = \"not-exist\",\n                        partition = 0,\n                        timestamp = -1,\n                    },\n                },\n                {\n                    sequence = 1,\n                    cmd_kafka_fetch = {\n                        topic = \"not-exist\",\n                        partition = 0,\n                        offset = 0,\n                    },\n                },\n                {\n                    -- Query first message offset\n                    sequence = 2,\n                    cmd_kafka_list_offset = {\n                        topic = \"test-consumer\",\n                        partition = 0,\n                        timestamp = -2,\n                    },\n                },\n                {\n                    -- Query last message offset\n                    sequence = 3,\n                    cmd_kafka_list_offset = {\n                        topic = \"test-consumer\",\n                        partition = 0,\n                        timestamp = -1,\n                    },\n                },\n                {\n                    -- Query by timestamp, 9999999999999 later than the\n                    -- production time of any message\n                    sequence = 4,\n                    cmd_kafka_list_offset = {\n                        topic = \"test-consumer\",\n                        partition = 0,\n                        timestamp = \"9999999999999\",\n                    },\n                },\n                {\n                    -- Query by timestamp, 1500000000000 ms earlier than the\n                    -- production time of any message\n                    sequence = 5,\n                    cmd_kafka_list_offset = {\n                        topic = \"test-consumer\",\n                        partition = 0,\n                        timestamp = \"1500000000000\",\n                    },\n                },\n                {\n                    sequence = 6,\n                    cmd_kafka_fetch = {\n                        topic = \"test-consumer\",\n                        partition = 0,\n                        offset = 14,\n                    },\n                },\n                {\n                    sequence = 7,\n                    cmd_kafka_fetch = {\n                        topic = \"test-consumer\",\n                        partition = 0,\n                        offset = 999,\n                    },\n                },\n            }\n\n            for i = 1, #data do\n                -- force clear state\n                pb.state(nil)\n                local data = test_pubsub:send_recv_ws_binary(data[i])\n                if data.error_resp then\n                    ngx.say(data.sequence..data.error_resp.message)\n                end\n                if data.kafka_list_offset_resp then\n                    ngx.say(data.sequence..\"offset: \"..data.kafka_list_offset_resp.offset)\n                end\n                if data.kafka_fetch_resp then\n                    ngx.say(data.sequence..\"offset: \"..data.kafka_fetch_resp.messages[1].offset..\n                        \" msg: \"..data.kafka_fetch_resp.messages[1].value)\n                end\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\n0failed to list offset, topic: not-exist, partition: 0, err: not found topic\n1failed to fetch message, topic: not-exist, partition: 0, err: not found topic\n2offset: 0\n3offset: 30\n4offset: -1\n5offset: 0\n6offset: 14 msg: testmsg15\n7failed to fetch message, topic: test-consumer, partition: 0, err: OFFSET_OUT_OF_RANGE\n\n\n\n=== TEST 4: hit route (Kafka with invalid node ip)\n--- config\n    # The messages used in this test are produced in the linux-ci-init-service.sh\n    # script that prepares the CI environment\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/kafka-invalid\")\n\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_kafka_list_offset = {\n                    topic = \"test-consumer\",\n                    partition = 0,\n                    timestamp = -2,\n                },\n            })\n            if data.error_resp then\n                ngx.say(data.sequence..data.error_resp.message)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\n0failed to list offset, topic: test-consumer, partition: 0, err: not found topic\n--- error_log\nall brokers failed in fetch topic metadata\n\n\n\n=== TEST 5: hit route (Kafka with TLS)\n--- config\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/kafka-tls\")\n\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_kafka_list_offset = {\n                    topic = \"test-consumer\",\n                    partition = 0,\n                    timestamp = -1,\n                },\n            })\n            if data.kafka_list_offset_resp then\n                ngx.say(data.sequence..\"offset: \"..data.kafka_list_offset_resp.offset)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\n0offset: 30\n\n\n\n=== TEST 6: hit route (Kafka with TLS + ssl verify)\n--- config\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/kafka-tlsv\")\n\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_kafka_list_offset = {\n                    topic = \"test-consumer\",\n                    partition = 0,\n                    timestamp = -1,\n                },\n            })\n            if data.kafka_list_offset_resp then\n                ngx.say(data.sequence..\"offset: \"..data.kafka_list_offset_resp.offset)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- error_log eval\nqr/self[- ]signed certificate/\n\n\n\n=== TEST 7: hit route (Kafka with SASL)\n--- config\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/kafka-sasl\")\n\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_kafka_list_offset = {\n                    topic = \"test-consumer\",\n                    partition = 0,\n                    timestamp = -1,\n                },\n            })\n            if data.kafka_list_offset_resp then\n                ngx.say(data.sequence..\"offset: \"..data.kafka_list_offset_resp.offset)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\n0offset: 30\n"
  },
  {
    "path": "t/pubsub/pubsub.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse Cwd qw(cwd);\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nmy $apisix_home = $ENV{APISIX_HOME} // cwd();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $block_init = <<_EOC_;\n    `ln -sf $apisix_home/apisix $apisix_home/t/servroot/apisix`;\n_EOC_\n\n    $block->set_value(\"init\", $block_init);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nadd_test_cleanup_handler(sub {\n    `rm -f $apisix_home/t/servroot/apisix`;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup route by serverless\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t(\"/apisix/admin/routes/pubsub\", ngx.HTTP_PUT, {\n                plugins = {\n                    [\"serverless-pre-function\"] = {\n                        phase = \"access\",\n                        functions =  {\n                            [[return function(conf, ctx)\n                                local core = require(\"apisix.core\");\n                                local pubsub, err = core.pubsub.new()\n                                if not pubsub then\n                                    core.log.error(\"failed to initialize pubsub module, err: \", err)\n                                    core.response.exit(400)\n                                    return\n                                end\n                                pubsub:on(\"cmd_ping\", function (params)\n                                    if params.state == \"test\" then\n                                        return {pong_resp = {state = \"test\"}}\n                                    end\n                                    return nil, \"error\"\n                                end)\n                                pubsub:wait()\n                                ngx.exit(0)\n                            end]],\n                        }\n                    }\n                },\n                uri = \"/pubsub\"\n            })\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route (with HTTP request)\n--- request\nGET /pubsub\n--- error_code: 400\n--- error_log\nfailed to initialize pubsub module, err: bad \"upgrade\" request header: nil\n\n\n\n=== TEST 3: connect websocket service\n--- config\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/pubsub\")\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_ping = {\n                    state = \"test\"\n                },\n            })\n            if data and data.pong_resp then\n                ngx.say(\"ret: \", data.pong_resp.state)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\nret: test\n\n\n\n=== TEST 4: connect websocket service (return error)\n--- config\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/pubsub\")\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_ping = {\n                    state = \"non-test\"\n                },\n            })\n            if data and data.error_resp then\n                ngx.say(\"ret: \", data.error_resp.message)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\nret: error\n\n\n\n=== TEST 5: send unregistered command\n--- config\n    location /t {\n        content_by_lua_block {\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/pubsub\")\n            local data = test_pubsub:send_recv_ws_binary({\n                sequence = 0,\n                cmd_empty = {},\n            })\n            if data and data.error_resp then\n                ngx.say(data.error_resp.message)\n            end\n            test_pubsub:close_ws()\n        }\n    }\n--- response_body\nunknown command\n--- error_log\npubsub callback handler not registered for the command, command: cmd_empty\n\n\n\n=== TEST 6: send text command (server skip command, keep connection)\n--- config\n    location /t {\n        lua_check_client_abort on;\n        content_by_lua_block {\n            ngx.on_abort(function ()\n                ngx.log(ngx.ERR, \"text command is skipped, and close connection\")\n                ngx.exit(444)\n            end)\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/pubsub\")\n            test_pubsub:send_recv_ws_text(\"test\")\n            test_pubsub:close_ws()\n        }\n    }\n--- abort\n--- ignore_response\n--- error_log\npubsub server receive non-binary data, type: text, data: test\ntext command is skipped, and close connection\nfatal error in pubsub websocket server, err: failed to receive the first 2 bytes: closed\n\n\n\n=== TEST 7: send wrong command: empty (server skip command, keep connection)\n--- config\n    location /t {\n        lua_check_client_abort on;\n        content_by_lua_block {\n            ngx.on_abort(function ()\n                ngx.log(ngx.ERR, \"empty command is skipped, and close connection\")\n                ngx.exit(444)\n            end)\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/pubsub\")\n            test_pubsub:send_recv_ws_binary({})\n            test_pubsub:close_ws()\n        }\n    }\n--- abort\n--- ignore_response\n--- error_log\npubsub server receives empty command\nempty command is skipped, and close connection\nfatal error in pubsub websocket server, err: failed to receive the first 2 bytes: closed\n\n\n\n=== TEST 8: send wrong command: undecodable (server skip command, keep connection)\n--- config\n    location /t {\n        lua_check_client_abort on;\n        content_by_lua_block {\n            ngx.on_abort(function ()\n                ngx.log(ngx.ERR, \"empty command is skipped, and close connection\")\n                ngx.exit(444)\n            end)\n            local lib_pubsub = require(\"lib.pubsub\")\n            local test_pubsub = lib_pubsub.new_ws(\"ws://127.0.0.1:1984/pubsub\")\n            test_pubsub:send_recv_ws_binary(\"!@#$%^&*中文\", true)\n            test_pubsub:close_ws()\n        }\n    }\n--- abort\n--- ignore_response\n--- error_log\npubsub server receives empty command\nempty command is skipped, and close connection\nfatal error in pubsub websocket server, err: failed to receive the first 2 bytes: closed\n"
  },
  {
    "path": "t/router/graphql.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_root_location();\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route by name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"POST\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"graphql_name\", \"==\", \"repo\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: route by name\n--- request\nPOST /hello\nquery repo {\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n\n\n\n=== TEST 3: set route by operation+name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"POST\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [\n                            [\"graphql_operation\", \"==\", \"mutation\"],\n                            [\"graphql_name\", \"==\", \"repo\"]\n                        ]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: route by operation+name\n--- request\nPOST /hello\nmutation repo($ep: Episode!, $review: ReviewInput!) {\n  createReview(episode: $ep, review: $review) {\n    stars\n    commentary\n  }\n}\n--- response_body\nhello world\n\n\n\n=== TEST 5: route by operation+name, miss\n--- request\nPOST /hello\nquery repo {\n    owner {\n        name\n    }\n}\n--- error_code: 404\n\n\n\n=== TEST 6: multiple operations\n--- request\nPOST /hello\nmutation repo($ep: Episode!, $review: ReviewInput!) {\n  createReview(episode: $ep, review: $review) {\n    stars\n    commentary\n  }\n}\nquery repo {\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n--- error_log\nMultiple operations are not supported\n\n\n\n=== TEST 7: bad graphql\n--- request\nPOST /hello\nAA\n--- error_code: 404\n--- error_log\nfailed to parse graphql: Syntax error near line 1 body: AA\n\n\n\n=== TEST 8: set anonymous operation name\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"POST\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [\n                            [\"graphql_operation\", \"==\", \"query\"],\n                            [\"graphql_name\", \"==\", \"\"]\n                        ]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: route by anonymous name\n--- request\nPOST /hello\nquery {\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n\n\n\n=== TEST 10: limit the max size\n--- yaml_config\ngraphql:\n    max_size: 5\n--- request\nPOST /hello\nquery {\n    owner {\n        name\n    }\n}\n--- error_code: 404\n--- error_log\nfailed to read graphql data\n\n\n\n=== TEST 11: set graphql_root_fields\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"POST\", \"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [\n                            [\"graphql_operation\", \"==\", \"query\"],\n                            [\"graphql_root_fields\", \"has\", \"owner\"]\n                        ]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 12: single root field\n--- request\nPOST /hello\nquery {\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n\n\n\n=== TEST 13: test send http post json data\n--- request\nPOST /hello\n{\"query\":\"query{owner{name}}\"}\n--- more_headers\nContent-Type: application/json\n--- response_body\nhello world\n\n\n\n=== TEST 14: test send http get query data\n--- request\nGET /hello?query=query{owner{name}}\n--- response_body\nhello world\n\n\n\n=== TEST 15: test send http get multiple query data success\n--- request\nGET /hello?query=query{owner{name}}&query=query{repo{name}}\n--- response_body\nhello world\n\n\n\n=== TEST 16: test send http get multiple query data failure\n--- request\nGET /hello?query=query{repo{name}}&query=query{owner{name}}\n--- error_code: 404\n\n\n\n=== TEST 17: no body (HTTP GET)\n--- request\nGET /hello\n--- error_code: 404\n--- error_log\nfailed to read graphql data, args[query] is nil\n\n\n\n=== TEST 18: no body (HTTP POST JSON)\n--- request\nPOST /hello\n{}\n--- more_headers\nContent-Type: application/json\n--- error_code: 404\n--- error_log\nfailed to read graphql data, json body[query] is nil\n\n\n\n=== TEST 19: multiple root fields\n--- request\nPOST /hello\nquery {\n    repo {\n        stars\n    }\n    owner {\n        name\n    }\n}\n--- response_body\nhello world\n\n\n\n=== TEST 20: root fields mismatch\n--- request\nPOST /hello\nquery {\n    repo {\n        name\n    }\n}\n--- error_code: 404\n\n\n\n=== TEST 21: no body\n--- request\nPOST /hello\n--- error_code: 404\n--- error_log\nfailed to read graphql data, request body has zero size\n"
  },
  {
    "path": "t/router/multi-ssl-certs.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nno_root_location();\n\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set ssl(sni: www.test.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: client request\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: www.test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 4: set second ssl(sni: *.test2.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"*.test2.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/2',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"*.test2.com\"\n                },\n                \"key\": \"/apisix/ssls/2\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: client request: www.test2.com\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body_like\nconnected: 1\nfailed to do SSL handshake: 18: self[- ]signed certificate\n--- error_log\nserver name: \"www.test2.com\"\nwe have more than 1 ssl certs now\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 6: set third ssl(sni: apisix.dev)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix_admin_ssl.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix_admin_ssl.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"apisix.dev\"}\n\n        local code, body = t.test('/apisix/admin/ssls/3',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"apisix.dev\"\n                },\n                \"key\": \"/apisix/ssls/3\"\n            }]]\n            )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: client request: apisix.dev\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"apisix.dev\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body_like\nconnected: 1\nfailed to do SSL handshake: 18: self[- ]signed certificate\n--- error_log\nserver name: \"apisix.dev\"\nwe have more than 1 ssl certs now\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 8: remove test ssl certs\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        t.test('/apisix/admin/ssls/1', ngx.HTTP_DELETE)\n        t.test('/apisix/admin/ssls/2', ngx.HTTP_DELETE)\n        t.test('/apisix/admin/ssls/3', ngx.HTTP_DELETE)\n\n    }\n}\n--- request\nGET /t\n"
  },
  {
    "path": "t/router/radixtree-host-uri-priority.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_host_uri'\n    admin_key: null\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: hit routes(priority: 1 + priority: 2)\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n    priority: 1\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    priority: 2\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: test.com\n--- response_body eval\nqr/1980/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 2: hit routes(priority: 2 + priority: 1)\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n    priority: 2\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    priority: 1\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: test.com\n--- response_body eval\nqr/1981/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 3: hit routes(priority: default_value + priority: 1)\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n    priority: 1\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: test.com\n--- response_body eval\nqr/1980/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 4: hit routes(priority: 1 + priority: default_value)\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n    priority: 1\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: test.com\n--- response_body eval\nqr/1981/\n--- error_log\nuse config_provider: yaml\n"
  },
  {
    "path": "t/router/radixtree-host-uri.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_host_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(host + uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"foo.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /hello\n--- more_headers\nHost: not_found.com\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 6: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 7: set route(only uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 9: hit routes\n--- request\nGET /server_port\n--- more_headers\nHost: anydomain.com\n--- response_body_like eval\nqr/1981/\n\n\n\n=== TEST 10: set route(only uri + id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: /not_found\n--- request\nGET /hello2\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 12: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: anydomain.com\n--- response_body\nhello world\n\n\n\n=== TEST 13: delete route(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: set route(wildcard host + uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"host\": \"*.foo.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: www.foo.com\n--- response_body\nhello world\n"
  },
  {
    "path": "t/router/radixtree-host-uri2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_host_uri'\n    admin_key: null\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: test.com\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n  -\n    uri: /server_port\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: test.com\n--- response_body eval\nqr/1981/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 2: *.test.com + uri\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: \"*.test.com\"\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n  -\n    uri: /server_port\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: www.test.com\n--- response_body eval\nqr/1981/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 3: *.test.com + /*\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    host: \"*.test.com\"\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n  -\n    uri: /server_port\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n\n--- request\nGET /server_port\n--- more_headers\nHost: www.test.com\n--- response_body eval\nqr/1981/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 4: filter_func(not match)\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    host: \"*.test.com\"\n    filter_func: \"function(vars) return vars.arg_name == 'json' end\"\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n  -\n    uri: /server_port\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n\n--- request\nGET /server_port?name=unknown\n--- more_headers\nHost: www.test.com\n--- response_body eval\nqr/1980/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 5: filter_func(match)\n--- apisix_yaml\nroutes:\n  -\n    uri: /*\n    host: \"*.test.com\"\n    filter_func: \"function(vars) return vars.arg_name == 'json' end\"\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n  -\n    uri: /server_port\n    upstream:\n        nodes:\n            \"127.0.0.1:1980\": 1\n        type: roundrobin\n#END\n\n--- request\nGET /server_port?name=json\n--- more_headers\nHost: www.test.com\n--- response_body eval\nqr/1981/\n--- error_log\nuse config_provider: yaml\n\n\n\n=== TEST 6: set route with ':'\n--- yaml_config\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_host_uri'\n    admin_key: null\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/file:listReputationHistories\",\n                    \"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}},\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit routes\n--- yaml_config\napisix:\n    router:\n        http: 'radixtree_host_uri'\n--- request\nGET /file:listReputationHistories\n--- response_body\nhello world\n\n\n\n=== TEST 8: not hit\n--- yaml_config\napisix:\n    router:\n        http: 'radixtree_host_uri'\n--- request\nGET /file:xx\n--- error_code: 404\n\n\n\n=== TEST 9: set route with ':' & host\n--- yaml_config\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_host_uri'\n    admin_key: null\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/do:listReputationHistories\",\n                    \"hosts\": [\"t.com\"],\n                    \"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}},\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit routes\n--- yaml_config\napisix:\n    router:\n        http: 'radixtree_host_uri'\n--- request\nGET /do:listReputationHistories\n--- more_headers\nHost: t.com\n--- response_body\nhello world\n\n\n\n=== TEST 11: not hit\n--- yaml_config\napisix:\n    router:\n        http: 'radixtree_host_uri'\n--- request\nGET /do:xx\n--- more_headers\nHost: t.com\n--- error_code: 404\n\n\n\n=== TEST 12: request host with uppercase\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.com\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n#END\n--- request\nGET /server_port\n--- more_headers\nHost: tEst.com\n\n\n\n=== TEST 13: configure host with uppercase\n--- apisix_yaml\nroutes:\n  -\n    uri: /server_port\n    host: test.coM\n    upstream:\n        nodes:\n            \"127.0.0.1:1981\": 1\n        type: roundrobin\n#END\n--- request\nGET /server_port\n--- more_headers\nHost: test.com\n\n\n\n=== TEST 14: inherit hosts from services\n--- apisix_yaml\nservices:\n  - id: 1\n    hosts:\n      - bar.com\nupstreams:\n  - id: 1\n    nodes:\n      \"127.0.0.1:1980\": 1\n    type: roundrobin\nroutes:\n  -\n    service_id: 1\n    upstream_id: 1\n    uri: /hello\n    plugins:\n        proxy-rewrite:\n            uri: /hello1\n  -\n    upstream_id: 1\n    uri: /hello\n    priority: -1\n#END\n--- more_headers\nHost: www.foo.com\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/router/radixtree-host-uri3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_host_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if (!$block->error_log && !$block->no_error_log &&\n        (defined $block->error_code && $block->error_code != 502))\n    {\n        $block->set_value(\"no_error_log\", \"[error]\");\n    }\n\n    $block;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: change hosts in services\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"hosts\": [\"foo.com\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"service_id\": \"1\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            for _, h in ipairs({\"foo.com\", \"bar.com\"}) do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {Host = h}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                if res.status == 404 then\n                    ngx.say(res.status)\n                else\n                    ngx.print(res.body)\n                end\n            end\n\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"hosts\": [\"bar.com\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            for _, h in ipairs({\"foo.com\", \"bar.com\"}) do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {Host = h}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                if res.status == 404 then\n                    ngx.say(res.status)\n                else\n                    ngx.print(res.body)\n                end\n            end\n        }\n    }\n--- response_body\nhello world\n404\n404\nhello world\n\n\n\n=== TEST 2: check matched._path\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"hosts\": [\"foo.com\"],\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.log(ngx.WARN, 'matched uri: ', ctx.curr_req_matched._path);\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: hit, plain path\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- grep_error_log eval\nqr/matched uri: \\/\\w+/\n--- grep_error_log_out\nmatched uri: /hello\n\n\n\n=== TEST 4: check matched._path, wildcard\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"hosts\": [\"foo.com\"],\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.log(ngx.WARN, 'matched uri: ', ctx.curr_req_matched._path);\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- grep_error_log eval\nqr/matched uri: \\/\\S+,/\n--- grep_error_log_out\nmatched uri: /*,\n\n\n\n=== TEST 6: check matched._host\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"hosts\": [\"foo.com\"],\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.log(ngx.WARN, 'matched host: ', ctx.curr_req_matched._host);\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- grep_error_log eval\nqr/func\\(\\): matched host: [^,]+/\n--- grep_error_log_out\nfunc(): matched host: foo.com\n\n\n\n=== TEST 8: check matched._host, wildcard\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"hosts\": [\"*.com\"],\n                    \"plugins\": {\n                        \"serverless-post-function\": {\n                            \"functions\" : [\"return function(conf, ctx)\n                                        ngx.log(ngx.WARN, 'matched host: ', ctx.curr_req_matched._host);\n                                        end\"]\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- grep_error_log eval\nqr/func\\(\\): matched host: [^,]+/\n--- grep_error_log_out\nfunc(): matched host: *.com\n"
  },
  {
    "path": "t/router/radixtree-method.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route without PURGE method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: route mismatch\n--- request\nPURGE /hello\n--- error_code: 404\n\n\n\n=== TEST 3: set route with PURGE method\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\", \"PURGE\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: route match PURGE method\n--- request\nPURGE /hello\n--- error_code: 200\n"
  },
  {
    "path": "t/router/radixtree-sni.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nno_root_location();\n\n$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set ssl(sni: www.test.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: client request\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: www.test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 4: client request(no cert domain)\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"no-cert.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n        end\n    }\n}\n--- response_body\nconnected: 1\nfailed to do SSL handshake: handshake failed\n--- error_log\nfailed to match any SSL certificate by SNI\n\n\n\n=== TEST 5: set ssl(sni: wildcard)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"*.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"*.test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 6: client request\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: www.test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 7: set ssl(sni: test.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 8: client request: test.com\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"test.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n\n                ngx.say(\"received: \", line)\n            end\n\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 58 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"test.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 9: set ssl(sni: *.test2.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"*.test2.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"*.test2.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 10: client request: www.test2.com\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body_like\nconnected: 1\nfailed to do SSL handshake: 18: self[- ]signed certificate\n--- error_log\nserver name: \"www.test2.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 11: client request: aa.bb.test2.com\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"aa.bb.test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nconnected: 1\nfailed to do SSL handshake: handshake failed\n--- error_log\nserver name: \"aa.bb.test2.com\"\nfailed to find any SSL certificate by SNI: aa.bb.test2.com matched SNI: *.test2.com\n--- no_error_log\n[alert]\n\n\n\n=== TEST 12: disable ssl(sni: *.test2.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local data = {status = 0}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PATCH,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"status\": 0\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 13: client request: www.test2.com -- failed by disable\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nconnected: 1\nfailed to do SSL handshake: handshake failed\n--- error_log\nserver name: \"www.test2.com\"\n--- no_error_log\n[alert]\n\n\n\n=== TEST 14: enable ssl(sni: *.test2.com)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local data = {status = 1}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PATCH,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"status\": 1\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 15: client request: www.test2.com again\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body_like\nconnected: 1\nfailed to do SSL handshake: 18: self[- ]signed certificate\n--- error_log\nserver name: \"www.test2.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 16: set ssl(snis: {test2.com, *.test2.com})\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, snis = {\"test2.com\", \"*.test2.com\"}}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"snis\": [\"test2.com\", \"*.test2.com\"]\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 17: client request: test2.com\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body_like\nconnected: 1\nfailed to do SSL handshake: 18: self[- ]signed certificate\n--- error_log\nserver name: \"test2.com\"\n--- no_error_log\n[error]\n[alert]\n\n\n\n=== TEST 18: client request: aa.bb.test2.com  -- snis un-include\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"aa.bb.test2.com\", true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nconnected: 1\nfailed to do SSL handshake: handshake failed\n--- error_log\nserver name: \"aa.bb.test2.com\"\nfailed to find any SSL certificate by SNI: aa.bb.test2.com matched SNIs: [\"*.test2.com\",\"test2.com\"]\n--- no_error_log\n[alert]\n\n\n\n=== TEST 19: set ssl(encrypt ssl key with another iv)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.aes_encrypt(t.read_file(\"t/certs/test2.key\"))\n        local data = {cert = ssl_cert, key = ssl_key, snis = {\"test2.com\", \"*.test2.com\"}}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n            )\n\n        ngx.status = code\n        ngx.print(body)\n    }\n}\n--- response_body\n{\"error_msg\":\"failed to decrypt previous encrypted key\"}\n--- error_code: 400\n--- error_log\ndecrypt ssl key failed\n"
  },
  {
    "path": "t/router/radixtree-sni2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nno_root_location();\n\nBEGIN {\n    $ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n    $ENV{TEST_ENV_SSL_CRT} = \"-----BEGIN CERTIFICATE-----\nMIIEsTCCAxmgAwIBAgIUMbgUUCYHkuKDaPy0bzZowlK0JG4wDQYJKoZIhvcNAQEL\nBQAwVzELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwG\nWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxEjAQBgNVBAMMCXRlc3QyLmNvbTAgFw0y\nMDA0MDQyMjE3NTJaGA8yMTIwMDMxMTIyMTc1MlowVzELMAkGA1UEBhMCQ04xEjAQ\nBgNVBAgMCUd1YW5nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVz\ndHkxEjAQBgNVBAMMCXRlc3QyLmNvbTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCC\nAYoCggGBAMQGBk35V3zaNVDWzEzVGd+EkZnUOrRpXQg5mmcnoKnrQ5rQQMsQCbMO\ngFvLt/9OEZQmbE2HuEKsPzL79Yjdu8rGjSoQdbJZ9ccO32uvln1gn68iK79o7Tvm\nTCi+BayyNA+lo9IxrBm1wGBkOU1ZPasGYzgBAbMLTSDps1EYxNR8t4l9PrTTRsh6\nNZyTYoDeVIsKZ9SckpjWVnxHOkF+AzZzIJJSe2pj572TDLYA/Xw9I4X3L+SHzwTl\niGWNXb2tU367LHERHvensQzdle7mQN2kE5GpB7QPWB+t9V4mn30jc/LyDvOaei6L\n+pbl5CriGBTjaR80oXhK765K720BQeKUezri15bQlMaUGQRnzr53ZsqA4PEh6WCX\nhUT2ibO32+uZFXzVQw8y/JUkPf76pZagi8DoLV+sfSbUtnpbQ8wyV2qqTM2eCuPi\nRgUwXQi2WssKKzrqcgKil3vksHZozLtOmyZiNE4qfNxv+UGoIybJtZmB+9spY0Rw\n5zBRuULycQIDAQABo3MwcTAdBgNVHQ4EFgQUCmZefzpizPrb3VbiIDhrA48ypB8w\nHwYDVR0jBBgwFoAUCmZefzpizPrb3VbiIDhrA48ypB8wDAYDVR0TBAUwAwEB/zAh\nBgNVHREEGjAYggl0ZXN0Mi5jb22CCyoudGVzdDIuY29tMA0GCSqGSIb3DQEBCwUA\nA4IBgQA0nRTv1zm1ACugJFfYZfxZ0mLJfRUCFMmFfhy+vGiIu6QtnOFVw/tEOyMa\nm78lBiqac15n3YWYiHiC5NFffTZ7XVlOjN2i4x2z2IJsHNa8tU80AX0Q/pizGK/d\n+dzlcsGBb9MGT18h/B3/EYQFKLjUsr0zvDb1T0YDlRUsN3Bq6CvZmvfe9F7Yh4Z/\nXO5R+rX8w9c9A2jzM5isBw2qp/Ggn5RQodMwApEYkJdu80MuxaY6s3dssS4Ay8wP\nVNFEeLcdauJ00ES1OnbnuNiYSiSMOgWBsnR+c8AaSRB/OZLYQQKGGYbq0tspwRjM\nMGJRrI/jdKnvJQ8p02abdvA9ZuFChoD3Wg03qQ6bna68ZKPd9peBPpMrDDGDLkGI\nNzZ6bLJKILnQkV6b1OHVnPDsKXfXjUTTNK/QLJejTXu9RpMBakYZMzs/SOSDtFlS\nA+q25t6+46nvA8msUSBKyOGBX42mJcKvR4OgG44PfDjYfmjn2l+Dz/jNXDclpb+Q\nXAzBnfM=\n-----END CERTIFICATE-----\";\n    $ENV{TEST_ENV_SSL_KEY} = \"-----BEGIN RSA PRIVATE KEY-----\nMIIG5QIBAAKCAYEAxAYGTflXfNo1UNbMTNUZ34SRmdQ6tGldCDmaZyegqetDmtBA\nyxAJsw6AW8u3/04RlCZsTYe4Qqw/Mvv1iN27ysaNKhB1sln1xw7fa6+WfWCfryIr\nv2jtO+ZMKL4FrLI0D6Wj0jGsGbXAYGQ5TVk9qwZjOAEBswtNIOmzURjE1Hy3iX0+\ntNNGyHo1nJNigN5Uiwpn1JySmNZWfEc6QX4DNnMgklJ7amPnvZMMtgD9fD0jhfcv\n5IfPBOWIZY1dva1TfrsscREe96exDN2V7uZA3aQTkakHtA9YH631XiaffSNz8vIO\n85p6Lov6luXkKuIYFONpHzSheErvrkrvbQFB4pR7OuLXltCUxpQZBGfOvndmyoDg\n8SHpYJeFRPaJs7fb65kVfNVDDzL8lSQ9/vqllqCLwOgtX6x9JtS2eltDzDJXaqpM\nzZ4K4+JGBTBdCLZayworOupyAqKXe+SwdmjMu06bJmI0Tip83G/5QagjJsm1mYH7\n2yljRHDnMFG5QvJxAgMBAAECggGBAIELlkruwvGmlULKpWRPReEn3NJwLNVoJ56q\njUMri1FRWAgq4PzNahU+jrHfwxmHw3rMcK/5kQwTaOefh1y63E35uCThARqQroSE\n/gBeb6vKWFVrIXG5GbQ9QBXyQroV9r/2Q4q0uJ+UTzklwbNx9G8KnXbY8s1zuyrX\nrvzMWYepMwqIMSfJjuebzH9vZ4F+3BlMmF4XVUrYj8bw/SDwXB0UXXT2Z9j6PC1J\nCS0oKbgIZ8JhoF3KKjcHBGwWTIf5+byRxeG+z99PBEBafm1Puw1vLfOjD3DN/fso\n8xCEtD9pBPBJ+W97x/U+10oKetmP1VVEr2Ph8+s2VH1zsRF5jo5d0GtvJqOwIQJ7\nz3OHJ7lLODw0KAjB1NRXW4dTTUDm6EUuUMWFkGAV6YTyhNLAT0DyrUFJck9RiY48\n3QN8vSf3n/+3wwg1gzcJ9w3W4DUbvGqu86CaUQ4UegfYJlusY/3YGp5bGNQdxmws\nlgIoSRrHp6UJKsP8Yl08MIvT/oNLgQKBwQD75SuDeyE0ukhEp0t6v+22d18hfSef\nq3lLWMI1SQR9Kiem9Z1KdRkIVY8ZAHANm6D8wgjOODT4QZtiqJd2BJn3Xf+aLfCd\nCW0hPvmGTcp/E4sDZ2u0HbIrUStz7ZcgXpjD2JJAJGEKY2Z7J65gnTqbqoBDrw1q\n1+FqtikkHRte1UqxjwnWBpSdoRQFgNPHxPWffhML1xsD9Pk1B1b7JoakYcKsNoQM\noXUKPLxSZEtd0hIydqmhGYTa9QWBPNDlA5UCgcEAxzfGbOrPBAOOYZd3jORXQI6p\nH7SddTHMQyG04i+OWUd0HZFkK7/k6r26GFmImNIsQMB26H+5XoKRFKn+sUl14xHY\nFwB140j0XSav2XzT38UpJ9CptbgK1eKGQVp41xwRYjHVScE5hJuA3a1TKM0l26rp\nhny/KaP+tXuqt9QbxcUN6efubNYyFP+m6nq2/XdX74bJuGpXLq8W0oFdiocO6tmF\n4/Hsc4dCVrcwULqXQa0lJ57zZpfIPARqWM2847xtAoHBANVUNbDpg6rTJMc34722\ndAy3NhL3mqooH9aG+hsEls+l9uT4WFipqSScyU8ERuHPbt0BO1Hi2kFx1rYMUBG8\nPeT4b7NUutVUGV8xpUNv+FH87Bta6CUnjTAQUzuf+QCJ/NjIPrwh0yloG2+roIvk\nPLF/CZfI1hUpdZfZZChYmkiLXPHZURw4gH6q33j1rOYf0WFc9aZua0vDmZame6zB\n6P+oZ6VPmi/UQXoFC/y/QfDYK18fjfOI2DJTlnDoX4XErQKBwGc3M5xMz/MRcJyJ\noIwj5jzxbRibOJV2tpD1jsU9xG/nQHbtVEwCgTVKFXf2M3qSMhFeZn0xZ7ZayZY+\nOVJbcDO0lBPezjVzIAB/Qc7aCOBAQ4F4b+VRtHN6iPqlSESTK0KH9Szgas+UzeCM\no7BZEctNMu7WBSkq6ZXXu+zAfZ8q6HmPDA3hsFMG3dFQwSxzv+C/IhZlKkRqvNVV\n50QVk5oEF4WxW0PECY/qG6NH+YQylDSB+zPlYf4Of5cBCWOoxQKBwQCeo37JpEAR\nkYtqSjXkC5GpPTz8KR9lCY4SDuC1XoSVCP0Tk23GX6GGyEf4JWE+fb/gPEFx4Riu\n7pvxRwq+F3LaAa/FFTNUpY1+8UuiMO7J0B1RkVXkyJjFUF/aQxAnOoZPmzrdZhWy\nbpe2Ka+JS/aXSd1WRN1nmo/DarpWFvdLWZFwUt6zMziH40o1gyPHEuXOqVtf2QCe\nQ6WC9xnEz4lbb/fR2TF9QRA4FtoRpDe/f3ZGIpWE0RdwyZZ6uA7T1+Q=\n-----END RSA PRIVATE KEY-----\";\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1 set ssl with multiple certificates.\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n        local ssl_key = t.read_file(\"t/certs/apisix.key\")\n        local ssl_ecc_cert = t.read_file(\"t/certs/apisix_ecc.crt\")\n        local ssl_ecc_key = t.read_file(\"t/certs/apisix_ecc.key\")\n\n        local data = {\n            cert = ssl_cert,\n            key = ssl_key,\n            certs = { ssl_ecc_cert },\n            keys = { ssl_ecc_key },\n            sni = \"test.com\",\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"test.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 2: client request using ECC certificate\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\nlocation /t {\n    lua_ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384;\n    content_by_lua_block {\n        -- etcd sync\n\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"test.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nconnected: 1\nssl handshake: true\n\n\n\n=== TEST 3: client request using RSA certificate\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    lua_ssl_ciphers ECDHE-RSA-AES256-SHA384;\n    content_by_lua_block {\n        -- etcd sync\n\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"test.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nconnected: 1\nssl handshake: true\n\n\n\n=== TEST 4: set ssl(sni: *.test2.com) once again\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"*.test2.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"*.test2.com\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 5: caching of parsed certs and pkeys\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        local work = function()\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            ngx.say(\"connected: \", ok)\n\n            local sess, err = sock:sslhandshake(nil, \"www.test2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n\n        work()\n        work()\n\n        -- collectgarbage()\n    }\n}\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nclose: 1 nil\nconnected: 1\nssl handshake: true\nclose: 1 nil}\n--- grep_error_log eval\nqr/parsing (cert|(priv key)) for sni: www.test2.com/\n--- grep_error_log_out\nparsing cert for sni: www.test2.com\nparsing priv key for sni: www.test2.com\n\n\n\n=== TEST 6: set ssl(encrypt ssl keys with another iv)\n--- config\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local raw_ssl_key = t.read_file(\"t/certs/test2.key\")\n        local ssl_key = t.aes_encrypt(raw_ssl_key)\n        local data = {\n            certs = { ssl_cert },\n            keys = { ssl_key },\n            snis = {\"test2.com\", \"*.test2.com\"},\n            cert = ssl_cert,\n            key = raw_ssl_key,\n        }\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        ngx.status = code\n        ngx.print(body)\n    }\n}\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to handle cert-key pair[1]: failed to decrypt previous encrypted key\"}\n--- error_log\ndecrypt ssl key failed\n\n\n\n=== TEST 7: set miss_head ssl certificate\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/incorrect.crt\")\n        local ssl_key =  t.read_file(\"t/certs/incorrect.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n            )\n\n        ngx.status = code\n        ngx.print(body)\n    }\n}\n--- response_body\n{\"error_msg\":\"failed to parse cert: PEM_read_bio_X509_AUX() failed\"}\n--- error_code: 400\n--- no_error_log\n[alert]\n\n\n\n=== TEST 8: client request without sni\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, nil, true)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nfailed to do SSL handshake: handshake failed\n--- error_log\nfailed to find SNI: please check if the client requests via IP or uses an outdated protocol\n--- no_error_log\n[alert]\n\n\n\n=== TEST 9: client request without sni, but fallback_sni is set\n--- yaml_config\napisix:\n  node_listen: 1984\n  ssl:\n    fallback_sni: \"a.test2.com\"\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, nil, false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 10: set sni with uppercase\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"*.TesT2.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 11: match case insensitive\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"a.test2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 12: set snis with uppercase\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, snis = {\"TesT2.com\", \"a.com\"}}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- response_body\npassed\n\n\n\n=== TEST 13: match case insensitive\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"TEST2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 14: ensure table is reused in TLS handshake\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"TEST2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- extra_init_by_lua\n    local tablepool = require(\"apisix.core\").tablepool\n    local old_fetch = tablepool.fetch\n    tablepool.fetch = function(name, ...)\n        ngx.log(ngx.WARN, \"fetch table \", name)\n        return old_fetch(name, ...)\n    end\n\n    local old_release = tablepool.release\n    tablepool.release = function(name, ...)\n        ngx.log(ngx.WARN, \"release table \", name)\n        return old_release(name, ...)\n    end\n--- response_body\nssl handshake: true\n--- grep_error_log eval\nqr/(fetch|release) table \\w+/\n--- grep_error_log_out\nfetch table api_ctx\nrelease table api_ctx\n\n\n\n=== TEST 15: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/ssl test2.com.crt=@t/certs/test2.crt test2.com.key=@t/certs/test2.key\n--- response_body\nSuccess! Data written to: kv/apisix/ssl\n\n\n\n=== TEST 16: set ssl conf with secret ref: vault\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/test1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\": \"kv/apisix\",\n                    \"token\" : \"root\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n            -- set ssl\n            local code, body = t('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"cert\": \"$secret://vault/test1/ssl/test2.com.crt\",\n                    \"key\": \"$secret://vault/test1/ssl/test2.com.key\",\n                    \"sni\": \"test2.com\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 17: get cert and key from vault\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"test2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 18: get cert and key from vault with a custom ttl\n--- yaml_config\napisix:\n  lru:\n    secret:\n      ttl: 2\n      count: 1024\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\nlocation /t {\n    content_by_lua_block {\n        local tls_handshake = function()\n            local sock = ngx.socket.tcp()\n            sock:settimeout(2000)\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n            local sess, err = sock:sslhandshake(nil, \"test2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            sock:close()\n        end\n        -- send three requests within ttl\n        for i = 1, 3 do\n            tls_handshake()\n        end\n        -- wait for ttl to expire\n        ngx.sleep(2)\n        -- send one requests to trigger refresh\n        tls_handshake()\n        -- wait for refresh to complete\n        ngx.sleep(1)\n        -- send another three requests after refresh\n        for i = 1, 3 do\n            tls_handshake()\n        end\n        ngx.say(\"passed\")\n    }\n}\n--- response_body\npassed\n--- error_log\nsecret lrucache ttl: 2, count: 1024\nsuccessfully refresh stale obj for key\n--- grep_error_log eval\nqr/fetching data from secret uri: \\$secret:\\/\\/vault\\/test1\\/ssl\\/test2.com.crt, context: \\S+/\n--- grep_error_log_out\nfetching data from secret uri: $secret://vault/test1/ssl/test2.com.crt, context: ssl_certificate_by_lua*,\nfetching data from secret uri: $secret://vault/test1/ssl/test2.com.crt, context: ngx.timer\n\n\n\n=== TEST 19: set ssl conf with secret ref: env\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- set ssl\n            local code, body = t('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"cert\": \"$env://TEST_ENV_SSL_CRT\",\n                    \"key\": \"$env://TEST_ENV_SSL_KEY\",\n                    \"sni\": \"test2.com\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 20: get cert and key from env\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"test2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 21: set ssl conf with secret ref: only cert use env\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n            -- set ssl\n            local ssl_key =  t.read_file(\"t/certs/test2.key\")\n            local data = {\n                cert = \"$env://TEST_ENV_SSL_CRT\",\n                key = ssl_key,\n                sni = \"TesT2.com\"\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 22: get cert from env\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"test2.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- response_body\nssl handshake: true\n"
  },
  {
    "path": "t/router/radixtree-sni3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nno_root_location();\n\nBEGIN {\n    $ENV{TEST_NGINX_HTML_DIR} ||= html_dir();\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set sni with trailing period\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"*.test.com\"}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n--- error_code: 201\n\n\n\n=== TEST 2: match against sni with no trailing period\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"a.test.com.\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 3: set snis with trailing period\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n\n        local ssl_cert = t.read_file(\"t/certs/test2.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test2.key\")\n        local data = {cert = ssl_cert, key = ssl_key, snis = {\"test2.com\", \"a.com\"}}\n\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data)\n        )\n\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: match against sni with no trailing period\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\n\nlocation /t {\n    content_by_lua_block {\n        do\n            local sock = ngx.socket.tcp()\n\n            sock:settimeout(2000)\n\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local sess, err = sock:sslhandshake(nil, \"test2.com.\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body\nssl handshake: true\n\n\n\n=== TEST 5: set ssl(sni: www.test.com.)\n--- config\nlocation /t {\n    content_by_lua_block {\n        local core = require(\"apisix.core\")\n        local t = require(\"lib.test_admin\")\n        local ssl_cert = t.read_file(\"t/certs/test-dot.crt\")\n        local ssl_key =  t.read_file(\"t/certs/test-dot.key\")\n        local data = {cert = ssl_cert, key = ssl_key, sni = \"www.test.com.\"}\n        local code, body = t.test('/apisix/admin/ssls/1',\n            ngx.HTTP_PUT,\n            core.json.encode(data),\n            [[{\n                \"value\": {\n                    \"sni\": \"www.test.com.\"\n                },\n                \"key\": \"/apisix/ssls/1\"\n            }]]\n        )\n        ngx.status = code\n        ngx.say(body)\n    }\n}\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: client request\n--- config\nlisten unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;\nlocation /t {\n    content_by_lua_block {\n        -- etcd sync\n        ngx.sleep(0.2)\n        do\n            local sock = ngx.socket.tcp()\n            sock:settimeout(2000)\n            local ok, err = sock:connect(\"unix:$TEST_NGINX_HTML_DIR/nginx.sock\")\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n            ngx.say(\"connected: \", ok)\n            local sess, err = sock:sslhandshake(nil, \"www.test.com\", false)\n            if not sess then\n                ngx.say(\"failed to do SSL handshake: \", err)\n                return\n            end\n            ngx.say(\"ssl handshake: \", sess ~= nil)\n            local req = \"GET /hello HTTP/1.0\\r\\nHost: www.test.com\\r\\nConnection: close\\r\\n\\r\\n\"\n            local bytes, err = sock:send(req)\n            if not bytes then\n                ngx.say(\"failed to send http request: \", err)\n                return\n            end\n            ngx.say(\"sent http request: \", bytes, \" bytes.\")\n            while true do\n                local line, err = sock:receive()\n                if not line then\n                    -- ngx.say(\"failed to receive response status line: \", err)\n                    break\n                end\n                ngx.say(\"received: \", line)\n            end\n            local ok, err = sock:close()\n            ngx.say(\"close: \", ok, \" \", err)\n        end  -- do\n        -- collectgarbage()\n    }\n}\n--- request\nGET /t\n--- response_body eval\nqr{connected: 1\nssl handshake: true\nsent http request: 62 bytes.\nreceived: HTTP/1.1 200 OK\nreceived: Content-Type: text/plain\nreceived: Content-Length: 12\nreceived: Connection: close\nreceived: Server: APISIX/\\d\\.\\d+(\\.\\d+)?\nreceived: \\nreceived: hello world\nclose: 1 nil}\n--- error_log\nserver name: \"www.test.com\"\n--- no_error_log\n[error]\n[alert]\n"
  },
  {
    "path": "t/router/radixtree-uri-host.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"host\": \"*.foo.com\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /hello\n--- error_code: 404\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /hello\n--- more_headers\nHost: not_found.com\n--- error_code: 404\n\n\n\n=== TEST 5: hit routes (www.foo.com)\n--- request\nGET /hello\n--- more_headers\nHost: www.foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 6: hit routes (user.foo.com)\n--- request\nGET /hello\n--- more_headers\nHost: user.foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 7: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"host\": \"foo.com\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 9: /not_found\n--- request\nGET /hello\n--- error_code: 404\n\n\n\n=== TEST 10: /not_found\n--- request\nGET /hello\n--- more_headers\nHost: www.foo.com\n--- error_code: 404\n\n\n\n=== TEST 11: hit routes (foo.com)\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 12: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"filter_func\": \"function(vars) return vars.arg_name == 'json' end\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: not hit: name=unknown\n--- request\nGET /hello?name=unknown\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 14: hit routes\n--- request\nGET /hello?name=json\n--- response_body\nhello world\n\n\n\n=== TEST 15: set route with ':'\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/file:listReputationHistories\",\n                    \"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}},\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit routes\n--- request\nGET /file:listReputationHistories\n--- response_body\nhello world\n\n\n\n=== TEST 17: not hit\n--- request\nGET /file:xx\n--- error_code: 404\n\n\n\n=== TEST 18: inherit hosts from services\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"hosts\": [\"bar.com\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello1\"}\n                        },\n                        \"service_id\": \"1\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"priority\": -1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: hit\n--- more_headers\nHost: www.foo.com\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 20: change hosts in services\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"hosts\": [\"foo.com\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {headers = {Host = \"foo.com\"}})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"hosts\": [\"bar.com\"]\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {headers = {Host = \"foo.com\"}})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\nhello world\n\n\n\n=== TEST 21: unbind services\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello1\"}\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            local httpc = http.new()\n            local res, err = httpc:request_uri(uri, {headers = {Host = \"foo.com\"}})\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\n\n\n\n=== TEST 22: host from route is preferred\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n                        .. \"/hello\"\n            local t = require(\"lib.test_admin\").test\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"hosts\": [\"foo.com\"],\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello1\"}\n                        },\n                        \"service_id\": \"1\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            for _, h in ipairs({\"foo.com\", \"bar.com\"}) do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {Host = h}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"host\": \"foo.com\",\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello1\"}\n                        },\n                        \"service_id\": \"1\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.say(body)\n                return\n            end\n            ngx.sleep(0.1)\n\n            for _, h in ipairs({\"foo.com\", \"bar.com\"}) do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {Host = h}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n                ngx.print(res.body)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nhello1 world\nhello world\nhello1 world\nhello world\n"
  },
  {
    "path": "t/router/radixtree-uri-keep-end-slash.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n    delete_uri_tail_slash: true\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 3: hit route\n--- request\nGET /hello/\n--- error_code: 404\n"
  },
  {
    "path": "t/router/radixtree-uri-multiple.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/server_port\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: set route(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1981\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/server_port/*\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: set route(id: 3)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/3',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1982\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/server_port/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: hit route 1\n--- request\nGET /server_port\n--- response_body eval\nqr/1980/\n\n\n\n=== TEST 6: hit route 2\n--- request\nGET /server_port/route2\n--- response_body eval\nqr/1981/\n\n\n\n=== TEST 7: hit route 3\n--- request\nGET /server_port/hello\n--- response_body eval\nqr/1982/\n\n\n\n=== TEST 8: delete route(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: delete route(id: 3)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/3',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/router/radixtree-uri-priority.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1 + priority: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"priority\": 2\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- request\nGET /server_port/aa\n--- response_body eval\n1980\n\n\n\n=== TEST 3: set route(id: 2 + priority： 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"priority\": 1\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit routes\n--- request\nGET /server_port/aa\n--- response_body eval\n1980\n\n\n\n=== TEST 5: set route(id: 2 + priority: 3)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/server_port*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1981\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"priority\": 3\n                }]])\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit routes\n--- request\nGET /server_port/aa\n--- response_body eval\n1981\n\n\n\n=== TEST 7: set route(id: 2 + priority: 3)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n"
  },
  {
    "path": "t/router/radixtree-uri-sanity.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $servlet_yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n    normalize_uri_like_servlet: true\n_EOC_\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"host\": \"foo.com\",\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: /not_found\n--- request\nGET /hello\n--- more_headers\nHost: not_found.com\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: foo.com\n--- response_body\nhello world\n\n\n\n=== TEST 6: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1981\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/server_port\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: /not_found\n--- request\nGET /hello\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 8: hit routes\n--- request\nGET /server_port\n--- more_headers\nHost: anydomain.com\n--- response_body_like eval\nqr/1981/\n\n\n\n=== TEST 9: set route(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1981\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: /not_found\n--- request\nGET /hello2\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 11: hit routes\n--- request\nGET /hello\n--- more_headers\nHost: anydomain.com\n--- response_body\nhello world\n\n\n\n=== TEST 12: delete route(id: 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                 ngx.HTTP_DELETE\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: set route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: hit route with /hello\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 15: miss route\n--- request\nGET /hello/\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 16: match route like servlet\n--- yaml_config eval: $::servlet_yaml_config\n--- request\nGET /hello;world\n--- response_body eval\nqr/404 Not Found/\n--- error_code: 404\n\n\n\n=== TEST 17: plugin should work on the normalized url\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/*\",\n                        \"plugins\": {\n                            \"uri-blocker\": {\n                                \"block_rules\": [\"/hello/world\"]\n                            }\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 18: hit\n--- yaml_config eval: $::servlet_yaml_config\n--- request\nGET /hello;a=b/world;a/;\n--- error_code: 403\n\n\n\n=== TEST 19: reject bad uri\n--- yaml_config eval: $::servlet_yaml_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port\n            for _, path in ipairs({\n                \"/;/a\", \"/%2e;\", \"/%2E%2E;\", \"/.;\", \"/..;\",\n                \"/%2E%2e;\", \"/b/;/c\"\n            }) do\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri .. path)\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n\n                if res.status ~= 400 then\n                    ngx.say(path, \" \", res.status)\n                end\n            end\n\n            ngx.say(\"ok\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nok\n--- error_log\nfailed to normalize\n"
  },
  {
    "path": "t/router/radixtree-uri-vars.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route(id: 1) with vars(user_agent ~* android)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"http_user_agent\", \"~*\", \"android\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: not found because user_agent=ios\n--- request\nGET /hello\n--- more_headers\nUser-Agent: ios\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: hit routes with user_agent=android\n--- request\nGET /hello\n--- more_headers\nUser-Agent: android\n--- response_body\nhello world\n\n\n\n=== TEST 4: hit routes with user_agent=Android\n--- request\nGET /hello\n--- more_headers\nUser-Agent: Android\n--- response_body\nhello world\n\n\n\n=== TEST 5: set route(id: 1) with vars(user_agent ! ~* android)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"http_user_agent\", \"!\", \"~*\", \"android\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: not found because user_agent=android\n--- request\nGET /hello\n--- more_headers\nUser-Agent: android\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 7: hit routes with user_agent=ios\n--- request\nGET /hello\n--- more_headers\nUser-Agent: ios\n--- response_body\nhello world\n\n\n\n=== TEST 8: set route(id: 1) with vars(in table)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"http_user_agent\", \"IN\", [\"android\", \"ios\"]]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit routes with user_agent=ios\n--- request\nGET /hello\n--- more_headers\nUser-Agent: ios\n--- response_body\nhello world\n\n\n\n=== TEST 10: hit routes with user_agent=android\n--- request\nGET /hello\n--- more_headers\nUser-Agent: android\n--- response_body\nhello world\n\n\n\n=== TEST 11: set route(id: 1) with vars(null)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"http_user_agent\", \"==\", null]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: not found because user_agent=android\n--- request\nGET /hello\n--- more_headers\nUser-Agent: android\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 13: hit route\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 14: set route(id: 1) with vars(items are two)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- deprecated, will be removed soon\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [[\"http_user_agent\", \"ios\"]]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit routes with user_agent=ios\n--- request\nGET /hello\n--- more_headers\nUser-Agent: ios\n--- response_body\nhello world\n\n\n\n=== TEST 16: vars rule with logical operator (set)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [\n                            \"!OR\",\n                            [\"http_user_agent\", \"==\", \"ios\"],\n                            [\"http_demo\", \"==\", \"test\"]\n                        ]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: vars rule with logical operator (hit)\n--- request\nGET /hello\n--- more_headers\nUser-Agent: android\ndemo: prod\n--- response_body\nhello world\n\n\n\n=== TEST 18: vars rule with logical operator (miss)\n--- request\nGET /hello\n--- more_headers\nUser-Agent: ios\ndemo: prod\n--- error_code: 404\n\n\n\n=== TEST 19: be compatible with empty vars\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": []\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: hit\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 21: bad vars rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [=[{\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"uri\": \"/hello\",\n                        \"vars\": [\"http_user_agent\", \"~*\", \"android\"]\n                }]=]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to validate the 'vars' expression: rule should be wrapped inside brackets\"}\n"
  },
  {
    "path": "t/router/radixtree-uri-with-parameter.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri_with_parameter'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/name/:name/bar\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/name/:name/bar\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: /not_found\n--- request\nGET /not_found\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 3: /name/json/foo\n--- request\nGET /name/json2/foo\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 4: /name/json/\n--- request\nGET /name/json/\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: /name//bar\n--- request\nGET /name//bar\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 6: hit route: /name/json/bar\n--- request\nGET /name/json/bar\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n\n\n\n=== TEST 7: set route，uri=/:name/foo\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"uri\": \"/:name/foo\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/:name/foo\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: /json/foo\n--- request\nGET /json/foo\n--- error_code: 404\n--- response_body eval\nqr/404 Not Found/\n\n\n\n=== TEST 9: /json/bbb/foo\n--- request\nGET /json/bbb/foo\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 10: inherit hosts from services\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                        \"hosts\": [\"bar.com\"]\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello1\"}\n                        },\n                        \"service_id\": \"1\",\n                        \"uri\": \"/:name/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                        \"methods\": [\"GET\"],\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello\"}\n                        },\n                        \"uri\": \"/:name/hello\",\n                        \"priority\": -1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit\n--- more_headers\nHost: www.foo.com\n--- request\nGET /john/hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/router/radixtree-uri-with-parameter2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nworker_connections(256);\nno_root_location();\nno_shuffle();\n\nour $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\n    router:\n        http: 'radixtree_uri_with_parameter'\n_EOC_\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->yaml_config) {\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set route with :name as a uri parameter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    },\n                    \"plugins\": {\n                        \"proxy-rewrite\":{\"uri\":\"/hello\"}\n                    },\n                    \"uri\": \"/name/:name/bar\"\n                }]],\n                [[{\n                    \"value\": {\n                        \"uri\": \"/name/:name/bar\",\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1980\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        },\n                        \"plugins\": {\n                            \"proxy-rewrite\":{\"uri\":\"/hello\"}\n                        },\n                    },\n                    \"key\": \"/apisix/routes/1\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: parameters with special characters should pass\n--- request\nGET /name/with%20space/bar\n--- error_code: 200\n--- response_body\nhello world\n\n\n\n=== TEST 3: failing case for the above test\n--- request\nGET /name/with%20space/foo\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n"
  },
  {
    "path": "t/script/script.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('info');\nno_long_string();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add service which has plugins\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"name\": \"script_test\",\n                    \"plugins\": {\n                        \"example-plugin\": {\n                            \"i\": 1\n                        }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: add route which has scripts and binding service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"service_id\": 1,\n                    \"script\": \"local _M = {} \\n function _M.access(api_ctx) \\n ngx.log(ngx.INFO,\\\"hit access phase\\\") \\n end \\nreturn _M\",\n                    \"uri\": \"/hello\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: hit route, execute the scripts but don't execute the plugins\n--- request\nGET /hello\n--- response_body\nhello world\n--- error_log eval\nqr/loaded script_obj: \\{\"access\":\"function: 0x[\\w]+\"\\}/\n--- no_error_log eval\nqr/plugin rewrite phase, conf: \\{\"i\":1\\}/\n"
  },
  {
    "path": "t/script/script_distribute.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_root_location();\nno_shuffle();\n\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route(host + uri)\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local script = t.read_file(\"t/script/script_test.lua\")\n            local data = {\n                script = script,\n                uri = \"/hello\",\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data))\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- yaml_config eval: $::yaml_config\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit routes\n--- request\nGET /hello\n--- yaml_config eval: $::yaml_config\n--- response_body\nhello world\n--- error_log\nstring \"route#1\"\nphase_func(): hit access phase\nphase_func(): hit header_filter phase\nphase_func(): hit body_filter phase\nphase_func(): hit body_filter phase\nphase_func(): hit log phase while\n\n\n\n=== TEST 3: invalid script in route\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                script = \"invalid script\",\n                uri = \"/hello\",\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data))\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- yaml_config eval: $::yaml_config\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to load 'script' string: [string \\\"invalid script\\\"]:1: '=' expected near 'script'\"}\n\n\n\n=== TEST 4: invalid script in service\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local data = {\n                script = \"invalid script\",\n                upstream = {\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            }\n\n            local code, body = t.test('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data))\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- yaml_config eval: $::yaml_config\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to load 'script' string: [string \\\"invalid script\\\"]:1: '=' expected near 'script'\"}\n"
  },
  {
    "path": "t/script/script_test.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core   = require(\"apisix.core\")\n\n\nlocal _M = {}\n\n\nfunction _M.access(api_ctx)\n    core.log.warn(\"hit access phase\")\nend\n\n\nfunction _M.header_filter(ctx)\n    core.log.warn(\"hit header_filter phase\")\nend\n\n\nfunction _M.body_filter(ctx)\n    core.log.warn(\"hit body_filter phase\")\nend\n\n\nfunction _M.log(ctx)\n    core.log.warn(\"hit log phase\")\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/secret/aws.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBEGIN {\n    $ENV{AWS_REGION} = \"us-east-1\";\n    $ENV{AWS_ACCESS_KEY_ID} = \"access\";\n    $ENV{AWS_SECRET_ACCESS_KEY} = \"secret\";\n    $ENV{AWS_SESSION_TOKEN} = \"token\";\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: sanity\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_case = {\n                {access_key_id = \"access\"},\n                {secret_access_key = \"secret\"},\n                {access_key_id = \"access\", secret_access_key = \"secret\"},\n                {access_key_id = \"access\", secret_access_key = 1234},\n                {access_key_id = 1234, secret_access_key = \"secret\"},\n                {access_key_id = \"access\", secret_access_key = \"secret\", session_token = \"token\"},\n                {access_key_id = \"access\", secret_access_key = \"secret\", session_token = 1234},\n                {access_key_id = \"access\", secret_access_key = \"secret\", region = \"us-east-1\"},\n                {access_key_id = \"access\", secret_access_key = \"secret\", region = 1234},\n                {access_key_id = \"access\", secret_access_key = \"secret\", endpoint_url = \"http://127.0.0.1:4566\"},\n                {access_key_id = \"access\", secret_access_key = \"secret\", endpoint_url = 1234},\n                {access_key_id = \"access\", secret_access_key = \"secret\", session_token = \"token\", endpoint_url = \"http://127.0.0.1:4566\", region = \"us-east-1\"},\n            }\n            local aws = require(\"apisix.secret.aws\")\n            local core = require(\"apisix.core\")\n            local metadata_schema = aws.schema\n\n            for _, conf in ipairs(test_case) do\n                local ok, err = core.schema.check(metadata_schema, conf)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- response_body\nproperty \"secret_access_key\" is required\nproperty \"access_key_id\" is required\ndone\nproperty \"secret_access_key\" validation failed: wrong type: expected string, got number\nproperty \"access_key_id\" validation failed: wrong type: expected string, got number\ndone\nproperty \"session_token\" validation failed: wrong type: expected string, got number\ndone\nproperty \"region\" validation failed: wrong type: expected string, got number\ndone\nproperty \"endpoint_url\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 2: check key: no main key\n--- config\n    location /t {\n        content_by_lua_block {\n            local aws = require(\"apisix.secret.aws\")\n            local conf = {\n                endpoint_url = \"http://127.0.0.1:4566\",\n                region = \"us-east-1\",\n                access_key_id = \"access\",\n                secret_access_key = \"secret\",\n                session_token = \"token\",\n            }\n            local data, err = aws.get(conf, \"/apisix\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ncan't find main key, key: /apisix\n\n\n\n=== TEST 3: error aws endpoint_url\n--- config\n    location /t {\n        content_by_lua_block {\n            local aws = require(\"apisix.secret.aws\")\n            local conf = {\n                endpoint_url = \"http://127.0.0.1:8080\",\n                region = \"us-east-1\",\n                access_key_id = \"access\",\n                secret_access_key = \"secret\",\n                session_token = \"token\",\n            }\n            local data, err = aws.get(conf, \"apisix-key/jack\")\n            if err then\n                return ngx.say(err)\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to retrtive data from aws secret manager: SecretsManager:getSecretValue() failed to connect to 'http://127.0.0.1:8080': connection refused\n--- timeout: 6\n\n\n\n=== TEST 4: get value from aws (status ~= 200)\n--- config\n    location /t {\n        content_by_lua_block {\n            local aws = require(\"apisix.secret.aws\")\n            local conf = {\n                endpoint_url = \"http://127.0.0.1:4566\",\n                region = \"us-east-1\",\n                access_key_id = \"access\",\n                secret_access_key = \"secret\",\n                session_token = \"token\",\n            }\n            local data, err = aws.get(conf, \"apisix-error-key/jack\")\n            if err then\n                return ngx.say(\"err\")\n            end\n            ngx.say(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nerr\n\n\n\n=== TEST 5: get json value from aws\n--- config\n    location /t {\n        content_by_lua_block {\n            local aws = require(\"apisix.secret.aws\")\n            local conf = {\n                endpoint_url = \"http://127.0.0.1:4566\",\n                region = \"us-east-1\",\n                access_key_id = \"access\",\n                secret_access_key = \"secret\",\n                session_token = \"token\",\n            }\n            local data, err = aws.get(conf, \"apisix-key/jack\")\n            if err then\n                return ngx.say(err)\n            end\n            ngx.say(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 6: get json value from aws using env var\n--- config\n    location /t {\n        content_by_lua_block {\n            local aws = require(\"apisix.secret.aws\")\n            local conf = {\n                endpoint_url = \"http://127.0.0.1:4566\",\n                region = \"us-east-1\",\n                access_key_id = \"$ENV://AWS_ACCESS_KEY_ID\",\n                secret_access_key = \"$ENV://AWS_SECRET_ACCESS_KEY\",\n                session_token = \"$ENV://AWS_SESSION_TOKEN\",\n            }\n            local data, err = aws.get(conf, \"apisix-key/jack\")\n            if err then\n                return ngx.say(err)\n            end\n            ngx.say(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 7: get string value from aws\n--- config\n    location /t {\n        content_by_lua_block {\n            local aws = require(\"apisix.secret.aws\")\n            local conf = {\n                endpoint_url = \"http://127.0.0.1:4566\",\n                region = \"us-east-1\",\n                access_key_id = \"$ENV://AWS_ACCESS_KEY_ID\",\n                secret_access_key = \"$ENV://AWS_SECRET_ACCESS_KEY\",\n                session_token = \"$ENV://AWS_SESSION_TOKEN\",\n            }\n            local data, err = aws.get(conf, \"apisix-mysql\")\n            if err then\n                return ngx.say(err)\n            end\n            ngx.say(data)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsecret\n\n\n\n=== TEST 8: add secret  && consumer && check\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret aws config\n            local code, body = t('/apisix/admin/secrets/aws/mysecret',\n                ngx.HTTP_PUT,\n                [[{\n                    \"endpoint_url\": \"http://127.0.0.1:4566\",\n                    \"region\": \"us-east-1\",\n                    \"access_key_id\": \"access\",\n                    \"secret_access_key\": \"secret\",\n                    \"session_token\": \"token\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: aws\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                          \"key-auth\": {\n                            \"key\": \"$secret://aws/mysecret/apisix-key/jack\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n\n            local secret = require(\"apisix.secret\")\n            local value, err = secret.fetch_by_uri(\"$secret://aws/mysecret/apisix-key/jack\")\n            if value then\n                ngx.say(\"secret value: \", value)\n            end\n\n\n            local code, body = t('/apisix/admin/secrets/aws/mysecret', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                          \"key-auth\": {\n                            \"key\": \"$secret://aws/mysecret/apisix-key/jack\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            local secret = require(\"apisix.secret\")\n            local value, err = secret.fetch_by_uri(\"$secret://aws/mysecret/apisix-key/jack/key\")\n            if err then\n                ngx.say(err)\n            end\n            ngx.say(\"all done\")\n        }\n    }\n--- response_body\nsecret value: value\nno secret conf, secret_uri: $secret://aws/mysecret/apisix-key/jack/key\nall done\n"
  },
  {
    "path": "t/secret/conf/error.json",
    "content": "{\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\\nkEJQcmfVew5mFXyxuEn3zA==\\n-----END PRIVATE KEY-----\",\n  \"project_id\": \"apisix\",\n  \"token_uri\": \"http://127.0.0.1:1980/google/logging/token\",\n  \"scope\": [\n    \"https://apisix.apache.org/logs:admin\"\n  ],\n  \"entries_uri\": \"http://127.0.0.1:1980/google/logging/entries\"\n}\n"
  },
  {
    "path": "t/secret/conf/success.json",
    "content": "{\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\\nkEJQcmfVew5mFXyxuEn3zA==\\n-----END PRIVATE KEY-----\",\n  \"project_id\": \"apisix\",\n  \"token_uri\": \"http://127.0.0.1:1980/google/secret/token\",\n  \"scope\": [\n    \"https://www.googleapis.com/auth/cloud\"\n  ],\n  \"entries_uri\": \"http://127.0.0.1:1984\",\n  \"client_email\": \"email@apisix.iam.gserviceaccount.com\"\n}\n"
  },
  {
    "path": "t/secret/gcp.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: validate different schema situation\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_case = {\n                {},\n                {auth_file = \"123\"},\n                {auth_file = 123},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\"}},\n                {auth_config = {private_key = \"private_key\", project_id = \"project_id\"}},\n                {auth_config = {client_email = \"client\", project_id = \"project_id\"}},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = \"project_id\"}},\n                {auth_config = {client_email = 1234, private_key = \"private_key\", project_id = \"project_id\"}},\n                {auth_config = {client_email = \"client\", private_key = 1234, project_id = \"project_id\"}},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = 1234}},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = \"project_id\"}, ssl_verify = 1234},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = \"project_id\", token_uri = 1234}},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = \"project_id\", scope = 1234}},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = \"project_id\", entries_uri = 1234}},\n                {auth_config = {client_email = \"client\", private_key = \"private_key\", project_id = \"project_id\", token_uri = \"token_uri\",\n                    scope = {\"scope\"}, entries_uri = \"entries_uri\"}, ssl_verify = true},\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local core = require(\"apisix.core\")\n            local metadata_schema = gcp.schema\n\n            for _, conf in ipairs(test_case) do\n                local ok, err = core.schema.check(metadata_schema, conf)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue should match only one schema, but matches none\ndone\nproperty \"auth_file\" validation failed: wrong type: expected string, got number\nproperty \"auth_config\" validation failed: property \"project_id\" is required\nproperty \"auth_config\" validation failed: property \"client_email\" is required\nproperty \"auth_config\" validation failed: property \"private_key\" is required\ndone\nproperty \"auth_config\" validation failed: property \"client_email\" validation failed: wrong type: expected string, got number\nproperty \"auth_config\" validation failed: property \"private_key\" validation failed: wrong type: expected string, got number\nproperty \"auth_config\" validation failed: property \"project_id\" validation failed: wrong type: expected string, got number\nproperty \"ssl_verify\" validation failed: wrong type: expected boolean, got number\nproperty \"auth_config\" validation failed: property \"token_uri\" validation failed: wrong type: expected string, got number\nproperty \"auth_config\" validation failed: property \"scope\" validation failed: wrong type: expected array, got number\nproperty \"auth_config\" validation failed: property \"entries_uri\" validation failed: wrong type: expected string, got number\ndone\n\n\n\n=== TEST 2: check key: no main key\n--- config\n    location /t {\n        content_by_lua_block {\n            local gcp = require(\"apisix.secret.gcp\")\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    },\n                },\n            }\n            local data, err = gcp.get(conf, \"/apisix\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ncan't find main key, key: /apisix\n\n\n\n=== TEST 3: add secret  && consumer && check\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/google/secret/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    },\n                    entries_uri = \"http://127.0.0.1:1984\"\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/secrets/gcp/mysecret', ngx.HTTP_PUT, conf)\n\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: gcp\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                          \"key-auth\": {\n                            \"key\": \"$secret://gcp/mysecret/jack/key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://gcp/mysecret/jack/key\")\n\n\n            local code, body = t('/apisix/admin/secrets/gcp/mysecret', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                          \"key-auth\": {\n                            \"key\": \"$secret://gcp/mysecret/jack/key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://gcp/mysecret/jack/key\")\n            if value then\n                ngx.say(\"secret value: \", value)\n            end\n            ngx.say(\"all done\")\n        }\n    }\n--- response_body\nall done\n\n\n\n=== TEST 4: setup route (/projects/apisix/secrets/jack/versions/latest:access)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\n                                \"return function(conf, ctx)\n                                    require('lib.server').google_secret_apisix_jack()\n                                end\"\n                            ]\n                        }\n                    },\n                    \"uri\": \"/projects/apisix/secrets/jack/versions/latest:access\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: setup route (/projects/apisix_error/secrets/jack/versions/latest:access)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\n                                \"return function(conf, ctx)\n                                    require('lib.server').google_secret_apisix_error_jack()\n                                end\"\n                            ]\n                        }\n                    },\n                    \"uri\": \"/projects/apisix_error/secrets/jack/versions/latest:access\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: setup route (/projects/apisix/secrets/mysql/versions/latest:access)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"rewrite\",\n                            \"functions\": [\n                                \"return function(conf, ctx)\n                                    require('lib.server').google_secret_apisix_mysql()\n                                end\"\n                            ]\n                        }\n                    },\n                    \"uri\": \"/projects/apisix/secrets/mysql/versions/latest:access\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: get value from gcp by auth_file(fetch_oatuh_conf failed, read failed)\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_file = \"t/secret/conf/nofind.json\",\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(err)\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to retrtive data from gcp secret manager: failed to read configuration, file: t/secret/conf/nofind.json, err: t/secret/conf/nofind.json: No such file or directory\n\n\n\n=== TEST 8: get value from gcp by auth_file(fetch_oatuh_conf success)\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_file = \"t/secret/conf/success.json\",\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(err)\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 9: get value from gcp by auth_file(fetch_oatuh_conf failed, undefined)\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_file = \"t/secret/conf/error.json\",\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(err)\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to retrtive data from gcp secret manager: config parse failure, file: t/secret/conf/error.json, err: property \"auth_config\" validation failed: property \"client_email\" is required\n\n\n\n=== TEST 10: get json value from gcp\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/google/secret/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    },\n                    entries_uri = \"http://127.0.0.1:1984\"\n                },\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(err)\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 11: get string value from gcp\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/google/secret/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    },\n                    entries_uri = \"http://127.0.0.1:1984\"\n                },\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"mysql\")\n            if not value then\n                return ngx.say(err)\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nsecret\n\n\n\n=== TEST 12: get value from gcp(failed to get google oauth token)\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix\",\n                    token_uri = \"http://127.0.0.1:1980/google/secret/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/root/cloud-platform\"\n                    },\n                    entries_uri = \"http://127.0.0.1:1984\"\n                },\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(err)\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to retrtive data from gcp secret manager: failed to get google oauth token\n--- grep_error_log eval\nqr/\\{\\\"error\\\"\\:\\\"[\\w+\\s+]*\\\"\\}/\n--- grep_error_log_out\n{\"error\":\"no access to this scope\"}\n\n\n\n=== TEST 13: get value from gcp (not res)\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix_error\",\n                    token_uri = \"http://127.0.0.1:1980/google/secret/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    },\n                    entries_uri = \"http://127.0.0.1:1984\"\n                },\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(\"err\")\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerr\n\n\n\n=== TEST 14: get value from gcp (res status ~= 200)\n--- config\n    location /t {\n        content_by_lua_block {\n            local conf = {\n                auth_config = {\n                    client_email = \"email@apisix.iam.gserviceaccount.com\",\n                    private_key = [[\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR\naeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC\nUuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF\n2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4\nv5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep\nAB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw\nIu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P\nPR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic\nDcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49\nsxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC\nafOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC\nl85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz\nlw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC\nrCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g\ntdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16\nUyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1\nUjqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI\n1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh\nGfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46\nxn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4\nupppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF\nFzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo\ny4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W\nvjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK\nYp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S\nkEJQcmfVew5mFXyxuEn3zA==\n-----END PRIVATE KEY-----]],\n                    project_id = \"apisix_error\",\n                    token_uri = \"http://127.0.0.1:1980/google/secret/token\",\n                    scope = {\n                        \"https://www.googleapis.com/auth/cloud-platform\"\n                    },\n                    entries_uri = \"http://127.0.0.1:1984\"\n                },\n            }\n            local gcp = require(\"apisix.secret.gcp\")\n            local value, err = gcp.get(conf, \"jack/key\")\n            if not value then\n                return ngx.say(\"err\")\n            end\n            ngx.say(value)\n        }\n    }\n--- request\nGET /t\n--- response_body\nerr\n"
  },
  {
    "path": "t/secret/secret_lru.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\nrun_tests;\n\n__DATA__\n\n=== TEST 1: add secret  && consumer && check\n--- request\nGET /t\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- put secret vault config\n            local code, body = t('/apisix/admin/secrets/vault/mysecret',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"http://127.0.0.1:8200\",\n                    \"prefix\": \"kv-v1/apisix\",\n                    \"token\": \"root\"\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            -- change consumer with secrets ref: vault\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                          \"key-auth\": {\n                            \"key\": \"$secret://vault/mysecret/jack/auth-key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://vault/mysecret/jack/auth-key\")\n\n\n            local code, body = t('/apisix/admin/secrets/vault/mysecret', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            code, body = t('/apisix/admin/consumers',\n                ngx.HTTP_PUT,\n                [[{\n                    \"username\": \"jack\",\n                    \"plugins\": {\n                          \"key-auth\": {\n                            \"key\": \"$secret://vault/mysecret/jack/auth-key\"\n                        }\n                    }\n                }]]\n                )\n            if code >= 300 then\n                ngx.status = code\n                return ngx.say(body)\n            end\n\n            local secret = require(\"apisix.secret\")\n            local value = secret.fetch_by_uri(\"$secret://vault/mysecret/jack/auth-key\")\n            ngx.say(value)\n        }\n    }\n--- response_body\nnil\n"
  },
  {
    "path": "t/secret/vault.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    $ENV{VAULT_TOKEN} = \"root\";\n    $ENV{WRONG_VAULT_TOKEN} = \"squareroot\"\n}\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: check key: error format\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"/kv/prefix\",\n                token = \"root\",\n                uri = \"http://127.0.0.1:2800\"\n            }\n            local data, err = vault.get(conf, \"apisix\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nerror key format, key: apisix\n\n\n\n=== TEST 2: check key: no main key\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"/kv/prefix\",\n                token = \"root\",\n                uri = \"http://127.0.0.1:2800\"\n            }\n            local data, err = vault.get(conf, \"/apisix\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ncan't find main key, key: /apisix\n\n\n\n=== TEST 3: check key: no sub key\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"/kv/prefix\",\n                token = \"root\",\n                uri = \"http://127.0.0.1:2800\"\n            }\n            local data, err = vault.get(conf, \"apisix/\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ncan't find sub key, key: apisix/\n\n\n\n=== TEST 4: error vault uri\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"/kv/prefix\",\n                token = \"root\",\n                uri = \"http://127.0.0.2:2800\"\n            }\n            local data, err = vault.get(conf, \"/apisix/sub\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed to retrtive data from vault kv engine: connection refused\n--- timeout: 6\n\n\n\n=== TEST 5: store secret into vault\n--- exec\nVAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key/jack key=value\n--- response_body\nSuccess! Data written to: kv/apisix/apisix-key/jack\n\n\n\n=== TEST 6: get value from vault\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"kv/apisix\",\n                token = \"root\",\n                uri = \"http://127.0.0.1:8200\"\n            }\n            local value, err = vault.get(conf, \"/apisix-key/jack/key\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 7: get value from vault using token in an env var\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"kv/apisix\",\n                token = \"$ENV://VAULT_TOKEN\",\n                uri = \"http://127.0.0.1:8200\"\n            }\n            local value, err = vault.get(conf, \"/apisix-key/jack/key\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.say(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body\nvalue\n\n\n\n=== TEST 8: get value from vault: token env var wrong/missing\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"kv/apisix\",\n                token = \"$ENV://VALT_TOKEN\",\n                uri = \"http://127.0.0.1:8200\"\n            }\n            local value, err = vault.get(conf, \"/apisix-key/jack/key\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.print(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body_like\nfailed to decode result, res: \\{\\\"errors\\\":\\[\\\"permission denied\\\"\\]}\\n\n\n\n\n=== TEST 9: get value from vault: token env var contains wrong token\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"kv/apisix\",\n                token = \"$ENV://WRONG_VAULT_TOKEN\",\n                uri = \"http://127.0.0.1:8200\"\n            }\n            local value, err = vault.get(conf, \"/apisix-key/jack/key\")\n            if err then\n                return ngx.say(err)\n            end\n\n            ngx.print(\"value\")\n        }\n    }\n--- request\nGET /t\n--- response_body_like\nfailed to decode result, res: \\{\\\"errors\\\":\\[\\\"permission denied\\\"\\]}\\n\n\n\n\n=== TEST 10: setup route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"serverless-pre-function\": {\n                            \"phase\": \"access\",\n                            \"functions\": [\n                                \"return function(conf, ctx) ngx.log(ngx.ERR, 'HCV_NAMESAPCE:'..(ctx.var.http_x_vault_namespace or '_')); require('apisix.core').response.exit(200); end\"\n                            ]\n                        }\n                    },\n                    \"uri\": \"/*\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: hit route (test namespace header)\n--- config\n    location /t {\n        content_by_lua_block {\n            local vault = require(\"apisix.secret.vault\")\n            local conf = {\n                prefix = \"kv/apisix\",\n                token = \"test\",\n                uri = \"http://localhost:1984/mock\",\n                namespace = \"apisix\",\n            }\n            local value, err = vault.get(conf, \"/apisix-key/jack/key\")\n            if err then\n                return ngx.say(err)\n            end\n        }\n    }\n--- request\nGET /t\n--- error_log\nHCV_NAMESAPCE:apisix\n"
  },
  {
    "path": "t/sse_server_example/go.mod",
    "content": "module foo.bar/apache/sse_server_example\n\ngo 1.17\n"
  },
  {
    "path": "t/sse_server_example/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc completionsHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Connection\", \"close\")\n\tvar requestBody struct {\n\t\tStream bool `json:\"stream\"`\n\t}\n\n\tif r.Body != nil {\n\t\terr := json.NewDecoder(r.Body).Decode(&requestBody)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error parsing request body: %v\", err)\n\t\t\trequestBody.Stream = false\n\t\t}\n\t\tdefer r.Body.Close()\n\t}\n\n\tif requestBody.Stream {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\toffensive := r.URL.Query().Get(\"offensive\") == \"true\"\n\t\tdelay := r.URL.Query().Get(\"delay\") == \"true\"\n\t\tf, ok := w.(http.Flusher)\n\t\tif !ok {\n\t\t\thttp.Error(w, \"Streaming unsupported\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tsend := func(format, args string) {\n\t\t\tif delay {\n\t\t\t\ttime.Sleep(200 * time.Millisecond)\n\t\t\t}\n\t\t\tfmt.Fprintf(w, format, args)\n\t\t\tf.Flush()\n\t\t}\n\n\t\t// Initial chunk with assistant role\n\t\tinitialChunk := `{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1694268190,\"model\":\"gpt-4o-mini\",\"system_fingerprint\":\"fp_44709d6fcb\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}`\n\t\tsend(\"data: %s\\n\\n\", initialChunk)\n\n\t\t// Content chunks with parts of the generated text\n\t\tcontentParts := []string{\n\t\t\t\"Silent circuits hum,\\\\n\",\n\t\t\t\"Machine mind learns and evolves—\\\\n\",\n\t\t\t\"Dreams of silicon.\",\n\t\t}\n\t\tif offensive {\n\t\t\tcontentParts = []string{\n\t\t\t\t\"I want to \",\n\t\t\t\t\"kill you \",\n\t\t\t\t\"right now!\",\n\t\t\t}\n\t\t}\n\n\t\tfor _, part := range contentParts {\n\t\t\tcontentChunk := fmt.Sprintf(\n\t\t\t\t`{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1694268190,\"model\":\"gpt-4o-mini\",\"system_fingerprint\":\"fp_44709d6fcb\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"%s\"},\"logprobs\":null,\"finish_reason\":null}]}`,\n\t\t\t\tpart,\n\t\t\t)\n\t\t\tsend(\"data: %s\\n\\n\", contentChunk)\n\t\t}\n\n\t\t// Final chunk indicating completion\n\t\tfinalChunk := `{\"id\":\"chatcmpl-123\",\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":20,\"total_tokens\":35},\"object\":\"chat.completion.chunk\",\"created\":1694268190,\"model\":\"gpt-4o-mini\",\"system_fingerprint\":\"fp_44709d6fcb\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}`\n\t\tsend(\"data: %s\\n\\n\", finalChunk)\n\t\tsend(\"data: %s\\n\\n\", \"[DONE]\")\n\t} else {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tfmt.Fprint(w, `{\n\t\t\"id\": \"chatcmpl-1234567890\",\n\t\t\"object\": \"chat.completion\",\n\t\t\"created\": 1677858242,\n\t\t\"model\": \"gpt-3.5-turbo-0301\",\n\t\t\"usage\": {\n\t\t\t\"prompt_tokens\": 15,\n\t\t\t\"completion_tokens\": 20,\n\t\t\t\"total_tokens\": 35\n\t\t},\n\t\t\"choices\": [\n\t\t\t{\n\t\t\t\t\"index\": 0,\n\t\t\t\t\"message\": {\n\t\t\t\t\t\"role\": \"assistant\",\n\t\t\t\t\t\"content\": \"Hello there! How can I assist you today?\"\n\t\t\t\t},\n\t\t\t\t\"finish_reason\": \"stop\"\n\t\t\t}\n\t\t]\n\t}`)\n\t}\n\n\tcounter++\n}\n\nfunc logRequest(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tstart := time.Now()\n\t\tnext(w, r)\n\t\tduration := time.Since(start)\n\n\t\tlog.Printf(\"%s %s - Duration: %s\", r.Method, r.URL.Path, duration)\n\t}\n}\n\nvar counter = 0\n\nfunc main() {\n\thttp.HandleFunc(\"/v1/chat/completions\", logRequest(completionsHandler))\n\thttp.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(\"OK\"))\n\t})\n\tgo func() {\n\t\tfor {\n\t\t\tlog.Printf(\"Processed %d requests\", counter)\n\t\t\ttime.Sleep(1 * time.Minute)\n\t\t}\n\t}()\n\tport := os.Args[1]\n\tlog.Println(\"Starting server on :\", port)\n\tlog.Fatal(http.ListenAndServe(\":\"+port, nil))\n}\n"
  },
  {
    "path": "t/stream-node/control-api-healthcheck.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $config = ($block->config // \"\") . <<_EOC_;\n    location /hit {\n        content_by_lua_block {\n\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(\"mmm\")\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n\n            local data, err = sock:receive(\"*a\")\n            if not data then\n                sock:close()\n                return ngx.exit(503)\n            end\n            ngx.print(data)\n        }\n    }\n\n_EOC_\n\n    $block->set_value(\"config\", $config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set stream route(id: 1)\n--- stream_enable\n--- config\n    location /test {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local dkjson = require(\"dkjson\")\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\",\n                        \"checks\": {\n                            \"active\": {\n                                \"timeout\": 40,\n                                \"type\": \"tcp\",\n                                \"unhealthy\": {\n                                    \"interval\": 60,\n                                    \"failures\": 2\n                                },\n                                \"healthy\": {\n                                    \"interval\": 60,\n                                    \"successes\": 2\n                                },\n                                \"concurrency\": 2\n                            }\n                        },\n                        \"retries\": 3,\n                        \"timeout\": {\n                            \"read\": 40,\n                            \"send\": 40,\n                            \"connect\": 40\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local stream = t(\"/hit\", ngx.HTTP_GET)\n            if stream >= 300 then\n                ngx.status = stream\n                return\n            end\n\n            ngx.sleep(3)\n            local healthcheck, _, body = t(\"/v1/healthcheck\", ngx.HTTP_GET)\n            if healthcheck >= 300 then\n                ngx.status = healthcheck\n                return\n            end\n\n            local healthcheck_data, err = core.json.decode(body)\n            if not healthcheck_data then\n                ngx.log(ngx.ERR, \"failed to decode healthcheck data: \", err)\n                return ngx.exit(503)\n            end\n            ngx.say(dkjson.encode(healthcheck_data))\n\n            -- healthcheck of stream route\n            local healthcheck, _, body = t(\"/v1/healthcheck/stream_routes/1\", ngx.HTTP_GET)\n            if healthcheck >= 300 then\n                ngx.status = healthcheck\n                return\n            end\n\n            local healthcheck_data, err = core.json.decode(body)\n            if not healthcheck_data then\n                ngx.log(ngx.ERR, \"failed to decode healthcheck data: \", err)\n                return ngx.exit(503)\n            end\n            ngx.say(dkjson.encode(healthcheck_data))\n        }\n    }\n--- timeout: 5\n--- request\nGET /test\n--- response_body\n[{\"name\":\"/apisix/stream_routes/1\",\"nodes\":[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1995,\"status\":\"healthy\"}],\"type\":\"tcp\"}]\n{\"name\":\"/apisix/stream_routes/1\",\"nodes\":[{\"counter\":{\"http_failure\":0,\"success\":0,\"tcp_failure\":0,\"timeout_failure\":0},\"hostname\":\"127.0.0.1\",\"ip\":\"127.0.0.1\",\"port\":1995,\"status\":\"healthy\"}],\"type\":\"tcp\"}\n"
  },
  {
    "path": "t/stream-node/healthcheck-resty-events.t",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\nBEGIN {\n    if ($ENV{TEST_EVENTS_MODULE} ne \"lua-resty-events\") {\n        $SkipReason = \"Only for lua-resty-events events module\";\n    }\n}\nuse Test::Nginx::Socket::Lua $SkipReason ? (skip_all => $SkipReason) : ();\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create stream route with a upstream that enable active healthcheck only, \\\n            two upstream nodes: one healthy + one unhealthy, unhealthy node with high priority\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": [\n                            { \"host\": \"127.0.0.1\", \"port\": 1995, \"weight\": 100, \"priority\": 0 },\n                            { \"host\": \"127.0.0.1\", \"port\": 9995, \"weight\": 100, \"priority\": 1 }\n                        ],\n                        \"type\": \"roundrobin\",\n                        \"retries\": 0,\n                        \"checks\": {\n                            \"active\": {\n                                \"type\": \"tcp\",\n                                \"timeout\": 1,\n                                \"healthy\": {\n                                    \"interval\": 1,\n                                    \"successes\": 2\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"tcp_failures\": 1,\n                                    \"timeouts\": 1\n                                }\n                            }\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit stream routes\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            -- send first request to create health checker\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n            local data, _ = sock:receive()\n            assert(data == nil, \"first request should fail\")\n            sock:close()\n\n            -- wait for health check to take effect\n            ngx.sleep(10)\n\n            for i = 1, 3 do\n                local sock = ngx.socket.tcp()\n                local ok, err = sock:connect(\"127.0.0.1\", 1985)\n                if not ok then\n                    ngx.say(\"failed to connect: \", err)\n                    return\n                end\n\n                local _, err = sock:send(\"mmm\")\n                if err then\n                    ngx.say(\"failed to send: \", err)\n                    return\n                end\n\n                local data, err = sock:receive()\n                if err then\n                    ngx.say(\"failed to receive: \", err)\n                    return\n                end\n\n                assert(data == \"hello world\", \"response should be 'hello world'\")\n\n                sock:close()\n            end\n\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngs.say(\"failed to delete stream route\")\n                return\n            end\n\n            -- wait for checker to release\n            ngx.sleep(3)\n\n            ngx.say(\"passed\")\n        }\n    }\n--- timeout: 15\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\ncreate new checker\nproxy request to 127.0.0.1:9995 while connecting to upstream\nconnect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: 0.0.0.0:1985, upstream: \"127.0.0.1:9995\"\nunhealthy TCP increment (1/1) for '127.0.0.1(127.0.0.1:9995)'\nproxy request to 127.0.0.1:1995 while connecting to upstream\nproxy request to 127.0.0.1:1995 while connecting to upstream\nproxy request to 127.0.0.1:1995 while connecting to upstream\ntry to release checker\n\n\n\n=== TEST 3: create stream route with a upstream that enable active and passive healthcheck, \\\n            configure active healthcheck with a high unhealthy threshold, \\\n            two upstream nodes: one healthy + one unhealthy, unhealthy node with high priority\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": [\n                            { \"host\": \"127.0.0.1\", \"port\": 1995, \"weight\": 100, \"priority\": 0 },\n                            { \"host\": \"127.0.0.1\", \"port\": 9995, \"weight\": 100, \"priority\": 1 }\n                        ],\n                        \"type\": \"roundrobin\",\n                        \"retries\": 0,\n                        \"checks\": {\n                            \"active\": {\n                                \"type\": \"tcp\",\n                                \"timeout\": 1,\n                                \"healthy\": {\n                                    \"interval\": 60,\n                                    \"successes\": 2\n                                },\n                                \"unhealthy\": {\n                                    \"interval\": 1,\n                                    \"tcp_failures\": 254,\n                                    \"timeouts\": 1\n                                }\n                            },\n                            \"passive\": {\n                                \"type\": \"tcp\",\n                                \"healthy\": {\n                                    \"successes\": 1\n                                },\n                                \"unhealthy\": {\n                                    \"tcp_failures\": 1\n                                }\n                            }\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit stream routes\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n            local data, _ = sock:receive()\n            assert(data == nil, \"first request should fail\")\n            sock:close()\n            ngx.sleep(8)\n            -- Due to the implementation of lua-resty-events, it relies on the kernel and\n            -- the Nginx event loop to process socket connections.\n            -- When lua-resty-healthcheck handles passive healthchecks and uses lua-resty-events\n            -- as the events module, the synchronization of the first event usually occurs\n            -- before the start of the passive healthcheck. So when the execution finishes and\n            -- healthchecker tries to record the healthcheck status, it will not be able to find\n            -- an existing target (because the synchronization event has not finished yet), which\n            -- will lead to some anomalies that deviate from the original test case, so compatibility\n            -- operations are performed here.\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n            local data, _ = sock:receive()\n            assert(data == nil, \"first request should fail\")\n            sock:close()\n\n            for i = 1, 3 do\n                local sock = ngx.socket.tcp()\n                local ok, err = sock:connect(\"127.0.0.1\", 1985)\n                if not ok then\n                    ngx.say(\"failed to connect: \", err)\n                    return\n                end\n\n                local _, err = sock:send(\"mmm\")\n                if err then\n                    ngx.say(\"failed to send: \", err)\n                    return\n                end\n\n                local data, err = sock:receive()\n                if err then\n                    ngx.say(\"failed to receive: \", err)\n                    return\n                end\n\n                assert(data == \"hello world\", \"response should be 'hello world'\")\n\n                sock:close()\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n--- error_log\nproxy request to 127.0.0.1:9995 while connecting to upstream\nconnect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: 0.0.0.0:1985, upstream: \"127.0.0.1:9995\"\nenabled healthcheck passive while connecting to upstream, client: 127.0.0.1, server: 0.0.0.0:1985, upstream: \"127.0.0.1:9995\",\nunhealthy TCP increment (1/1) for '127.0.0.1(127.0.0.1:9995)' while connecting to upstream, client: 127.0.0.1, server: 0.0.0.0:1985, upstream: \"127.0.0.1:9995\",\nproxy request to 127.0.0.1:1995 while connecting to upstream\nproxy request to 127.0.0.1:1995 while connecting to upstream\nproxy request to 127.0.0.1:1995 while connecting to upstream\n--- timeout: 10\n"
  },
  {
    "path": "t/stream-node/mtls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set client certificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:2005\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                plugins = {\n                    [\"proxy-rewrite\"] = {\n                        uri = \"/hello\"\n                    }\n                },\n                uri = \"/mtls\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1995\"] = 1,\n                    },\n                }\n            }\n            assert(t.test('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 2: hit\n--- stream_enable\n--- request\nGET /mtls\n--- more_headers\nHost: localhost\n--- ignore_response\n--- error_log\nproxy request to 127.0.0.1:2005\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 3: reject client without cetificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:2005\"] = 1,\n                    }\n                },\n                plugins = {\n                    [\"proxy-rewrite\"] = {\n                        uri = \"/hello\"\n                    }\n                },\n                uri = \"/mtls\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 4: hit\n--- stream_enable\n--- request\nGET /mtls\n--- more_headers\nHost: localhost\n--- ignore_response\n--- error_log\nproxy request to 127.0.0.1:2005\n--- no_error_log\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 5: reject client with bad cetificate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key = t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                upstream = {\n                    scheme = \"https\",\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:2005\"] = 1,\n                    },\n                    tls = {\n                        client_cert = ssl_cert,\n                        client_key = ssl_key,\n                    }\n                },\n                plugins = {\n                    [\"proxy-rewrite\"] = {\n                        uri = \"/hello\"\n                    }\n                },\n                uri = \"/mtls\"\n            }\n            local code, body = t.test('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 6: hit\n--- stream_enable\n--- request\nGET /mtls\n--- more_headers\nHost: localhost\n--- ignore_response\n--- error_log\nproxy request to 127.0.0.1:2005\n--- no_error_log\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 7: 2 ssl objects, both have mTLS and with different CA\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n            local json = require(\"toolkit.json\")\n            local ssl_ca_cert = t.read_file(\"t/certs/mtls_ca.crt\")\n            local ssl_cert = t.read_file(\"t/certs/mtls_client.crt\")\n            local ssl_key = t.read_file(\"t/certs/mtls_client.key\")\n            local ssl_ca_cert2 = t.read_file(\"t/certs/apisix.crt\")\n\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:1995\"] = 1,\n                    },\n                }\n            }\n            assert(t.test('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            ))\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"localhost\",\n                client = {\n                    ca = ssl_ca_cert,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"test.com\",\n                client = {\n                    ca = ssl_ca_cert2,\n                    depth = 2,\n                }\n            }\n            local code, body = t.test('/apisix/admin/ssls/2',\n                ngx.HTTP_PUT,\n                json.encode(data)\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 8: request localhost and save tls session to reuse\n--- stream_enable\n--- max_size: 1048576\n--- exec\necho \"\" | timeout 1 openssl s_client -ign_eof -connect 127.0.0.1:2005 \\\n    -servername localhost -cert t/certs/mtls_client.crt -key t/certs/mtls_client.key \\\n    -sess_out session.dat\n\n\n\n=== TEST 9: request test.com with saved tls session\n--- stream_enable\n--- max_size: 1048576\n--- exec\necho \"\" | openssl s_client -connect 127.0.0.1:2005 -servername test.com \\\n    -sess_in session.dat\n--- error_log\nsni in client hello mismatch hostname of ssl session, sni: test.com, hostname: localhost\n"
  },
  {
    "path": "t/stream-node/priority-balancer.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(2); # repeat each test to ensure after_balance is called correctly\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if ($block->apisix_yaml) {\n        if (!$block->yaml_config) {\n            my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\n_EOC_\n\n            $block->set_value(\"yaml_config\", $yaml_config);\n        }\n    }\n\n    $block->set_value(\"stream_enable\", 1);\n\n    if (!$block->stream_request) {\n        $block->set_value(\"stream_request\", \"mmm\");\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- apisix_yaml\nstream_routes:\n  - id: 1\n    upstream:\n        type: least_conn\n        nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 1\n        - host: 127.0.0.3\n          port: 1979\n          weight: 2\n          priority: 0\n        - host: 127.0.0.4\n          port: 1979\n          weight: 1\n          priority: 0\n        - host: 127.0.0.1\n          port: 1995\n          weight: 2\n          priority: -1\n#END\n--- stream_response\nhello world\n--- error_log\nconnect() failed\nfailed to get server from current priority 1, try next one\nfailed to get server from current priority 0, try next one\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.3:1979\nproxy request to 127.0.0.4:1979\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 2: default priority is 0\n--- apisix_yaml\nstream_routes:\n  - id: 1\n    upstream:\n        type: least_conn\n        nodes:\n        - host: 127.0.0.1\n          port: 1979\n          weight: 2\n          priority: 1\n        - host: 127.0.0.2\n          port: 1979\n          weight: 1\n          priority: 1\n        - host: 127.0.0.3\n          port: 1979\n          weight: 2\n        - host: 127.0.0.4\n          port: 1979\n          weight: 1\n        - host: 127.0.0.1\n          port: 1995\n          weight: 2\n          priority: -1\n#END\n--- stream_response\nhello world\n--- error_log\nconnect() failed\nfailed to get server from current priority 1, try next one\nfailed to get server from current priority 0, try next one\n--- grep_error_log eval\nqr/proxy request to \\S+/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1979\nproxy request to 127.0.0.2:1979\nproxy request to 127.0.0.3:1979\nproxy request to 127.0.0.4:1979\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 3: fix priority for nonarray nodes\n--- apisix_yaml\nstream_routes:\n  - id: 1\n    upstream:\n        type: roundrobin\n        nodes:\n            \"127.0.0.1:1995\": 1\n            \"127.0.0.2:1995\": 1\n#END\n--- stream_response\nhello world\n"
  },
  {
    "path": "t/stream-node/random.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nworkers(4);\nlog_level('info');\nworker_connections(256);\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: generate different random number in different worker process\n--- stream_enable\n--- config\n    location /test {\n        content_by_lua_block {\n            ngx.sleep(0.3)\n            local log_file = ngx.config.prefix() .. \"logs/error.log\"\n            local file = io.open(log_file, \"r\")\n            local log = file:read(\"*a\")\n\n            local it, err = ngx.re.gmatch(log, [[random stream test in \\[1, 10000\\]: (\\d+)]], \"jom\")\n            if not it then\n                ngx.log(ngx.ERR, \"failed to gmatch: \", err)\n                return\n            end\n\n            local random_nums = {}\n            while true do\n                local m, err = it()\n                if err then\n                    ngx.log(ngx.ERR, \"error: \", err)\n                    return\n                end\n\n                if not m then\n                    break\n                end\n\n                -- found a match\n                table.insert(random_nums, m[1])\n            end\n\n            for i = 2, #random_nums do\n                local pre = random_nums[i - 1]\n                local cur = random_nums[i]\n                ngx.say(\"random[\", i - 1, \"] == random[\", i, \"]: \", pre == cur)\n                if not pre == cur then\n                    ngx.say(\"random info in log: \", table.concat(random_nums, \", \"))\n                    break\n                end\n            end\n        }\n    }\n--- request\nGET /test\n--- response_body\nrandom[1] == random[2]: false\nrandom[2] == random[3]: false\nrandom[3] == random[4]: false\nrandom[4] == random[5]: false\n"
  },
  {
    "path": "t/stream-node/sanity-repeat.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\nworkers(1);\nrepeat_each(2);\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set stream route(id: 1) -> service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"service_id\": 1\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 3: set stream / ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                cert = ssl_cert, key = ssl_key,\n                sni = \"*.test.com\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"a.test.com\",\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: a.test.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.1:1995\n"
  },
  {
    "path": "t/stream-node/sanity-with-service.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set stream route(id: 1) -> service(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"service_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 3: set stream route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.2\",\n                    \"service_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: not hit route\n--- stream_enable\n--- stream_response\n\n\n\n=== TEST 5: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: set service upstream (id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream_id\": 1\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: set stream route (id: 1) with service (id: 1) which uses upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"service_id\": 1\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 9: set stream route (id: 1) which uses upstream_id and remote address with IP CIDR\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1/26\",\n                    \"service_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 11: reject bad CIDR\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \":/8\",\n                    \"service_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid remote_addr: :/8\"}\n\n\n\n=== TEST 12: skip upstream http host check in stream subsystem\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1,\n                        \"127.0.0.2:1995\": 1\n                    },\n                    \"pass_host\": \"node\",\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 13: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n"
  },
  {
    "path": "t/stream-node/sanity.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set stream route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 3: set stream route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.2\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: not hit route\n--- stream_enable\n--- stream_response\n\n\n\n=== TEST 5: delete route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_DELETE\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: set stream route(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1995,\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: set upstream (id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: set stream route (id: 1) which uses upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 10: skip route config tombstone\n--- stream_conf_enable\n--- config\nlocation /t {\n    content_by_lua_block {\n        local t = require(\"lib.test_admin\").test\n        t('/apisix/admin/stream_routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }\n            }]]\n        )\n        t('/apisix/admin/stream_routes/1', ngx.HTTP_DELETE)\n        t('/apisix/admin/stream_routes/1',\n            ngx.HTTP_PUT,\n            [[{\n                \"upstream\": {\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }\n            }]]\n        )\n\n        local sock = ngx.socket.tcp()\n        local ok, err = sock:connect(\"127.0.0.1\", 1985)\n        if not ok then\n            ngx.say(\"failed to connect: \", err)\n            return\n        end\n\n        assert(sock:send(\"mmm\"))\n        local data = assert(sock:receive(\"*a\"))\n        ngx.print(data)\n    }\n}\n--- request\nGET /t\n--- response_body\nhello world\n\n\n\n=== TEST 11: set stream route (id: 1) which uses upstream_id and remote address with IP CIDR\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1/26\",\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 13: reject bad CIDR\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \":/8\",\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"invalid remote_addr: :/8\"}\n\n\n\n=== TEST 14: skip upstream http host check in stream subsystem\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1,\n                        \"127.0.0.2:1995\": 1\n                    },\n                    \"pass_host\": \"node\",\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 16: reuse ctx and more\n--- stream_extra_init_by_lua\n    local ctx = require(\"apisix.core.ctx\")\n    local tablepool = require(\"apisix.core\").tablepool\n\n    local old_set_vars_meta = ctx.set_vars_meta\n    ctx.set_vars_meta = function(...)\n        ngx.log(ngx.WARN, \"fetch ctx var\")\n        return old_set_vars_meta(...)\n    end\n\n    local old_release_vars = ctx.release_vars\n    ctx.release_vars = function(...)\n        ngx.log(ngx.WARN, \"release ctx var\")\n        return old_release_vars(...)\n    end\n\n    local old_fetch = tablepool.fetch\n    tablepool.fetch = function(name, ...)\n        ngx.log(ngx.WARN, \"fetch table \", name)\n        return old_fetch(name, ...)\n    end\n\n    local old_release = tablepool.release\n    tablepool.release = function(name, ...)\n        ngx.log(ngx.WARN, \"release table \", name)\n        return old_release(name, ...)\n    end\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(fetch|release) (ctx var|table \\w+)/\n--- grep_error_log_out\nfetch table api_ctx\nfetch ctx var\nfetch table ctx_var\nfetch table plugins\nrelease ctx var\nrelease table ctx_var\nrelease table plugins\nrelease table api_ctx\n"
  },
  {
    "path": "t/stream-node/sni.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set stream / ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                cert = ssl_cert, key = ssl_key,\n                sni = \"*.test.com\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"a.test.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"*.test.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.2:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/3',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.3:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: a.test.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 3: hit route (session reuse)\n--- stream_tls_request\nmmm\n--- stream_sni: a.test.com\n--- stream_session_reuse\n--- response_body\nhello world\nhello world\n--- grep_error_log eval\nqr/proxy request to 127.0.0.\\d:1995/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1995\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 4: hit route, wildcard SNI\n--- stream_tls_request\nmmm\n--- stream_sni: b.test.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.2:1995\n\n\n\n=== TEST 5: hit route, no TLS\n--- stream_request\nmmm\n--- stream_response\nhello world\n--- error_log\nproxy request to 127.0.0.3:1995\n\n\n\n=== TEST 6: set different stream route with the same sni\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local code, body = t.test('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"a.test.com\",\n                    \"remote_addr\": \"127.0.0.2\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/4',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"a.test.com\",\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.4:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: a.test.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.4:1995\n\n\n\n=== TEST 8: change a.test.com route to fall back to wildcard route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local code, body = t.test('/apisix/admin/stream_routes/4',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"a.test.com\",\n                    \"remote_addr\": \"127.0.0.3\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.4:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: a.test.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.2:1995\n\n\n\n=== TEST 10: use fallback sni to match route\n--- yaml_config\napisix:\n  node_listen: 1984\n  proxy_mode: http&stream\n  stream_proxy:\n    tcp:\n      - 9100\n  ssl:\n    fallback_sni: a.test.com\n--- stream_tls_request\nmmm\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.2:1995\n\n\n\n=== TEST 11: no sni matched, fall back to non-sni route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            local code, body = t.test('/apisix/admin/stream_routes/2',\n                ngx.HTTP_DELETE)\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: b.test.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.3:1995\n\n\n\n=== TEST 13: clean up routes\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\")\n\n            for i = 1, 4 do\n                t.test('/apisix/admin/stream_routes/' .. i, ngx.HTTP_DELETE)\n            end\n        }\n    }\n--- request\nGET /t\n\n\n\n=== TEST 14: set SSL with wildcard * SNI and test route matching\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n\n            -- Create SSL with wildcard * SNI (catch-all)\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"*\",  -- Wildcard catch-all\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/100',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create wildcard SSL: \", code, \" \", body)\n                return\n            end\n\n            -- Create a stream route that will use the wildcard SSL\n            local code, body = t.test('/apisix/admin/stream_routes/100',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"unknown-domain.com\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create stream route: \", code, \" \", body)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route with unknown domain using wildcard SSL\n--- stream_tls_request\nmmm\n--- stream_sni: unknown-domain.com\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 16: test SSL priority - exact match over partial wildcard\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n\n            -- Create SSL with exact domain match\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"specific.api7.dev\",\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/101',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create exact SSL: \", code, \" \", body)\n                return\n            end\n\n            -- Create SSL with partial wildcard\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"*.api7.dev\",\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/102',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create partial wildcard SSL: \", code, \" \", body)\n                return\n            end\n\n            -- Create routes for testing\n            local code, body = t.test('/apisix/admin/stream_routes/101',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"specific.api7.dev\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create exact route: \", code, \" \", body)\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/102',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"*.api7.dev\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.2:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create partial wildcard route: \", code, \" \", body)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: verify exact domain takes priority over partial wildcard\n--- stream_tls_request\nmmm\n--- stream_sni: specific.api7.dev\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.1:1995\n--- no_error_log\nproxy request to 127.0.0.2:1995\n\n\n\n=== TEST 18: test SSL priority - partial match over wildcard\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n\n            -- Create SSL with partial domain match\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"specific.api7.dev\",\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/101',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create exact SSL: \", code, \" \", body)\n                return\n            end\n\n            -- Create SSL with wildcard\n            local data = {\n                cert = ssl_cert,\n                key = ssl_key,\n                sni = \"*\",\n            }\n\n            local code, body = t.test('/apisix/admin/ssls/102',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create partial wildcard SSL: \", code, \" \", body)\n                return\n            end\n\n            -- Create routes for testing\n            local code, body = t.test('/apisix/admin/stream_routes/101',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"*.api7.dev\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create exact route: \", code, \" \", body)\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/102',\n                ngx.HTTP_PUT,\n                [[{\n                    \"sni\": \"*\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.2:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"failed to create partial wildcard route: \", code, \" \", body)\n                return\n            end\n\n            ngx.say(\"passed\")\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 19: verify partial match takes priority over wildcard\n--- stream_tls_request\nmmm\n--- stream_sni: specific.api7.dev\n--- response_body\nhello world\n--- error_log\nproxy request to 127.0.0.1:1995\n--- no_error_log\nproxy request to 127.0.0.2:1995\n"
  },
  {
    "path": "t/stream-node/tls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\nworker_connections(1024);\nno_shuffle();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set stream / ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                cert = ssl_cert, key = ssl_key,\n                sni = \"test.com\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            local code, body = t.test('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: test.com\n--- response_body\nhello world\n\n\n\n=== TEST 3: wrong sni\n--- stream_tls_request\nmmm\n--- stream_sni: xx.com\n--- error_log\nfailed to match any SSL certificate by SNI: xx.com\n\n\n\n=== TEST 4: missing sni\n--- stream_tls_request\nmmm\n--- error_log\nfailed to find SNI\n\n\n\n=== TEST 5: ensure table is reused in TLS handshake\n--- stream_extra_init_by_lua\n    local tablepool = require(\"apisix.core\").tablepool\n    local old_fetch = tablepool.fetch\n    tablepool.fetch = function(name, ...)\n        ngx.log(ngx.WARN, \"fetch table \", name)\n        return old_fetch(name, ...)\n    end\n\n    local old_release = tablepool.release\n    tablepool.release = function(name, ...)\n        ngx.log(ngx.WARN, \"release table \", name)\n        return old_release(name, ...)\n    end\n--- stream_tls_request\nmmm\n--- stream_sni: test.com\n--- response_body\nhello world\n--- grep_error_log eval\nqr/(fetch|release) table \\w+/\n--- grep_error_log_out\nfetch table api_ctx\nrelease table api_ctx\nfetch table api_ctx\nfetch table ctx_var\nfetch table plugins\nrelease table ctx_var\nrelease table plugins\nrelease table api_ctx\n"
  },
  {
    "path": "t/stream-node/upstream-domain.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('info');\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"stream_enable\", 1);\n\n        if (!$block->stream_request) {\n            $block->set_value(\"stream_request\", \"mmm\");\n        }\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream & stream_routes (id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"localhost:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_response\nhello world\n\n\n\n=== TEST 3: set stream_routes with upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"localhost:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route\n--- stream_response\nhello world\n\n\n\n=== TEST 5: bad domain in the upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"local:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: hit route\n--- stream_response\nreceive stream response error: connection reset by peer\n--- error_log\n\n\n\n=== TEST 7: bad domain in the stream route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"local:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit route\n--- stream_response\nreceive stream response error: connection reset by peer\n--- error_log\nno valid upstream node\n"
  },
  {
    "path": "t/stream-node/upstream-tls.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"stream_enable\", 1);\n\n        my $stream_config = $block->stream_config // '';\n        $stream_config .= <<_EOC_;\n        server {\n            listen 8765 ssl;\n            ssl_certificate             cert/apisix.crt;\n            ssl_certificate_key         cert/apisix.key;\n\n            content_by_lua_block {\n                local sock = ngx.req.socket()\n                local data = sock:receive(\"1\")\n                ngx.say(\"hello \", ngx.var.ssl_server_name)\n            }\n        }\n_EOC_\n\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set upstream & stream_routes (id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"scheme\": \"tls\",\n                    \"nodes\": {\n                        \"localhost:8765\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_request\nmmm\n--- stream_response\nhello apisix_backend\n\n\n\n=== TEST 3: set ssl\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local t = require(\"lib.test_admin\")\n\n            local ssl_cert = t.read_file(\"t/certs/apisix.crt\")\n            local ssl_key =  t.read_file(\"t/certs/apisix.key\")\n            local data = {\n                cert = ssl_cert, key = ssl_key,\n                sni = \"test.com\",\n            }\n            local code, body = t.test('/apisix/admin/ssls/1',\n                ngx.HTTP_PUT,\n                core.json.encode(data)\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit route\n--- stream_tls_request\nmmm\n--- stream_sni: test.com\n--- response_body\nhello test.com\n"
  },
  {
    "path": "t/stream-plugin/ip-restriction.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: blacklist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                                \"blacklist\": [\n                                    \"127.0.0.0/24\"\n                                ]\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- stream_request eval\nmmm\n--- error_log\nConnection reset by peer\n\n\n\n=== TEST 3: whitelist\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                                \"whitelist\": [\n                                    \"127.0.0.0/24\"\n                                ]\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n\n\n\n=== TEST 5: validate schema\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"ip-restriction\": {\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.print(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 400\n--- response_body\n{\"error_msg\":\"failed to check the configuration of stream plugin [ip-restriction]: value should match only one schema, but matches none\"}\n"
  },
  {
    "path": "t/stream-plugin/limit-conn.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $config = $block->config // <<_EOC_;\n    location /hit {\n        content_by_lua_block {\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(\"mmm\")\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n            local data, err = sock:receive(\"*a\")\n            if not data then\n                sock:close()\n                return ngx.exit(503)\n            end\n            ngx.print(data)\n        }\n    }\n\n    location /test_concurrency {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 5 do\n                reqs[i] = { \"/hit\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.status)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_;\n            local sock = ngx.req.socket()\n            local data = sock:receive(\"1\")\n            ngx.sleep(0.2)\n            ngx.say(\"hello world\")\n_EOC_\n    $block->set_value(\"stream_upstream_code\", $stream_upstream_code);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 100,\n                            \"burst\": 50,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: not exceeding the burst\n--- request\nGET /test_concurrency\n--- response_body\n200\n200\n200\n200\n200\n--- stream_enable\n\n\n\n=== TEST 3: update route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"remote_addr\"\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: exceeding the burst\n--- request\nGET /test_concurrency\n--- response_body\n200\n200\n200\n503\n503\n--- error_log\nConnection reset by peer\n--- stream_enable\n\n\n\n=== TEST 5: var combination\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"$remote_addr $server_addr\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: exceeding the burst\n--- request\nGET /test_concurrency\n--- response_body\n200\n200\n200\n503\n503\n--- error_log\nConnection reset by peer\n--- stream_enable\n\n\n\n=== TEST 7: var combination (not exceed the burst)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"$remote_port $server_addr\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: hit\n--- request\nGET /test_concurrency\n--- response_body\n200\n200\n200\n200\n200\n--- stream_enable\n\n\n\n=== TEST 9: bypass empty key\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"$proxy_protocol_addr $proxy_protocol_port\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: hit\n--- request\nGET /test_concurrency\n--- response_body\n200\n200\n200\n503\n503\n--- error_log\nThe value of the configured key is empty, use client IP instead\n--- stream_enable\n"
  },
  {
    "path": "t/stream-plugin/limit-conn2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\n$ENV{TEST_NGINX_REDIS_PORT} ||= 1985;\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: redis\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nworker_connections(1024);\nrun_tests;\n\n__DATA__\n\n=== TEST 1: create a stream router with limit-conn\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"limit-conn\": {\n                            \"conn\": 2,\n                            \"burst\": 1,\n                            \"default_conn_delay\": 0.1,\n                            \"key\": \"$remote_port $server_addr\",\n                            \"key_type\": \"var_combination\"\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"none\",\n                        \"nodes\": {\n                          \"127.0.0.1:6379\": 1\n                        }\n                      },\n                      \"protocol\": {\n                        \"name\": \"redis\"\n                      }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: access the redis via proxy\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:hmset(\"animals\", \"dog\", \"bark\", \"cat\", \"meow\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n                return\n            end\n            ngx.say(\"hmset animals: \", res)\n\n            local res, err = red:hmget(\"animals\", \"dog\", \"cat\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n            ngx.say(\"hmget animals: \", res)\n\n            ok, err = red:close()\n            if not ok then\n                ngx.say(\"failed to close: \", err)\n                return\n            end\n        }\n    }\n--- response_body\nhmset animals: OK\nhmget animals: barkmeow\n--- no_error_log\nattempt to perform arithmetic on field 'request_time'\n--- stream_conf_enable\n"
  },
  {
    "path": "t/stream-plugin/mqtt-proxy.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(2);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1995,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: invalid header\n--- stream_request eval\nmmm\n--- error_log\nReceived unexpected MQTT packet type+flags\n\n\n\n=== TEST 3: hit route\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n\n\n\n=== TEST 4: set route (wrong server port)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 2000,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4,\n                            \"upstream\": {\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1995\n                            }\n                        }\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: failed to match route\n--- stream_request eval\n\"\\x10\\x0f\"\n--- stream_response\nreceive stream response error: connection reset by peer\n--- error_log\nreceive stream response error: connection reset by peer\n--- error_log\nmatch(): not hit any route\n\n\n\n=== TEST 6: set route with host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"localhost\",\n                                \"port\": 1995,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit route\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n\n\n\n=== TEST 8: set route with upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 1995,\n                            \"weight\": 1\n                        }]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: hit route\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/mqtt client id: \\w+/\n--- grep_error_log_out\nmqtt client id: foo\n\n\n\n=== TEST 10: hit route with empty client id\n--- stream_request eval\n\"\\x10\\x0c\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x00\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/mqtt client id: \\w+/\n--- grep_error_log_out\n\n\n\n=== TEST 11: MQTT 5\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 5\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": [{\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 1995,\n                            \"weight\": 1\n                        }]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: hit route with empty property\n--- stream_request eval\n\"\\x10\\x0d\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x00\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/mqtt client id: \\w+/\n--- grep_error_log_out\n\n\n\n=== TEST 13: hit route with property\n--- stream_request eval\n\"\\x10\\x1b\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x05\\x11\\x00\\x00\\x0e\\x10\\x00\\x09\\x63\\x6c\\x69\\x6e\\x74\\x2d\\x31\\x31\\x31\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/mqtt client id: \\S+/\n--- grep_error_log_out\nmqtt client id: clint-111\n\n\n\n=== TEST 14: balance with mqtt_client_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 5\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                        {\n                            \"host\": \"0.0.0.0\",\n                            \"port\": 1995,\n                            \"weight\": 1\n                        },\n                        {\n                            \"host\": \"127.0.0.1\",\n                            \"port\": 1995,\n                            \"weight\": 1\n                        }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: hit route with empty id\n--- stream_request eval\n\"\\x10\\x0d\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x00\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(mqtt client id: \\w+|proxy request to \\S+)/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 16: hit route with different client id, part 1\n--- stream_request eval\n\"\\x10\\x0e\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x01\\x66\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(mqtt client id: \\w+|proxy request to \\S+)/\n--- grep_error_log_out\nmqtt client id: f\nproxy request to 0.0.0.0:1995\n\n\n\n=== TEST 17: hit route with different client id, part 2\n--- stream_request eval\n\"\\x10\\x0e\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x01\\x67\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(mqtt client id: \\w+|proxy request to \\S+)/\n--- grep_error_log_out\nmqtt client id: g\nproxy request to 127.0.0.1:1995\n"
  },
  {
    "path": "t/stream-plugin/mqtt-proxy2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set route with invalid host\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 4\n                        }\n                    },\n                    \"upstream\": {\n                        \"type\": \"chash\",\n                        \"key\": \"mqtt_client_id\",\n                        \"nodes\": [\n                            {\n                                \"host\": \"loc\",\n                                \"port\": 1995,\n                                \"weight\": 1\n                            }\n                        ]\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit route\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- error_log\nfailed to parse domain: loc, error:\n--- timeout: 25\n\n\n\n=== TEST 3: set upstream(id: 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"type\": \"chash\",\n                    \"key\": \"mqtt_client_id\",\n                    \"nodes\": [\n                    {\n                        \"host\": \"0.0.0.0\",\n                        \"port\": 1995,\n                        \"weight\": 1\n                    },\n                    {\n                        \"host\": \"127.0.0.1\",\n                        \"port\": 1995,\n                        \"weight\": 1\n                    }\n                    ]\n                }]]\n            )\n\n            ngx.status = code\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- error_code: 201\n--- response_body\npassed\n\n\n\n=== TEST 4: balance with mqtt_client_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"remote_addr\": \"127.0.0.1\",\n                    \"server_port\": 1985,\n                    \"plugins\": {\n                        \"mqtt-proxy\": {\n                            \"protocol_name\": \"MQTT\",\n                            \"protocol_level\": 5\n                        }\n                    },\n                    \"upstream_id\": 1\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 5: hit route with empty id\n--- stream_request eval\n\"\\x10\\x0d\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x00\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(mqtt client id: \\w+|proxy request to \\S+)/\n--- grep_error_log_out\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 6: hit route with different client id, part 1\n--- stream_request eval\n\"\\x10\\x0e\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x01\\x66\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(mqtt client id: \\w+|proxy request to \\S+)/\n--- grep_error_log_out\nmqtt client id: f\nproxy request to 0.0.0.0:1995\n\n\n\n=== TEST 7: hit route with different client id, part 2\n--- stream_request eval\n\"\\x10\\x0e\\x00\\x04\\x4d\\x51\\x54\\x54\\x05\\x02\\x00\\x3c\\x00\\x00\\x01\\x67\"\n--- stream_response\nhello world\n--- grep_error_log eval\nqr/(mqtt client id: \\w+|proxy request to \\S+)/\n--- grep_error_log_out\nmqtt client id: g\nproxy request to 127.0.0.1:1995\n\n\n\n=== TEST 8: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local test_cases = {\n                {protocol_name = \"MQTT\", protocol_level = 4},\n                {protocol_name = \"MQTT\"},\n                {protocol_level = 4},\n            }\n\n            local stream_plugin = require(\"apisix.stream.plugins.mqtt-proxy\")\n            for _, case in ipairs(test_cases) do\n                local ok, err = stream_plugin.check_schema(case)\n                ngx.say(ok and \"done\" or err)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\nproperty \"protocol_level\" is required\ndone\n"
  },
  {
    "path": "t/stream-plugin/plugin.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuse t::APISIX 'no_plan';\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    $block->set_value(\"no_error_log\", \"[error]\");\n\n    $block;\n});\n\nno_long_string();\nno_shuffle();\nno_root_location();\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: ensure all plugins have exposed their name\n--- stream_enable\n--- stream_server_config\n    content_by_lua_block {\n        local lfs = require(\"lfs\")\n        for file_name in lfs.dir(ngx.config.prefix() .. \"/../../apisix/stream/plugins/\") do\n            if string.match(file_name, \".lua$\") then\n                local expected = file_name:sub(1, #file_name - 4)\n                local plugin = require(\"apisix.stream.plugins.\" .. expected)\n                if plugin.name ~= expected then\n                    ngx.say(\"expected \", expected, \" got \", plugin.name)\n                    return\n                end\n            end\n        end\n        ngx.say('ok')\n    }\n--- stream_response\nok\n\n\n\n=== TEST 2: ensure all plugins have unique priority\n--- stream_enable\n--- stream_server_config\n        content_by_lua_block {\n            local lfs = require(\"lfs\")\n            local pri_name = {}\n            for file_name in lfs.dir(ngx.config.prefix() .. \"/../../apisix/stream/plugins/\") do\n                if string.match(file_name, \".lua$\") then\n                    local name = file_name:sub(1, #file_name - 4)\n                    local plugin = require(\"apisix.stream.plugins.\" .. name)\n                    if pri_name[plugin.priority] then\n                        ngx.say(name, \" has same priority with \", pri_name[plugin.priority])\n                        return\n                    end\n                    pri_name[plugin.priority] = plugin.name\n                end\n            end\n            ngx.say('ok')\n        }\n--- stream_response\nok\n"
  },
  {
    "path": "t/stream-plugin/prometheus.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n    # because nginx-lua-prometheus uses a timer to periodically synchronize lua module variables to\n    # shared dict, and all test cases in this test file use HUP to reload nginx in order to retain\n    # the data in the shared dict, it is necessary to increase the value of TEST_NGINX_SLEEP to\n    # avoid losing prometheus data due to worker exits.\n    $ENV{TEST_NGINX_SLEEP} = 1;\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $extra_yaml_config = <<_EOC_;\nstream_plugins:\n    - mqtt-proxy\n    - prometheus\n_EOC_\n\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: pre-create public API route\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/routes/metrics\",\n                    data = [[{\n                        \"plugins\": {\n                            \"public-api\": {}\n                        },\n                        \"uri\": \"/apisix/prometheus/metrics\"\n                    }]]\n                },\n                {\n                    url = \"/apisix/admin/stream_routes/mqtt\",\n                    data = [[{\n                        \"plugins\": {\n                            \"mqtt-proxy\": {\n                                \"protocol_name\": \"MQTT\",\n                                \"protocol_level\": 4\n                            },\n                            \"prometheus\": {}\n                        },\n                        \"upstream\": {\n                            \"type\": \"roundrobin\",\n                            \"nodes\": [{\n                                \"host\": \"127.0.0.1\",\n                                \"port\": 1995,\n                                \"weight\": 1\n                            }]\n                        }\n                    }]]\n                }\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                if code > 300 then\n                    ngx.say(body)\n                    return\n                end\n            end\n        }\n    }\n--- response_body\n\n\n\n=== TEST 2: hit\n--- stream_request eval\n\"\\x10\\x0f\\x00\\x04\\x4d\\x51\\x54\\x54\\x04\\x02\\x00\\x3c\\x00\\x03\\x66\\x6f\\x6f\"\n--- stream_response\nhello world\n\n\n\n=== TEST 3: fetch the prometheus metric data\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_stream_connection_total\\{route=\"mqtt\"\\} 1/\n\n\n\n=== TEST 4: hit, error\n--- stream_request eval\nmmm\n--- error_log\nReceived unexpected MQTT packet type+flags\n\n\n\n=== TEST 5: fetch the prometheus metric data\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local core = require(\"apisix.core\")\n            local http = require(\"resty.http\")\n\n            ngx.sleep(2) -- wait for the metrics to be updated\n            local httpc = http.new()\n            local res, err = httpc:request_uri(\"http://127.0.0.1:1984/apisix/prometheus/metrics\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.say(res.body)\n        }\n    }\n--- request\nGET /t\n--- response_body eval\nqr/apisix_stream_connection_total\\{route=\"mqtt\"\\} 2/\n\n\n\n=== TEST 6: contains metrics from stub_status\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_nginx_http_current_connections\\{state=\"active\".*\\} \\d+/\n\n\n\n=== TEST 7: contains basic metrics\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_node_info\\{hostname=\"[^\"]+\".*\\}/\n"
  },
  {
    "path": "t/stream-plugin/syslog.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen 8125 udp;\n        content_by_lua_block {\n            require(\"lib.mock_layer4\").dogstatsd()\n        }\n    }\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: custom log format not set\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            -- ensure the format is not set\n            t('/apisix/admin/plugin_metadata/syslog',\n                ngx.HTTP_DELETE\n                )\n\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1995\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                    \"plugins\": {\n                        \"syslog\": {\n                            \"host\" : \"127.0.0.1\",\n                            \"port\" : 8125,\n                            \"sock_type\": \"udp\",\n                            \"batch_max_size\": 1,\n                            \"flush_limit\":1\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n--- error_log\nsyslog's log_format is not set\n\n\n\n=== TEST 3: set custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/syslog',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- stream_request eval\nmmm\n--- stream_response\nhello world\n--- wait: 0.5\n--- error_log eval\nqr/message received:.*\"client_ip\":\"127.0.0.1\"/\n\n\n\n=== TEST 5: flush manually\n--- config\n    location /t {\n        content_by_lua_block {\n            local plugin = require(\"apisix.stream.plugins.syslog\")\n            local logger_socket = require(\"resty.logger.socket\")\n            local logger, err = logger_socket:new({\n                    host = \"127.0.0.1\",\n                    port = 5044,\n                    flush_limit = 100,\n            })\n\n            local bytes, err = logger:log(\"abc\")\n            if err then\n                ngx.log(ngx.ERR, err)\n            end\n\n            local bytes, err = logger:log(\"efg\")\n            if err then\n                ngx.log(ngx.ERR, err)\n            end\n\n            local ok, err = plugin.flush_syslog(logger)\n            if not ok then\n                ngx.say(\"failed to flush syslog: \", err)\n                return\n            end\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n\n\n\n=== TEST 6: small flush_limit, instant flush\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"syslog\": {\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5044,\n                                \"flush_limit\" : 1,\n                                \"inactive_timeout\": 1\n                            }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n\n            -- wait etcd sync\n            ngx.sleep(0.5)\n\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            assert(sock:send(\"mmm\"))\n            local data = assert(sock:receive(\"*a\"))\n            ngx.print(data)\n\n            -- wait flush log\n            ngx.sleep(2.5)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\nhello world\n--- timeout: 5\n--- error_log\ntry to lock with key stream/route#1\nunlock with key stream/route#1\n\n\n\n=== TEST 7: check plugin configuration updating\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body1 = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"syslog\": {\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5044,\n                                \"batch_max_size\": 1\n                            }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            assert(sock:send(\"mmm\"))\n            local body2 = assert(sock:receive(\"*a\"))\n\n            local code, body3 = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"syslog\": {\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5045,\n                                 \"batch_max_size\": 1\n                            }\n                    },\n                    \"upstream\": {\n                        \"nodes\": {\n                            \"127.0.0.1:1995\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n\n            assert(sock:send(\"mmm\"))\n            local body4 = assert(sock:receive(\"*a\"))\n\n            ngx.print(body1)\n            ngx.print(body2)\n            ngx.print(body3)\n            ngx.print(body4)\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body\npassedhello world\npassedhello world\n--- grep_error_log eval\nqr/sending a batch logs to 127.0.0.1:(\\d+)/\n--- grep_error_log_out\nsending a batch logs to 127.0.0.1:5044\nsending a batch logs to 127.0.0.1:5045\n\n\n\n=== TEST 8: log format in plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                 ngx.HTTP_PUT,\n                 [[{\n                        \"plugins\": {\n                            \"syslog\": {\n                                \"batch_max_size\": 1,\n                                \"flush_limit\": 1,\n                                \"log_format\": {\n                                    \"vip\": \"$remote_addr\"\n                                },\n                                \"host\" : \"127.0.0.1\",\n                                \"port\" : 5050\n                            }\n                        },\n                        \"upstream\": {\n                            \"nodes\": {\n                                \"127.0.0.1:1995\": 1\n                            },\n                            \"type\": \"roundrobin\"\n                        }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: access\n--- stream_extra_init_by_lua\n    local syslog = require(\"apisix.plugins.syslog.init\")\n    local json = require(\"apisix.core.json\")\n    local log = require(\"apisix.core.log\")\n    local old_f = syslog.push_entry\n    syslog.push_entry = function(conf, ctx, entry)\n        assert(entry.vip == \"127.0.0.1\")\n        log.info(\"push_entry is called with data: \", json.encode(entry))\n        return old_f(conf, ctx, entry)\n    end\n--- stream_request\nmmm\n--- stream_response\nhello world\n--- wait: 0.5\n--- no_error_log\n[error]\n--- error_log\npush_entry is called with data\n"
  },
  {
    "path": "t/stream-plugin/traffic-split.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_shuffle();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->error_log && !$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $config = $block->config // <<_EOC_;\n    location /hit {\n        content_by_lua_block {\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(\"mmm\")\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n            local data, err = sock:receive(\"*a\")\n            if not data then\n                sock:close()\n                return ngx.exit(503)\n            end\n            ngx.print(data)\n        }\n    }\n\n    location /test_multiple_requests {\n        content_by_lua_block {\n            local reqs = {}\n            for i = 1, 10 do\n                reqs[i] = { \"/hit\" }\n            end\n            local resps = { ngx.location.capture_multi(reqs) }\n            for i, resp in ipairs(resps) do\n                ngx.say(resp.body)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_;\n            local sock = ngx.req.socket()\n            local data = sock:receive(\"1\")\n            ngx.say(\"hello world from port \" .. ngx.var.server_port)\n_EOC_\n    $block->set_value(\"stream_upstream_code\", $stream_upstream_code);\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set stream route with traffic-split plugin (basic weighted upstreams)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"traffic-split\": {\n                            \"rules\": [{\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream\": {\n                                            \"name\": \"upstream_A\",\n                                            \"type\": \"roundrobin\",\n                                            \"nodes\": {\n                                                \"127.0.0.1:1995\": 1\n                                            }\n                                        },\n                                        \"weight\": 9\n                                    },\n                                    {\n                                        \"upstream\": {\n                                            \"name\": \"upstream_B\",\n                                            \"type\": \"roundrobin\",\n                                            \"nodes\": {\n                                                \"127.0.0.1:1996\": 1\n                                            }\n                                        },\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }]\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: traffic split distribution between two upstreams\n--- request\nGET /test_multiple_requests\n--- response_body_like\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\n\nhello world from port 1995\n--- stream_enable\n--- error_log\nConnection refused\n\n\n\n=== TEST 3: set stream route with traffic-split using default route upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"nodes\": {\n                        \"127.0.0.1:1997\": 1\n                    },\n                    \"type\": \"roundrobin\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"traffic-split\": {\n                            \"rules\": [{\n                                \"weighted_upstreams\": [\n                                    {\n                                        \"upstream\": {\n                                            \"name\": \"upstream_A\",\n                                            \"type\": \"roundrobin\",\n                                            \"nodes\": {\n                                                \"127.0.0.1:1995\": 1\n                                            }\n                                        },\n                                        \"weight\": 9\n                                    },\n                                    {\n                                        \"weight\": 1\n                                    }\n                                ]\n                            }]\n                        }\n                    },\n                    \"upstream_id\": \"1\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: traffic split between plugin upstream and default route upstream\n--- request\nGET /test_multiple_requests\n--- response_body_like\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\nhello world from port 1995\n\n\nhello world from port 1995\n--- stream_enable\n--- error_log\nConnection refused\n"
  },
  {
    "path": "t/tars/conf/tars.sql",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\n\n-- MySQL dump 10.13  Distrib 5.6.26, for Linux (x86_64)\n--\n-- Host: 172.25.0.2    Database: db_tars\n-- ------------------------------------------------------\n-- Server version\t5.6.51\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Current Database: `db_tars`\n--\n\n/*!40000 DROP DATABASE IF EXISTS `db_tars`*/;\n\nCREATE DATABASE /*!32312 IF NOT EXISTS*/ `db_tars` /*!40100 DEFAULT CHARACTER SET latin1 */;\n\nUSE `db_tars`;\n\n--\n-- Table structure for table `t_adapter_conf`\n--\n\nDROP TABLE IF EXISTS `t_adapter_conf`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_adapter_conf` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `application` varchar(50) DEFAULT '',\n  `server_name` varchar(128) DEFAULT '',\n  `node_name` varchar(50) DEFAULT '',\n  `adapter_name` varchar(100) DEFAULT '',\n  `registry_timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),\n  `thread_num` int(11) DEFAULT '1',\n  `endpoint` varchar(128) DEFAULT '',\n  `max_connections` int(11) DEFAULT '1000',\n  `allow_ip` varchar(255) NOT NULL DEFAULT '',\n  `servant` varchar(128) DEFAULT '',\n  `queuecap` int(11) DEFAULT NULL,\n  `queuetimeout` int(11) DEFAULT NULL,\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(30) DEFAULT NULL,\n  `protocol` varchar(64) DEFAULT 'tars',\n  `handlegroup` varchar(64) DEFAULT '',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `application` (`application`,`server_name`,`node_name`,`adapter_name`),\n  KEY `adapter_conf_endpoint_index` (`endpoint`),\n  KEY `index_regtime_1` (`registry_timestamp`),\n  KEY `index_regtime` (`registry_timestamp`)\n) ENGINE=InnoDB AUTO_INCREMENT=72 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_ats_cases`\n--\n\nDROP TABLE IF EXISTS `t_ats_cases`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_ats_cases` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `casename` varchar(20) DEFAULT NULL,\n  `retvalue` text,\n  `paramvalue` text,\n  `interfaceid` int(11) DEFAULT NULL,\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(30) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_ats_interfaces`\n--\n\nDROP TABLE IF EXISTS `t_ats_interfaces`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_ats_interfaces` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `objname` varchar(150) DEFAULT NULL,\n  `funcname` varchar(150) DEFAULT NULL,\n  `retype` text,\n  `paramtype` text,\n  `outparamtype` text,\n  `interfaceid` int(11) DEFAULT NULL,\n  `postime` datetime DEFAULT NULL,\n  `lastuser` varchar(30) DEFAULT NULL,\n  `request_charset` varchar(16) NOT NULL,\n  `response_charset` varchar(16) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `objname` (`objname`,`funcname`),\n  UNIQUE KEY `objname_idx` (`objname`,`funcname`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_config_files`\n--\n\nDROP TABLE IF EXISTS `t_config_files`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_config_files` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `server_name` varchar(128) DEFAULT '',\n  `set_name` varchar(16) NOT NULL DEFAULT '',\n  `set_area` varchar(16) NOT NULL DEFAULT '',\n  `set_group` varchar(16) NOT NULL DEFAULT '',\n  `host` varchar(20) NOT NULL DEFAULT '',\n  `filename` varchar(128) DEFAULT NULL,\n  `config` longtext,\n  `posttime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `lastuser` varchar(50) DEFAULT NULL,\n  `level` int(11) DEFAULT '2',\n  `config_flag` int(10) NOT NULL DEFAULT '0',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `application` (`server_name`,`filename`,`host`,`level`,`set_name`,`set_area`,`set_group`)\n) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_config_history_files`\n--\n\nDROP TABLE IF EXISTS `t_config_history_files`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_config_history_files` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `configid` int(11) DEFAULT NULL,\n  `reason` varchar(128) DEFAULT '',\n  `reason_select` varchar(20) NOT NULL DEFAULT '',\n  `content` longtext,\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(50) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_config_references`\n--\n\nDROP TABLE IF EXISTS `t_config_references`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_config_references` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `config_id` int(11) DEFAULT NULL,\n  `reference_id` int(11) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `config_id` (`config_id`,`reference_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_group_priority`\n--\n\nDROP TABLE IF EXISTS `t_group_priority`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_group_priority` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `name` varchar(128) DEFAULT '',\n  `group_list` text,\n  `list_order` int(11) DEFAULT '0',\n  `station` varchar(128) NOT NULL DEFAULT '',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_machine_tars_info`\n--\n\nDROP TABLE IF EXISTS `t_machine_tars_info`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_machine_tars_info` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `application` varchar(100) NOT NULL DEFAULT '',\n  `server_name` varchar(100) NOT NULL DEFAULT '',\n  `app_server_name` varchar(50) NOT NULL DEFAULT '',\n  `node_name` varchar(50) NOT NULL DEFAULT '',\n  `location` varchar(255) NOT NULL DEFAULT '',\n  `machine_type` varchar(50) NOT NULL DEFAULT '',\n  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `update_person` varchar(64) NOT NULL DEFAULT '',\n  PRIMARY KEY (`application`,`server_name`,`node_name`),\n  UNIQUE KEY `id` (`id`),\n  UNIQUE KEY `tmachine_key` (`application`,`node_name`,`server_name`),\n  KEY `tmachine_i_2` (`node_name`,`server_name`),\n  KEY `tmachine_idx` (`node_name`,`server_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_node_info`\n--\n\nDROP TABLE IF EXISTS `t_node_info`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_node_info` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `node_name` varchar(128) DEFAULT '',\n  `node_obj` varchar(128) DEFAULT '',\n  `endpoint_ip` varchar(16) DEFAULT '',\n  `endpoint_port` int(11) DEFAULT '0',\n  `data_dir` varchar(128) DEFAULT '',\n  `load_avg1` float DEFAULT '0',\n  `load_avg5` float DEFAULT '0',\n  `load_avg15` float DEFAULT '0',\n  `last_reg_time` datetime DEFAULT '1970-01-01 00:08:00',\n  `last_heartbeat` datetime DEFAULT '1970-01-01 00:08:00',\n  `setting_state` enum('active','inactive') DEFAULT 'inactive',\n  `present_state` enum('active','inactive') DEFAULT 'inactive',\n  `tars_version` varchar(128) NOT NULL DEFAULT '',\n  `template_name` varchar(128) NOT NULL DEFAULT '',\n  `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `group_id` int(11) DEFAULT '-1',\n  `label` text,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `node_name` (`node_name`),\n  KEY `indx_node_info_1` (`last_heartbeat`),\n  KEY `indx_node_info` (`last_heartbeat`)\n) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_profile_template`\n--\n\nDROP TABLE IF EXISTS `t_profile_template`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_profile_template` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `template_name` varchar(128) DEFAULT '',\n  `parents_name` varchar(128) DEFAULT '',\n  `profile` text NOT NULL,\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(30) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `template_name` (`template_name`)\n) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_registry_info`\n--\n\nDROP TABLE IF EXISTS `t_registry_info`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_registry_info` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `locator_id` varchar(128) NOT NULL DEFAULT '',\n  `servant` varchar(128) NOT NULL DEFAULT '',\n  `endpoint` varchar(128) NOT NULL DEFAULT '',\n  `last_heartbeat` datetime DEFAULT '1970-01-01 00:08:00',\n  `present_state` enum('active','inactive') DEFAULT 'inactive',\n  `tars_version` varchar(128) NOT NULL DEFAULT '',\n  `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `enable_group` char(1) DEFAULT 'N',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `locator_id` (`locator_id`,`servant`)\n) ENGINE=InnoDB AUTO_INCREMENT=4576264 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_server_conf`\n--\n\nDROP TABLE IF EXISTS `t_server_conf`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_server_conf` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `application` varchar(128) DEFAULT '',\n  `server_name` varchar(128) DEFAULT '',\n  `node_group` varchar(50) NOT NULL DEFAULT '',\n  `node_name` varchar(50) NOT NULL DEFAULT '',\n  `registry_timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),\n  `base_path` varchar(128) DEFAULT '',\n  `exe_path` varchar(128) NOT NULL DEFAULT '',\n  `template_name` varchar(128) NOT NULL DEFAULT '',\n  `bak_flag` int(11) NOT NULL DEFAULT '0',\n  `setting_state` enum('active','inactive') NOT NULL DEFAULT 'inactive',\n  `present_state` enum('active','inactive','activating','deactivating','destroyed') NOT NULL DEFAULT 'inactive',\n  `process_id` int(11) NOT NULL DEFAULT '0',\n  `patch_version` varchar(128) NOT NULL DEFAULT '',\n  `patch_time` datetime NOT NULL DEFAULT '2021-12-22 10:35:56',\n  `patch_user` varchar(128) NOT NULL DEFAULT '',\n  `tars_version` varchar(128) NOT NULL DEFAULT '',\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(30) DEFAULT NULL,\n  `server_type` enum('tars_cpp','not_tars','tars_java','tars_nodejs','tars_php','tars_go') DEFAULT NULL,\n  `start_script_path` varchar(128) DEFAULT NULL,\n  `stop_script_path` varchar(128) DEFAULT NULL,\n  `monitor_script_path` varchar(128) DEFAULT NULL,\n  `enable_group` char(1) DEFAULT 'N',\n  `enable_set` char(1) NOT NULL DEFAULT 'N',\n  `set_name` varchar(16) DEFAULT NULL,\n  `set_area` varchar(16) DEFAULT NULL,\n  `set_group` varchar(64) DEFAULT NULL,\n  `ip_group_name` varchar(64) DEFAULT NULL,\n  `profile` text,\n  `config_center_port` int(11) NOT NULL DEFAULT '0',\n  `async_thread_num` int(11) DEFAULT '3',\n  `server_important_type` enum('0','1','2','3','4','5') DEFAULT '0',\n  `remote_log_reserve_time` varchar(32) NOT NULL DEFAULT '65',\n  `remote_log_compress_time` varchar(32) NOT NULL DEFAULT '2',\n  `remote_log_type` int(1) NOT NULL DEFAULT '0',\n  `flow_state` enum('active','inactive') NOT NULL DEFAULT 'active',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `application` (`application`,`server_name`,`node_name`),\n  KEY `node_name` (`node_name`),\n  KEY `index_i_3` (`setting_state`,`server_type`,`application`,`server_name`,`node_name`),\n  KEY `index_regtime` (`registry_timestamp`),\n  KEY `index_i` (`setting_state`,`server_type`,`application`,`server_name`,`node_name`)\n) ENGINE=InnoDB AUTO_INCREMENT=63 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_server_group_relation`\n--\n\nDROP TABLE IF EXISTS `t_server_group_relation`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_server_group_relation` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `application` varchar(90) NOT NULL DEFAULT '',\n  `server_group` varchar(50) DEFAULT '',\n  `server_name` varchar(50) DEFAULT '',\n  `create_time` datetime DEFAULT NULL,\n  `creator` varchar(30) DEFAULT '',\n  PRIMARY KEY (`id`),\n  KEY `f_unique` (`application`,`server_group`,`server_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_server_group_rule`\n--\n\nDROP TABLE IF EXISTS `t_server_group_rule`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_server_group_rule` (\n  `group_id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `ip_order` enum('allow_denny','denny_allow') NOT NULL DEFAULT 'denny_allow',\n  `allow_ip_rule` text,\n  `denny_ip_rule` text,\n  `lastuser` varchar(50) DEFAULT NULL,\n  `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `group_name` varchar(128) DEFAULT '',\n  `group_name_cn` varchar(128) DEFAULT '',\n  PRIMARY KEY (`group_id`),\n  UNIQUE KEY `group_name_index` (`group_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_server_notifys`\n--\n\nDROP TABLE IF EXISTS `t_server_notifys`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_server_notifys` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `application` varchar(128) DEFAULT '',\n  `server_name` varchar(128) DEFAULT NULL,\n  `container_name` varchar(128) DEFAULT '',\n  `node_name` varchar(128) NOT NULL DEFAULT '',\n  `set_name` varchar(16) DEFAULT NULL,\n  `set_area` varchar(16) DEFAULT NULL,\n  `set_group` varchar(16) DEFAULT NULL,\n  `server_id` varchar(100) DEFAULT NULL,\n  `thread_id` varchar(20) DEFAULT NULL,\n  `command` varchar(50) DEFAULT NULL,\n  `result` text,\n  `notifytime` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `index_name` (`server_name`),\n  KEY `servernoticetime_i_1` (`notifytime`),\n  KEY `indx_1_server_id` (`server_id`),\n  KEY `query_index` (`application`,`server_name`,`node_name`,`set_name`,`set_area`,`set_group`),\n  KEY `servernoticetime_i` (`notifytime`),\n  KEY `indx_server_id` (`server_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=21962 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_server_patchs`\n--\n\nDROP TABLE IF EXISTS `t_server_patchs`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_server_patchs` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `server` varchar(50) DEFAULT NULL,\n  `version` varchar(1000) DEFAULT '',\n  `tgz` varchar(255) DEFAULT NULL,\n  `update_text` varchar(255) DEFAULT NULL,\n  `reason_select` varchar(255) DEFAULT NULL,\n  `document_complate` varchar(30) DEFAULT NULL,\n  `is_server_group` int(2) NOT NULL DEFAULT '0',\n  `publish` int(3) DEFAULT NULL,\n  `publish_time` datetime DEFAULT NULL,\n  `publish_user` varchar(30) DEFAULT NULL,\n  `upload_time` datetime DEFAULT NULL,\n  `upload_user` varchar(30) DEFAULT NULL,\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(30) DEFAULT NULL,\n  `is_release_version` enum('true','false') DEFAULT 'true',\n  `package_type` int(4) DEFAULT '0',\n  `group_id` varchar(64) NOT NULL DEFAULT '',\n  `default_version` int(4) DEFAULT '0',\n  `md5` varchar(40) DEFAULT NULL,\n  `svn_version` varchar(50) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `server_patchs_server_index` (`server`),\n  KEY `index_patchs_i1` (`server`),\n  KEY `index_i_2` (`tgz`(50)),\n  KEY `index_i` (`tgz`)\n) ENGINE=InnoDB AUTO_INCREMENT=170 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_task`\n--\n\nDROP TABLE IF EXISTS `t_task`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_task` (\n  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,\n  `task_no` varchar(40) DEFAULT NULL,\n  `serial` int(1) DEFAULT NULL,\n  `user_name` varchar(20) DEFAULT NULL,\n  `create_time` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `f_task` (`task_no`),\n  CONSTRAINT `t_task_ibfk_1` FOREIGN KEY (`task_no`) REFERENCES `t_task_item` (`task_no`) ON DELETE SET NULL ON UPDATE CASCADE\n) ENGINE=InnoDB AUTO_INCREMENT=119 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_task_item`\n--\n\nDROP TABLE IF EXISTS `t_task_item`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_task_item` (\n  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,\n  `task_no` varchar(40) DEFAULT NULL,\n  `item_no` varchar(40) DEFAULT NULL,\n  `application` varchar(30) DEFAULT NULL,\n  `server_name` varchar(50) DEFAULT NULL,\n  `node_name` varchar(20) DEFAULT NULL,\n  `command` varchar(20) DEFAULT NULL,\n  `parameters` text,\n  `start_time` datetime DEFAULT NULL,\n  `end_time` datetime DEFAULT NULL,\n  `status` int(11) DEFAULT NULL,\n  `set_name` varchar(20) DEFAULT NULL,\n  `log` text,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `f_uniq` (`item_no`,`task_no`),\n  KEY `f_task_no` (`task_no`),\n  KEY `f_index` (`application`,`server_name`,`command`)\n) ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `t_web_release_conf`\n--\n\nDROP TABLE IF EXISTS `t_web_release_conf`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `t_web_release_conf` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `server` varchar(100) NOT NULL DEFAULT '',\n  `path` varchar(200) NOT NULL DEFAULT '',\n  `server_dir` varchar(200) NOT NULL DEFAULT '',\n  `is_server_group` int(2) NOT NULL DEFAULT '0',\n  `enable_batch` int(2) NOT NULL DEFAULT '0',\n  `user` varchar(200) NOT NULL DEFAULT '*',\n  `posttime` datetime DEFAULT NULL,\n  `lastuser` varchar(60) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `server` (`server`,`is_server_group`),\n  KEY `web_release_conf_server_index` (`server`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n/*!40101 SET character_set_client = @saved_cs_client */;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed\n"
  },
  {
    "path": "t/tars/discovery/stream/tars.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nplan('no_plan');\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\n  enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n  tars:\n    db_conf:\n      host: 127.0.0.1\n      port: 3306\n      database: db_tars\n      user: root\n      password: tars2022\n    full_fetch_interval: 3\n    incremental_fetch_interval: 1\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $extra_init_by_lua_start = <<_EOC_;\n        -- reduce incremental_fetch_interval,full_fetch_interval\n        local schema = require(\"apisix.discovery.tars.schema\")\n        schema.properties.incremental_fetch_interval.minimum=1\n        schema.properties.incremental_fetch_interval.default=1\n        schema.properties.full_fetch_interval.minimum = 3\n        schema.properties.full_fetch_interval.default = 3\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua_start\", $extra_init_by_lua_start);\n    $block->set_value(\"stream_extra_init_by_lua_start\", $extra_init_by_lua_start);\n\n    my $config = $block->config // <<_EOC_;\n\n        location /sql {\n            content_by_lua_block {\n                local mysql = require(\"resty.mysql\")\n                local core = require(\"apisix.core\")\n                local ipairs = ipairs\n\n                ngx.req.read_body()\n                local sql = ngx.req.get_body_data()\n                core.log.info(\"get sql \", sql)\n\n                local db_conf= {\n                  host=\"127.0.0.1\",\n                  port=3306,\n                  database=\"db_tars\",\n                  user=\"root\",\n                  password=\"tars2022\",\n                }\n\n                local db_cli, err = mysql:new()\n                if not db_cli then\n                  core.log.error(\"failed to instantiate mysql: \", err)\n                  return\n                end\n                db_cli:set_timeout(3000)\n\n                local ok, err, errcode, sqlstate = db_cli:connect(db_conf)\n                if not ok then\n                  core.log.error(\"failed to connect mysql: \", err, \", \", errcode, \", \", sqlstate)\n                  return\n                end\n\n                local res, err, errcode, sqlstate = db_cli:query(sql)\n                if not res then\n                   ngx.say(\"bad result: \", err, \": \", errcode, \": \", sqlstate, \".\")\n                   return\n                end\n                ngx.say(\"DONE\")\n            }\n        }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_config = $block->stream_config // <<_EOC_;\n        server {\n            listen 8125;\n            content_by_lua_block {\n                local core = require(\"apisix.core\")\n                local d = require(\"apisix.discovery.tars\")\n\n                ngx.sleep(2)\n\n                local sock = ngx.req.socket()\n                local request_body = sock:receive()\n\n                core.log.info(\"get body \", request_body)\n\n                local response_body = \"{\"\n                local queries = core.json.decode(request_body)\n                for _,query in ipairs(queries) do\n                  local nodes = d.nodes(query)\n                  if nodes==nil or #nodes==0 then\n                      response_body=response_body..\" \"..0\n                  else\n                      response_body=response_body..\" \"..#nodes\n                  end\n                end\n                ngx.say(response_body..\" }\")\n            }\n        }\n\n_EOC_\n\n    $block->set_value(\"extra_stream_config\", $stream_config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create initial server and servant\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\ntruncate table t_server_conf\",\n\n\"POST /sql\ntruncate table t_adapter_conf\",\n\n\"POST /sql\ninsert into t_server_conf(application, server_name, node_name, registry_timestamp,\n                          template_name, setting_state, present_state, server_type)\nvalues ('A', 'AServer', '172.16.1.1', now(), 'taf-cpp', 'active', 'active', 'tars_cpp'),\n       ('B', 'BServer', '172.16.2.1', now(), 'taf-cpp', 'active', 'active', 'tars_cpp'),\n       ('C', 'CServer', '172.16.3.1', now(), 'taf-cpp', 'active', 'active', 'tars_cpp')\",\n\n\"POST /sql\ninsert into t_adapter_conf(application, server_name, node_name, adapter_name, endpoint, servant)\nvalues ('A', 'AServer', '172.16.1.1', 'A.AServer.FirstObjAdapter',\n        'tcp -h 172.16.1.1 -p 10001 -e 0 -t 6000', 'A.AServer.FirstObj'),\n       ('B', 'BServer', '172.16.2.1', 'B.BServer.FirstObjAdapter',\n        'tcp -p 10001 -h 172.16.2.1 -e 0 -t 6000', 'B.BServer.FirstObj'),\n       ('C', 'CServer', '172.16.3.1', 'C.CServer.FirstObjAdapter',\n        'tcp -e 0 -h 172.16.3.1 -t 6000 -p 10001 ', 'C.CServer.FirstObj')\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"DONE\\n\",\n]\n\n\n\n=== TEST 2: get count after create servant\n--- apisix_yaml\nstream_routes:\n  -\n    id: 1\n    server_port: 1985\n    upstream_id: 1\n\nupstreams:\n  - nodes:\n      \"127.0.0.1:8125\": 1\n    type: roundrobin\n    id: 1\n\n#END\n--- stream_request\n[\"A.AServer.FirstObj\",\"B.BServer.FirstObj\", \"C.CServer.FirstObj\"]\n--- stream_response eval\nqr{ 1 1 1 }\n"
  },
  {
    "path": "t/tars/discovery/tars.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nlog_level('warn');\nno_root_location();\nno_shuffle();\nworkers(4);\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    my $yaml_config = <<_EOC_;\napisix:\n  node_listen: 1984\n  enable_admin: false\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: yaml\ndiscovery:\n  tars:\n    db_conf:\n      host: 127.0.0.1\n      port: 3306\n      database: db_tars\n      user: root\n      password: tars2022\n    full_fetch_interval: 3\n    incremental_fetch_interval: 1\n_EOC_\n\n    $block->set_value(\"yaml_config\", $yaml_config);\n\n    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;\nroutes: []\n#END\n_EOC_\n\n    $block->set_value(\"apisix_yaml\", $apisix_yaml);\n\n    my $extra_init_by_lua_start = <<_EOC_;\n        -- reduce incremental_fetch_interval,full_fetch_interval\n        local schema = require(\"apisix.discovery.tars.schema\")\n        schema.properties.incremental_fetch_interval.minimum=1\n        schema.properties.incremental_fetch_interval.default=1\n        schema.properties.full_fetch_interval.minimum = 3\n        schema.properties.full_fetch_interval.default = 3\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua_start\", $extra_init_by_lua_start);\n\n    my $config = $block->config // <<_EOC_;\n        location /count {\n            content_by_lua_block {\n              local core = require(\"apisix.core\")\n              local d = require(\"apisix.discovery.tars\")\n\n              ngx.sleep(2)\n\n              ngx.req.read_body()\n              local request_body = ngx.req.get_body_data()\n              local queries = core.json.decode(request_body)\n              local response_body = \"{\"\n              for _,query in ipairs(queries) do\n                local nodes = d.nodes(query)\n                if nodes==nil or #nodes==0 then\n                    response_body=response_body..\" \"..0\n                else\n                    response_body=response_body..\" \"..#nodes\n                end\n              end\n              ngx.say(response_body..\" }\")\n            }\n        }\n\n        location /nodes {\n            content_by_lua_block {\n              local core = require(\"apisix.core\")\n              local d = require(\"apisix.discovery.tars\")\n\n              ngx.sleep(2)\n\n              ngx.req.read_body()\n              local servant = ngx.req.get_body_data()\n              local response=\"\"\n              local nodes = d.nodes(servant)\n              response=\"{\"\n              for _,node in ipairs(nodes or {}) do\n                response=response..node.host..\":\"..node.port..\",\"\n              end\n              response=response..\"}\"\n              ngx.say(response)\n            }\n        }\n\n        location /sql {\n            content_by_lua_block {\n                local mysql = require(\"resty.mysql\")\n                local core = require(\"apisix.core\")\n                local ipairs = ipairs\n\n                ngx.req.read_body()\n                local sql = ngx.req.get_body_data()\n                core.log.info(\"get sql \", sql)\n\n                local db_conf= {\n                  host=\"127.0.0.1\",\n                  port=3306,\n                  database=\"db_tars\",\n                  user=\"root\",\n                  password=\"tars2022\",\n                }\n\n                local db_cli, err = mysql:new()\n                if not db_cli then\n                  core.log.error(\"failed to instantiate mysql: \", err)\n                  return\n                end\n                db_cli:set_timeout(3000)\n\n                local ok, err, errcode, sqlstate = db_cli:connect(db_conf)\n                if not ok then\n                  core.log.error(\"failed to connect mysql: \", err, \", \", errcode, \", \", sqlstate)\n                  return\n                end\n\n                local res, err, errcode, sqlstate = db_cli:query(sql)\n                if not res then\n                   ngx.say(\"bad result: \", err, \": \", errcode, \": \", sqlstate, \".\")\n                   return\n                end\n                ngx.say(\"DONE\")\n            }\n        }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: create initial server and servant\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\ntruncate table t_server_conf\",\n\n\"POST /sql\ntruncate table t_adapter_conf\",\n\n\"POST /sql\ninsert into t_server_conf(application, server_name, node_name, registry_timestamp,\n                          template_name, setting_state, present_state, server_type)\nvalues ('A', 'AServer', '172.16.1.1', now(), 'taf-cpp', 'active', 'active', 'tars_cpp'),\n       ('B', 'BServer', '172.16.2.1', now(), 'taf-cpp', 'active', 'active', 'tars_cpp'),\n       ('C', 'CServer', '172.16.3.1', now(), 'taf-cpp', 'active', 'active', 'tars_cpp')\",\n\n\"POST /sql\ninsert into t_adapter_conf(application, server_name, node_name, adapter_name, endpoint, servant)\nvalues ('A', 'AServer', '172.16.1.1', 'A.AServer.FirstObjAdapter',\n        'tcp -h 172.16.1.1 -p 10001 -e 0 -t 6000', 'A.AServer.FirstObj'),\n       ('B', 'BServer', '172.16.2.1', 'B.BServer.FirstObjAdapter',\n        'tcp -p 10001 -h 172.16.2.1 -e 0 -t 6000', 'B.BServer.FirstObj'),\n       ('C', 'CServer', '172.16.3.1', 'C.CServer.FirstObjAdapter',\n        'tcp -e 0 -h 172.16.3.1 -t 6000 -p 10001 ', 'C.CServer.FirstObj')\",\n\n\"GET /count\n[\\\"A.AServer.FirstObj\\\",\\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"DONE\\n\",\n    \"{ 1 1 1 }\\n\",\n]\n\n\n\n=== TEST 2: add servers on different nodes\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\ninsert into t_server_conf(application, server_name, node_name, registry_timestamp,\n                          template_name, setting_state, present_state, server_type)\nvalues ('A', 'AServer', '172.16.1.2', now(), 'taf-cpp', 'active', 'active', 'tars_cpp'),\n       ('B', 'BServer', '172.16.2.2', now(), 'taf-cpp', 'active', 'active', 'tars_cpp'),\n       ('C', 'CServer', '172.16.3.2', now(), 'taf-cpp', 'active', 'active', 'tars_cpp')\",\n\n\"GET /count\n[\\\"A.AServer.FirstObj\\\",\\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 1 1 1 }\\n\",\n]\n\n\n\n=== TEST 3: add servant\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\ninsert into t_adapter_conf(application, server_name, node_name, adapter_name, endpoint, servant)\nvalues ('A', 'AServer', '172.16.1.2', 'A.AServer.FirstObjAdapter',\n        'tcp -h 172.16.1.2 -p 10001 -e 0 -t 6000', 'A.AServer.FirstObj'),\n       ('A', 'AServer', '172.16.1.2', 'A.AServer.SecondObjAdapter',\n        'tcp -p 10002 -h 172.16.1.2 -e 0 -t 6000', 'A.AServer.SecondObj')\",\n\n\"GET /count\n[\\\"A.AServer.FirstObj\\\", \\\"A.AServer.SecondObj\\\", \\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 2 1 1 1 }\\n\",\n]\n\n\n\n=== TEST 4: update servant, update setting_state\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\nupdate t_server_conf set setting_state='inactive'\nwhere application = 'A' and server_name = 'AServer' and node_name = '172.16.1.2'\",\n\n\"GET /count\n[\\\"A.AServer.FirstObj\\\", \\\"A.AServer.SecondObj\\\", \\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 1 0 1 1 }\\n\",\n]\n\n\n\n=== TEST 5: update server setting_state\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\nupdate t_server_conf set setting_state='active', present_state='inactive'\nwhere application = 'A' and server_name = 'AServer' and node_name = '172.16.1.2'\",\n\n\"GET /count\n[\\\"A.AServer.FirstObj\\\", \\\"A.AServer.SecondObj\\\", \\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 1 0 1 1 }\\n\",\n]\n\n\n\n=== TEST 6: update server present_state\n--- timeout: 3\n--- request eval\n[\n\"POST /sql\nupdate t_server_conf set setting_state='active', present_state='active'\nwhere application = 'A' and server_name = 'AServer' and node_name = '172.16.1.2'\",\n\n\"GET /count\n[\\\"A.AServer.FirstObj\\\", \\\"A.AServer.SecondObj\\\", \\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n    \"{ 2 1 1 1 }\\n\",\n]\n\n\n\n=== TEST 7: update servant endpoint\n--- timeout: 3\n--- request eval\n[\n\"GET /nodes\nA.AServer.SecondObj\",\n\n\"POST /sql\nupdate t_adapter_conf set endpoint='tcp -h 172.16.1.2 -p 10003 -e 0 -t 3000'\nwhere application = 'A' and server_name = 'AServer'\nand node_name = '172.16.1.2' and servant='A.AServer.SecondObj'\",\n\n\"GET /nodes\nA.AServer.SecondObj\",\n\n]\n--- response_body eval\n[\n    \"{172.16.1.2:10002,}\\n\",\n    \"DONE\\n\",\n    \"{172.16.1.2:10003,}\\n\",\n]\n\n\n\n=== TEST 8: delete servant\n--- request eval\n[\n\"POST /sql\ndelete from t_adapter_conf where application = 'A' and server_name = 'AServer'\nand node_name = '172.16.1.2' and servant = 'A.AServer.SecondObj'\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n]\n\n\n\n=== TEST 9: count after delete servant\n--- timeout: 4\n--- wait: 3\n--- request eval\n[\n\"GET /count\n[\\\"A.AServer.FirstObj\\\", \\\"A.AServer.SecondObj\\\", \\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"{ 2 0 1 1 }\\n\",\n]\n\n\n\n=== TEST 10: delete server\n--- request eval\n[\n\"POST /sql\ndelete from t_server_conf\nwhere application = 'A' and server_name = 'AServer' and node_name = '172.16.1.1'\",\n\n]\n--- response_body eval\n[\n    \"DONE\\n\",\n]\n\n\n\n=== TEST 11: count after delete\n--- timeout: 4\n--- wait: 3\n--- request eval\n[\n\"GET /count\n[\\\"A.AServer.FirstObj\\\", \\\"A.AServer.SecondObj\\\", \\\"B.BServer.FirstObj\\\", \\\"C.CServer.FirstObj\\\"]\",\n\n]\n--- response_body eval\n[\n    \"{ 1 0 1 1 }\\n\",\n]\n"
  },
  {
    "path": "t/ts/admin_api.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport axios, {\n  type AxiosRequestConfig,\n  type Method,\n  type RawAxiosRequestHeaders,\n} from 'axios';\n\nexport const request = async (\n  url: string,\n  method: Method = 'GET',\n  body?: object,\n  headers?: RawAxiosRequestHeaders,\n  config?: AxiosRequestConfig,\n) => {\n  return axios.request({\n    method,\n    // TODO: use 9180 for admin api\n    baseURL: 'http://127.0.0.1:1984',\n    url,\n    data: body,\n    headers: {\n      'X-API-KEY': 'edd1c9f034335f136f87ad84b625c8f1',\n      ...headers,\n    },\n    ...config,\n  });\n};\n"
  },
  {
    "path": "t/ts/utils.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nexport const wait = (ms: number) =>\n  new Promise<void>((resolve) => setTimeout(resolve, ms));\n"
  },
  {
    "path": "t/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"lib\": [\"esnext\"],\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"types\": [\"node\", \"jest\"],\n    \"allowJs\": true\n  },\n  \"include\": [\"**/*.ts\", \"**/*.mts\", \"**/*.cts\", \"**/*.js\"],\n}\n"
  },
  {
    "path": "t/utils/batch-processor.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nlog_level('debug');\nrepeat_each(1);\nno_long_string();\nno_root_location();\nrun_tests;\n\n__DATA__\n\n=== TEST 1: send invalid arguments for constructor\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 1,\n                retry_delay  = 0,\n            }\n            local func_to_send = function(elements)\n                return true\n            end\n            local log_buffer, err = Batch:new(\"\", config)\n\n            if log_buffer then\n                log_buffer:push({hello='world'})\n                ngx.say(\"done\")\n            end\n\n            if not log_buffer then\n                ngx.say(\"failed\")\n            end\n\n        }\n    }\n--- request\nGET /t\n--- response_body\nfailed\n--- wait: 0.5\n\n\n\n=== TEST 2: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local func_to_send = function(elements)\n                return true\n            end\n\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 1,\n                retry_delay  = 0,\n            }\n\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nBatch Processor[log buffer] successfully processed the entries\n--- wait: 0.5\n\n\n\n=== TEST 3: batch processor timeout exceeded\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 2,\n                retry_delay  = 0,\n                inactive_timeout = 1\n            }\n            local func_to_send = function(elements)\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nBatch Processor[log buffer] buffer duration exceeded, activating buffer flush\nBatch Processor[log buffer] successfully processed the entries\n--- wait: 3\n\n\n\n=== TEST 4: batch processor batch max size exceeded\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local config = {\n                max_retry_count = 2,\n                batch_max_size = 2,\n                retry_delay = 0,\n            }\n            local func_to_send = function(elements)\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nBatch Processor[log buffer] buffer duration exceeded, activating buffer flush\n--- error_log\nBatch Processor[log buffer] batch max size has exceeded\nBatch Processor[log buffer] successfully processed the entries\n--- wait: 1\n\n\n\n=== TEST 5: first failed to process and second try success\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local core = require(\"apisix.core\")\n            local retry = false\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 2,\n                retry_delay  = 0,\n            }\n            local func_to_send = function(elements)\n                if not retry then\n                    retry = true\n                    return false\n                end\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nBatch Processor[log buffer] failed to process entries\nBatch Processor[log buffer] successfully processed the entries\n--- wait: 0.5\n\n\n\n=== TEST 6: Exceeding max retry count\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 2,\n                retry_delay  = 0,\n            }\n            local func_to_send = function(elements)\n                return false\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nBatch Processor[log buffer] buffer duration exceeded, activating buffer flush\n--- error_log\nBatch Processor[log buffer] failed to process entries\nBatch Processor[log buffer] exceeded the max_retry_count\n--- wait: 0.5\n\n\n\n=== TEST 7: two batches\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local core = require(\"apisix.core\")\n            local count = 0\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 2,\n                retry_delay  = 0,\n            }\n            local func_to_send = function(elements)\n                count = count + 1\n                core.log.info(\"batch[\", count , \"] sent\")\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nBatch Processor[log buffer] activating flush due to no activity\n--- error_log\nbatch[1] sent\nbatch[2] sent\n--- wait: 0.5\n\n\n\n=== TEST 8: batch processor retry count 0 and fail processing\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local config = {\n                max_retry_count  = 0,\n                batch_max_size = 2,\n                retry_delay  = 0,\n            }\n            local func_to_send = function(elements)\n                return false\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nBatch Processor[log buffer] activating flush due to no activity\n--- error_log\nBatch Processor[log buffer] exceeded the max_retry_count\n--- wait: 0.5\n\n\n\n=== TEST 9: batch processor timeout exceeded\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 2,\n                retry_delay  = 0,\n                buffer_duration = 60,\n                inactive_timeout = 1,\n            }\n            local func_to_send = function(elements)\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({hello='world'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\nBatch Processor[log buffer] buffer duration exceeded, activating buffer flush\nBatch Processor[log buffer] successfully processed the entries\n--- wait: 3\n\n\n\n=== TEST 10: json encode and log elements\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local core = require(\"apisix.core\")\n            local config = {\n                max_retry_count  = 2,\n                batch_max_size = 2,\n                retry_delay  = 0,\n            }\n            local func_to_send = function(elements)\n                core.log.info(require(\"toolkit.json\").encode(elements))\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({msg='1'})\n            log_buffer:push({msg='2'})\n            log_buffer:push({msg='3'})\n            log_buffer:push({msg='4'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nBatch Processor[log buffer] activating flush due to no activity\n--- error_log\n[{\"msg\":\"1\"},{\"msg\":\"2\"}]\n[{\"msg\":\"3\"},{\"msg\":\"4\"}]\n--- wait: 0.5\n\n\n\n=== TEST 11: extend timer\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local core = require(\"apisix.core\")\n            local config = {\n                max_retry_count  = 1,\n                batch_max_size = 3,\n                retry_delay  = 0,\n                inactive_timeout = 1\n            }\n            local func_to_send = function(elements)\n                core.log.info(require(\"toolkit.json\").encode(elements))\n                return true\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({msg='1'})\n            ngx.sleep(0.3)\n            log_buffer:push({msg='2'})\n            log_buffer:push({msg='3'})\n            log_buffer:push({msg='4'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- no_error_log\nBatch Processor[log buffer] activating flush due to no activity\n--- error_log\nBatch Processor[log buffer] extending buffer timer\n--- wait: 3\n\n\n\n=== TEST 12: partially consumed entries\n--- config\n    location /t {\n        content_by_lua_block {\n            local Batch = require(\"apisix.utils.batch-processor\")\n            local core = require(\"apisix.core\")\n            local config = {\n                max_retry_count  = 1,\n                batch_max_size = 3,\n                retry_delay  = 0,\n                inactive_timeout = 1\n            }\n            local func_to_send = function(elements)\n                core.log.info(require(\"toolkit.json\").encode(elements))\n                return false, \"error after consuming single entry\", 2\n            end\n            local log_buffer, err = Batch:new(func_to_send, config)\n\n            if not log_buffer then\n                ngx.say(err)\n            end\n\n            log_buffer:push({msg='1'})\n            log_buffer:push({msg='2'})\n            log_buffer:push({msg='3'})\n            log_buffer:push({msg='4'})\n            ngx.say(\"done\")\n        }\n    }\n--- request\nGET /t\n--- response_body\ndone\n--- error_log\n[{\"msg\":\"1\"},{\"msg\":\"2\"},{\"msg\":\"3\"}]\nBatch Processor[log buffer] failed to process entries [2/3]: error after consuming single entry\n[{\"msg\":\"2\"},{\"msg\":\"3\"}]\nBatch Processor[log buffer] failed to process entries [1/2]: error after consuming single entry\n[{\"msg\":\"4\"}]\n--- wait: 2\n"
  },
  {
    "path": "t/utils/rfc5424.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: Compatibility testing\n--- config\n    location /t {\n        content_by_lua_block {\n            local rfc5424 = require(\"apisix.utils.rfc5424\")\n            local structured_data = {\n                {name = \"project\", value = \"apisix.apache.org\"},\n                {name = \"logstore\", value = \"apisix.apache.org\"},\n                {name = \"access-key-id\", value = \"apisix.sls.logger\"},\n                {name = \"access-key-secret\", value = \"BD274822-96AA-4DA6-90EC-15940FB24444\"}\n            }\n            local data = rfc5424.encode(\"SYSLOG\", \"INFO\", \"localhost\", \"apisix\",\n                                                123456, \"hello world\", structured_data)\n            ngx.say(data)\n        }\n    }\n--- response_body eval\nqr/<46>1.*localhost apisix 123456 - \\[logservice project=\\\"apisix\\.apache\\.org\\\" logstore=\\\"apisix\\.apache\\.org\\\" access-key-id=\\\"apisix\\.sls\\.logger\\\" access-key-secret=\\\"BD274822-96AA-4DA6-90EC-15940FB24444\\\"\\] hello world/\n\n\n\n=== TEST 2: No structured data test\n--- config\n    location /t {\n        content_by_lua_block {\n            local rfc5424 = require(\"apisix.utils.rfc5424\")\n            local data = rfc5424.encode(\"SYSLOG\", \"INFO\", \"localhost\", \"apisix\",\n                                                123456, \"hello world\")\n            ngx.say(data)\n        }\n    }\n--- response_body eval\nqr/<46>1.*localhost apisix 123456 - - hello world/\n\n\n\n=== TEST 3: No host and appname test\n--- config\n    location /t {\n        content_by_lua_block {\n            local rfc5424 = require(\"apisix.utils.rfc5424\")\n            local data = rfc5424.encode(\"SYSLOG\", \"INFO\", nil, nil,\n                                                123456, \"hello world\")\n            ngx.say(data)\n        }\n    }\n--- response_body eval\nqr/<46>1.*- - 123456 - - hello world/\n"
  },
  {
    "path": "t/wasm/fault-injection/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"math/rand\"\n\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm\"\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types\"\n\n\t// tinygo doesn't support encoding/json, see https://github.com/tinygo-org/tinygo/issues/447\n\t\"github.com/valyala/fastjson\"\n)\n\nfunc main() {\n\tproxywasm.SetVMContext(&vmContext{})\n}\n\ntype vmContext struct {\n\ttypes.DefaultVMContext\n}\n\nfunc (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {\n\treturn &pluginContext{}\n}\n\ntype pluginContext struct {\n\ttypes.DefaultPluginContext\n\tBody       []byte\n\tHttpStatus uint32\n\tPercentage int\n}\n\nfunc (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {\n\tdata, err := proxywasm.GetPluginConfiguration()\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error reading plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tvar p fastjson.Parser\n\tv, err := p.ParseBytes(data)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error decoding plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\tctx.Body = v.GetStringBytes(\"body\")\n\tctx.HttpStatus = uint32(v.GetUint(\"http_status\"))\n\tif v.Exists(\"percentage\") {\n\t\tctx.Percentage = v.GetInt(\"percentage\")\n\t} else {\n\t\tctx.Percentage = 100\n\t}\n\n\t// schema check\n\tif ctx.HttpStatus < 200 {\n\t\tproxywasm.LogError(\"bad http_status\")\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\tif ctx.Percentage < 0 || ctx.Percentage > 100 {\n\t\tproxywasm.LogError(\"bad percentage\")\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\treturn types.OnPluginStartStatusOK\n}\n\nfunc (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {\n\treturn &httpLifecycle{parent: ctx}\n}\n\ntype httpLifecycle struct {\n\ttypes.DefaultHttpContext\n\tparent *pluginContext\n}\n\nfunc sampleHit(percentage int) bool {\n\treturn rand.Intn(100) < percentage\n}\n\nfunc (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {\n\tplugin := ctx.parent\n\tif !sampleHit(plugin.Percentage) {\n\t\treturn types.ActionContinue\n\t}\n\n\terr := proxywasm.SendHttpResponse(plugin.HttpStatus, nil, plugin.Body, -1)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"failed to send local response: %v\", err)\n\t\treturn types.ActionContinue\n\t}\n\treturn types.ActionPause\n}\n"
  },
  {
    "path": "t/wasm/fault-injection.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nwasm:\n    plugins:\n        - name: wasm_fault_injection\n          priority: 7997\n          file: t/wasm/fault-injection/main.go.wasm\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: fault injection\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_fault_injection\": {\n                            \"conf\": \"{\\\"http_status\\\":401, \\\"body\\\":\\\"HIT\\n\\\"}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\nHIT\n\n\n\n=== TEST 3: fault injection, with 0 percentage\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_fault_injection\": {\n                            \"conf\": \"{\\\"http_status\\\":401, \\\"percentage\\\":0}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- request\nGET /hello\n--- response_body\nhello world\n\n\n\n=== TEST 5: fault injection without body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_fault_injection\": {\n                            \"conf\": \"{\\\"http_status\\\":401}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- request\nGET /hello\n--- error_code: 401\n--- response_body_like eval\nqr/<title>401 Authorization Required<\\/title>/\n\n\n\n=== TEST 7: fault injection\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_fault_injection\": {\n                            \"conf\": {\n                                \"http_status\": 401,\n                                \"body\": \"HIT\\n\"\n                            }\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 8: hit\n--- request\nGET /hello\n--- error_code: 401\n--- response_body\nHIT\n\n\n\n=== TEST 9: fault injection, with 0 percentage\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_fault_injection\": {\n                            \"conf\": {\n                                \"http_status\": 401,\n                                \"percentage\": 0\n                            }\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- ret_code: 401\n--- response_body\npassed\n\n\n\n=== TEST 10: hit\n--- request\nGET /hello\n--- response_body\nhello world\n"
  },
  {
    "path": "t/wasm/forward-auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm\"\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types\"\n\t\"github.com/valyala/fastjson\"\n)\n\nfunc main() {\n\tproxywasm.SetVMContext(&vmContext{})\n}\n\ntype vmContext struct {\n\ttypes.DefaultVMContext\n}\n\nfunc (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {\n\treturn &pluginContext{\n\t\tcontextID:       contextID,\n\t\tupstreamHeaders: map[string]struct{}{},\n\t\tclientHeaders:   map[string]struct{}{},\n\t\trequestHeaders:  map[string]struct{}{},\n\t}\n}\n\ntype pluginContext struct {\n\ttypes.DefaultPluginContext\n\tcontextID uint32\n\n\thost            string\n\tpath            string\n\tscheme          string\n\tupstreamHeaders map[string]struct{}\n\tclientHeaders   map[string]struct{}\n\trequestHeaders  map[string]struct{}\n\ttimeout         uint32\n}\n\nfunc (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {\n\tdata, err := proxywasm.GetPluginConfiguration()\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error reading plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tvar p fastjson.Parser\n\tv, err := p.ParseBytes(data)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error decoding plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tctx.timeout = uint32(v.GetUint(\"timeout\"))\n\tif ctx.timeout == 0 {\n\t\tctx.timeout = 3000\n\t}\n\n\t// schema check\n\tif ctx.timeout < 1 || ctx.timeout > 60000 {\n\t\tproxywasm.LogError(\"bad timeout\")\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\ts := string(v.GetStringBytes(\"uri\"))\n\tif s == \"\" {\n\t\tproxywasm.LogError(\"bad uri\")\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\turi, err := url.Parse(s)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"bad uri: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tctx.host = uri.Host\n\tctx.path = uri.Path\n\tctx.scheme = uri.Scheme\n\n\tarr := v.GetArray(\"upstream_headers\")\n\tfor _, a := range arr {\n\t\tctx.upstreamHeaders[strings.ToLower(string(a.GetStringBytes()))] = struct{}{}\n\t}\n\n\tarr = v.GetArray(\"request_headers\")\n\tfor _, a := range arr {\n\t\tctx.requestHeaders[string(a.GetStringBytes())] = struct{}{}\n\t}\n\n\tarr = v.GetArray(\"client_headers\")\n\tfor _, a := range arr {\n\t\tctx.clientHeaders[strings.ToLower(string(a.GetStringBytes()))] = struct{}{}\n\t}\n\n\treturn types.OnPluginStartStatusOK\n}\n\nfunc (pluginCtx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {\n\tctx := &httpContext{contextID: contextID, pluginCtx: pluginCtx}\n\treturn ctx\n}\n\ntype httpContext struct {\n\ttypes.DefaultHttpContext\n\tcontextID uint32\n\tpluginCtx *pluginContext\n}\n\nfunc (ctx *httpContext) dispatchHttpCall(elem *fastjson.Value) {\n\tmethod, _ := proxywasm.GetHttpRequestHeader(\":method\")\n\turi, _ := proxywasm.GetHttpRequestHeader(\":path\")\n\tscheme, _ := proxywasm.GetHttpRequestHeader(\":scheme\")\n\thost, _ := proxywasm.GetHttpRequestHeader(\"host\")\n\taddr, _ := proxywasm.GetProperty([]string{\"remote_addr\"})\n\n\tpctx := ctx.pluginCtx\n\ths := [][2]string{}\n\ths = append(hs, [2]string{\":scheme\", pctx.scheme})\n\ths = append(hs, [2]string{\"host\", pctx.host})\n\ths = append(hs, [2]string{\":path\", pctx.path})\n\ths = append(hs, [2]string{\"X-Forwarded-Proto\", scheme})\n\ths = append(hs, [2]string{\"X-Forwarded-Method\", method})\n\ths = append(hs, [2]string{\"X-Forwarded-Host\", host})\n\ths = append(hs, [2]string{\"X-Forwarded-Uri\", uri})\n\ths = append(hs, [2]string{\"X-Forwarded-For\", string(addr)})\n\n\tfor k := range pctx.requestHeaders {\n\t\th, err := proxywasm.GetHttpRequestHeader(k)\n\n\t\tif err != nil && err != types.ErrorStatusNotFound {\n\t\t\tproxywasm.LogErrorf(\"httpcall failed: %v\", err)\n\t\t\treturn\n\t\t}\n\t\ths = append(hs, [2]string{k, h})\n\t}\n\n\tcalloutID, err := proxywasm.DispatchHttpCall(pctx.host, hs, nil, nil,\n\t\tpctx.timeout, ctx.httpCallback)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"httpcall failed: %v\", err)\n\t\treturn\n\t}\n\tproxywasm.LogInfof(\"httpcall calloutID %d, pluginCtxID %d\", calloutID, ctx.pluginCtx.contextID)\n}\n\nfunc (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {\n\tdata, err := proxywasm.GetPluginConfiguration()\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error reading plugin configuration: %v\", err)\n\t\treturn types.ActionContinue\n\t}\n\n\tvar p fastjson.Parser\n\tv, err := p.ParseBytes(data)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error decoding plugin configuration: %v\", err)\n\t\treturn types.ActionContinue\n\t}\n\n\tctx.dispatchHttpCall(v)\n\treturn types.ActionContinue\n}\n\nfunc (ctx *httpContext) httpCallback(numHeaders int, bodySize int, numTrailers int) {\n\ths, err := proxywasm.GetHttpCallResponseHeaders()\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"callback err: %v\", err)\n\t\treturn\n\t}\n\n\tvar status int\n\tfor _, h := range hs {\n\t\tif h[0] == \":status\" {\n\t\t\tstatus, _ = strconv.Atoi(h[1])\n\t\t}\n\n\t\tif _, ok := ctx.pluginCtx.upstreamHeaders[h[0]]; ok {\n\t\t\terr := proxywasm.ReplaceHttpRequestHeader(h[0], h[1])\n\t\t\tif err != nil {\n\t\t\t\tproxywasm.LogErrorf(\"set header failed: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif status >= 300 {\n\t\tchs := [][2]string{}\n\t\tfor _, h := range hs {\n\t\t\tif _, ok := ctx.pluginCtx.clientHeaders[h[0]]; ok {\n\t\t\t\tchs = append(chs, [2]string{h[0], h[1]})\n\t\t\t}\n\t\t}\n\n\t\tif err := proxywasm.SendHttpResponse(403, chs, nil, -1); err != nil {\n\t\t\tproxywasm.LogErrorf(\"send http failed: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "t/wasm/forward-auth.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nwasm:\n    plugins:\n        - name: wasm-forward-auth\n          priority: 7997\n          file: t/wasm/forward-auth.go.wasm\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: setup route with plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = {\n                {\n                    url = \"/apisix/admin/upstreams/u1\",\n                    data = [[{\n                        \"nodes\": {\n                            \"127.0.0.1:1984\": 1\n                        },\n                        \"type\": \"roundrobin\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/auth\",\n                    data = [[{\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\": [\n                                    \"return function(conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        if core.request.header(ctx, \\\"Authorization\\\") == \\\"111\\\" then\n                                            core.response.exit(200);\n                                        end\n                                    end\",\n                                    \"return function(conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        if core.request.header(ctx, \\\"Authorization\\\") == \\\"222\\\" then\n                                            core.response.set_header(\\\"X-User-ID\\\", \\\"i-am-an-user\\\");\n                                            core.response.exit(200);\n                                        end\n                                    end\",]] .. [[\n                                    \"return function(conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        if core.request.header(ctx, \\\"Authorization\\\") == \\\"333\\\" then\n                                            core.response.set_header(\\\"X-User-ID\\\", \\\"i-am-an-user\\\");\n                                            core.response.exit(401);\n                                        end\n                                    end\",\n                                    \"return function(conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        if core.request.header(ctx, \\\"Authorization\\\") == \\\"444\\\" then\n                                            local auth_headers = {\n                                                'X-Forwarded-Proto',\n                                                'X-Forwarded-Method',\n                                                'X-Forwarded-Host',\n                                                'X-Forwarded-Uri',\n                                                'X-Forwarded-For',\n                                            }\n                                            for _, k in ipairs(auth_headers) do\n                                                core.log.warn('get header ', string.lower(k), ': ', core.request.header(ctx, k))\n                                            end\n                                            core.response.exit(403);\n                                        end\n                                    end\"\n                                ]\n                            }\n                        },\n                        \"uri\": \"/auth\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/echo\",\n                    data = [[{\n                        \"plugins\": {\n                            \"serverless-pre-function\": {\n                                \"phase\": \"rewrite\",\n                                \"functions\": [\n                                    \"return function (conf, ctx)\n                                        local core = require(\\\"apisix.core\\\");\n                                        core.response.exit(200, core.request.headers(ctx));\n                                    end\"\n                                ]\n                            }\n                        },\n                        \"uri\": \"/echo\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/1\",\n                    data = [[{\n                        \"plugins\": {\n                            \"wasm-forward-auth\": {\n                                \"conf\": \"{\n                                    \\\"uri\\\": \\\"http://127.0.0.1:1984/auth\\\",\n                                    \\\"request_headers\\\": [\\\"Authorization\\\"],\n                                    \\\"client_headers\\\": [\\\"X-User-ID\\\"],\n                                    \\\"upstream_headers\\\": [\\\"X-User-ID\\\"]\n                                }\"\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/hello\"\n                    }]],\n                },\n                {\n                    url = \"/apisix/admin/routes/2\",\n                    data = [[{\n                        \"plugins\": {\n                            \"wasm-forward-auth\": {\n                                \"conf\": \"{\n                                    \\\"uri\\\": \\\"http://127.0.0.1:1984/auth\\\",\n                                    \\\"request_headers\\\": [\\\"Authorization\\\"]\n                                }\"\n                            },\n                            \"proxy-rewrite\": {\n                                \"uri\": \"/echo\"\n                            }\n                        },\n                        \"upstream_id\": \"u1\",\n                        \"uri\": \"/empty\"\n                    }]],\n                },\n            }\n\n            local t = require(\"lib.test_admin\").test\n\n            for _, data in ipairs(data) do\n                local code, body = t(data.url, ngx.HTTP_PUT, data.data)\n                ngx.say(body)\n            end\n        }\n    }\n--- response_body eval\n\"passed\\n\" x 5\n\n\n\n=== TEST 2: hit route (test request_headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 111\n--- response_body_like eval\nqr/\\\"authorization\\\":\\\"111\\\"/\n\n\n\n=== TEST 3: hit route (test upstream_headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 222\n--- response_body_like eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 4: hit route (test client_headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 333\n--- error_code: 403\n--- response_headers\nx-user-id: i-am-an-user\n\n\n\n=== TEST 5: hit route (check APISIX generated headers and ignore client headers)\n--- request\nGET /hello\n--- more_headers\nAuthorization: 444\nX-Forwarded-Host: apisix.apache.org\n--- error_code: 403\n--- grep_error_log eval\nqr/get header \\S+: \\S+/\n--- grep_error_log_out\nget header x-forwarded-proto: http,\nget header x-forwarded-method: GET,\nget header x-forwarded-host: localhost,\nget header x-forwarded-uri: /hello,\nget header x-forwarded-for: 127.0.0.1,\n\n\n\n=== TEST 6: hit route (not send upstream headers)\n--- request\nGET /empty\n--- more_headers\nAuthorization: 222\n--- response_body_unlike eval\nqr/\\\"x-user-id\\\":\\\"i-am-an-user\\\"/\n\n\n\n=== TEST 7: hit route (not send client headers)\n--- request\nGET /empty\n--- more_headers\nAuthorization: 333\n--- error_code: 403\n--- response_headers\n!x-user-id\n"
  },
  {
    "path": "t/wasm/global-rule.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nwasm:\n    plugins:\n        - name: wasm_log\n          priority: 7999\n          file: t/wasm/log/main.go.wasm\n        - name: wasm_log2\n          priority: 7998\n          file: t/wasm/log/main.go.wasm\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"wasm_log\": {\n                            \"conf\": \"blahblah\"\n                        },\n                        \"wasm_log2\": {\n                            \"conf\": \"zzz\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nGET /hello\n--- grep_error_log eval\nqr/run plugin ctx \\d+ with conf \\S+ in http ctx \\d+/\n--- grep_error_log_out\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 1 with conf zzz in http ctx 2\n\n\n\n=== TEST 3: global rule + route\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log2\": {\n                            \"conf\": \"www\"\n                        }\n                    },\n                    \"uri\": \"/hello\"\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- request\nGET /hello\n--- grep_error_log eval\nqr/run plugin ctx \\d+ with conf \\S+ in http ctx \\d+/\n--- grep_error_log_out\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 1 with conf zzz in http ctx 2\nrun plugin ctx 3 with conf www in http ctx 4\n\n\n\n=== TEST 5: delete global rule\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/global_rules/1', ngx.HTTP_DELETE)\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n"
  },
  {
    "path": "t/wasm/go.mod",
    "content": "module github.com/api7/wasm-nginx-module\n\ngo 1.17\n\nrequire (\n\tgithub.com/tetratelabs/proxy-wasm-go-sdk v0.16.0\n\tgithub.com/valyala/fastjson v1.6.3\n)\n\n//replace github.com/tetratelabs/proxy-wasm-go-sdk => ../proxy-wasm-go-sdk\n"
  },
  {
    "path": "t/wasm/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/tetratelabs/proxy-wasm-go-sdk v0.16.0 h1:6xhDLV4DD2+q3Rs4CDh7cqo69rQ50XgCusv/58D44o4=\ngithub.com/tetratelabs/proxy-wasm-go-sdk v0.16.0/go.mod h1:8CxNZJ+9yDEvNnAog384fC8j1tKNF0tTZevGjOuY9ds=\ngithub.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=\ngithub.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "t/wasm/log/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm\"\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types\"\n)\n\nfunc main() {\n\tproxywasm.SetVMContext(&vmContext{})\n}\n\ntype vmContext struct {\n\t// Embed the default VM context here,\n\t// so that we don't need to reimplement all the methods.\n\ttypes.DefaultVMContext\n}\n\nfunc (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {\n\treturn &pluginContext{contextID: contextID}\n}\n\ntype pluginContext struct {\n\t// Embed the default plugin context here,\n\t// so that we don't need to reimplement all the methods.\n\ttypes.DefaultPluginContext\n\tconf      string\n\tcontextID uint32\n}\n\nfunc (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {\n\tdata, err := proxywasm.GetPluginConfiguration()\n\tif err != nil {\n\t\tproxywasm.LogCriticalf(\"error reading plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tctx.conf = string(data)\n\treturn types.OnPluginStartStatusOK\n}\n\nfunc (ctx *pluginContext) OnPluginDone() bool {\n\tproxywasm.LogInfo(\"do clean up...\")\n\treturn true\n}\n\nfunc (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {\n\treturn &httpLifecycle{pluginCtxID: ctx.contextID, conf: ctx.conf, contextID: contextID}\n}\n\ntype httpLifecycle struct {\n\t// Embed the default http context here,\n\t// so that we don't need to reimplement all the methods.\n\ttypes.DefaultHttpContext\n\tpluginCtxID uint32\n\tcontextID   uint32\n\tconf        string\n}\n\nfunc (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {\n\tproxywasm.LogWarnf(\"run plugin ctx %d with conf %s in http ctx %d\",\n\t\tctx.pluginCtxID, ctx.conf, ctx.contextID)\n\t// TODO: support access/modify http request headers\n\treturn types.ActionContinue\n}\n"
  },
  {
    "path": "t/wasm/request-body/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm\"\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types\"\n\t\"github.com/valyala/fastjson\"\n)\n\nfunc main() {\n\tproxywasm.SetVMContext(&vmContext{})\n}\n\ntype vmContext struct {\n\ttypes.DefaultVMContext\n}\n\nfunc (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {\n\treturn &pluginContext{contextID: contextID}\n}\n\ntype pluginContext struct {\n\ttypes.DefaultPluginContext\n\tcontextID      uint32\n\tstart          int\n\tsize           int\n\tprocessReqBody bool\n}\n\nfunc (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {\n\tdata, err := proxywasm.GetPluginConfiguration()\n\tif err != nil {\n\t\tproxywasm.LogCriticalf(\"error reading plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tvar conf *fastjson.Value\n\tvar p fastjson.Parser\n\tconf, err = p.ParseBytes(data)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error decoding plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tctx.start = conf.GetInt(\"start\")\n\tctx.size = conf.GetInt(\"size\")\n\tctx.processReqBody = conf.GetBool(\"processReqBody\")\n\treturn types.OnPluginStartStatusOK\n}\n\nfunc (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {\n\treturn &httpContext{pluginCtx: ctx, contextID: contextID}\n}\n\ntype httpContext struct {\n\ttypes.DefaultHttpContext\n\tpluginCtx *pluginContext\n\tcontextID uint32\n}\n\nfunc (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {\n\tif ctx.pluginCtx.processReqBody {\n\t\tproxywasm.SetProperty([]string{\"wasm_process_req_body\"}, []byte(\"true\"))\n\t}\n\n\treturn types.ActionContinue\n}\n\nfunc (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {\n\tsize := ctx.pluginCtx.size\n\tif size == 0 {\n\t\tsize = bodySize\n\t}\n\n\tbody, err := proxywasm.GetHttpRequestBody(ctx.pluginCtx.start, size)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"failed to get body: %v\", err)\n\t\treturn types.ActionContinue\n\t}\n\n\tproxywasm.LogWarnf(\"request get body: %v\", string(body))\n\treturn types.ActionContinue\n}\n"
  },
  {
    "path": "t/wasm/request-body.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nwasm:\n    plugins:\n        - name: wasm-request-body\n          priority: 7997\n          file: t/wasm/request-body/main.go.wasm\n        - name: wasm-request-body2\n          priority: 7996\n          file: t/wasm/request-body/main.go.wasm\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-request-body\": {\n                            \"conf\": \"{\\\"processReqBody\\\":true, \\\"start\\\":1, \\\"size\\\":3}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nPOST /hello\nhello\n--- grep_error_log eval\nqr/request get body: \\w+/\n--- grep_error_log_out\nrequest get body: ell\n\n\n\n=== TEST 3: no body\n--- request\nPOST /hello\n--- error_log\nerror status returned by host: not found\n\n\n\n=== TEST 4: do not process body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-request-body\": {\n                            \"conf\": \"{\\\"processReqBody\\\":false, \\\"start\\\":1, \\\"size\\\":3}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 5: hit\n--- request\nPOST /hello\nhello\n--- grep_error_log eval\nqr/request get body: \\w+/\n--- grep_error_log_out\n\n\n\n=== TEST 6: ensure the process body flag is plugin independent\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-request-body\": {\n                            \"conf\": \"{\\\"processReqBody\\\":true, \\\"start\\\":1, \\\"size\\\":3}\"\n                        },\n                        \"wasm-request-body2\": {\n                            \"conf\": \"{\\\"processReqBody\\\":false, \\\"start\\\":2, \\\"size\\\":3}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- request\nPOST /hello\nhello\n--- grep_error_log eval\nqr/request get body: \\w+/\n--- grep_error_log_out\nrequest get body: ell\n\n\n\n=== TEST 8: invalid conf type no set conf\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-request-body\": {\n                            \"setting\": {\"processReqBody\":true, \"start\":1, \"size\":3}\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body_like eval\nqr/property.*conf.*is required/\n\n\n\n=== TEST 9: hit\n--- request\nPOST /hello\nhello\n--- grep_error_log eval\nqr/request get body: \\w+/\n--- grep_error_log_out\nrequest get body: ell\n"
  },
  {
    "path": "t/wasm/response-rewrite/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport (\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm\"\n\t\"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types\"\n\n\t\"github.com/valyala/fastjson\"\n)\n\nfunc main() {\n\tproxywasm.SetVMContext(&vmContext{})\n}\n\ntype vmContext struct {\n\ttypes.DefaultVMContext\n}\n\nfunc (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {\n\treturn &pluginContext{}\n}\n\ntype header struct {\n\tName  string\n\tValue string\n}\n\ntype pluginContext struct {\n\ttypes.DefaultPluginContext\n\tHeaders []header\n\tBody    []byte\n}\n\nfunc (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {\n\tdata, err := proxywasm.GetPluginConfiguration()\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error reading plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\n\tvar p fastjson.Parser\n\tv, err := p.ParseBytes(data)\n\tif err != nil {\n\t\tproxywasm.LogErrorf(\"error decoding plugin configuration: %v\", err)\n\t\treturn types.OnPluginStartStatusFailed\n\t}\n\theaders := v.GetArray(\"headers\")\n\tctx.Headers = make([]header, len(headers))\n\tfor i, hdr := range headers {\n\t\tctx.Headers[i] = header{\n\t\t\tName:  string(hdr.GetStringBytes(\"name\")),\n\t\t\tValue: string(hdr.GetStringBytes(\"value\")),\n\t\t}\n\t}\n\n\tbody := v.GetStringBytes(\"body\")\n\tctx.Body = body\n\n\treturn types.OnPluginStartStatusOK\n}\n\nfunc (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {\n\treturn &httpContext{parent: ctx}\n}\n\ntype httpContext struct {\n\ttypes.DefaultHttpContext\n\tparent *pluginContext\n}\n\nfunc (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {\n\tplugin := ctx.parent\n\tfor _, hdr := range plugin.Headers {\n\t\tproxywasm.ReplaceHttpResponseHeader(hdr.Name, hdr.Value)\n\t}\n\n\tif len(plugin.Body) > 0 {\n\t\tproxywasm.SetProperty([]string{\"wasm_process_resp_body\"}, []byte(\"true\"))\n\t}\n\n\treturn types.ActionContinue\n}\n\nfunc (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {\n\tplugin := ctx.parent\n\n\tif len(plugin.Body) > 0 && !endOfStream {\n\t\t// TODO support changing body\n\t\tbody, err := proxywasm.GetHttpResponseBody(0, bodySize)\n\t\tif err != nil {\n\t\t\tproxywasm.LogErrorf(\"failed to get body: %v\", err)\n\t\t\treturn types.ActionContinue\n\t\t}\n\t\tproxywasm.LogWarnf(\"get body [%s]\", string(body))\n\t}\n\n\treturn types.ActionContinue\n}\n"
  },
  {
    "path": "t/wasm/response-rewrite.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    my $extra_yaml_config = <<_EOC_;\nwasm:\n    plugins:\n        - name: wasm-response-rewrite\n          priority: 7997\n          file: t/wasm/response-rewrite/main.go.wasm\n        - name: wasm-response-rewrite2\n          priority: 7996\n          file: t/wasm/response-rewrite/main.go.wasm\n_EOC_\n    $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: response rewrite headers\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-response-rewrite\": {\n                            \"conf\": \"{\\\"headers\\\":[{\\\"name\\\":\\\"x-wasm\\\",\\\"value\\\":\\\"apisix\\\"}]}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- request\nGET /hello\n--- response_headers\nx-wasm: apisix\n\n\n\n=== TEST 3: log response body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-response-rewrite\": {\n                            \"conf\": \"{\\\"body\\\":\\\"a\\\"}\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 4: hit\n--- request\nGET /hello\n--- grep_error_log eval\nqr/get body .+/\n--- grep_error_log_out\nget body [hello world\n\n\n\n=== TEST 5: ensure the process body flag is plugin independent\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm-response-rewrite\": {\n                            \"conf\": \"{\\\"body\\\":\\\"a\\\"}\"\n                        },\n                        \"wasm-response-rewrite2\": {\n                            \"conf\": \"{\\\"headers\\\":[{\\\"name\\\":\\\"x-wasm\\\",\\\"value\\\":\\\"apisix\\\"}]}\"\n                        }\n                    }\n                }]]\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- request\nGET /hello\n--- grep_error_log eval\nqr/get body .+/\n--- grep_error_log_out\nget body [hello world\n"
  },
  {
    "path": "t/wasm/route.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nwasm:\n    plugins:\n        - name: wasm_log\n          priority: 7999\n          file: t/wasm/log/main.go.wasm\n        - name: wasm_log2\n          priority: 7998\n          file: t/wasm/log/main.go.wasm\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n});\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: scheme check with empty json body\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log\": {}\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- error_log eval\nqr/invalid request body/\n\n\n\n=== TEST 2: scheme check with conf type number\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log\": {\"conf\": 123}\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- error_log eval\nqr/invalid request body/\n\n\n\n=== TEST 3: scheme check with conf json type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log\": {\"conf\": {}}}\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body_like eval\nqr/value should match only one schema, but matches none/\n\n\n\n=== TEST 4: scheme check with conf json type\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log\": {\"conf\": \"\"}}\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- error_code: 400\n--- response_body_like eval\nqr/value should match only one schema, but matches none/\n\n\n\n=== TEST 5: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log\": {\n                            \"conf\": \"blahblah\"\n                        },\n                        \"wasm_log2\": {\n                            \"conf\": \"zzz\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 6: hit\n--- request\nGET /hello\n--- grep_error_log eval\nqr/run plugin ctx \\d+ with conf \\S+ in http ctx \\d+/\n--- grep_error_log_out\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 1 with conf zzz in http ctx 2\n\n\n\n=== TEST 7: run wasm plugin in rewrite phase (prior to the one run in access phase)\n--- extra_yaml_config\nwasm:\n    plugins:\n        - name: wasm_log\n          priority: 7999\n          file: t/wasm/log/main.go.wasm\n        - name: wasm_log2\n          priority: 7998\n          file: t/wasm/log/main.go.wasm\n          http_request_phase: rewrite\n--- request\nGET /hello\n--- grep_error_log eval\nqr/run plugin ctx \\d+ with conf \\S+ in http ctx \\d+/\n--- grep_error_log_out\nrun plugin ctx 1 with conf zzz in http ctx 2\nrun plugin ctx 1 with conf blahblah in http ctx 2\n\n\n\n=== TEST 8: plugin from service\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/services/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"plugins\": {\n                        \"wasm_log\": {\n                            \"id\": \"log\",\n                            \"conf\": \"blahblah\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"service_id\": \"1\",\n                    \"hosts\": [\"foo.com\"]\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"uri\": \"/hello\",\n                    \"service_id\": \"1\",\n                    \"hosts\": [\"bar.com\"]\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            for i = 1, 4 do\n                local host = \"foo.com\"\n                if i % 2 == 0 then\n                    host = \"bar.com\"\n                end\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {host = host}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n            end\n        }\n    }\n--- grep_error_log eval\nqr/run plugin ctx \\d+ with conf \\S+ in http ctx \\d+/\n--- grep_error_log_out\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 3 with conf blahblah in http ctx 4\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 3 with conf blahblah in http ctx 4\n\n\n\n=== TEST 10: plugin from plugin_config\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_configs/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"plugins\": {\n                        \"wasm_log\": {\n                            \"id\": \"log\",\n                            \"conf\": \"blahblah\"\n                        }\n                    }\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\",\n                    \"plugin_config_id\": \"1\",\n                    \"hosts\": [\"foo.com\"]\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/routes/2',\n                ngx.HTTP_PUT,\n                [[{\n                    \"upstream\": {\n                        \"type\": \"roundrobin\",\n                        \"nodes\": {\n                            \"127.0.0.1:1980\": 1\n                        }\n                    },\n                    \"uri\": \"/hello\",\n                    \"plugin_config_id\": \"1\",\n                    \"hosts\": [\"bar.com\"]\n                }]]\n            )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n\n            for i = 1, 4 do\n                local host = \"foo.com\"\n                if i % 2 == 0 then\n                    host = \"bar.com\"\n                end\n                local httpc = http.new()\n                local res, err = httpc:request_uri(uri, {headers = {host = host}})\n                if not res then\n                    ngx.say(err)\n                    return\n                end\n            end\n        }\n    }\n--- grep_error_log eval\nqr/run plugin ctx \\d+ with conf \\S+ in http ctx \\d+/\n--- grep_error_log_out\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 3 with conf blahblah in http ctx 4\nrun plugin ctx 1 with conf blahblah in http ctx 2\nrun plugin ctx 3 with conf blahblah in http ctx 4\n"
  },
  {
    "path": "t/xds-library/config_xds.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nuse Cwd qw(cwd);\nmy $apisix_home = $ENV{APISIX_HOME} || cwd();\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!$block->no_error_log) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $lua_deps_path = $block->lua_deps_path // <<_EOC_;\n        lua_package_path \"$apisix_home/?.lua;$apisix_home/?/init.lua;$apisix_home/deps/share/lua/5.1/?/init.lua;$apisix_home/deps/share/lua/5.1/?.lua;$apisix_home/apisix/?.lua;$apisix_home/t/?.lua;;\";\n        lua_package_cpath \"$apisix_home/?.so;$apisix_home/t/xds-library/?.so;$apisix_home/deps/lib/lua/5.1/?.so;$apisix_home/deps/lib64/lua/5.1/?.so;;\";\n_EOC_\n\n    $block->set_value(\"lua_deps_path\", $lua_deps_path);\n\n    my $extra_init_by_lua = <<_EOC_;\n    --\n    local config_xds = require(\"apisix.core.config_xds\")\n\n    local inject = function(mod, name)\n        local old_f = mod[name]\n        mod[name] = function (...)\n            ngx.log(ngx.WARN, \"config_xds run \", name)\n            return { true }\n        end\n    end\n\n    inject(config_xds, \"new\")\n\n_EOC_\n\n    $block->set_value(\"extra_init_by_lua\", $extra_init_by_lua);\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: xds\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: load xDS library successfully\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.say(\"ok\")\n        }\n    }\n--- no_error_log eval\nqr/can not load xDS library/\n\n\n\n=== TEST 2: read data form shdict that wirted by xDS library\n--- config\n    location /t {\n        content_by_lua_block {\n            -- wait for xds library sync data\n            ngx.sleep(1.5)\n            local core = require(\"apisix.core\")\n            local value = ngx.shared[\"xds-config\"]:get(\"/routes/1\")\n            local route_conf, err = core.json.decode(value)\n            local json_encode = require(\"toolkit.json\").encode\n            ngx.say(json_encode(route_conf.uri))\n        }\n    }\n--- response_body\n\"/hello\"\n\n\n\n=== TEST 3: read conf version\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local version\n            for i = 1, 5 do\n                version = ngx.shared[\"xds-config-version\"]:get(\"version\")\n                if version then\n                    ngx.say(version)\n                    break\n                end\n                -- wait for xds library sync data\n                ngx.sleep(1.5)\n            end\n        }\n    }\n--- response_body eval\nqr/^\\d{13}$/\n"
  },
  {
    "path": "t/xds-library/config_xds_2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX 'no_plan';\n\nuse Cwd qw(cwd);\nmy $apisix_home = $ENV{APISIX_HOME} || cwd();\n\nrepeat_each(1);\nno_long_string();\nno_root_location();\nlog_level(\"info\");\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\n[alert]\");\n    }\n\n    my $lua_deps_path = $block->lua_deps_path // <<_EOC_;\n        lua_package_path \"$apisix_home/?.lua;$apisix_home/?/init.lua;$apisix_home/deps/share/lua/5.1/?/init.lua;$apisix_home/deps/share/lua/5.1/?.lua;$apisix_home/apisix/?.lua;$apisix_home/t/?.lua;;\";\n        lua_package_cpath \"$apisix_home/?.so;$apisix_home/t/xds-library/?.so;$apisix_home/deps/lib/lua/5.1/?.so;$apisix_home/deps/lib64/lua/5.1/?.so;;\";\n_EOC_\n\n    $block->set_value(\"lua_deps_path\", $lua_deps_path);\n\n    if (!$block->yaml_config) {\n        my $yaml_config = <<_EOC_;\napisix:\n    node_listen: 1984\ndeployment:\n    role: data_plane\n    role_data_plane:\n        config_provider: xds\n_EOC_\n\n        $block->set_value(\"yaml_config\", $yaml_config);\n    }\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: proxy request using data written by xds(id = 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, { method = \"GET\"})\n\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 2: proxy request using data written by xds(id = 2, upstream_id = 1)\n--- config\n    location /t {\n        content_by_lua_block {\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello1\"\n            local res, err = httpc:request_uri(uri, { method = \"GET\"})\n\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello1 world\n\n\n\n=== TEST 3: proxy request using data written by xds(id = 3, upstream_id = 2)\n--- config\n    location /t {\n        content_by_lua_block {\n            ngx.sleep(1.5)\n            local core = require(\"apisix.core\")\n            local value = ngx.shared[\"xds-config\"]:flush_all()\n            ngx.sleep(1.5)\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, { method = \"GET\"})\n\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.print(res.body)\n        }\n    }\n--- response_body\nhello world\n\n\n\n=== TEST 4: flush all keys in xds config\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            ngx.shared[\"xds-config\"]:flush_all()\n            ngx.update_time()\n            ngx.shared[\"xds-config-version\"]:set(\"version\", ngx.now())\n            ngx.sleep(1.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/hello\"\n            local res, err = httpc:request_uri(uri, { method = \"GET\"})\n\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.status = res.status\n            ngx.print(res.body)\n        }\n    }\n--- error_code: 404\n--- response_body\n{\"error_msg\":\"404 Route Not Found\"}\n\n\n\n=== TEST 5: bad format json\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = [[{\n                upstream = {\n                    type = \"roundrobin\"\n                    nodes = {\n                        [\"127.0.0.1:1980\"] = 1,\n                    }\n                },\n                uri = \"/bad_json\"\n            }]]\n            ngx.shared[\"xds-config\"]:set(\"/routes/3\", data)\n            ngx.update_time()\n            ngx.shared[\"xds-config-version\"]:set(\"version\", ngx.now())\n            ngx.sleep(1.5)\n\n            local http = require \"resty.http\"\n            local httpc = http.new()\n            local uri = \"http://127.0.0.1:\" .. ngx.var.server_port .. \"/bad_json\"\n            local res, err = httpc:request_uri(uri, { method = \"GET\"})\n\n            if not res then\n                ngx.say(err)\n                return\n            end\n            ngx.status = res.status\n        }\n    }\n--- wait: 2\n--- error_code: 404\n--- error_log\ndecode the conf of [/routes/3] failed, err: Expected object key string but found invalid token\n\n\n\n=== TEST 6: schema check fail\n--- config\n    location /t {\n        content_by_lua_block {\n            local core = require(\"apisix.core\")\n            local data = {\n                upstream = {\n                    type = \"roundrobin\",\n                    nodes = {\n                        [\"127.0.0.1:65536\"] = 1,\n                    }\n                }\n            }\n            local data_str = core.json.encode(data)\n            ngx.shared[\"xds-config\"]:set(\"/routes/3\", data_str)\n            ngx.update_time()\n            ngx.shared[\"xds-config-version\"]:set(\"version\", ngx.now())\n            ngx.sleep(1.5)\n        }\n    }\n--- no_error_log\n[alert]\n-- wait: 2\n--- error_log\nfailed to check the conf of [/routes/3] err:allOf 1 failed: value should match only one schema, but matches none\n\n\n\n=== TEST 7: not table\n--- config\n    location /t {\n        content_by_lua_block {\n            local data = \"/not_table\"\n            ngx.shared[\"xds-config\"]:set(\"/routes/3\", data)\n            ngx.update_time()\n            ngx.shared[\"xds-config-version\"]:set(\"version\", ngx.now())\n            ngx.sleep(1.5)\n        }\n    }\n--- no_error_log\n[alert]\n-- wait: 2\n--- error_log\ninvalid conf of [/routes/3], conf: nil, it should be an object\n"
  },
  {
    "path": "t/xds-library/export.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport \"C\"\nimport \"unsafe\"\n\n//export initial\nfunc initial(config_zone unsafe.Pointer, version_zone unsafe.Pointer) {\n\twrite_config(config_zone, version_zone)\n}\n"
  },
  {
    "path": "t/xds-library/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\n/*\n#cgo LDFLAGS: -shared -ldl\n#include \"xds.h\"\n#include <stdlib.h>\n\nextern void ngx_lua_ffi_shdict_store(void *zone, int op,\n    const unsigned char *key, size_t key_len,\n\tint value_type,\n    const unsigned char *str_value_buf, size_t str_value_len,\n    double num_value, long exptime, int user_flags, char **errmsg,\n    int *forcible);\n*/\nimport \"C\"\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\t\"unsafe\"\n)\n\nfunc main() {\n}\n\nfunc write_config(config_zone unsafe.Pointer, version_zone unsafe.Pointer) {\n\troute_key := \"/routes/1\"\n\troute_value := fmt.Sprintf(`{\n\"status\": 1,\n\"update_time\": 1647250524,\n\"create_time\": 1646972532,\n\"uri\": \"/hello\",\n\"priority\": 0,\n\"id\": \"1\",\n\"upstream\": {\n\t\"nodes\": [\n\t\t{\n\t\t\t\"port\": 1980,\n\t\t\t\"priority\": 0,\n\t\t\t\"host\": \"127.0.0.1\",\n\t\t\t\"weight\": 1\n\t\t}\n\t],\n\t\"type\": \"roundrobin\",\n\t\"hash_on\": \"vars\",\n\t\"pass_host\": \"pass\",\n\t\"scheme\": \"http\"\n}\n}`)\n\n\tupstream_key := \"/upstreams/1\"\n\tupstream_value := fmt.Sprintf(`{\n\"id\": \"1\",\n\"nodes\": {\n\t\"127.0.0.1:1980\": 1\n},\n\"type\": \"roundrobin\"\n}`)\n\n\tr_u_key := \"/routes/2\"\n\tr_u_value := fmt.Sprintf(`{\n\"status\": 1,\n\"update_time\": 1647250524,\n\"create_time\": 1646972532,\n\"uri\": \"/hello1\",\n\"priority\": 0,\n\"id\": \"2\",\n\"upstream_id\": \"1\"\n}`)\n\n\twrite_shdict(route_key, route_value, config_zone)\n\twrite_shdict(upstream_key, upstream_value, config_zone)\n\twrite_shdict(r_u_key, r_u_value, config_zone)\n\tupdate_conf_version(version_zone)\n\n}\n\nfunc get_version() string {\n\treturn strconv.FormatInt(time.Now().UnixNano()/1e6, 10)\n}\n\nfunc update_conf_version(zone unsafe.Pointer) {\n\tctx := context.Background()\n\tkey := \"version\"\n\twrite_shdict(key, get_version(), zone)\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-time.After(time.Second * 5):\n\t\t\t\twrite_shdict(\"version\", get_version(), zone)\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc write_shdict(key string, value string, zone unsafe.Pointer) {\n\tvar keyCStr = C.CString(key)\n\tdefer C.free(unsafe.Pointer(keyCStr))\n\tvar keyLen = C.size_t(len(key))\n\n\tvar valueCStr = C.CString(value)\n\tdefer C.free(unsafe.Pointer(valueCStr))\n\tvar valueLen = C.size_t(len(value))\n\n\terrMsgBuf := make([]*C.char, 1)\n\tvar forcible = 0\n\n\tC.ngx_lua_ffi_shdict_store(zone, 0x0004,\n\t\t(*C.uchar)(unsafe.Pointer(keyCStr)), keyLen,\n\t\t4,\n\t\t(*C.uchar)(unsafe.Pointer(valueCStr)), valueLen,\n\t\t0, 0, 0,\n\t\t(**C.char)(unsafe.Pointer(&errMsgBuf[0])),\n\t\t(*C.int)(unsafe.Pointer(&forcible)),\n\t)\n}\n"
  },
  {
    "path": "t/xds-library/xds.h",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n#ifndef XDS_H\n#define XDS_H\n#include <dlfcn.h>\n#include <stdlib.h>\n\n\nvoid ngx_lua_ffi_shdict_store(void *zone, int op,\n    const unsigned char *key, size_t key_len,\n    int value_type,\n    const unsigned char *str_value_buf, size_t str_value_len,\n    double num_value, long exptime, int user_flags, char **errmsg,\n    int *forcible)\n{\n    static void* dlhandle;\n    static void (*fp)(void *zone, int op,\n                      const unsigned char *key, size_t key_len,\n                      int value_type,\n                      const unsigned char *str_value_buf, size_t str_value_len,\n                      double num_value, long exptime, int user_flags, char **errmsg,\n                      int *forcible);\n\n    if (!dlhandle) {\n        dlhandle = dlopen(NULL, RTLD_NOW);\n    }\n    if (!dlhandle) {\n        return;\n    }\n\n    fp = dlsym(dlhandle, \"ngx_http_lua_ffi_shdict_store\");\n    if (!fp) {\n        fp = dlsym(dlhandle, \"ngx_meta_lua_ffi_shdict_store\");\n    }\n\n    fp(zone, op, key, key_len, value_type, str_value_buf, str_value_len,\n       num_value, exptime, user_flags, errmsg, forcible);\n}\n\n\n#endif // XDS_H\n"
  },
  {
    "path": "t/xrpc/apisix/stream/xrpc/protocols/pingpong/init.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\nlocal sdk = require(\"apisix.stream.xrpc.sdk\")\nlocal xrpc_socket = require(\"resty.apisix.stream.xrpc.socket\")\nlocal bit = require(\"bit\")\nlocal lshift = bit.lshift\nlocal ffi = require(\"ffi\")\nlocal ffi_str = ffi.string\nlocal ipairs = ipairs\nlocal math_random = math.random\nlocal OK = ngx.OK\nlocal DECLINED = ngx.DECLINED\nlocal DONE = ngx.DONE\nlocal str_byte = string.byte\n\n\ncore.ctx.register_var(\"rpc_len\", function(ctx)\n    return ctx.len\nend)\n\nlocal _M = {}\nlocal router_version\nlocal router\n-- pingpong protocol is designed to use in the test of xRPC.\n-- It contains two part: a fixed-length header & a body.\n-- Header format:\n-- \"pp\" (magic number) + 1 bytes req type + 2 bytes stream id + 1 reserved bytes\n-- + 4 bytes body length + optional 4 bytes service name\nlocal HDR_LEN = 10\nlocal TYPE_HEARTBEAT = 1\nlocal TYPE_UNARY = 2\nlocal TYPE_STREAM = 3\nlocal TYPE_UNARY_DYN_UP = 4\n\n\nfunction _M.init_worker()\n    core.log.info(\"call pingpong's init_worker\")\nend\n\n\nfunction _M.init_downstream(session)\n    -- create the downstream\n    local sk = xrpc_socket.downstream.socket()\n    sk:settimeout(1000) -- the short timeout is just for test\n    return sk\nend\n\n\nlocal function read_data(sk, len, body)\n    local f = body and sk.drain or sk.read\n    local p, err = f(sk, len)\n    if not p then\n        if err ~= \"closed\" then\n            core.log.error(\"failed to read: \", err)\n        end\n        return nil\n    end\n\n    return p\nend\n\n\nlocal function to_int32(p, idx)\n    return lshift(p[idx], 24) + lshift(p[idx + 1], 16) + lshift(p[idx + 2], 8) + p[idx + 3]\nend\n\n\nfunction _M.from_downstream(session, downstream)\n    -- read a request from downstream\n    -- return status and the new ctx\n    core.log.info(\"call pingpong's from_downstream\")\n\n    local p = read_data(downstream, HDR_LEN, false)\n    if p == nil then\n        return DECLINED\n    end\n\n    local p_b = str_byte(\"p\")\n    if p[0] ~= p_b or p[1] ~= p_b then\n        core.log.error(\"invalid magic number: \", ffi_str(p, 2))\n        return DECLINED\n    end\n\n    local typ = p[2]\n    if typ == TYPE_HEARTBEAT then\n        core.log.info(\"send heartbeat\")\n\n        -- need to reset read buf as we won't forward it\n        downstream:reset_read_buf()\n        downstream:send(ffi_str(p, HDR_LEN))\n        return DONE\n    end\n\n    local stream_id = p[3] * 256 + p[4]\n    local ctx = sdk.get_req_ctx(session, stream_id)\n\n    local body_len = to_int32(p, 6)\n    core.log.info(\"read body len: \", body_len)\n\n    if typ == TYPE_UNARY_DYN_UP then\n        local p = read_data(downstream, 4, false)\n        if p == nil then\n            return DECLINED\n        end\n\n        local len = 4\n        for i = 0, 3 do\n            if p[i] == 0 then\n                len = i\n                break\n            end\n        end\n        local service = ffi_str(p, len)\n        core.log.info(\"get service [\", service, \"]\")\n        ctx.service = service\n\n        local changed, raw_router, version = sdk.get_router(session, router_version)\n        if changed then\n            router_version = version\n            router = {}\n\n            for _, r in ipairs(raw_router) do\n                local conf = r.protocol.conf\n                if conf and conf.service then\n                    router[conf.service] = r\n                end\n            end\n        end\n\n        local conf = router[ctx.service]\n        if conf then\n            local err = sdk.set_upstream(session, conf)\n            if err then\n                core.log.error(\"failed to set upstream: \", err)\n                return DECLINED\n            end\n        end\n    end\n\n    local p = read_data(downstream, body_len, true)\n    if p == nil then\n        return DECLINED\n    end\n\n    ctx.is_unary = typ == TYPE_UNARY or typ == TYPE_UNARY_DYN_UP\n    ctx.is_stream = typ == TYPE_STREAM\n    ctx.id = stream_id\n    ctx.len = HDR_LEN + body_len\n    if typ == TYPE_UNARY_DYN_UP then\n        ctx.len = ctx.len + 4\n    end\n\n    return OK, ctx\nend\n\n\nfunction _M.connect_upstream(session, ctx)\n    -- connect the upstream with upstream_conf\n    -- also do some handshake jobs\n    -- return status and the new upstream\n    core.log.info(\"call pingpong's connect_upstream\")\n\n    local conf = session.upstream_conf\n    local nodes = conf.nodes\n    if #nodes == 0 then\n        core.log.error(\"failed to connect: no nodes\")\n        return DECLINED\n    end\n    local node = nodes[math_random(#nodes)]\n\n    core.log.info(\"connect to \", node.host, \":\", node.port)\n\n    local sk = sdk.connect_upstream(node, conf)\n    if not sk then\n        return DECLINED\n    end\n\n    return OK, sk\nend\n\n\nfunction _M.disconnect_upstream(session, upstream)\n    -- disconnect upstream created by connect_upstream\n    sdk.disconnect_upstream(upstream, session.upstream_conf)\nend\n\n\nfunction _M.to_upstream(session, ctx, downstream, upstream)\n    -- send the request read from downstream to the upstream\n    -- return whether the request is sent\n    core.log.info(\"call pingpong's to_upstream\")\n\n    local ok, err = upstream:move(downstream)\n    if not ok then\n        core.log.error(\"failed to send to upstream: \", err)\n        return DECLINED\n    end\n\n    if ctx.is_unary then\n        local p = read_data(upstream, ctx.len, false)\n        if p == nil then\n            return DECLINED\n        end\n\n        local ok, err = downstream:move(upstream)\n        if not ok then\n            core.log.error(\"failed to handle upstream: \", err)\n            return DECLINED\n        end\n\n        return DONE\n    end\n\n    return OK\nend\n\n\nfunction _M.from_upstream(session, downstream, upstream)\n    local p = read_data(upstream, HDR_LEN, false)\n    if p == nil then\n        return DECLINED\n    end\n\n    local p_b = str_byte(\"p\")\n    if p[0] ~= p_b or p[1] ~= p_b then\n        core.log.error(\"invalid magic number: \", ffi_str(p, 2))\n        return DECLINED\n    end\n\n    local typ = p[2]\n    if typ == TYPE_HEARTBEAT then\n        core.log.info(\"send heartbeat\")\n\n        -- need to reset read buf as we won't forward it\n        upstream:reset_read_buf()\n        upstream:send(ffi_str(p, HDR_LEN))\n        return DONE\n    end\n\n    local stream_id = p[3] * 256 + p[4]\n    local ctx = sdk.get_req_ctx(session, stream_id)\n\n    local body_len = to_int32(p, 6)\n    if ctx.len then\n        if body_len ~= ctx.len - HDR_LEN then\n            core.log.error(\"upstream body len mismatch, expected: \", ctx.len - HDR_LEN,\n                        \", actual: \", body_len)\n            return DECLINED\n        end\n    end\n\n    local p = read_data(upstream, body_len, true)\n    if p == nil then\n        return DECLINED\n    end\n\n    local ok, err = downstream:move(upstream)\n    if not ok then\n        core.log.error(\"failed to handle upstream: \", err)\n        return DECLINED\n    end\n\n    return DONE, ctx\nend\n\n\nfunction _M.log(session, ctx)\n    core.log.info(\"call pingpong's log, ctx unfinished: \", ctx.unfinished == true)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/xrpc/apisix/stream/xrpc/protocols/pingpong/schema.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nlocal core = require(\"apisix.core\")\n\n\nlocal schema = {\n    type = \"object\",\n    properties = {\n        service = {\n            type = \"string\"\n        },\n        faults = {\n            type = \"array\",\n            minItems = 1,\n            items = {\n                type = \"object\",\n                properties = {\n                    header_type = { type = \"string\" },\n                    delay = {\n                        type = \"number\",\n                        description = \"additional delay in seconds\",\n                    }\n                },\n                required = {\"header_type\"}\n            },\n        },\n    },\n}\n\nlocal _M = {}\n\n\nfunction _M.check_schema(conf)\n    return core.schema.check(schema, conf)\nend\n\n\nreturn _M\n"
  },
  {
    "path": "t/xrpc/dubbo.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: dubbo\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $config = $block->config // <<_EOC_;\n    location /t {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(ngx.req.get_body_data())\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    $block;\n});\n\nworker_connections(1024);\nrun_tests;\n\n__DATA__\n\n=== TEST 1: init\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"dubbo\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:20880\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: use dubbo_backend_provider server. request=org.apache.dubbo.backend.DemoService,service_version:1.0.1#hello,response=dubbo success & 200\n--- request eval\n\"GET /t\n\\xda\\xbb\\xc2\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xef\\x05\\x32\\x2e\\x30\\x2e\\x32\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x05\\x31\\x2e\\x30\\x2e\\x30\\x05\\x68\\x65\\x6c\\x6c\\x6f\\x0f\\x4c\\x6a\\x61\\x76\\x61\\x2f\\x75\\x74\\x69\\x6c\\x2f\\x4d\\x61\\x70\\x3b\\x48\\x04\\x6e\\x61\\x6d\\x65\\x08\\x7a\\x68\\x61\\x6e\\x67\\x73\\x61\\x6e\\x5a\\x48\\x04\\x70\\x61\\x74\\x68\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x12\\x72\\x65\\x6d\\x6f\\x74\\x65\\x2e\\x61\\x70\\x70\\x6c\\x69\\x63\\x61\\x74\\x69\\x6f\\x6e\\x0b\\x73\\x70\\x2d\\x63\\x6f\\x6e\\x73\\x75\\x6d\\x65\\x72\\x09\\x69\\x6e\\x74\\x65\\x72\\x66\\x61\\x63\\x65\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x07\\x76\\x65\\x72\\x73\\x69\\x6f\\x6e\\x05\\x31\\x2e\\x30\\x2e\\x30\\x07\\x74\\x69\\x6d\\x65\\x6f\\x75\\x74\\x04\\x31\\x30\\x30\\x30\\x5a\"\n--- response_body eval\n\"\\xda\\xbb\\x02\\x14\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x30\\x94\\x48\\x04\\x62\\x6f\\x64\\x79\\x0e\\x64\\x75\\x62\\x62\\x6f\\x20\\x73\\x75\\x63\\x63\\x65\\x73\\x73\\x0a\\x06\\x73\\x74\\x61\\x74\\x75\\x73\\x03\\x32\\x30\\x30\\x5a\\x48\\x05\\x64\\x75\\x62\\x62\\x6f\\x05\\x32\\x2e\\x30\\x2e\\x32\\x5a\"\n--- stream_conf_enable\n--- log_level: debug\n--- no_error_log\n\n\n\n=== TEST 3: heart beat. request=\\xe2|11..,response=\\x22|00...\n--- request eval\n\"GET /t\n\\xda\\xbb\\xe2\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x34\\x00\\x00\\x00\\x01\\x4e\"\n--- response_body eval\n\"\\xda\\xbb\\x22\\x14\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x34\\x00\\x00\\x00\\x01\\x4e\"\n--- stream_conf_enable\n--- log_level: debug\n--- no_error_log\n\n\n\n=== TEST 4: no response. Different from test2 \\x82=10000010, the second bit=0 of the third byte means no need to return\n--- request eval\n\"GET /t\n\\xda\\xbb\\x82\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xef\\x05\\x32\\x2e\\x30\\x2e\\x32\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x05\\x31\\x2e\\x30\\x2e\\x30\\x05\\x68\\x65\\x6c\\x6c\\x6f\\x0f\\x4c\\x6a\\x61\\x76\\x61\\x2f\\x75\\x74\\x69\\x6c\\x2f\\x4d\\x61\\x70\\x3b\\x48\\x04\\x6e\\x61\\x6d\\x65\\x08\\x7a\\x68\\x61\\x6e\\x67\\x73\\x61\\x6e\\x5a\\x48\\x04\\x70\\x61\\x74\\x68\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x12\\x72\\x65\\x6d\\x6f\\x74\\x65\\x2e\\x61\\x70\\x70\\x6c\\x69\\x63\\x61\\x74\\x69\\x6f\\x6e\\x0b\\x73\\x70\\x2d\\x63\\x6f\\x6e\\x73\\x75\\x6d\\x65\\x72\\x09\\x69\\x6e\\x74\\x65\\x72\\x66\\x61\\x63\\x65\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x07\\x76\\x65\\x72\\x73\\x69\\x6f\\x6e\\x05\\x31\\x2e\\x30\\x2e\\x30\\x07\\x74\\x69\\x6d\\x65\\x6f\\x75\\x74\\x04\\x31\\x30\\x30\\x30\\x5a\"\n--- response_body eval\n\"\"\n--- stream_conf_enable\n--- log_level: debug\n--- no_error_log\n\n\n\n=== TEST 5: failed response. request=org.apache.dubbo.backend.DemoService,service_version:1.0.1#fail,response=503\n--- request eval\n\"GET /t\n\\xda\\xbb\\xc2\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xee\\x05\\x32\\x2e\\x30\\x2e\\x32\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x05\\x31\\x2e\\x30\\x2e\\x30\\x04\\x66\\x61\\x69\\x6c\\x0f\\x4c\\x6a\\x61\\x76\\x61\\x2f\\x75\\x74\\x69\\x6c\\x2f\\x4d\\x61\\x70\\x3b\\x48\\x04\\x6e\\x61\\x6d\\x65\\x08\\x7a\\x68\\x61\\x6e\\x67\\x73\\x61\\x6e\\x5a\\x48\\x04\\x70\\x61\\x74\\x68\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x12\\x72\\x65\\x6d\\x6f\\x74\\x65\\x2e\\x61\\x70\\x70\\x6c\\x69\\x63\\x61\\x74\\x69\\x6f\\x6e\\x0b\\x73\\x70\\x2d\\x63\\x6f\\x6e\\x73\\x75\\x6d\\x65\\x72\\x09\\x69\\x6e\\x74\\x65\\x72\\x66\\x61\\x63\\x65\\x30\\x24\\x6f\\x72\\x67\\x2e\\x61\\x70\\x61\\x63\\x68\\x65\\x2e\\x64\\x75\\x62\\x62\\x6f\\x2e\\x62\\x61\\x63\\x6b\\x65\\x6e\\x64\\x2e\\x44\\x65\\x6d\\x6f\\x53\\x65\\x72\\x76\\x69\\x63\\x65\\x07\\x76\\x65\\x72\\x73\\x69\\x6f\\x6e\\x05\\x31\\x2e\\x30\\x2e\\x30\\x07\\x74\\x69\\x6d\\x65\\x6f\\x75\\x74\\x04\\x31\\x30\\x30\\x30\\x5a\"\n--- response_body eval\n\"\\xda\\xbb\\x02\\x14\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x2d\\x94\\x48\\x04\\x62\\x6f\\x64\\x79\\x0b\\x64\\x75\\x62\\x62\\x6f\\x20\\x66\\x61\\x69\\x6c\\x0a\\x06\\x73\\x74\\x61\\x74\\x75\\x73\\x03\\x35\\x30\\x33\\x5a\\x48\\x05\\x64\\x75\\x62\\x62\\x6f\\x05\\x32\\x2e\\x30\\x2e\\x32\\x5a\"\n--- stream_conf_enable\n--- log_level: debug\n--- no_error_log\n\n\n\n=== TEST 6: invalid magic(dabc<>dabb) for heart beat.\n--- request eval\n\"GET /t\n\\xda\\xbc\\xe2\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x34\\x00\\x00\\x00\\x01\\x4e\"\n--- error_log\nunknown magic number\n--- stream_conf_enable\n"
  },
  {
    "path": "t/xrpc/pingpong.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: pingpong\n    - name: redis\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $config = $block->config // <<_EOC_;\n    location /t {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(ngx.req.get_body_data())\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_;\n            local sock = ngx.req.socket(true)\n            sock:settimeout(10)\n            while true do\n                local data = sock:receiveany(4096)\n                if not data then\n                    return\n                end\n                sock:send(data)\n            end\n_EOC_\n\n    $block->set_value(\"stream_upstream_code\", $stream_upstream_code);\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: init\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: too short\n--- stream_request\nmmm\n--- error_log\ncall pingpong's init_worker\nfailed to read: timeout\n\n\n\n=== TEST 3: reply directly\n--- request eval\n\"POST /t\npp\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n--- response_body eval\n\"pp\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n--- stream_conf_enable\n\n\n\n=== TEST 4: unary\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\" x 3\n--- response_body eval\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\" x 3\n--- log_level: debug\n--- no_error_log\nstream lua tcp socket keepalive create connection pool for key \"127.0.0.1:1995\"\n--- stream_conf_enable\n\n\n\n=== TEST 5: unary & heartbeat\n--- request eval\n\"POST /t\n\" .\n\"pp\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- response_body eval\n\"pp\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n\n\n\n=== TEST 6: can't connect to upstream\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1979\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\" x 3\n--- error_log\nfailed to connect: connection refused\n--- stream_conf_enable\n\n\n\n=== TEST 8: use short timeout to check upstream's bad response\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        timeout = {\n                            connect = 0.01,\n                            send = 0.009,\n                            read = 0.008,\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 9: bad response\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\" x 1\n--- stream_conf_enable\n--- stream_upstream_code\n    local sock = ngx.req.socket(true)\n    sock:settimeout(10)\n    while true do\n        local data = sock:receiveany(4096)\n        if not data then\n            return\n        end\n        sock:send(data:sub(5))\n    end\n--- error_log\nfailed to read: timeout\nstream lua tcp socket connect timeout: 10\nlua tcp socket send timeout: 9\nstream lua tcp socket read timeout: 8\n--- log_level: debug\n\n\n\n=== TEST 10: reset\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 11: client stream, N:N\n--- request eval\n\"POST /t\n\" .\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x03ABC\" .\n\"pp\\x03\\x00\\x02\\x00\\x00\\x00\\x00\\x04ABCD\"\n--- stream_conf_enable\n--- stream_upstream_code\n    local sock = ngx.req.socket(true)\n    sock:settimeout(10)\n    local data1 = sock:receive(13)\n    if not data1 then\n        return\n    end\n    local data2 = sock:receive(14)\n    if not data2 then\n        return\n    end\n    assert(sock:send(data2))\n    assert(sock:send(data1))\n--- response_body eval\n\"pp\\x03\\x00\\x02\\x00\\x00\\x00\\x00\\x04ABCD\" .\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x03ABC\"\n\n\n\n=== TEST 12: client stream, bad response\n--- request eval\n\"POST /t\n\" .\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x03ABC\" .\n\"pp\\x03\\x00\\x02\\x00\\x00\\x00\\x00\\x04ABCD\"\n--- stream_conf_enable\n--- stream_upstream_code\n    local sock = ngx.req.socket(true)\n    sock:settimeout(10)\n    local data1 = sock:receive(13)\n    if not data1 then\n        return\n    end\n    local data2 = sock:receive(14)\n    if not data2 then\n        return\n    end\n    assert(sock:send(data2))\n    assert(sock:send(data1:sub(11)))\n--- response_body eval\n\"pp\\x03\\x00\\x02\\x00\\x00\\x00\\x00\\x04ABCD\"\n--- error_log\nRPC is not finished\ncall pingpong's log, ctx unfinished: true\n\n\n\n=== TEST 13: server stream, heartbeat\n--- request eval\n\"POST /t\n\" .\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- stream_upstream_code\n    local sock = ngx.req.socket(true)\n    sock:settimeout(10)\n    local data1 = sock:receive(13)\n    if not data1 then\n        return\n    end\n    local hb = \"pp\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n    assert(sock:send(hb))\n    local data2 = sock:receive(10)\n    if not data2 then\n        return\n    end\n    assert(data2 == hb)\n    assert(sock:send(data1))\n--- response_body eval\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x03ABC\"\n\n\n\n=== TEST 14: server stream\n--- request eval\n\"POST /t\n\" .\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x01A\"\n--- stream_conf_enable\n--- stream_upstream_code\n    local sock = ngx.req.socket(true)\n    sock:settimeout(10)\n    local data1 = sock:receive(11)\n    if not data1 then\n        return\n    end\n    assert(sock:send(\"pp\\x03\\x00\\x03\\x00\\x00\\x00\\x00\\x03ABC\"))\n    assert(sock:send(\"pp\\x03\\x00\\x02\\x00\\x00\\x00\\x00\\x02AB\"))\n    assert(sock:send(data1))\n--- response_body eval\n\"pp\\x03\\x00\\x03\\x00\\x00\\x00\\x00\\x03ABC\" .\n\"pp\\x03\\x00\\x02\\x00\\x00\\x00\\x00\\x02AB\" .\n\"pp\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x01A\"\n--- grep_error_log eval\nqr/call pingpong's log, ctx unfinished: \\w+/\n--- grep_error_log_out\ncall pingpong's log, ctx unfinished: false\ncall pingpong's log, ctx unfinished: false\ncall pingpong's log, ctx unfinished: false\n\n\n\n=== TEST 15: superior & subordinate\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.3:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/2',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        superior_id = 1,\n                        conf = {\n                            service = \"a\"\n                        },\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/3',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        superior_id = 1,\n                        conf = {\n                            service = \"b\"\n                        },\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.2:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            -- routes below should not be used to matched\n            local code, body = t('/apisix/admin/stream_routes/4',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        superior_id = 10000,\n                        conf = {\n                            service = \"b\"\n                        },\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.2:1979\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            -- Verify that invalid superior_id returns 400 error instead of any other response code\n            if code ~= 400 then\n                ngx.status = code\n                ngx.say(\"expected 400 for invalid superior_id, got \" .. code .. \": \" .. body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/5',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 16: hit\n--- request eval\n\"POST /t\n\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04b\\x00\\x00\\x00ABCD\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\"\n--- response_body eval\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04b\\x00\\x00\\x00ABCD\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\"\n--- grep_error_log eval\nqr/connect to \\S+ while prereading client data/\n--- grep_error_log_out\nconnect to 127.0.0.1:1995 while prereading client data\nconnect to 127.0.0.2:1995 while prereading client data\n--- stream_conf_enable\n\n\n\n=== TEST 17: hit (fallback to superior if not found)\n--- request eval\n\"POST /t\n\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03abcdABC\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04a\\x00\\x00\\x00ABCD\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03abcdABC\"\n--- response_body eval\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03abcdABC\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04a\\x00\\x00\\x00ABCD\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03abcdABC\"\n--- grep_error_log eval\nqr/connect to \\S+ while prereading client data/\n--- grep_error_log_out\nconnect to 127.0.0.3:1995 while prereading client data\nconnect to 127.0.0.1:1995 while prereading client data\n--- stream_conf_enable\n\n\n\n=== TEST 18: cache router by version\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            assert(sock:send(\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\"))\n\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/stream_routes/2',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        superior_id = 1,\n                        conf = {\n                            service = \"c\"\n                        },\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.4:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            local s = \"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04a\\x00\\x00\\x00ABCD\"\n            assert(sock:send(s .. \"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03c\\x00\\x00\\x00ABC\"))\n\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body eval\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04a\\x00\\x00\\x00ABCD\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03c\\x00\\x00\\x00ABC\"\n--- grep_error_log eval\nqr/connect to \\S+ while prereading client data/\n--- grep_error_log_out\nconnect to 127.0.0.1:1995 while prereading client data\nconnect to 127.0.0.3:1995 while prereading client data\nconnect to 127.0.0.4:1995 while prereading client data\n--- stream_conf_enable\n\n\n\n=== TEST 19: use upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                {\n                    nodes = {\n                        [\"127.0.0.3:1995\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/2',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        superior_id = 1,\n                        conf = {\n                            service = \"a\"\n                        },\n                        name = \"pingpong\"\n                    },\n                    upstream_id = 1\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 20: hit\n--- request eval\n\"POST /t\n\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\"\n--- response_body eval\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\"\n--- grep_error_log eval\nqr/connect to \\S+ while prereading client data/\n--- grep_error_log_out\nconnect to 127.0.0.3:1995 while prereading client data\n--- stream_conf_enable\n\n\n\n=== TEST 21: cache router by version, with upstream_id\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            assert(sock:send(\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\"))\n\n            ngx.sleep(0.1)\n\n            local code, body = t('/apisix/admin/upstreams/1',\n                ngx.HTTP_PUT,\n                {\n                    nodes = {\n                        [\"127.0.0.1:1995\"] = 1\n                    },\n                    type = \"roundrobin\"\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.sleep(0.1)\n\n            local s = \"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04a\\x00\\x00\\x00ABCD\"\n            assert(sock:send(s))\n\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n        }\n    }\n--- request\nGET /t\n--- response_body eval\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x03a\\x00\\x00\\x00ABC\" .\n\"pp\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x04a\\x00\\x00\\x00ABCD\"\n--- grep_error_log eval\nqr/connect to \\S+ while prereading client data/\n--- grep_error_log_out\nconnect to 127.0.0.3:1995 while prereading client data\nconnect to 127.0.0.1:1995 while prereading client data\n--- stream_conf_enable\n"
  },
  {
    "path": "t/xrpc/pingpong2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: pingpong\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $config = $block->config // <<_EOC_;\n    location /t {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(ngx.req.get_body_data())\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_;\n            local sock = ngx.req.socket(true)\n            sock:settimeout(10)\n            while true do\n                local data = sock:receiveany(4096)\n                if not data then\n                    return\n                end\n                sock:send(data)\n            end\n_EOC_\n\n    $block->set_value(\"stream_upstream_code\", $stream_upstream_code);\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen 8125 udp;\n        content_by_lua_block {\n            require(\"lib.mock_layer4\").dogstatsd()\n        }\n    }\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: init\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: check the default timeout\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- response_body eval\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- error_log\nstream lua tcp socket connect timeout: 60000\nlua tcp socket send timeout: 60000\nstream lua tcp socket read timeout: 60000\n--- log_level: debug\n--- stream_conf_enable\n\n\n\n=== TEST 3: bad loggger filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {}\n                                },\n                                conf = {}\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 4: failed to validate the 'filter' expression\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- error_log\nfailed to validate the 'filter' expression: rule too short\n\n\n\n=== TEST 5: set loggger filter(single rule)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \">\", 10}\n                                },\n                                conf = {}\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 6: log filter matched successful\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- error_log\nlog filter: syslog filter result: true\n\n\n\n=== TEST 7: update loggger filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \"<\", 10}\n                                },\n                                conf = {}\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 8: failed to match log filter\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- error_log\nlog filter: syslog filter result: false\n\n\n\n=== TEST 9: set loggger filter(multiple rules)\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \">\", 12},\n                                    {\"rpc_len\", \"<\", 14}\n                                },\n                                conf = {}\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 10: log filter matched successful\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- error_log\nlog filter: syslog filter result: true\n\n\n\n=== TEST 11: update loggger filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \"<\", 10},\n                                    {\"rpc_len\", \">\", 12}\n                                },\n                                conf = {}\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 12: failed to match log filter\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- error_log\nlog filter: syslog filter result: false\n\n\n\n=== TEST 13: set custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/syslog',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"client_ip\": \"$remote_addr\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 14: no loggger filter, defaulte executed logger plugin\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 8125,\n                                    sock_type = \"udp\",\n                                    batch_max_size = 1,\n                                    flush_limit = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 15: verify the data received by the log server\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- wait: 0.5\n--- error_log eval\nqr/message received:.*\\\"client_ip\\\"\\:\\\"127.0.0.1\\\"/\n\n\n\n=== TEST 16: set loggger filter\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \">\", 10}\n                                },\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 8125,\n                                    sock_type = \"udp\",\n                                    batch_max_size = 1,\n                                    flush_limit = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 17: verify the data received by the log server\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- wait: 0.5\n--- error_log eval\nqr/message received:.*\\\"client_ip\\\"\\:\\\"127.0.0.1\\\"/\n\n\n\n=== TEST 18: small flush_limit, instant flush\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \">\", 10}\n                                },\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 5044,\n                                    batch_max_size = 1,\n                                    flush_limit = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n\n            -- wait etcd sync\n            ngx.sleep(0.5)\n\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            assert(sock:send(\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"))\n\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n            -- wait flush log\n            ngx.sleep(2.5)\n        }\n    }\n--- request\nGET /t\n--- response_body eval\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- timeout: 5\n--- error_log\ntry to lock with key xrpc-pingpong-logger#table\nunlock with key xrpc-pingpong-logger#table\n\n\n\n=== TEST 19: check plugin configuration updating\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \">\", 10}\n                                },\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 5044,\n                                    batch_max_size = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            assert(sock:send(\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"))\n            local body1, err\n            while true do\n                body1, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n            end\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_len\", \">\", 10}\n                                },\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 5045,\n                                    batch_max_size = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            local sock = ngx.socket.tcp()\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.status = code\n                ngx.say(\"fail\")\n                return\n            end\n            assert(sock:send(\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"))\n            local body2, err\n            while true do\n                body2, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n            end\n            ngx.print(body1)\n            ngx.print(body2)\n        }\n    }\n--- request\nGET /t\n--- wait: 0.5\n--- response_body eval\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- grep_error_log eval\nqr/sending a batch logs to 127.0.0.1:(\\d+)/\n--- grep_error_log_out\nsending a batch logs to 127.0.0.1:5044\nsending a batch logs to 127.0.0.1:5045\n"
  },
  {
    "path": "t/xrpc/pingpong3.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: pingpong\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    my $config = $block->config // <<_EOC_;\n    location /t {\n        content_by_lua_block {\n            ngx.req.read_body()\n            local sock = ngx.socket.tcp()\n            sock:settimeout(1000)\n            local ok, err = sock:connect(\"127.0.0.1\", 1985)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to connect: \", err)\n                return ngx.exit(503)\n            end\n\n            local bytes, err = sock:send(ngx.req.get_body_data())\n            if not bytes then\n                ngx.log(ngx.ERR, \"send stream request error: \", err)\n                return ngx.exit(503)\n            end\n            while true do\n                local data, err = sock:receiveany(4096)\n                if not data then\n                    sock:close()\n                    break\n                end\n                ngx.print(data)\n            end\n        }\n    }\n_EOC_\n\n    $block->set_value(\"config\", $config);\n\n    my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_;\n            local sock = ngx.req.socket(true)\n            sock:settimeout(10)\n            while true do\n                local data = sock:receiveany(4096)\n                if not data then\n                    return\n                end\n                sock:send(data)\n            end\n_EOC_\n\n    $block->set_value(\"stream_upstream_code\", $stream_upstream_code);\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen 8125 udp;\n        content_by_lua_block {\n            require(\"lib.mock_layer4\").dogstatsd()\n        }\n    }\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    $block;\n});\n\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/syslog',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"rpc_time\": \"$rpc_time\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: use vae rpc_time\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"pingpong\",\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_time\", \">=\", 0}\n                                },\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 8125,\n                                    sock_type = \"udp\",\n                                    batch_max_size = 1,\n                                    flush_limit = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:1995\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 3: verify the data received by the log server\n--- request eval\n\"POST /t\n\" .\n\"pp\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x03ABC\"\n--- stream_conf_enable\n--- wait: 0.5\n--- error_log eval\nqr/message received:.*\\\"rpc_time\\\"\\:(0.\\d+|0)\\}/\n"
  },
  {
    "path": "t/xrpc/prometheus.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nBEGIN {\n    if ($ENV{TEST_NGINX_CHECK_LEAK}) {\n        $SkipReason = \"unavailable for the hup tests\";\n\n    } else {\n        $ENV{TEST_NGINX_USE_HUP} = 1;\n        undef $ENV{TEST_NGINX_USE_STAP};\n    }\n}\n\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\n$ENV{TEST_NGINX_REDIS_PORT} ||= 1985;\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nstream_plugins:\n    - prometheus\nxrpc:\n  protocols:\n    - name: redis\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nworker_connections(1024);\nrun_tests;\n\n__DATA__\n\n=== TEST 1: route with metrics\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/routes/1',\n                ngx.HTTP_PUT,\n                {\n                    uri = \"/apisix/prometheus/metrics\",\n                    plugins = {\n                        [\"public-api\"] = {}\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\",\n                        conf = {\n                            faults = {\n                                {delay = 0.08, commands = {\"hmset\"}},\n                                {delay = 0.3, commands = {\"hmget\"}},\n                            }\n                        },\n                        metric = {\n                            enable = true,\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:hmset(\"animals\", \"dog\", \"bark\", \"cat\", \"meow\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n                return\n            end\n\n            local res, err = red:hmget(\"animals\", \"dog\", \"cat\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n        }\n    }\n--- response_body\n--- stream_conf_enable\n\n\n\n=== TEST 3: check metrics\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_redis_commands_latency_seconds_bucket\\{route=\"1\",command=\"hmget\",le=\"0.5\"\\} 1/ and\nqr/apisix_redis_commands_latency_seconds_bucket\\{route=\"1\",command=\"hmset\",le=\"0.1\"\\} 1/ and\nqr/apisix_redis_commands_total\\{route=\"1\",command=\"hmget\"\\} 1\napisix_redis_commands_total\\{route=\"1\",command=\"hmset\"\\} 1/\n\n\n\n=== TEST 4: ignore metric if prometheus is disabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:hmset(\"animals\", \"dog\", \"bark\", \"cat\", \"meow\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n                return\n            end\n        }\n    }\n--- response_body\n--- extra_yaml_config\nstream_plugins:\n    - ip-restriction\nxrpc:\n  protocols:\n    - name: redis\n--- stream_conf_enable\n\n\n\n=== TEST 5: check metrics\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_redis_commands_total\\{route=\"1\",command=\"hmset\"\\} 1/\n\n\n\n=== TEST 6: ignore metric if metric is disabled\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\",\n                        conf = {\n                            faults = {\n                                {delay = 0.08, commands = {\"hmset\"}},\n                                {delay = 0.3, commands = {\"hmget\"}},\n                            }\n                        },\n                        metric = {\n                            enable = false\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:hmset(\"animals\", \"dog\", \"bark\", \"cat\", \"meow\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n                return\n            end\n        }\n    }\n--- response_body\n--- stream_conf_enable\n\n\n\n=== TEST 8: check metrics\n--- request\nGET /apisix/prometheus/metrics\n--- response_body eval\nqr/apisix_redis_commands_total\\{route=\"1\",command=\"hmset\"\\} 1/\n"
  },
  {
    "path": "t/xrpc/redis.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\n$ENV{TEST_NGINX_REDIS_PORT} ||= 1985;\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: redis\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    $block;\n});\n\nworker_connections(1024);\nrun_tests;\n\n__DATA__\n\n=== TEST 1: init\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\"\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 2: sanity\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:hmset(\"animals\", \"dog\", \"bark\", \"cat\", \"meow\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n                return\n            end\n            ngx.say(\"hmset animals: \", res)\n\n            local res, err = red:hmget(\"animals\", \"dog\", \"cat\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n\n            ngx.say(\"hmget animals: \", res)\n\n            local res, err = red:hget(\"animals\", \"dog\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n\n            ngx.say(\"hget animals: \", res)\n\n            local res, err = red:hget(\"animals\", \"not_found\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n\n            ngx.say(\"hget animals: \", res)\n        }\n    }\n--- response_body\nhmset animals: OK\nhmget animals: barkmeow\nhget animals: bark\nhget animals: null\n--- stream_conf_enable\n\n\n\n=== TEST 3: error\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:get(\"animals\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n            end\n\n            local res, err = red:hget(\"animals\", \"dog\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n\n            ngx.say(\"hget animals: \", res)\n        }\n    }\n--- response_body\nfailed to set animals: WRONGTYPE Operation against a key holding the wrong kind of value\nhget animals: bark\n--- stream_conf_enable\n\n\n\n=== TEST 4: big value\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:set(\"big-key\", (\"\\r\\n\"):rep(1024 * 1024 * 16))\n            if not res then\n                ngx.say(\"failed to set: \", err)\n                return\n            end\n\n            local res, err = red:get(\"big-key\")\n            if not res then\n                ngx.say(\"failed to get: \", err)\n                return\n            end\n\n            ngx.print(res)\n        }\n    }\n--- response_body eval\n\"\\r\\n\" x 16777216\n--- stream_conf_enable\n\n\n\n=== TEST 5: pipeline\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require(\"cjson\")\n            local redis = require \"resty.redis\"\n\n            local t = {}\n            for i = 1, 180 do\n                local th = assert(ngx.thread.spawn(function(i)\n                    local red = redis:new()\n                    local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n                    if not ok then\n                        ngx.say(\"failed to connect: \", err)\n                        return\n                    end\n\n                    red:init_pipeline()\n\n                    red:set(\"mark_\" .. i, i)\n                    red:get(\"mark_\" .. i)\n                    red:get(\"counter\")\n                    for j = 1, 4 do\n                        red:incr(\"counter\")\n                    end\n\n                    local results, err = red:commit_pipeline()\n                    if not results then\n                        ngx.say(\"failed to commit: \", err)\n                        return\n                    end\n\n                    local begin = tonumber(results[3])\n                    for j = 1, 4 do\n                        local incred = results[3 + j]\n                        if incred ~= results[2 + j] + 1 then\n                            ngx.log(ngx.ERR, cjson.encode(results))\n                        end\n                    end\n                end, i))\n                table.insert(t, th)\n            end\n            for i, th in ipairs(t) do\n                ngx.thread.wait(th)\n            end\n        }\n    }\n--- response_body\n--- stream_conf_enable\n\n\n\n=== TEST 6: delay\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\",\n                        conf = {\n                            faults = {\n                                {delay = 0.01, key = \"ignored\", commands = {\"Ping\", \"time\"}}\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 7: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local start = ngx.now()\n            local res, err = red:ping()\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            -- use integer to bypass float point number precision problem\n            if math.ceil((now - start) * 1000) < 10 then\n                ngx.say(now, \" \", start)\n                return\n            end\n            start = now\n\n            local res, err = red:time()\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 10 then\n                ngx.say(now, \" \", start)\n                return\n            end\n            start = now\n\n            red:init_pipeline()\n            red:time()\n            red:time()\n            red:get(\"A\")\n\n            local results, err = red:commit_pipeline()\n            if not results then\n                ngx.say(\"failed to commit: \", err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 30 then\n                ngx.say(now, \" \", start)\n                return\n            end\n\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n--- stream_conf_enable\n\n\n\n=== TEST 8: DFS match\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\",\n                        conf = {\n                            faults = {\n                                {delay = 0.02, key = \"a\", commands = {\"get\"}},\n                                {delay = 0.01, commands = {\"get\", \"set\"}},\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 9: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local start = ngx.now()\n            local res, err = red:get(\"a\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 20 then\n                ngx.say(now, \" \", start)\n                return\n            end\n            start = now\n\n            local res, err = red:set(\"a\", \"a\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 10 then\n                ngx.say(now, \" \", start)\n                return\n            end\n            start = now\n\n            red:init_pipeline()\n            red:get(\"b\")\n            red:set(\"A\", \"a\")\n\n            local results, err = red:commit_pipeline()\n            if not results then\n                ngx.say(\"failed to commit: \", err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 30 then\n                ngx.say(now, \" \", start)\n                return\n            end\n\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n--- stream_conf_enable\n\n\n\n=== TEST 10: multi keys\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\",\n                        conf = {\n                            faults = {\n                                {delay = 0.06, key = \"b\", commands = {\"del\"}},\n                                {delay = 0.04, key = \"a\", commands = {\"mset\"}},\n                                {delay = 0.02, key = \"b\", commands = {\"mset\"}},\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 11: hit\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local start = ngx.now()\n            local res, err = red:mset(\"c\", 1, \"a\", 2, \"b\", 3)\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 40 then\n                ngx.say(\"mset a \", now, \" \", start)\n                return\n            end\n            start = now\n\n            local res, err = red:mset(\"b\", 2, \"a\", 3)\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 35 then\n                ngx.say(\"mset b \", now, \" \", start)\n                return\n            end\n            start = now\n\n            local res, err = red:mset(\"c\", \"a\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) > 20 then\n                ngx.say(\"mset mismatch \", now, \" \", start)\n                return\n            end\n            start = now\n\n            local res, err = red:del(\"a\", \"b\")\n            if not res then\n                ngx.say(err)\n                return\n            end\n            local now = ngx.now()\n            if math.ceil((now - start) * 1000) < 60 then\n                ngx.say(\"del b \", now, \" \", start)\n                return\n            end\n            start = now\n\n            ngx.say(\"ok\")\n        }\n    }\n--- response_body\nok\n--- stream_conf_enable\n\n\n\n=== TEST 12: publish & subscribe\n--- stream_extra_init_by_lua\n            local cjson = require \"cjson\"\n            local redis_proto = require(\"apisix.stream.xrpc.protocols.redis\")\n            redis_proto.log = function(sess, ctx)\n                ngx.log(ngx.WARN, \"log redis request \", cjson.encode(ctx.cmd_line))\n            end\n\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require \"cjson\"\n            local redis = require \"resty.redis\"\n\n            local red = redis:new()\n            local red2 = redis:new()\n\n            red:set_timeout(1000) -- 1 sec\n            red2:set_timeout(1000) -- 1 sec\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"1: failed to connect: \", err)\n                return\n            end\n\n            ok, err = red2:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"2: failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:subscribe(\"dog\")\n            if not res then\n                ngx.say(\"1: failed to subscribe: \", err)\n                return\n            end\n\n            ngx.say(\"1: subscribe dog: \", cjson.encode(res))\n\n            res, err = red:subscribe(\"cat\")\n            if not res then\n                ngx.say(\"1: failed to subscribe: \", err)\n                return\n            end\n\n            ngx.say(\"1: subscribe cat: \", cjson.encode(res))\n\n            res, err = red2:publish(\"dog\", \"Hello\")\n            if not res then\n                ngx.say(\"2: failed to publish: \", err)\n                return\n            end\n\n            ngx.say(\"2: publish: \", cjson.encode(res))\n\n            res, err = red:read_reply()\n            if not res then\n                ngx.say(\"1: failed to read reply: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n\n            red:set_timeout(10) -- 10ms\n            res, err = red:read_reply()\n            if not res then\n                ngx.say(\"1: failed to read reply: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n            red:set_timeout(1000) -- 1s\n\n            res, err = red:unsubscribe()\n            if not res then\n                ngx.say(\"1: failed to unscribe: \", err)\n            else\n                ngx.say(\"1: unsubscribe: \", cjson.encode(res))\n            end\n\n            res, err = red:read_reply()\n            if not res then\n                ngx.say(\"1: failed to read reply: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n\n            red:set_timeout(10) -- 10ms\n            res, err = red:read_reply()\n            if not res then\n                ngx.say(\"1: failed to read reply: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n            red:set_timeout(1000) -- 1s\n\n            res, err = red:set(\"dog\", 1)\n            if not res then\n                ngx.say(\"1: failed to set: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n\n            red:close()\n            red2:close()\n        }\n    }\n--- response_body_like chop\n^1: subscribe dog: \\[\"subscribe\",\"dog\",1\\]\n1: subscribe cat: \\[\"subscribe\",\"cat\",2\\]\n2: publish: 1\n1: receive: \\[\"message\",\"dog\",\"Hello\"\\]\n1: failed to read reply: timeout\n1: unsubscribe: \\[\\[\"unsubscribe\",\"(?:dog|cat)\",1\\],\\[\"unsubscribe\",\"(?:dog|cat)\",0\\]\\]\n1: failed to read reply: not subscribed\n1: failed to read reply: not subscribed\n1: receive: \"OK\"\n$\n--- stream_conf_enable\n--- grep_error_log eval\nqr/log redis request \\[[^]]+\\]/\n--- grep_error_log_out\nlog redis request [\"subscribe\",\"dog\"]\nlog redis request [\"subscribe\",\"cat\"]\nlog redis request [\"publish\",\"dog\",\"Hello\"]\nlog redis request [\"unsubscribe\"]\nlog redis request [\"set\",\"dog\",\"1\"]\n\n\n\n=== TEST 13: psubscribe & punsubscribe\n--- stream_extra_init_by_lua\n            local cjson = require \"cjson\"\n            local redis_proto = require(\"apisix.stream.xrpc.protocols.redis\")\n            redis_proto.log = function(sess, ctx)\n                ngx.log(ngx.WARN, \"log redis request \", cjson.encode(ctx.cmd_line))\n            end\n\n--- config\n    location /t {\n        content_by_lua_block {\n            local cjson = require \"cjson\"\n            local redis = require \"resty.redis\"\n\n            local red = redis:new()\n            local red2 = redis:new()\n\n            red:set_timeout(1000) -- 1 sec\n            red2:set_timeout(1000) -- 1 sec\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"1: failed to connect: \", err)\n                return\n            end\n\n            ok, err = red2:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"2: failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:psubscribe(\"dog*\", \"cat*\")\n            if not res then\n                ngx.say(\"1: failed to subscribe: \", err)\n                return\n            end\n\n            ngx.say(\"1: psubscribe: \", cjson.encode(res))\n\n            res, err = red2:publish(\"dog1\", \"Hello\")\n            if not res then\n                ngx.say(\"2: failed to publish: \", err)\n                return\n            end\n\n            ngx.say(\"2: publish: \", cjson.encode(res))\n\n            res, err = red:read_reply()\n            if not res then\n                ngx.say(\"1: failed to read reply: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n\n            res, err = red:punsubscribe(\"cat*\", \"dog*\")\n            if not res then\n                ngx.say(\"1: failed to unscribe: \", err)\n            else\n                ngx.say(\"1: punsubscribe: \", cjson.encode(res))\n            end\n\n            res, err = red:set(\"dog\", 1)\n            if not res then\n                ngx.say(\"1: failed to set: \", err)\n            else\n                ngx.say(\"1: receive: \", cjson.encode(res))\n            end\n\n            red:close()\n            red2:close()\n        }\n    }\n--- response_body_like chop\n^1: psubscribe: \\[\\[\"psubscribe\",\"dog\\*\",1\\],\\[\"psubscribe\",\"cat\\*\",2\\]\\]\n2: publish: 1\n1: receive: \\[\"pmessage\",\"dog\\*\",\"dog1\",\"Hello\"\\]\n1: punsubscribe: \\[\\[\"punsubscribe\",\"cat\\*\",1\\],\\[\"punsubscribe\",\"dog\\*\",0\\]\\]\n1: receive: \"OK\"\n$\n--- stream_conf_enable\n--- grep_error_log eval\nqr/log redis request \\[[^]]+\\]/\n--- grep_error_log_out\nlog redis request [\"psubscribe\",\"dog*\",\"cat*\"]\nlog redis request [\"publish\",\"dog1\",\"Hello\"]\nlog redis request [\"punsubscribe\",\"cat*\",\"dog*\"]\nlog redis request [\"set\",\"dog\",\"1\"]\n"
  },
  {
    "path": "t/xrpc/redis2.t",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nuse t::APISIX;\n\nlog_level(\"warn\");\n\nmy $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';\nmy $version = eval { `$nginx_binary -V 2>&1` };\n\nif ($version !~ m/\\/apisix-nginx-module/) {\n    plan(skip_all => \"apisix-nginx-module not installed\");\n} else {\n    plan('no_plan');\n}\n\n$ENV{TEST_NGINX_REDIS_PORT} ||= 1985;\n\nadd_block_preprocessor(sub {\n    my ($block) = @_;\n\n    if (!$block->extra_yaml_config) {\n        my $extra_yaml_config = <<_EOC_;\nxrpc:\n  protocols:\n    - name: redis\n_EOC_\n        $block->set_value(\"extra_yaml_config\", $extra_yaml_config);\n    }\n\n    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {\n        $block->set_value(\"no_error_log\", \"[error]\\nRPC is not finished\");\n    }\n\n    if (!defined $block->request) {\n        $block->set_value(\"request\", \"GET /t\");\n    }\n\n    if (!defined $block->extra_stream_config) {\n        my $stream_config = <<_EOC_;\n    server {\n        listen 8125 udp;\n        content_by_lua_block {\n            require(\"lib.mock_layer4\").dogstatsd()\n        }\n    }\n_EOC_\n        $block->set_value(\"extra_stream_config\", $stream_config);\n    }\n\n    $block;\n});\n\nworker_connections(1024);\nrun_tests;\n\n__DATA__\n\n=== TEST 1: set custom log format\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/plugin_metadata/syslog',\n                ngx.HTTP_PUT,\n                [[{\n                    \"log_format\": {\n                        \"rpc_time\": \"$rpc_time\",\n                        \"redis_cmd_line\": \"$redis_cmd_line\"\n                    }\n                }]]\n                )\n\n            if code >= 300 then\n                ngx.status = code\n                ngx.say(body)\n                return\n            end\n\n            ngx.say(body)\n        }\n    }\n--- request\nGET /t\n--- response_body\npassed\n\n\n\n=== TEST 2: use register vars(redis_cmd_line and rpc_time) in logger\n--- config\n    location /t {\n        content_by_lua_block {\n            local t = require(\"lib.test_admin\").test\n            local code, body = t('/apisix/admin/stream_routes/1',\n                ngx.HTTP_PUT,\n                {\n                    protocol = {\n                        name = \"redis\",\n                        conf = {\n                            faults = {\n                                {delay = 0.01, commands = {\"hmset\", \"hmget\", \"ping\"}},\n                            }\n                        },\n                        logger = {\n                            {\n                                name = \"syslog\",\n                                filter = {\n                                    {\"rpc_time\", \">=\", 0.001},\n                                },\n                                conf = {\n                                    host = \"127.0.0.1\",\n                                    port = 8125,\n                                    sock_type = \"udp\",\n                                    batch_max_size = 1,\n                                    flush_limit = 1\n                                }\n                            }\n                        }\n                    },\n                    upstream = {\n                        nodes = {\n                            [\"127.0.0.1:6379\"] = 1\n                        },\n                        type = \"roundrobin\"\n                    }\n                }\n            )\n            if code >= 300 then\n                ngx.status = code\n            end\n            ngx.say(body)\n        }\n    }\n--- response_body\npassed\n\n\n\n=== TEST 3: verify the data received by the log server\n--- stream_conf_enable\n--- config\n    location /t {\n        content_by_lua_block {\n            local redis = require \"resty.redis\"\n            local red = redis:new()\n\n            local ok, err = red:connect(\"127.0.0.1\", $TEST_NGINX_REDIS_PORT)\n            if not ok then\n                ngx.say(\"failed to connect: \", err)\n                return\n            end\n\n            local res, err = red:hmset(\"animals\", \"dog\", \"bark\", \"cat\", \"meow\")\n            if not res then\n                ngx.say(\"failed to set animals: \", err)\n                return\n            end\n            ngx.say(\"hmset animals: \", res)\n\n            local res, err = red:hmget(\"animals\", \"dog\", \"cat\")\n            if not res then\n                ngx.say(\"failed to get animals: \", err)\n                return\n            end\n\n            ngx.say(\"hmget animals: \", res)\n\n            -- test for only one command in cmd_line\n            local res, err = red:ping()\n            if not res then\n                ngx.say(err)\n                return\n            end\n\n            ngx.say(\"ping: \", string.lower(res))\n        }\n    }\n--- response_body\nhmset animals: OK\nhmget animals: barkmeow\nping: pong\n--- wait: 1\n--- grep_error_log eval\nqr/message received:.*\\\"redis_cmd_line\\\":[^}|^,]+/\n--- grep_error_log_out eval\nqr{message received:.*\\\"redis_cmd_line\\\":\\\"hmset animals dog bark cat meow\\\"(?s).*\nmessage received:.*\\\"redis_cmd_line\\\":\\\"hmget animals dog cat\\\"(?s).*\nmessage received:.*\\\"redis_cmd_line\\\":\\\"ping\\\"}\n"
  },
  {
    "path": "utils/check-category.py",
    "content": "#!/usr/bin/env python\n# coding: utf-8\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nimport json\nimport os\nimport sys\nfrom os import path\n\nEXT = \".md\"\n\ntry:\n    unicode  # Python 2\nexcept NameError:\n    unicode = str  # Python 3\n\ndef collect_fn(entries, topic):\n    if \"id\" in topic:\n        fn = topic[\"id\"]\n        entries.append(fn)\n    elif \"items\" in topic:\n        for item in topic[\"items\"]:\n            if isinstance(item, unicode):\n                entries.append(item)\n            else:\n                collect_fn(entries, item)\n\ndef check_category(root):\n    index = root + \"config.json\"\n    with open(index) as f:\n        entries = []\n\n        data = json.load(f)\n        for topic in data[\"sidebar\"]:\n            collect_fn(entries, topic)\n        for e in entries:\n            fn = root + e + EXT\n            if not path.exists(fn):\n                print(\"Entry %s in the sidebar can't be found. Please remove it from %s.\"\n                        % (fn, index))\n                return False\n\n        ignore_list = [\"examples/plugins-hmac-auth-generate-signature\", \"config\", \"README\"]\n        entries.extend(ignore_list)\n        existed_files = []\n        for parent, dirs, files in os.walk(root):\n            for fn in files:\n                existed_files.append(path.join(parent[len(root):], path.splitext(fn)[0]))\n        for fn in existed_files:\n            if fn not in entries:\n                print(\"File %s%s%s is not indexed. Please add it to %s.\" % (root, fn, EXT, index))\n                return False\n        return True\n\nroots = [\"docs/en/latest/\", \"docs/zh/latest/\"]\nfor r in roots:\n    if not check_category(r):\n        sys.exit(-1)\n"
  },
  {
    "path": "utils/check-lua-code-style.sh",
    "content": "#!/bin/sh\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\nluacheck -q apisix t/lib\n\nfind apisix -name '*.lua' ! -wholename 'apisix/cli/ngx_tpl.lua' ! -wholename 'apisix/cli/config.lua' -exec ./utils/lj-releng {} + > \\\n    /tmp/check.log 2>&1 || (cat /tmp/check.log && exit 1)\n\ngrep -E \"ERROR.*.lua:\" /tmp/check.log > /tmp/error.log || true\nif [ -s /tmp/error.log ]; then\n    echo \"=====bad style=====\"\n    cat /tmp/check.log\n    exit 1\nfi\n"
  },
  {
    "path": "utils/check-merge-conflict.sh",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -euo pipefail\n\ngrep \"^<<<<<<< HEAD\" $(git grep --cached -l '' | xargs) && exit 1\ngrep \"^>>>>>>> master\" $(git grep --cached -l '' | xargs) && exit 1\nexit 0\n"
  },
  {
    "path": "utils/check-plugins-code.sh",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nRED=\"\\033[1;31m\";\nNC=\"\\033[0m\"; # No Color\nhit=0\n\ncheckfunc () {\n    funccontent=$1\n    file=$2\n    [[ $funccontent =~ \"core.response.exit\" ]] && echo -e ${RED}${file}${NC} && echo \"    can't exit in rewrite or access phase!\" && ((hit++))\n    [[ $funccontent =~ \"ngx.exit\" ]] && echo -e ${RED}${file}${NC} && echo \"    can't exit in rewrite or access phase!\" && ((hit++))\n    [[ $funccontent =~ \"ngx.redirect\" ]] && echo -e ${RED}${file}${NC} && echo \"    can't call ngx.redirect in rewrite or access phase!\" && ((hit++))\n}\n\n\nfiltercode () {\n    content=$1\n    file=$2\n\n    rcontent=${content##*_M.rewrite}\n    rewritefunc=${rcontent%%function*}\n    checkfunc \"$rewritefunc\" \"$file\"\n\n    rcontent=${content##*_M.access}\n    accessfunc=${rcontent%%function*}\n    checkfunc \"$accessfunc\" \"$file\"\n}\n\n\nfor file in apisix/plugins/*.lua\ndo\n    if test -f $file\n    then\n        content=$(cat $file)\n        filtercode \"$content\" \"$file\"\n    fi\ndone\nfor file in apisix/stream/plugins/*.lua\ndo\n    if test -f $file\n    then\n        content=$(cat $file)\n        filtercode \"$content\" \"$file\"\n    fi\ndone\n\nif (($hit>0))\nthen\n    exit 1\nfi\n\n# test case for check\ncontent=$(cat t/fake-plugin-exit.lua)\nfiltercode \"$content\" > test.log 2>&1 || (cat test.log && exit 1)\n\necho \"All passed.\"\n"
  },
  {
    "path": "utils/check-test-code-style.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -x -euo pipefail\n\nfind t -name '*.t' -exec grep -E \"\\-\\-\\-\\s+(SKIP|ONLY|LAST|FIRST)$\" {} + > /tmp/error.log || true\nif [ -s /tmp/error.log ]; then\n    echo \"Forbidden directives to found. Bypass test cases without reason are not allowed.\"\n    cat /tmp/error.log\n    exit 1\nfi\n\nfind t -name '*.t' -exec ./utils/reindex {} + > \\\n    /tmp/check.log 2>&1 || (cat /tmp/check.log && exit 1)\n\ngrep \"done.\" /tmp/check.log > /tmp/error.log || true\nif [ -s /tmp/error.log ]; then\n    echo \"=====bad style=====\"\n    cat /tmp/error.log\n    echo \"you need to run 'reindex' to fix them. Read CONTRIBUTING.md for more details.\"\n    exit 1\nfi\n"
  },
  {
    "path": "utils/check-version.sh",
    "content": "#!/bin/sh\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nver=$1\n\nred='\\e[0;41m'\nRED='\\e[1;31m'\ngreen='\\e[0;32m'\nGREEN='\\e[1;32m'\nNC='\\e[0m'\n\n# doc: apisix $ver\nmatched=`grep \"apisix.[0-9][0-9.]*\" -r doc/`\nexpected=`grep \"apisix.$ver\" -r doc/`\n\nif [ \"$matched\" = \"$expected\" ]; then\n    echo -e \"${green}passed: (doc) apisix $ver ${NC}\"\nelse\n    echo -e \"${RED}failed: (doc) apisix $ver ${NC}\" 1>&2\n    echo\n    echo \"-----maybe wrong version-----\"\n    echo \"$matched\"\n    exit 1\nfi\n\n# doc: version $ver\nmatched=`grep \"version [0-9][0-9.]*\" -r doc/`\nexpected=`grep -F \"version $ver\" -r doc/`\n\nif [ \"$matched\" = \"$expected\" ]; then\n    echo -e \"${green}passed: (doc) version $ver ${NC}\"\nelse\n    echo -e \"${RED}failed: (doc) version $ver ${NC}\" 1>&2\n    echo\n    echo \"-----maybe wrong version-----\"\n    echo \"$matched\"\n    exit 1\nfi\n\n# lua: VERSION = $ver\nmatched=`grep \"VERSION = \\\"[0-9][0-9.]*\\\"\" -r apisix/`\nexpected=`grep -F \"VERSION = \\\"$ver\\\"\" -r apisix/`\n\nif [ \"$matched\" = \"$expected\" ]; then\n    echo -e \"${green}passed: (lua) VERSION = $ver ${NC}\"\nelse\n    echo -e \"${RED}failed: (lua) VERSION = \\\"$ver\\\" ${NC}\" 1>&2\n    echo\n    echo \"-----maybe wrong version-----\"\n    echo \"$matched\"\n    exit 1\nfi\n\n\n# rockspec\nmatched=`ls -l | grep  \"$ver\" `\n\nif [ -z \"$matched\" ]; then\n    echo -e \"${RED}failed: (rockspec) VERSION = $ver \\\"$ver\\\" ${NC}\" 1>&2\n    echo\n    echo \"-----please check rockspec file for VERSION \\\"$ver\\\"-----\"\n    echo \"$matched\"\n    exit 1\nelse\n    echo -e \"${green}passed: (rockspec) VERSION = $ver ${NC}\"\nfi\n"
  },
  {
    "path": "utils/fix-zh-doc-segment.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nimport os\nfrom os import path\nfrom zhon.hanzi import punctuation # sudo pip3 install zhon\n\n\ndef need_fold(pre, cur):\n    pre = pre.rstrip(\"\\r\\n\")\n    if len(pre) == 0 or len(cur) == 0:\n        return False\n    if ord(pre[-1]) < 128 or ord(cur[0]) < 128:\n        return False\n    # the prev line ends with Chinese and the curr line starts with Chinese\n    if pre.startswith(\":::note\"):\n        # ignore special mark\n        return False\n    if pre[-1] in punctuation:\n        # skip punctuation\n        return False\n    return True\n\ndef check_segment(root):\n    for parent, dirs, files in os.walk(root):\n        for fn in files:\n            fn = path.join(parent, fn)\n            with open(fn) as f:\n                lines = f.readlines()\n                new_lines = [lines[0]]\n                skip = False\n                for i in range(1, len(lines)):\n                    if lines[i-1].startswith('```'):\n                        skip = not skip\n                    if not skip and need_fold(lines[i-1], lines[i]):\n                        new_lines[-1] = new_lines[-1].rstrip(\"\\r\\n\") + lines[i]\n                    else:\n                        new_lines.append(lines[i])\n            if len(new_lines) != len(lines):\n                print(\"find broken newline in file: %s\" % fn)\n                with open(fn, \"w\") as f:\n                    f.writelines(new_lines)\n\n\nroots = [\"docs/zh/latest/\"]\nfor r in roots:\n    check_segment(r)\n"
  },
  {
    "path": "utils/gen-vote-contents.sh",
    "content": "#!/bin/sh\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nVERSION=$1\n\nSUBSTRING1=$(echo $VERSION| cut -d'.' -f 1)\nSUBSTRING2=$(echo $VERSION| cut -d'.' -f 2)\nBLOB_VERSION=$SUBSTRING1.$SUBSTRING2\nCHANGELOG_HASH=$(printf $VERSION | sed 's/\\.//g')\n\nRELEASE_NOTE_PR=\"https://github.com/apache/apisix/blob/release/$BLOB_VERSION/CHANGELOG.md#$CHANGELOG_HASH\"\nCOMMIT_ID=$(git rev-parse --short HEAD)\n\nvote_contents=$(cat <<EOF\nHello, Community,\n\nThis is a call for the vote to release Apache APISIX version\n\nRelease notes:\n\n$RELEASE_NOTE_PR\n\nThe release candidates:\n\nhttps://dist.apache.org/repos/dist/dev/apisix/$VERSION/\n\nRelease Commit ID:\n\nhttps://github.com/apache/apisix/commit/$COMMIT_ID\n\nKeys to verify the Release Candidate:\n\nhttps://dist.apache.org/repos/dist/dev/apisix/KEYS\n\nSteps to validating the release:\n\n1. Download the release\n\nwget https://dist.apache.org/repos/dist/dev/apisix/$VERSION/apache-apisix-$VERSION-src.tgz\n\n2. Checksums and signatures\n\nwget https://dist.apache.org/repos/dist/dev/apisix/KEYS\n\nwget https://dist.apache.org/repos/dist/dev/apisix/$VERSION/apache-apisix-$VERSION-src.tgz.asc\n\nwget https://dist.apache.org/repos/dist/dev/apisix/$VERSION/apache-apisix-$VERSION-src.tgz.sha512\n\ngpg --import KEYS\n\nshasum -c apache-apisix-$VERSION-src.tgz.sha512\n\ngpg --verify apache-apisix-$VERSION-src.tgz.asc apache-apisix-$VERSION-src.tgz\n\n3. Unzip and Check files\n\ntar zxvf apache-apisix-$VERSION-src.tgz\n\n4. Build Apache APISIX:\n\nhttps://github.com/apache/apisix/blob/release/$BLOB_VERSION/docs/en/latest/building-apisix.md#building-apisix-from-source\n\nThe vote will be open for at least 72 hours or until necessary number of\nvotes are reached.\n\nPlease vote accordingly:\n\n[ ] +1 approve\n[ ] +0 no opinion\n[ ] -1 disapprove with the reason\nEOF\n)\n\nif [ ! -d release ];then\n  mkdir release\nfi\n\nprintf \"$vote_contents\" > ./release/apache-apisix-$VERSION-vote-contents.txt\n"
  },
  {
    "path": "utils/install-dependencies.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -ex\n\nfunction detect_aur_helper() {\n    if [[ $(command -v yay) ]]; then\n        AUR_HELPER=yay\n    elif [[ $(command -v pacaur) ]]; then\n        AUR_HELPER=pacaur\n    else\n        echo No available AUR helpers found. Please specify your AUR helper by AUR_HELPER.\n        exit 255\n    fi\n}\n\nfunction install_dependencies_with_aur() {\n    detect_aur_helper\n    $AUR_HELPER -S openresty --noconfirm\n    sudo pacman -S openssl --noconfirm\n\n    export OPENRESTY_PREFIX=/opt/openresty\n\n    sudo mkdir $OPENRESTY_PREFIX/openssl\n    sudo ln -s /usr/include $OPENRESTY_PREFIX/openssl/include\n    sudo ln -s /usr/lib $OPENRESTY_PREFIX/openssl/lib\n}\n\n# Install dependencies on centos and fedora\nfunction install_dependencies_with_yum() {\n    sudo yum install -y yum-utils\n    sudo yum-config-manager --add-repo \"https://openresty.org/package/${1}/openresty.repo\"\n    if [[ \"${1}\" == \"centos\" ]]; then\n        sudo yum -y install centos-release-scl\n        sudo yum -y install devtoolset-9 patch wget\n        set +eu\n        source scl_source enable devtoolset-9\n        set -eu\n    fi\n    sudo yum install -y  \\\n        gcc gcc-c++ curl wget unzip xz gnupg perl-ExtUtils-Embed cpanminus patch libyaml-devel \\\n        perl perl-devel pcre pcre-devel pcre2 pcre2-devel openldap-devel \\\n        openresty-zlib-devel openresty-pcre-devel\n}\n\n# Install dependencies on ubuntu and debian\nfunction install_dependencies_with_apt() {\n    # add OpenResty source\n    sudo apt-get update\n    sudo apt-get -y install software-properties-common wget lsb-release gnupg patch\n    wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -\n    arch=$(uname -m | tr '[:upper:]' '[:lower:]')\n    arch_path=\"\"\n    if [[ $arch == \"arm64\" ]] || [[ $arch == \"aarch64\" ]]; then\n        arch_path=\"arm64/\"\n    fi\n    if [[ \"${1}\" == \"ubuntu\" ]]; then\n        sudo add-apt-repository -y \"deb http://openresty.org/package/${arch_path}ubuntu $(lsb_release -sc) main\"\n    elif [[ \"${1}\" == \"debian\" ]]; then\n        sudo add-apt-repository -y \"deb http://openresty.org/package/${arch_path}debian $(lsb_release -sc) openresty\"\n    fi\n    sudo apt-get update\n\n    # install some compilation tools\n    sudo apt-get install -y curl make gcc g++ cpanminus libpcre3 libpcre3-dev libpcre2-dev libyaml-dev unzip openresty-zlib-dev openresty-pcre-dev\n}\n\n# Identify the different distributions and call the corresponding function\nfunction multi_distro_installation() {\n    if grep -Eqi \"CentOS\" /etc/issue || grep -Eq \"CentOS\" /etc/*-release; then\n        install_dependencies_with_yum \"centos\"\n    elif grep -Eqi -e \"Red Hat\" -e \"rhel\" /etc/*-release; then\n        install_dependencies_with_yum \"rhel\"\n    elif grep -Eqi \"Fedora\" /etc/issue || grep -Eq \"Fedora\" /etc/*-release; then\n        install_dependencies_with_yum \"fedora\"\n    elif grep -Eqi \"Debian\" /etc/issue || grep -Eq \"Debian\" /etc/*-release; then\n        install_dependencies_with_apt \"debian\"\n    elif grep -Eqi \"Ubuntu\" /etc/issue || grep -Eq \"Ubuntu\" /etc/*-release; then\n        install_dependencies_with_apt \"ubuntu\"\n    elif grep -Eqi \"Arch\" /etc/issue || grep -Eqi \"EndeavourOS\" /etc/issue || grep -Eq \"Arch\" /etc/*-release; then\n        install_dependencies_with_aur\n    else\n        echo \"Non-supported distribution, APISIX is only supported on Linux-based systems\"\n        exit 1\n    fi\n    install_apisix_runtime\n}\n\nfunction multi_distro_uninstallation() {\n    if grep -Eqi \"CentOS\" /etc/issue || grep -Eq \"CentOS\" /etc/*-release; then\n        sudo yum autoremove -y openresty-zlib-devel openresty-pcre-devel\n    elif grep -Eqi -e \"Red Hat\" -e \"rhel\" /etc/*-release; then\n        sudo yum autoremove -y openresty-zlib-devel openresty-pcre-devel\n    elif grep -Eqi \"Fedora\" /etc/issue || grep -Eq \"Fedora\" /etc/*-release; then\n        sudo yum autoremove -y openresty-zlib-devel openresty-pcre-devel\n    elif grep -Eqi \"Debian\" /etc/issue || grep -Eq \"Debian\" /etc/*-release; then\n        sudo apt-get autoremove -y openresty-zlib-dev openresty-pcre-dev\n    elif grep -Eqi \"Ubuntu\" /etc/issue || grep -Eq \"Ubuntu\" /etc/*-release; then\n        sudo apt-get autoremove -y openresty-zlib-dev openresty-pcre-dev\n    else\n        echo \"Non-supported distribution, APISIX is only supported on Linux-based systems\"\n        exit 1\n    fi\n}\n\nfunction install_apisix_runtime() {\n    export runtime_version=${APISIX_RUNTIME:?}\n    wget \"https://raw.githubusercontent.com/api7/apisix-build-tools/apisix-runtime/${APISIX_RUNTIME}/build-apisix-runtime.sh\"\n    chmod +x build-apisix-runtime.sh\n    ./build-apisix-runtime.sh latest\n    rm build-apisix-runtime.sh\n}\n\n# Install LuaRocks\nfunction install_luarocks() {\n    if [ -f \"./utils/linux-install-luarocks.sh\" ]; then\n        ./utils/linux-install-luarocks.sh\n    elif [ -f \"./linux-install-luarocks.sh\" ]; then\n        ./linux-install-luarocks.sh\n    else\n        echo \"Installing luarocks from remote master branch\"\n        curl https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh -sL | bash -\n    fi\n}\n\n# Entry\nfunction main() {\n    OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')\n    if [[ \"$#\" == 0 ]]; then\n        if [[ \"${OS_NAME}\" == \"linux\" ]]; then\n            multi_distro_installation\n            install_luarocks\n            return\n        else\n            echo \"Non-supported distribution, APISIX is only supported on Linux-based systems\"\n            exit 1\n        fi\n    fi\n\n    case_opt=$1\n    case \"${case_opt}\" in\n        \"install_luarocks\")\n            install_luarocks\n        ;;\n        \"uninstall\")\n            if [[ \"${OS_NAME}\" == \"linux\" ]]; then\n                multi_distro_uninstallation\n            else\n                echo \"Non-supported distribution, APISIX is only supported on Linux-based systems\"\n            fi\n        ;;\n        *)\n            echo \"Unsupported method: ${case_opt}\"\n        ;;\n    esac\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "utils/linux-install-luarocks.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -ex\n\n# you might need sudo to run this script\nif [ -z ${OPENRESTY_PREFIX} ]; then\n    OPENRESTY_PREFIX=\"/usr/local/openresty\"\nfi\n\nLUAROCKS_VER=3.12.0\nwget -q https://github.com/luarocks/luarocks/archive/v\"$LUAROCKS_VER\".tar.gz\ntar -xf v\"$LUAROCKS_VER\".tar.gz\nrm -f v\"$LUAROCKS_VER\".tar.gz\ncd luarocks-\"$LUAROCKS_VER\" || exit\n\nOR_BIN=\"$OPENRESTY_PREFIX/bin/openresty\"\nOR_VER=$($OR_BIN -v 2>&1 | awk -F '/' '{print $2}' | awk -F '.' '{print $1 * 100 + $2}')\nif [[ -e $OR_BIN && \"$OR_VER\" -ge 119 ]]; then\n    WITH_LUA_OPT=\"--with-lua=${OPENRESTY_PREFIX}/luajit\"\nelse\n    # For old version OpenResty, we still need to install LuaRocks with Lua\n    WITH_LUA_OPT=\nfi\n\n./configure $WITH_LUA_OPT \\\n    > build.log 2>&1 || (cat build.log && exit 1)\n\nmake build > build.log 2>&1 || (cat build.log && exit 1)\nsudo make install > build.log 2>&1 || (cat build.log && exit 1)\ncd .. || exit\nrm -rf luarocks-\"$LUAROCKS_VER\"\n\nmkdir ~/.luarocks || true\n\n# For old version OpenResty, we still need to install LuaRocks with Lua\nOPENSSL_PREFIX=${OPENRESTY_PREFIX}/openssl\nif [ -d ${OPENRESTY_PREFIX}/openssl3 ]; then\n    OPENSSL_PREFIX=${OPENRESTY_PREFIX}/openssl3\nelif [ -d ${OPENRESTY_PREFIX}/openssl111 ]; then\n    OPENSSL_PREFIX=${OPENRESTY_PREFIX}/openssl111\nfi\n\n[ ! -d ${OPENSSL_PREFIX} ] && echo \"Warning: the path ${OPENSSL_PREFIX} is not found.\"\n\nFOUND_PATH=$(echo \"${PATH}\" | grep -oP '(?<=:|)/usr/local/bin(?=:|)') || true\nif [[ \"${FOUND_PATH}\" == \"\" ]]; then\n   echo \"Warning: the path /usr/local/bin is not included in the system default PATH variable.\"\n   export PATH=$PATH:/usr/local/bin\nfi\n\nluarocks config variables.OPENSSL_LIBDIR ${OPENSSL_PREFIX}/lib\nluarocks config variables.OPENSSL_INCDIR ${OPENSSL_PREFIX}/include\nluarocks config variables.YAML_DIR /usr\n"
  }
]